@7365admin1/core 3.68.0 → 3.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -21257,6 +21257,7 @@ import { promises as fsAsync } from "fs";
21257
21257
  import Joi26 from "joi";
21258
21258
  import * as path2 from "path";
21259
21259
  import { request } from "urllib";
21260
+ import { createHash as createHash2 } from "crypto";
21260
21261
 
21261
21262
  // src/repositories/visitor-transaction.repo.ts
21262
21263
  import {
@@ -23773,6 +23774,69 @@ function useVehicleRepo() {
23773
23774
  throw error;
23774
23775
  }
23775
23776
  }
23777
+ async function getLivePlatesBySite(site) {
23778
+ let siteId;
23779
+ try {
23780
+ siteId = new ObjectId47(site);
23781
+ } catch (error) {
23782
+ throw new BadRequestError46("Invalid site ID format.");
23783
+ }
23784
+ try {
23785
+ return await collection.aggregate([
23786
+ {
23787
+ $match: {
23788
+ site: siteId,
23789
+ status: "active" /* ACTIVE */,
23790
+ plateNumber: { $nin: [null, ""] }
23791
+ }
23792
+ },
23793
+ { $sort: { name: 1, plateNumber: 1 } },
23794
+ {
23795
+ $lookup: {
23796
+ from: "buildings",
23797
+ localField: "block",
23798
+ foreignField: "_id",
23799
+ pipeline: [{ $project: { name: 1 } }],
23800
+ as: "blockDoc"
23801
+ }
23802
+ },
23803
+ {
23804
+ $lookup: {
23805
+ from: "building-levels",
23806
+ localField: "level",
23807
+ foreignField: "_id",
23808
+ pipeline: [{ $project: { name: 1 } }],
23809
+ as: "levelDoc"
23810
+ }
23811
+ },
23812
+ {
23813
+ $lookup: {
23814
+ from: "building-units",
23815
+ localField: "unit",
23816
+ foreignField: "_id",
23817
+ pipeline: [{ $project: { name: 1 } }],
23818
+ as: "unitDoc"
23819
+ }
23820
+ },
23821
+ {
23822
+ $project: {
23823
+ plateNumber: 1,
23824
+ name: 1,
23825
+ start: 1,
23826
+ end: 1,
23827
+ type: 1,
23828
+ blockedFrom: 1,
23829
+ seasonPassType: 1,
23830
+ blockName: { $ifNull: [{ $arrayElemAt: ["$blockDoc.name", 0] }, ""] },
23831
+ levelName: { $ifNull: [{ $arrayElemAt: ["$levelDoc.name", 0] }, ""] },
23832
+ unitName: { $ifNull: [{ $arrayElemAt: ["$unitDoc.name", 0] }, ""] }
23833
+ }
23834
+ }
23835
+ ]).toArray();
23836
+ } catch (error) {
23837
+ throw new InternalServerError24("Failed to list the site's vehicles.");
23838
+ }
23839
+ }
23776
23840
  async function getAllExpiredVehicles() {
23777
23841
  try {
23778
23842
  const items = await collection.find(expiredVehicleSweepFilter()).toArray();
@@ -23977,6 +24041,7 @@ function useVehicleRepo() {
23977
24041
  getVehicleByPlateNumber,
23978
24042
  getVehiclesByNRIC,
23979
24043
  deleteExpiredVehicles,
24044
+ getLivePlatesBySite,
23980
24045
  getAllVehiclesByUnitId,
23981
24046
  getAllExpiredVehicles,
23982
24047
  bulkUpsertVehicles,
@@ -24176,6 +24241,73 @@ function classifyDahuaFailure(message) {
24176
24241
  }
24177
24242
  return "transient";
24178
24243
  }
24244
+ var DAHUA_COMMAND_ATTEMPTS = 10;
24245
+ var DAHUA_COMMAND_DEADLINE_MS = 45e3;
24246
+ var DAHUA_LOGIN_PROVEN_FOR_MS = 10 * 6e4;
24247
+ var COMMAND_RETRY_DELAYS_MS = [1e3, 2e3, 3e3, 4e3, 5e3];
24248
+ function dahuaCommandRetryDelayMs(failedAttempt) {
24249
+ const index = Math.min(
24250
+ Math.max(Math.floor(failedAttempt), 1),
24251
+ COMMAND_RETRY_DELAYS_MS.length
24252
+ );
24253
+ return COMMAND_RETRY_DELAYS_MS[index - 1];
24254
+ }
24255
+ var NOT_SENT_PATTERN = /ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|ENOTFOUND|UND_ERR_CONNECT_TIMEOUT|connect ?timeout|ConnectTimeout/i;
24256
+ function classifyDahuaAttempt(attempt) {
24257
+ if (attempt.error !== void 0 && attempt.error !== null) {
24258
+ const error = attempt.error;
24259
+ const message = [error?.name, error?.code, error?.cause?.code, error?.message ?? error].filter(Boolean).join(" ");
24260
+ if (/invalid authority|\b401\b|unauthori[sz]ed/i.test(message)) {
24261
+ return "login-refused";
24262
+ }
24263
+ if (/\b403\b|forbidden/i.test(message))
24264
+ return "forbidden";
24265
+ if (NOT_SENT_PATTERN.test(message))
24266
+ return "not-sent";
24267
+ return "maybe-sent";
24268
+ }
24269
+ const status = attempt.statusCode;
24270
+ if (status === 401 || /invalid authority/i.test(String(attempt.text ?? ""))) {
24271
+ return "login-refused";
24272
+ }
24273
+ if (status === 403)
24274
+ return "forbidden";
24275
+ if (status === 502 || status === 503 || status === 504)
24276
+ return "busy";
24277
+ if (status === void 0)
24278
+ return "maybe-sent";
24279
+ return "answered";
24280
+ }
24281
+ function shouldRetryDahuaCommand(options) {
24282
+ const {
24283
+ kind,
24284
+ attempt,
24285
+ loginFailures,
24286
+ loginProven,
24287
+ repeatable,
24288
+ elapsedMs,
24289
+ nextDelayMs,
24290
+ maxAttempts = DAHUA_COMMAND_ATTEMPTS,
24291
+ deadlineMs = DAHUA_COMMAND_DEADLINE_MS
24292
+ } = options;
24293
+ if (kind === "answered" || kind === "forbidden")
24294
+ return false;
24295
+ if (attempt >= maxAttempts)
24296
+ return false;
24297
+ if (elapsedMs + nextDelayMs > deadlineMs)
24298
+ return false;
24299
+ switch (kind) {
24300
+ case "login-refused":
24301
+ return loginProven || loginFailures < DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT;
24302
+ case "busy":
24303
+ case "not-sent":
24304
+ return true;
24305
+ case "maybe-sent":
24306
+ return repeatable;
24307
+ default:
24308
+ return false;
24309
+ }
24310
+ }
24179
24311
  var enc = (value) => encodeURIComponent(String(value));
24180
24312
  function buildPlateUpdateEndpoint(value) {
24181
24313
  return `/cgi-bin/recordUpdater.cgi?action=update&name=${enc(value.mode)}&recno=${enc(value.recno)}&PlateNumber=${enc(value.plateNumber)}&BeginTime=${enc(value.start)}&CancelTime=${enc(value.end)}&+OpenGate=${value.openGate}&MasterOfCar=${enc(value.owner)}`;
@@ -24186,6 +24318,13 @@ function buildPlateInsertEndpoint(value) {
24186
24318
  function buildPlateRemoveEndpoint(value) {
24187
24319
  return `/cgi-bin/recordUpdater.cgi?action=remove&recno=${enc(value.recno)}&name=${enc(value.mode)}`;
24188
24320
  }
24321
+ var CLEARABLE_PLATE_LISTS = ["TrafficRedList", "TrafficBlackList"];
24322
+ function buildPlateClearEndpoint(value) {
24323
+ if (!CLEARABLE_PLATE_LISTS.includes(value.mode)) {
24324
+ throw new Error(`Refusing to clear record set ${JSON.stringify(value.mode)}.`);
24325
+ }
24326
+ return `/cgi-bin/recordUpdater.cgi?action=clear&name=${enc(value.mode)}`;
24327
+ }
24189
24328
 
24190
24329
  // src/utils/anpr-revoke.util.ts
24191
24330
  function anprRevokeOutcome(response) {
@@ -24311,6 +24450,148 @@ function anprNoCameraRefusal(outcome) {
24311
24450
  return `There is no ANPR camera set up at this site, so the plate cannot be registered on a barrier. The vehicle has NOT been ${outcome}. Add a camera in Settings first, then try again.`;
24312
24451
  }
24313
24452
 
24453
+ // src/utils/anpr-plate.util.ts
24454
+ var ALLOW_CAMERA_DIRECTIONS = ["entry", "both", "residents"];
24455
+ var BLOCK_CAMERA_DIRECTIONS = ["entry", "both", "residents", "visitors"];
24456
+ function isAllowCameraDirection(direction) {
24457
+ return ALLOW_CAMERA_DIRECTIONS.includes(
24458
+ String(direction ?? "").trim().toLowerCase()
24459
+ );
24460
+ }
24461
+ function isBlockCameraDirection(direction) {
24462
+ return BLOCK_CAMERA_DIRECTIONS.includes(
24463
+ String(direction ?? "").trim().toLowerCase()
24464
+ );
24465
+ }
24466
+ function listsFilledByDirectionChange(from, to) {
24467
+ const before = String(from ?? "").trim().toLowerCase();
24468
+ const after = String(to ?? "").trim().toLowerCase();
24469
+ if (!after || after === before)
24470
+ return { allow: false, block: false };
24471
+ return {
24472
+ allow: isAllowCameraDirection(after) && !isAllowCameraDirection(before),
24473
+ block: isBlockCameraDirection(after) && !isBlockCameraDirection(before)
24474
+ };
24475
+ }
24476
+ function listsClearedByDirectionChange(from, to) {
24477
+ const before = String(from ?? "").trim().toLowerCase();
24478
+ const after = String(to ?? "").trim().toLowerCase();
24479
+ if (!after || after === before)
24480
+ return { allow: false, block: false };
24481
+ if (after === "exit")
24482
+ return { allow: true, block: true };
24483
+ return {
24484
+ allow: isAllowCameraDirection(before) && !isAllowCameraDirection(after),
24485
+ block: false
24486
+ };
24487
+ }
24488
+ function mustRevokeFrom(direction, list, vehicleIsBlocked = false) {
24489
+ if (list === "allowed")
24490
+ return isAllowCameraDirection(direction);
24491
+ if (isBlockCameraDirection(direction))
24492
+ return true;
24493
+ return vehicleIsBlocked;
24494
+ }
24495
+ function normalisePlate(plate) {
24496
+ return String(plate ?? "").replace(/\s+/g, "").toUpperCase();
24497
+ }
24498
+ function samePlate(a, b) {
24499
+ const left = normalisePlate(a);
24500
+ return left !== "" && left === normalisePlate(b);
24501
+ }
24502
+ function parseDahuaFindRecords(text2) {
24503
+ const body = String(text2 ?? "");
24504
+ const found = Number(body.match(/found=(\d+)/i)?.[1] || 0);
24505
+ const byIndex = /* @__PURE__ */ new Map();
24506
+ const pattern = /records\[(\d+)\]\.(RecNo|PlateNumber|BeginTime|CancelTime|MasterOfCar)=([^\r\n]*)/gi;
24507
+ for (const match of body.matchAll(pattern)) {
24508
+ const index = Number(match[1]);
24509
+ const key = match[2].toLowerCase();
24510
+ const value = match[3].trim();
24511
+ const record = byIndex.get(index) ?? { recNo: "", plateNumber: "" };
24512
+ if (key === "recno")
24513
+ record.recNo = value;
24514
+ else if (key === "platenumber")
24515
+ record.plateNumber = value;
24516
+ else if (key === "begintime")
24517
+ record.beginTime = value;
24518
+ else if (key === "canceltime")
24519
+ record.cancelTime = value;
24520
+ else
24521
+ record.owner = value;
24522
+ byIndex.set(index, record);
24523
+ }
24524
+ const records = Array.from(byIndex.entries()).sort(([a], [b]) => a - b).map(([, record]) => record).filter((record) => record.recNo !== "");
24525
+ return { found, records };
24526
+ }
24527
+ function matchingPlateRecords(text2, plateNumber) {
24528
+ return parseDahuaFindRecords(text2).records.filter(
24529
+ (record) => record.plateNumber === "" || samePlate(record.plateNumber, plateNumber)
24530
+ );
24531
+ }
24532
+ function listsHeldBy(vehicle) {
24533
+ const type = String(vehicle?.type ?? "");
24534
+ return {
24535
+ allow: type === "whitelist" || String(vehicle?.blockedFrom ?? "") === "whitelist",
24536
+ block: type === "blocklist"
24537
+ };
24538
+ }
24539
+ function listsToRevoke(othersStillLive) {
24540
+ const others = (othersStillLive ?? []).map(listsHeldBy);
24541
+ return {
24542
+ allow: !others.some((held) => held.allow),
24543
+ block: !others.some((held) => held.block)
24544
+ };
24545
+ }
24546
+ var DEFAULT_CAMERA_TIMEZONE = "Asia/Singapore";
24547
+ function usableTimezone(timezone) {
24548
+ const zone = String(timezone ?? "").trim();
24549
+ if (!zone)
24550
+ return DEFAULT_CAMERA_TIMEZONE;
24551
+ try {
24552
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
24553
+ return zone;
24554
+ } catch {
24555
+ return DEFAULT_CAMERA_TIMEZONE;
24556
+ }
24557
+ }
24558
+ var WALL_CLOCK = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(?::(\d{2}))?$/;
24559
+ function dahuaWallClock(value, timezone) {
24560
+ if (value === null || value === void 0 || value === "")
24561
+ return null;
24562
+ if (typeof value === "string") {
24563
+ const wall = value.trim().match(WALL_CLOCK);
24564
+ if (wall) {
24565
+ const [, y, mo, d, h, mi, se] = wall;
24566
+ return `${y}-${mo}-${d} ${h}:${mi}:${se ?? "00"}`;
24567
+ }
24568
+ }
24569
+ const date = value instanceof Date ? value : new Date(value);
24570
+ if (Number.isNaN(date.getTime()))
24571
+ return null;
24572
+ const parts = new Intl.DateTimeFormat("en-GB", {
24573
+ timeZone: usableTimezone(timezone),
24574
+ year: "numeric",
24575
+ month: "2-digit",
24576
+ day: "2-digit",
24577
+ hour: "2-digit",
24578
+ minute: "2-digit",
24579
+ second: "2-digit",
24580
+ hourCycle: "h23"
24581
+ }).formatToParts(date);
24582
+ const part = (type) => parts.find((entry) => entry.type === type)?.value ?? "00";
24583
+ return `${part("year")}-${part("month")}-${part("day")} ${part("hour")}:${part("minute")}:${part("second")}`;
24584
+ }
24585
+ function plateWindowMatches(record, wanted) {
24586
+ const same = (held, want) => {
24587
+ const a = dahuaWallClock(held);
24588
+ return a !== null && a === dahuaWallClock(want);
24589
+ };
24590
+ if (!wanted.start || !wanted.end)
24591
+ return false;
24592
+ return same(record.beginTime, wanted.start) && same(record.cancelTime, wanted.end);
24593
+ }
24594
+
24314
24595
  // src/services/dahua.service.ts
24315
24596
  function notifyCameraFault(camera, message) {
24316
24597
  NotificationService.cameraFault({
@@ -24382,12 +24663,14 @@ async function useDahuaDigestWithRetry({
24382
24663
  const statusCode = getDahuaStatusCode(response);
24383
24664
  if (streaming) {
24384
24665
  if (statusCode === 200) {
24666
+ markLoginProven(host, username, password);
24385
24667
  return response;
24386
24668
  }
24387
24669
  } else {
24388
24670
  const text2 = getDahuaResponseText(response);
24389
24671
  const isOk = statusCode === 200 && text2.includes("OK");
24390
24672
  if (isOk) {
24673
+ markLoginProven(host, username, password);
24391
24674
  return response;
24392
24675
  }
24393
24676
  }
@@ -24421,6 +24704,123 @@ async function useDahuaDigestWithRetry({
24421
24704
  }
24422
24705
  throw lastError;
24423
24706
  }
24707
+ var provenLogins = /* @__PURE__ */ new Map();
24708
+ function loginKey(host, username, password) {
24709
+ return createHash2("sha256").update(`${host ?? ""}
24710
+ ${username ?? ""}
24711
+ ${password ?? ""}`).digest("hex");
24712
+ }
24713
+ function markLoginProven(host, username, password) {
24714
+ provenLogins.set(loginKey(host, username, password), Date.now());
24715
+ }
24716
+ function forgetLogin(host, username, password) {
24717
+ provenLogins.delete(loginKey(host, username, password));
24718
+ }
24719
+ function isLoginProven(host, username, password) {
24720
+ const at2 = provenLogins.get(loginKey(host, username, password));
24721
+ return at2 !== void 0 && Date.now() - at2 < DAHUA_LOGIN_PROVEN_FOR_MS;
24722
+ }
24723
+ var ATTEMPT_WORDS = {
24724
+ "login-refused": "login refused",
24725
+ forbidden: "credentials refused",
24726
+ busy: "camera busy",
24727
+ "not-sent": "could not connect",
24728
+ "maybe-sent": "connection lost"
24729
+ };
24730
+ async function sendCameraCommand({
24731
+ host,
24732
+ username,
24733
+ password,
24734
+ endpoint,
24735
+ timeout = 1e4,
24736
+ repeatable = true,
24737
+ label = "command",
24738
+ deadlineMs = DAHUA_COMMAND_DEADLINE_MS
24739
+ }) {
24740
+ const startedAt = Date.now();
24741
+ let loginFailures = 0;
24742
+ for (let attempt = 1; ; attempt++) {
24743
+ let response;
24744
+ let error;
24745
+ try {
24746
+ response = await useDahuaDigest({
24747
+ host,
24748
+ username,
24749
+ password,
24750
+ endpoint,
24751
+ headers: { Connection: "close" },
24752
+ timeout
24753
+ });
24754
+ } catch (thrown) {
24755
+ error = thrown ?? new Error("the request failed with no error");
24756
+ }
24757
+ const kind = classifyDahuaAttempt(
24758
+ error ? { error } : {
24759
+ statusCode: getDahuaStatusCode(response),
24760
+ text: getDahuaResponseText(response)
24761
+ }
24762
+ );
24763
+ if (kind === "answered") {
24764
+ markLoginProven(host, username, password);
24765
+ if (attempt > 1) {
24766
+ loggerDahua.info(`[${host}] ${label}: went through on try ${attempt}`);
24767
+ }
24768
+ return response;
24769
+ }
24770
+ if (kind === "login-refused")
24771
+ loginFailures++;
24772
+ if (kind === "forbidden")
24773
+ forgetLogin(host, username, password);
24774
+ const nextDelayMs = dahuaCommandRetryDelayMs(attempt);
24775
+ const retry = shouldRetryDahuaCommand({
24776
+ kind,
24777
+ attempt,
24778
+ loginFailures,
24779
+ loginProven: isLoginProven(host, username, password),
24780
+ repeatable,
24781
+ elapsedMs: Date.now() - startedAt,
24782
+ nextDelayMs,
24783
+ deadlineMs
24784
+ });
24785
+ const why = ATTEMPT_WORDS[kind] ?? kind;
24786
+ if (!retry) {
24787
+ if (attempt > 1 || kind !== "forbidden") {
24788
+ loggerDahua.warn(
24789
+ `[${host}] ${label}: ${why} on try ${attempt}/${DAHUA_COMMAND_ATTEMPTS}; giving up`
24790
+ );
24791
+ }
24792
+ if (error)
24793
+ throw error;
24794
+ return response;
24795
+ }
24796
+ loggerDahua.warn(
24797
+ `[${host}] ${label}: ${why} on try ${attempt}/${DAHUA_COMMAND_ATTEMPTS}; trying again in ${nextDelayMs} ms`
24798
+ );
24799
+ await sleep(nextDelayMs);
24800
+ }
24801
+ }
24802
+ async function sendCameraCommandExpectingOk(options) {
24803
+ const response = await sendCameraCommand(options);
24804
+ const statusCode = getDahuaStatusCode(response);
24805
+ const text2 = getDahuaResponseText(response);
24806
+ if (statusCode === 200 && text2.includes("OK"))
24807
+ return response;
24808
+ const error = new Error(`Dahua response: ${text2 || `HTTP ${statusCode}`}`);
24809
+ error.answeredByCamera = true;
24810
+ throw error;
24811
+ }
24812
+ function anprOwnerName(owner) {
24813
+ return String(owner ?? "").replace(/['"]/g, "").replace(/[\/\\]/g, " - ").substring(0, 15).trim() || "unknown";
24814
+ }
24815
+ function plateWindowOnCamera(value) {
24816
+ const tenMinutesAgo = new Date(Date.now() - 10 * 6e4);
24817
+ const tenYearsOn = new Date(tenMinutesAgo);
24818
+ tenYearsOn.setFullYear(tenYearsOn.getFullYear() + 10);
24819
+ return {
24820
+ start: dahuaWallClock(value.start, value.timezone) ?? dahuaWallClock(tenMinutesAgo, value.timezone),
24821
+ end: dahuaWallClock(value.end, value.timezone) ?? dahuaWallClock(tenYearsOn, value.timezone)
24822
+ };
24823
+ }
24424
24824
  function createFileNameFromEvent(utcSeconds, utcMilliseconds) {
24425
24825
  const date = new Date(utcSeconds * 1e3 + utcMilliseconds);
24426
24826
  const year = date.getUTCFullYear();
@@ -25022,111 +25422,115 @@ function useDahuaService() {
25022
25422
  start: Joi26.string().isoDate().optional().allow("", null),
25023
25423
  end: Joi26.string().isoDate().optional().allow("", null),
25024
25424
  owner: Joi26.string().optional().allow("", null),
25025
- isOpenGate: Joi26.boolean().optional().allow(null)
25425
+ isOpenGate: Joi26.boolean().optional().allow(null),
25426
+ timezone: Joi26.string().optional().allow("", null)
25026
25427
  });
25027
25428
  const { error: validationError } = validation.validate(value);
25028
25429
  if (validationError) {
25029
25430
  throw new BadRequestError47(`Validation error: ${validationError.message}`);
25030
25431
  }
25031
25432
  try {
25032
- value.owner = String(value.owner ?? "").replace(/['"]/g, "").replace(/[\/\\]/g, " - ").substring(0, 15).trim() || "unknown";
25033
- const formatDahuaDate3 = (dateStr, fallbackYearsAhead = 0) => {
25034
- const date = dateStr ? new Date(dateStr) : /* @__PURE__ */ new Date();
25035
- if (!dateStr) {
25036
- date.setMinutes(date.getMinutes() - 10);
25037
- }
25038
- if (fallbackYearsAhead > 0 && !dateStr) {
25039
- date.setFullYear(date.getFullYear() + fallbackYearsAhead);
25040
- }
25041
- const pad = (num) => String(num).padStart(2, "0");
25042
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
25043
- };
25044
- const formattedStart = formatDahuaDate3(value.start);
25045
- const formattedEnd = formatDahuaDate3(value.end, 10);
25433
+ value.owner = anprOwnerName(value.owner);
25434
+ const { start: formattedStart, end: formattedEnd } = plateWindowOnCamera(value);
25046
25435
  const beginTime = encodeURIComponent(formattedStart);
25047
25436
  const cancelTime = encodeURIComponent(formattedEnd);
25048
25437
  const plateNumber = encodeURIComponent(value.plateNumber);
25049
25438
  const ownerName = encodeURIComponent(value.owner);
25050
25439
  const endpoint = `/cgi-bin/recordUpdater.cgi?action=insert&name=${value.mode}&PlateNumber=${plateNumber}&BeginTime=${beginTime}&CancelTime=${cancelTime}&MasterOfCar=${ownerName}`;
25051
- try {
25052
- const insertResponse = await useDahuaDigestWithRetry({
25053
- host: value.host,
25054
- username: value.username,
25055
- password: value.password,
25056
- endpoint,
25057
- retries: 10,
25058
- retryDelayMs: 0
25059
- });
25060
- const insertText = getDahuaResponseText(insertResponse);
25061
- const insertedId = insertText.match(/\d+/)?.[0] || "unknown";
25062
- return {
25063
- ...insertResponse,
25064
- statusCode: 200,
25065
- data: Buffer.from(`recno=${insertedId}`)
25066
- };
25067
- } catch (insertError) {
25068
- const errorMessage = insertError?.message || String(insertError);
25069
- if (/recno=/i.test(errorMessage)) {
25070
- loggerDahua.info(`[${value.host}] Insert confirmed successfully via raw RecNo response text.`);
25071
- const insertedId = errorMessage.match(/\d+/)?.[0] || "unknown";
25440
+ const INSERT_ROUNDS = 3;
25441
+ for (let round = 1; ; round++) {
25442
+ try {
25443
+ const insertResponse = await sendCameraCommandExpectingOk({
25444
+ host: value.host,
25445
+ username: value.username,
25446
+ password: value.password,
25447
+ endpoint,
25448
+ repeatable: false,
25449
+ label: `add plate to ${value.mode}`
25450
+ });
25451
+ const insertText = getDahuaResponseText(insertResponse);
25452
+ const insertedId = insertText.match(/\d+/)?.[0] || "unknown";
25072
25453
  return {
25454
+ ...insertResponse,
25073
25455
  statusCode: 200,
25074
25456
  data: Buffer.from(`recno=${insertedId}`)
25075
25457
  };
25076
- }
25077
- if (errorMessage.includes("Bad Request") || errorMessage.includes("Dahua response: Error")) {
25078
- loggerDahua.info(`[${value.host}] Insert failed (duplicate). Checking if plate ${value.plateNumber} already exists...`);
25079
- const findEndpoint = `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${plateNumber}`;
25080
- try {
25081
- const findResponse = await useDahuaDigestWithRetry({
25082
- host: value.host,
25083
- username: value.username,
25084
- password: value.password,
25085
- endpoint: findEndpoint,
25086
- retries: 5
25087
- });
25088
- const textData = getDahuaResponseText(findResponse);
25089
- recno = extractRecNo(textData);
25090
- } catch (findError) {
25091
- const findErrorMessage = findError?.message || String(findError);
25092
- recno = extractRecNo(findErrorMessage);
25093
- if (!recno) {
25094
- loggerDahua.error(`[${value.host}] Failed handling existing duplicate plate flow`, findError);
25095
- }
25458
+ } catch (insertError) {
25459
+ const errorMessage = insertError?.message || String(insertError);
25460
+ if (/recno=/i.test(errorMessage)) {
25461
+ loggerDahua.info(`[${value.host}] Insert confirmed successfully via raw RecNo response text.`);
25462
+ const insertedId = errorMessage.match(/\d+/)?.[0] || "unknown";
25463
+ return {
25464
+ statusCode: 200,
25465
+ data: Buffer.from(`recno=${insertedId}`)
25466
+ };
25096
25467
  }
25097
- if (recno) {
25098
- loggerDahua.info(`[${value.host}] Found existing record ID: ${recno}. Updating registration instead...`);
25099
- const updateEndpoint = `/cgi-bin/recordUpdater.cgi?action=update&name=${value.mode}&recno=${recno}&PlateNumber=${plateNumber}&BeginTime=${beginTime}&CancelTime=${cancelTime}&MasterOfCar=${ownerName}`;
25468
+ const duplicate = errorMessage.includes("Bad Request") || errorMessage.includes("Dahua response: Error");
25469
+ const lostInFlight = !insertError?.answeredByCamera && classifyDahuaAttempt({ error: insertError }) === "maybe-sent";
25470
+ if (duplicate || lostInFlight) {
25471
+ loggerDahua.info(
25472
+ `[${value.host}] Insert ${duplicate ? "failed (duplicate)" : "lost its connection"}. Checking if the plate is already on ${value.mode}...`
25473
+ );
25474
+ const findEndpoint = `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${plateNumber}`;
25100
25475
  try {
25101
- const updateResponse = await useDahuaDigestWithRetry({
25476
+ const findResponse = await sendCameraCommand({
25102
25477
  host: value.host,
25103
25478
  username: value.username,
25104
25479
  password: value.password,
25105
- endpoint: updateEndpoint,
25106
- retries: 5
25480
+ endpoint: findEndpoint,
25481
+ label: `find plate on ${value.mode}`
25107
25482
  });
25108
- return {
25109
- ...updateResponse,
25110
- statusCode: 200,
25111
- data: Buffer.from(`recno=${recno}`)
25112
- };
25113
- } catch (updateError) {
25114
- const updateErrorMessage = updateError?.message || String(updateError);
25115
- if (/recno=/i.test(updateErrorMessage) || /ok/i.test(updateErrorMessage)) {
25116
- loggerDahua.info(`[${value.host}] Update confirmed successfully despite wrapper omission.`);
25117
- if (!recno) {
25118
- recno = extractRecNo(updateErrorMessage);
25119
- }
25483
+ if (getDahuaStatusCode(findResponse) === 200) {
25484
+ recno = extractRecNo(getDahuaResponseText(findResponse));
25485
+ } else {
25486
+ loggerDahua.error(
25487
+ `[${value.host}] Plate lookup after a failed insert answered HTTP ${getDahuaStatusCode(findResponse)}`
25488
+ );
25489
+ }
25490
+ } catch (findError) {
25491
+ loggerDahua.error(`[${value.host}] Failed handling existing duplicate plate flow`, findError);
25492
+ }
25493
+ if (recno) {
25494
+ loggerDahua.info(`[${value.host}] Found existing record ID: ${recno}. Updating registration instead...`);
25495
+ const updateEndpoint = `/cgi-bin/recordUpdater.cgi?action=update&name=${value.mode}&recno=${recno}&PlateNumber=${plateNumber}&BeginTime=${beginTime}&CancelTime=${cancelTime}&MasterOfCar=${ownerName}`;
25496
+ try {
25497
+ const updateResponse = await sendCameraCommandExpectingOk({
25498
+ host: value.host,
25499
+ username: value.username,
25500
+ password: value.password,
25501
+ endpoint: updateEndpoint,
25502
+ label: `update plate on ${value.mode}`
25503
+ });
25120
25504
  return {
25505
+ ...updateResponse,
25121
25506
  statusCode: 200,
25122
25507
  data: Buffer.from(`recno=${recno}`)
25123
25508
  };
25509
+ } catch (updateError) {
25510
+ const updateErrorMessage = updateError?.message || String(updateError);
25511
+ if (/recno=/i.test(updateErrorMessage) || /ok/i.test(updateErrorMessage)) {
25512
+ loggerDahua.info(`[${value.host}] Update confirmed successfully despite wrapper omission.`);
25513
+ if (!recno) {
25514
+ recno = extractRecNo(updateErrorMessage);
25515
+ }
25516
+ return {
25517
+ statusCode: 200,
25518
+ data: Buffer.from(`recno=${recno}`)
25519
+ };
25520
+ }
25521
+ throw updateError;
25124
25522
  }
25125
- throw updateError;
25523
+ }
25524
+ if (lostInFlight && round < INSERT_ROUNDS) {
25525
+ loggerDahua.warn(
25526
+ `[${value.host}] Plate not found after a lost insert; sending the insert again (round ${round + 1}/${INSERT_ROUNDS})`
25527
+ );
25528
+ await sleep(dahuaCommandRetryDelayMs(round));
25529
+ continue;
25126
25530
  }
25127
25531
  }
25532
+ throw insertError;
25128
25533
  }
25129
- throw insertError;
25130
25534
  }
25131
25535
  } catch (finalError) {
25132
25536
  loggerDahua.error(
@@ -25146,7 +25550,8 @@ function useDahuaService() {
25146
25550
  start: Joi26.string().isoDate().optional().allow("", null),
25147
25551
  end: Joi26.string().isoDate().optional().allow("", null),
25148
25552
  owner: Joi26.string().optional().allow("", null),
25149
- isOpenGate: Joi26.boolean().optional()
25553
+ isOpenGate: Joi26.boolean().optional(),
25554
+ timezone: Joi26.string().optional().allow("", null)
25150
25555
  });
25151
25556
  const { error } = validation.validate(value);
25152
25557
  if (error) {
@@ -25162,17 +25567,20 @@ function useDahuaService() {
25162
25567
  mode: value.mode,
25163
25568
  recno: value.recno,
25164
25569
  plateNumber: value.plateNumber,
25165
- start: value.start,
25166
- end: value.end,
25570
+ // On the camera's clock (see `dahuaWallClock`); a time already written
25571
+ // for the camera passes through unchanged.
25572
+ start: dahuaWallClock(value.start, value.timezone) ?? value.start,
25573
+ end: dahuaWallClock(value.end, value.timezone) ?? value.end,
25167
25574
  owner: value.owner,
25168
25575
  openGate: _isOpenGate
25169
25576
  });
25170
25577
  try {
25171
- const response = await useDahuaDigest({
25578
+ const response = await sendCameraCommand({
25172
25579
  host: value.host,
25173
25580
  username: value.username,
25174
25581
  password: value.password,
25175
- endpoint
25582
+ endpoint,
25583
+ label: `update plate on ${value.mode}`
25176
25584
  });
25177
25585
  return response;
25178
25586
  } catch (error2) {
@@ -25195,14 +25603,16 @@ function useDahuaService() {
25195
25603
  throw new BadRequestError47(`Validation error: ${error.message}`);
25196
25604
  }
25197
25605
  try {
25198
- const response = await useDahuaDigest({
25606
+ const response = await sendCameraCommand({
25199
25607
  host: value.host,
25200
25608
  username: value.username,
25201
25609
  password: value.password,
25202
25610
  endpoint: buildPlateRemoveEndpoint({
25203
25611
  recno: value.recno,
25204
25612
  mode: value.mode
25205
- })
25613
+ }),
25614
+ repeatable: false,
25615
+ label: `remove plate from ${value.mode}`
25206
25616
  });
25207
25617
  return anprRevokeOutcome(response);
25208
25618
  } catch (error2) {
@@ -25224,11 +25634,18 @@ function useDahuaService() {
25224
25634
  throw new BadRequestError47(`Validation error: ${error.message}`);
25225
25635
  }
25226
25636
  try {
25227
- const response = await useDahuaDigest({
25637
+ const findEndpoint = `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${encodeURIComponent(value.plateNumber)}`;
25638
+ const response = value.requireOk ? await sendCameraCommand({
25228
25639
  host: value.host,
25229
25640
  username: value.username,
25230
25641
  password: value.password,
25231
- endpoint: `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${encodeURIComponent(value.plateNumber)}`
25642
+ endpoint: findEndpoint,
25643
+ label: `find plate on ${value.mode}`
25644
+ }) : await useDahuaDigest({
25645
+ host: value.host,
25646
+ username: value.username,
25647
+ password: value.password,
25648
+ endpoint: findEndpoint
25232
25649
  });
25233
25650
  if (value.requireOk) {
25234
25651
  const statusCode = getDahuaStatusCode(response);
@@ -25255,7 +25672,8 @@ function useDahuaService() {
25255
25672
  owner: Joi26.string().optional().allow("", null),
25256
25673
  isOpenGate: Joi26.boolean().optional().allow(null),
25257
25674
  vehicleType: Joi26.string().optional().allow("", null),
25258
- vehicleColor: Joi26.string().optional().allow("", null)
25675
+ vehicleColor: Joi26.string().optional().allow("", null),
25676
+ timezone: Joi26.string().optional().allow("", null)
25259
25677
  });
25260
25678
  const { error } = validation.validate(value);
25261
25679
  if (error) {
@@ -25271,17 +25689,20 @@ function useDahuaService() {
25271
25689
  plateNumber: value.plateNumber,
25272
25690
  vehicleType: value.vehicleType,
25273
25691
  vehicleColor: value.vehicleColor,
25274
- start: value.start,
25275
- end: value.end,
25692
+ // On the camera's clock (see `dahuaWallClock`).
25693
+ start: dahuaWallClock(value.start, value.timezone) ?? value.start,
25694
+ end: dahuaWallClock(value.end, value.timezone) ?? value.end,
25276
25695
  owner: value.owner,
25277
25696
  openGate: isOpenGateString
25278
25697
  });
25279
25698
  try {
25280
- const response = await useDahuaDigest({
25699
+ const response = await sendCameraCommand({
25281
25700
  host: value.host,
25282
25701
  username: value.username,
25283
25702
  password: value.password,
25284
- endpoint
25703
+ endpoint,
25704
+ repeatable: false,
25705
+ label: `bulk add plate to ${value.mode}`
25285
25706
  });
25286
25707
  return response;
25287
25708
  } catch (error2) {
@@ -25335,11 +25756,91 @@ function useDahuaService() {
25335
25756
  throw error2;
25336
25757
  }
25337
25758
  }
25759
+ async function setPlateWindow(value) {
25760
+ const validation = Joi26.object({
25761
+ host: Joi26.string().required(),
25762
+ username: Joi26.string().required(),
25763
+ password: Joi26.string().required(),
25764
+ mode: Joi26.string().valid(...Object.values(ANPRMode)).required(),
25765
+ recno: Joi26.string().required(),
25766
+ plateNumber: Joi26.string().required(),
25767
+ owner: Joi26.string().optional().allow("", null),
25768
+ start: Joi26.string().isoDate().optional().allow("", null),
25769
+ end: Joi26.string().isoDate().optional().allow("", null),
25770
+ timezone: Joi26.string().optional().allow("", null)
25771
+ });
25772
+ const { error } = validation.validate(value);
25773
+ if (error) {
25774
+ throw new BadRequestError47(`Validation error: ${error.message}`);
25775
+ }
25776
+ const window = plateWindowOnCamera(value);
25777
+ const endpoint = `/cgi-bin/recordUpdater.cgi?action=update&name=${encodeURIComponent(value.mode)}&recno=${encodeURIComponent(value.recno)}&PlateNumber=${encodeURIComponent(value.plateNumber)}&BeginTime=${encodeURIComponent(window.start)}&CancelTime=${encodeURIComponent(window.end)}&MasterOfCar=${encodeURIComponent(anprOwnerName(value.owner))}`;
25778
+ try {
25779
+ return await sendCameraCommandExpectingOk({
25780
+ host: value.host,
25781
+ username: value.username,
25782
+ password: value.password,
25783
+ endpoint,
25784
+ label: `set plate window on ${value.mode}`
25785
+ });
25786
+ } catch (error2) {
25787
+ loggerDahua.error(
25788
+ `[${value.host}] Error setting plate window: ${error2?.message || error2}`
25789
+ );
25790
+ throw new BadRequestError47(anprWriteReason(error2));
25791
+ }
25792
+ }
25793
+ async function clearPlateList(value) {
25794
+ const validation = Joi26.object({
25795
+ host: Joi26.string().required(),
25796
+ username: Joi26.string().required(),
25797
+ password: Joi26.string().required(),
25798
+ mode: Joi26.string().valid(...Object.values(ANPRMode)).required()
25799
+ });
25800
+ const { error } = validation.validate(value);
25801
+ if (error) {
25802
+ return anprRevokeThrew(`Validation error: ${error.message}`);
25803
+ }
25804
+ loggerDahua.info(`clearPlateList called for host ${value.host} mode ${value.mode}`);
25805
+ try {
25806
+ const response = await sendCameraCommand({
25807
+ host: value.host,
25808
+ username: value.username,
25809
+ password: value.password,
25810
+ endpoint: buildPlateClearEndpoint({ mode: value.mode }),
25811
+ // A long list takes the device a while to empty.
25812
+ timeout: 6e4,
25813
+ deadlineMs: 9e4,
25814
+ label: `clear ${value.mode}`
25815
+ });
25816
+ const outcome = anprRevokeOutcome(response);
25817
+ if (!outcome.ok)
25818
+ return outcome;
25819
+ const text2 = getDahuaResponseText(response);
25820
+ if (!/\bOK\b/i.test(text2)) {
25821
+ loggerDahua.error(
25822
+ `[${value.host}] clearPlateList ${value.mode} answered 200 without OK: ${text2.slice(0, 200)}`
25823
+ );
25824
+ return {
25825
+ ok: false,
25826
+ statusCode: 200,
25827
+ reason: "the camera did not accept the command to clear its list",
25828
+ detail: `the camera answered 200 without OK: ${text2.slice(0, 200)}`
25829
+ };
25830
+ }
25831
+ return outcome;
25832
+ } catch (error2) {
25833
+ loggerDahua.error(`[${value.host}] Error clearing ${value.mode}:`, error2);
25834
+ return anprRevokeThrew(error2);
25835
+ }
25836
+ }
25338
25837
  return {
25339
25838
  getSnapshot,
25340
25839
  listenToCamera,
25341
25840
  addPlateNumber,
25342
25841
  removePlateNumber,
25842
+ clearPlateList,
25843
+ setPlateWindow,
25343
25844
  updatePlateNumber,
25344
25845
  getPlateNumber,
25345
25846
  bulkInsertPlateNumber,
@@ -25781,6 +26282,11 @@ function useSiteCameraRepo() {
25781
26282
  {
25782
26283
  $match: query2
25783
26284
  },
26285
+ // A fixed order, so paging through a site's cameras 20 at a time
26286
+ // cannot skip one camera and return another twice.
26287
+ {
26288
+ $sort: { _id: 1 }
26289
+ },
25784
26290
  {
25785
26291
  $skip: value.page * value.limit
25786
26292
  },
@@ -26743,7 +27249,7 @@ function formatCapabilityTrace(trace) {
26743
27249
  }
26744
27250
 
26745
27251
  // src/services/camera-device-http.service.ts
26746
- import { createHash as createHash2, randomBytes } from "crypto";
27252
+ import { createHash as createHash3, randomBytes } from "crypto";
26747
27253
  var DIGEST_UNAUTHORIZED = 401;
26748
27254
  var DIGEST_FORBIDDEN = 403;
26749
27255
  function parseDigestChallenge(header) {
@@ -26759,7 +27265,7 @@ function parseDigestChallenge(header) {
26759
27265
  return out;
26760
27266
  }
26761
27267
  function md5(input) {
26762
- return createHash2("md5").update(input, "utf8").digest("hex");
27268
+ return createHash3("md5").update(input, "utf8").digest("hex");
26763
27269
  }
26764
27270
  function buildDigestAuthorization(params) {
26765
27271
  const realm = params.challenge.realm ?? "";
@@ -36421,6 +36927,7 @@ import Joi50 from "joi";
36421
36927
  import {
36422
36928
  BadRequestError as BadRequestError90,
36423
36929
  logger as logger66,
36930
+ NotFoundError as NotFoundError25,
36424
36931
  useAtlas as useAtlas54
36425
36932
  } from "@7365admin1/node-server-utils";
36426
36933
  import { ObjectId as ObjectId72 } from "mongodb";
@@ -37632,6 +38139,7 @@ function useVehicleService() {
37632
38139
  getVehicleByPlateNumber: _getVehicleByPlateNumber,
37633
38140
  getAllExpiredVehicles: _getAllExpiredVehicles,
37634
38141
  bulkUpsertVehicles: _bulkUpsertVehicles,
38142
+ getLivePlatesBySite: _getLivePlatesBySite,
37635
38143
  findOne
37636
38144
  } = useVehicleRepo();
37637
38145
  const {
@@ -37642,12 +38150,441 @@ function useVehicleService() {
37642
38150
  bulkInsertPlateNumber: _bulkInsertPlateNumber
37643
38151
  } = useDahuaService();
37644
38152
  const { getAllCameraWithPassword: _getAllSiteCameras } = useSiteCameraRepo();
38153
+ const { setPlateWindow: _setPlateWindow } = useDahuaService();
37645
38154
  const { getById: _getById } = useOrgRepo();
37646
38155
  const {
37647
38156
  pushVehicleById: _pushVehicleById,
37648
38157
  pullVehicleByRecNo: _pullVehicleByRecNo
37649
38158
  } = usePersonRepo();
37650
38159
  const { getUnitByBlockLevelUnitNumber: _getUnitByBlockLevelUnitNumber } = useBuildingUnitRepo();
38160
+ async function camerasAt(site, options = {}) {
38161
+ const siteId = String(site ?? "");
38162
+ if (!siteId)
38163
+ return [];
38164
+ const cameras = [];
38165
+ let page = 1;
38166
+ let pages = 1;
38167
+ const limit = 20;
38168
+ do {
38169
+ const siteCameraReq = await _getAllSiteCameras({
38170
+ site: siteId,
38171
+ type: "anpr",
38172
+ ...options.directions ? { direction: options.directions } : {},
38173
+ ...options.activeOnly ? { status: "active" } : {},
38174
+ page,
38175
+ limit
38176
+ });
38177
+ pages = siteCameraReq.pages || 1;
38178
+ cameras.push(...siteCameraReq.items);
38179
+ page++;
38180
+ } while (page <= pages);
38181
+ return cameras;
38182
+ }
38183
+ function allowCamerasAt(site) {
38184
+ return camerasAt(site, {
38185
+ directions: ALLOW_CAMERA_DIRECTIONS,
38186
+ activeOnly: true
38187
+ });
38188
+ }
38189
+ function blockCamerasAt(site) {
38190
+ return camerasAt(site, {
38191
+ directions: BLOCK_CAMERA_DIRECTIONS,
38192
+ activeOnly: true
38193
+ });
38194
+ }
38195
+ function listOf(mode) {
38196
+ return mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */ ? "blocked" : "allowed";
38197
+ }
38198
+ function listMode(type) {
38199
+ return type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
38200
+ }
38201
+ function validDate(value) {
38202
+ if (value === null || value === void 0 || value === "")
38203
+ return null;
38204
+ const date = new Date(value);
38205
+ return Number.isNaN(date.getTime()) ? null : date;
38206
+ }
38207
+ function cameraWindow(start, end) {
38208
+ const startDate = validDate(start);
38209
+ const endDate = validDate(end);
38210
+ return {
38211
+ ...startDate ? { start: startDate.toISOString() } : {},
38212
+ ...endDate ? { end: endDate.toISOString() } : {}
38213
+ };
38214
+ }
38215
+ const siteZones = /* @__PURE__ */ new Map();
38216
+ async function siteTimezone(site) {
38217
+ const key = String(site ?? "");
38218
+ const cached = siteZones.get(key);
38219
+ if (cached && Date.now() - cached.at < 5 * 6e4)
38220
+ return cached.zone;
38221
+ let zone = resolveSiteTimezone(null);
38222
+ if (key) {
38223
+ try {
38224
+ const found = await useSiteRepo().getSiteById(key);
38225
+ zone = resolveSiteTimezone(found);
38226
+ } catch {
38227
+ zone = resolveSiteTimezone(null);
38228
+ }
38229
+ }
38230
+ siteZones.set(key, { zone, at: Date.now() });
38231
+ return zone;
38232
+ }
38233
+ function blockWindow(vehicleEnd) {
38234
+ const now = /* @__PURE__ */ new Date();
38235
+ let end = validDate(vehicleEnd);
38236
+ if (!end || end.getTime() <= now.getTime()) {
38237
+ end = new Date(now);
38238
+ end.setFullYear(end.getFullYear() + 10);
38239
+ }
38240
+ return { start: now.toISOString(), end: end.toISOString() };
38241
+ }
38242
+ async function findPlateOnCamera(camera, mode, plateNumber) {
38243
+ const text2 = await _getPlateNumber({
38244
+ host: camera.host,
38245
+ username: camera.username,
38246
+ password: camera.password,
38247
+ mode,
38248
+ plateNumber,
38249
+ requireOk: true
38250
+ });
38251
+ return matchingPlateRecords(text2 ?? "", plateNumber);
38252
+ }
38253
+ async function removePlateFromCamera(camera, mode, plateNumber) {
38254
+ if (!normalisePlate(plateNumber)) {
38255
+ return { ...ANPR_NOTHING_TO_REVOKE, changed: false };
38256
+ }
38257
+ let changed = false;
38258
+ const MAX_PASSES = 3;
38259
+ for (let pass = 0; pass <= MAX_PASSES; pass++) {
38260
+ let records;
38261
+ try {
38262
+ records = await findPlateOnCamera(camera, mode, plateNumber);
38263
+ } catch (error) {
38264
+ const outcome = anprRevokeThrew(error);
38265
+ logCameraFailure("plate lookup", camera, outcome.detail);
38266
+ return { ...outcome, changed };
38267
+ }
38268
+ if (!records.length)
38269
+ return { ok: true, reason: "", changed };
38270
+ if (pass === MAX_PASSES)
38271
+ break;
38272
+ for (const record of records) {
38273
+ const outcome = await _removePlateNumber({
38274
+ host: camera.host,
38275
+ username: camera.username,
38276
+ password: camera.password,
38277
+ recno: record.recNo,
38278
+ mode
38279
+ }).catch((error) => anprRevokeThrew(error));
38280
+ if (!outcome?.ok) {
38281
+ logCameraFailure(
38282
+ "plate remove",
38283
+ camera,
38284
+ outcome?.detail ?? "no outcome returned"
38285
+ );
38286
+ return {
38287
+ ...outcome ?? anprRevokeThrew("no outcome returned"),
38288
+ changed
38289
+ };
38290
+ }
38291
+ changed = true;
38292
+ }
38293
+ }
38294
+ logCameraFailure(
38295
+ "plate remove",
38296
+ camera,
38297
+ `plate still listed after ${MAX_PASSES} rounds of removal`
38298
+ );
38299
+ return {
38300
+ ok: false,
38301
+ reason: "the camera did not remove the plate number",
38302
+ detail: `plate still listed after ${MAX_PASSES} rounds of removal`,
38303
+ changed
38304
+ };
38305
+ }
38306
+ async function insertPlateOnCamera(camera, write) {
38307
+ const response = await _addPlateNumber({
38308
+ host: camera.host,
38309
+ username: camera.username,
38310
+ password: camera.password,
38311
+ plateNumber: write.plateNumber,
38312
+ mode: write.mode,
38313
+ owner: write.owner,
38314
+ ...write.start ? { start: write.start } : {},
38315
+ ...write.end ? { end: write.end } : {},
38316
+ timezone: await siteTimezone(camera.site)
38317
+ });
38318
+ const status = response?.statusCode ?? response?.status ?? response?.res?.status;
38319
+ if (status !== 200)
38320
+ throw new Error(`HTTP ${status ?? "none"}`);
38321
+ return String(
38322
+ response?.data?.toString("utf-8")?.split("=")[1]?.trim() ?? ""
38323
+ );
38324
+ }
38325
+ async function ensurePlateOnCamera(camera, write) {
38326
+ const existing = await findPlateOnCamera(
38327
+ camera,
38328
+ write.mode,
38329
+ write.plateNumber
38330
+ );
38331
+ if (!existing.length) {
38332
+ await insertPlateOnCamera(camera, write);
38333
+ return { inserted: true };
38334
+ }
38335
+ const timezone = await siteTimezone(camera.site);
38336
+ const [keep, ...extra] = existing;
38337
+ const now = dahuaWallClock(/* @__PURE__ */ new Date(), timezone);
38338
+ const wanted = {
38339
+ start: dahuaWallClock(write.start, timezone),
38340
+ end: dahuaWallClock(write.end, timezone)
38341
+ };
38342
+ const windowIsRight = wanted.start && wanted.end ? plateWindowMatches(keep, wanted) : (
38343
+ // No dates of its own: any copy that lets it in now will do.
38344
+ Boolean(
38345
+ keep.beginTime && keep.cancelTime && now && (dahuaWallClock(keep.beginTime) ?? "") <= now && (dahuaWallClock(keep.cancelTime) ?? "") > now
38346
+ )
38347
+ );
38348
+ if (!windowIsRight) {
38349
+ await _setPlateWindow({
38350
+ host: camera.host,
38351
+ username: camera.username,
38352
+ password: camera.password,
38353
+ mode: write.mode,
38354
+ recno: keep.recNo,
38355
+ plateNumber: write.plateNumber,
38356
+ owner: write.owner,
38357
+ ...write.start ? { start: write.start } : {},
38358
+ ...write.end ? { end: write.end } : {},
38359
+ timezone
38360
+ });
38361
+ }
38362
+ for (const copy of extra) {
38363
+ const outcome = await _removePlateNumber({
38364
+ host: camera.host,
38365
+ username: camera.username,
38366
+ password: camera.password,
38367
+ recno: copy.recNo,
38368
+ mode: write.mode
38369
+ }).catch((error) => anprRevokeThrew(error));
38370
+ if (!outcome?.ok) {
38371
+ throw new Error(
38372
+ outcome?.reason || "the camera did not remove an old copy of the plate"
38373
+ );
38374
+ }
38375
+ }
38376
+ return { inserted: false };
38377
+ }
38378
+ function cameraJournal() {
38379
+ const steps = [];
38380
+ return {
38381
+ /** A plate was inserted; undo = take it off again. */
38382
+ inserted(camera, mode, plateNumber) {
38383
+ steps.push({
38384
+ camera,
38385
+ undo: () => removePlateFromCamera(camera, mode, plateNumber).then(
38386
+ (outcome) => outcome.ok,
38387
+ () => false
38388
+ )
38389
+ });
38390
+ },
38391
+ /** A plate was removed; undo = put it back as it was. */
38392
+ removed(camera, previous) {
38393
+ steps.push({
38394
+ camera,
38395
+ undo: () => insertPlateOnCamera(camera, previous).then(
38396
+ () => true,
38397
+ () => false
38398
+ )
38399
+ });
38400
+ },
38401
+ /**
38402
+ * Undo everything, newest first. Never throws. Returns the cameras that
38403
+ * could not be put back, so the operator can be told which barrier is
38404
+ * now wrong.
38405
+ */
38406
+ async rollback() {
38407
+ const stranded = [];
38408
+ for (const step of steps.reverse()) {
38409
+ if (!await step.undo())
38410
+ stranded.push(cameraLabel(step.camera));
38411
+ }
38412
+ steps.length = 0;
38413
+ return Array.from(new Set(stranded));
38414
+ }
38415
+ };
38416
+ }
38417
+ function strandedWarning(stranded) {
38418
+ if (!stranded.length)
38419
+ return "";
38420
+ return ` WARNING: ${stranded.join(", ")} could not be put back the way ${stranded.length === 1 ? "it was" : "they were"} - check this plate on ${stranded.length === 1 ? "that camera" : "those cameras"} by hand.`;
38421
+ }
38422
+ async function rollbackCameras(journal, error) {
38423
+ const stranded = await journal.rollback().catch(() => []);
38424
+ if (stranded.length) {
38425
+ logger66.error(
38426
+ `vehicle camera rollback could not restore: ${stranded.join(", ")}`
38427
+ );
38428
+ if (error && typeof error === "object") {
38429
+ error.message = `${error.message ?? ""}${strandedWarning(stranded)}`;
38430
+ }
38431
+ }
38432
+ }
38433
+ async function revokableModes(site, plateNumber, exclude) {
38434
+ const others = [];
38435
+ let siteId = null;
38436
+ try {
38437
+ siteId = new ObjectId72(String(site));
38438
+ } catch {
38439
+ siteId = null;
38440
+ }
38441
+ if (siteId && plateNumber) {
38442
+ const excludeIds = exclude.map((id) => {
38443
+ try {
38444
+ return new ObjectId72(String(id));
38445
+ } catch {
38446
+ return null;
38447
+ }
38448
+ }).filter((id) => id !== null);
38449
+ const base = {
38450
+ site: siteId,
38451
+ plateNumber,
38452
+ status: "active" /* ACTIVE */,
38453
+ _id: { $nin: excludeIds }
38454
+ };
38455
+ const [allowHolder, blockHolder] = await Promise.all([
38456
+ findOne({
38457
+ ...base,
38458
+ $or: [
38459
+ { type: "whitelist" /* WHITELIST */ },
38460
+ { blockedFrom: "whitelist" /* WHITELIST */ }
38461
+ ]
38462
+ }),
38463
+ findOne({ ...base, type: "blocklist" /* BLOCKLIST */ })
38464
+ ]);
38465
+ if (allowHolder)
38466
+ others.push(allowHolder);
38467
+ if (blockHolder)
38468
+ others.push(blockHolder);
38469
+ }
38470
+ const lists = listsToRevoke(others);
38471
+ return [
38472
+ ...lists.allow ? ["TrafficRedList" /* TRAFFIC_REDLIST */] : [],
38473
+ ...lists.block ? ["TrafficBlackList" /* TRAFFIC_BLACKLIST */] : []
38474
+ ];
38475
+ }
38476
+ async function removeFromCameras(cameras, modes, plateNumber, previous, journal, vehicleIsBlocked = false) {
38477
+ const failures = [];
38478
+ for (const camera of cameras) {
38479
+ for (const mode of modes) {
38480
+ if (!mustRevokeFrom(camera.direction, listOf(mode), vehicleIsBlocked)) {
38481
+ continue;
38482
+ }
38483
+ const outcome = await removePlateFromCamera(camera, mode, plateNumber);
38484
+ if (outcome.changed) {
38485
+ journal.removed(camera, { ...previous, plateNumber, mode });
38486
+ }
38487
+ if (!outcome.ok) {
38488
+ failures.push({
38489
+ camera: cameraLabel(camera),
38490
+ reason: outcome.reason || "the camera could not be reached"
38491
+ });
38492
+ }
38493
+ }
38494
+ }
38495
+ return failures;
38496
+ }
38497
+ async function writeToCameras(cameras, write, journal, options = {}) {
38498
+ const failures = [];
38499
+ let recNo = "";
38500
+ for (const camera of cameras) {
38501
+ try {
38502
+ if (options.onlyIfMissing) {
38503
+ const { inserted } = await ensurePlateOnCamera(camera, write);
38504
+ if (inserted)
38505
+ journal.inserted(camera, write.mode, write.plateNumber);
38506
+ } else {
38507
+ recNo = await insertPlateOnCamera(camera, write) || recNo;
38508
+ journal.inserted(camera, write.mode, write.plateNumber);
38509
+ }
38510
+ } catch (error) {
38511
+ logCameraFailure(`write ${write.mode}`, camera, error);
38512
+ failures.push({
38513
+ camera: cameraLabel(camera),
38514
+ reason: anprWriteReason(error)
38515
+ });
38516
+ }
38517
+ }
38518
+ return { failures, recNo };
38519
+ }
38520
+ async function blockOnCameras(vehicle, site, owner, journal) {
38521
+ const plateNumber = String(vehicle.plateNumber ?? "");
38522
+ if (!plateNumber)
38523
+ return;
38524
+ const cameras = await blockCamerasAt(site);
38525
+ if (!cameras.length) {
38526
+ throw new BadRequestError90(anprNoCameraRefusal("blocked"));
38527
+ }
38528
+ const { failures } = await writeToCameras(
38529
+ cameras,
38530
+ {
38531
+ plateNumber,
38532
+ mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
38533
+ owner,
38534
+ ...blockWindow(vehicle.end)
38535
+ },
38536
+ journal,
38537
+ { onlyIfMissing: true }
38538
+ );
38539
+ if (failures.length) {
38540
+ throw new BadRequestError90(anprBlockFailureMessage(failures));
38541
+ }
38542
+ }
38543
+ async function unblockOnCameras(vehicle, site, options, journal) {
38544
+ const plateNumber = String(vehicle.plateNumber ?? "");
38545
+ if (!plateNumber)
38546
+ return;
38547
+ const window = cameraWindow(vehicle.start, vehicle.end);
38548
+ if (options.restore) {
38549
+ const allowCameras = await allowCamerasAt(site);
38550
+ const { failures: failures2 } = await writeToCameras(
38551
+ allowCameras,
38552
+ {
38553
+ plateNumber,
38554
+ mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
38555
+ owner: options.owner,
38556
+ ...window
38557
+ },
38558
+ journal,
38559
+ { onlyIfMissing: true }
38560
+ );
38561
+ if (failures2.length) {
38562
+ throw new BadRequestError90(
38563
+ cameraFailureSentence({
38564
+ failures: failures2,
38565
+ operation: "added to",
38566
+ outcome: "unblocked",
38567
+ consequence: "it is still blocked at the barrier"
38568
+ })
38569
+ );
38570
+ }
38571
+ }
38572
+ const revokable = await revokableModes(site, plateNumber, [vehicle._id]);
38573
+ const modes = options.restore ? revokable.filter((mode) => mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */) : revokable;
38574
+ if (!modes.length)
38575
+ return;
38576
+ const failures = await removeFromCameras(
38577
+ await camerasAt(site, { activeOnly: true }),
38578
+ modes,
38579
+ plateNumber,
38580
+ { owner: options.owner, ...window },
38581
+ journal,
38582
+ true
38583
+ );
38584
+ if (failures.length) {
38585
+ throw new BadRequestError90(anprUnblockFailureMessage(failures));
38586
+ }
38587
+ }
37651
38588
  async function add(value, session, user) {
37652
38589
  const isExternalSession = !!session;
37653
38590
  if (!session) {
@@ -37702,18 +38639,18 @@ function useVehicleService() {
37702
38639
  endDate.setFullYear(endDate.getFullYear() + 10);
37703
38640
  start = startDate.toISOString();
37704
38641
  end = endDate.toISOString();
37705
- startDateDahua = formatDahuaDate(startDate);
37706
- endDateDahua = formatDahuaDate(endDate);
38642
+ startDateDahua = start;
38643
+ endDateDahua = end;
37707
38644
  } else {
37708
38645
  if (hasStart) {
37709
38646
  startDate = new Date(value.start);
37710
38647
  start = startDate.toISOString();
37711
- startDateDahua = formatDahuaDate(startDate);
38648
+ startDateDahua = start;
37712
38649
  }
37713
38650
  if (hasEnd) {
37714
38651
  endDate = new Date(value.end);
37715
38652
  end = endDate.toISOString();
37716
- endDateDahua = formatDahuaDate(endDate);
38653
+ endDateDahua = end;
37717
38654
  }
37718
38655
  }
37719
38656
  const owner = value.name;
@@ -37726,22 +38663,7 @@ function useVehicleService() {
37726
38663
  let message = "Vehicle plate number needs approval from property management.";
37727
38664
  let siteCameras = [];
37728
38665
  if (value.status && value.status !== "pending" /* PENDING */) {
37729
- let page = 1;
37730
- let pages = 1;
37731
- const limit = 20;
37732
- do {
37733
- const siteCameraReq = await _getAllSiteCameras({
37734
- site: value.site,
37735
- type: "anpr",
37736
- direction: ["both", "entry", "residents"],
37737
- page,
37738
- limit,
37739
- status: "active"
37740
- });
37741
- pages = siteCameraReq.pages || 1;
37742
- siteCameras.push(...siteCameraReq.items);
37743
- page++;
37744
- } while (page <= pages);
38666
+ siteCameras = isBlocklist ? await blockCamerasAt(value.site) : await allowCamerasAt(value.site);
37745
38667
  logger66.info(
37746
38668
  `addVehicle resolved ${siteCameras.length} site camera(s) for site ${value?.site}`
37747
38669
  );
@@ -37776,7 +38698,10 @@ function useVehicleService() {
37776
38698
  mode: _mode,
37777
38699
  owner,
37778
38700
  ...startDateDahua ? { start: startDateDahua } : {},
37779
- ...endDateDahua ? { end: endDateDahua } : {}
38701
+ ...endDateDahua ? { end: endDateDahua } : {},
38702
+ // The camera enforces the season pass window on its own clock,
38703
+ // in the site's zone.
38704
+ timezone: await siteTimezone(camera.site ?? value.site)
37780
38705
  };
37781
38706
  try {
37782
38707
  const dahuaResponse = await _addPlateNumber(dahuaPayload);
@@ -37796,8 +38721,7 @@ function useVehicleService() {
37796
38721
  }
37797
38722
  const recNo = String(responseData.split("=")[1]?.trim() ?? "");
37798
38723
  vehicleValue.recNo = recNo;
37799
- if (recNo)
37800
- written.push({ camera, recno: recNo, mode: _mode });
38724
+ written.push({ camera, recno: recNo, mode: _mode, plateNumber: plateNumber2 });
37801
38725
  if (value.peopleId && recNo) {
37802
38726
  await _pushVehicleById(
37803
38727
  value.peopleId,
@@ -37827,13 +38751,18 @@ function useVehicleService() {
37827
38751
  } catch (error) {
37828
38752
  const stranded = [];
37829
38753
  for (const entry of written) {
37830
- const outcome = await _removePlateNumber({
38754
+ const usableId = entry.recno && /^\d+$/.test(entry.recno);
38755
+ const outcome = usableId ? await _removePlateNumber({
37831
38756
  host: entry.camera.host,
37832
38757
  username: entry.camera.username,
37833
38758
  password: entry.camera.password,
37834
38759
  recno: entry.recno,
37835
38760
  mode: entry.mode
37836
- }).catch((removeError) => anprRevokeThrew(removeError));
38761
+ }).catch((removeError) => anprRevokeThrew(removeError)) : await removePlateFromCamera(
38762
+ entry.camera,
38763
+ entry.mode,
38764
+ entry.plateNumber
38765
+ );
37837
38766
  if (!outcome?.ok)
37838
38767
  stranded.push(cameraLabel(entry.camera));
37839
38768
  }
@@ -37859,80 +38788,28 @@ function useVehicleService() {
37859
38788
  if (!session) {
37860
38789
  throw new Error("Unable to start session for vehicle service.");
37861
38790
  }
37862
- const _mode = type !== "whitelist" /* WHITELIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
38791
+ const journal = cameraJournal();
37863
38792
  try {
37864
38793
  session.startTransaction();
37865
- const siteCameras = [];
37866
- let page = 1;
37867
- let pages = 1;
37868
- const limit = 20;
37869
- do {
37870
- const siteCameraReq = await _getAllSiteCameras({
37871
- site,
37872
- type: "anpr",
37873
- page,
37874
- limit
37875
- });
37876
- pages = siteCameraReq.pages || 1;
37877
- siteCameras.push(...siteCameraReq.items);
37878
- page++;
37879
- } while (page <= pages);
37880
- logger66.info(
37881
- `deleteVehicle resolved ${siteCameras.length} ANPR camera(s) for site ${site}`
37882
- );
37883
38794
  const vehicle = await findOne({ _id: new ObjectId72(_id) });
37884
38795
  const plateNumber = String(vehicle?.plateNumber ?? "");
37885
- const failures = [];
37886
- for (const camera of siteCameras) {
37887
- const host = camera.host;
37888
- const username = camera.username;
37889
- const password = camera.password;
37890
- const label = cameraLabel(camera);
37891
- let recnoForCamera = recno;
37892
- if (plateNumber) {
37893
- try {
37894
- const found = parseDahuaFind(
37895
- await _getPlateNumber({
37896
- host,
37897
- username,
37898
- password,
37899
- mode: _mode,
37900
- plateNumber,
37901
- requireOk: true
37902
- }) ?? ""
37903
- );
37904
- if (!found.exists)
37905
- continue;
37906
- if (found.recNo)
37907
- recnoForCamera = found.recNo;
37908
- } catch (error) {
37909
- const outcome2 = anprRevokeThrew(error);
37910
- logCameraFailure("deleteVehicle lookup", camera, outcome2.detail);
37911
- failures.push({ camera: label, reason: outcome2.reason });
37912
- continue;
37913
- }
37914
- }
37915
- if (!recnoForCamera)
37916
- continue;
37917
- const outcome = await _removePlateNumber({
37918
- host,
37919
- username,
37920
- password,
37921
- recno: recnoForCamera,
37922
- mode: _mode
37923
- });
37924
- if (!outcome?.ok) {
37925
- logCameraFailure(
37926
- "deleteVehicle revoke",
37927
- camera,
37928
- outcome?.detail ?? "no outcome returned"
37929
- );
37930
- failures.push({
37931
- camera: label,
37932
- reason: outcome?.reason || "the camera could not be reached"
37933
- });
37934
- }
37935
- }
38796
+ const cameraSite = vehicle?.site ?? site;
38797
+ const siteCameras = await camerasAt(cameraSite, { activeOnly: true });
38798
+ logger66.info(
38799
+ `deleteVehicle resolved ${siteCameras.length} ANPR camera(s) for site ${cameraSite}`
38800
+ );
38801
+ const modes = plateNumber ? await revokableModes(cameraSite, plateNumber, [_id]) : [];
38802
+ const failures = await removeFromCameras(
38803
+ siteCameras,
38804
+ modes,
38805
+ plateNumber,
38806
+ {
38807
+ owner: String(vehicle?.name ?? ""),
38808
+ ...cameraWindow(vehicle?.start, vehicle?.end)
38809
+ },
38810
+ journal,
38811
+ vehicle?.type === "blocklist" /* BLOCKLIST */
38812
+ );
37936
38813
  if (failures.length) {
37937
38814
  const message = anprRevokeFailureMessage(failures);
37938
38815
  if (!bypass)
@@ -37942,13 +38819,14 @@ function useVehicleService() {
37942
38819
  await _deleteVehicle(_id, session);
37943
38820
  await _pullVehicleByRecNo(
37944
38821
  recno,
37945
- { peopleId: vehicle?.peopleId, site, plateNumber },
38822
+ { peopleId: vehicle?.peopleId, site: cameraSite, plateNumber },
37946
38823
  session
37947
38824
  );
37948
38825
  await session.commitTransaction();
37949
38826
  return `Vehicle plate number deleted from ${type} record successfully.`;
37950
38827
  } catch (error) {
37951
38828
  logger66.error("Error in vehicle service delete:", error);
38829
+ await rollbackCameras(journal, error);
37952
38830
  await session.abortTransaction();
37953
38831
  throw error;
37954
38832
  } finally {
@@ -37963,13 +38841,14 @@ function useVehicleService() {
37963
38841
  const org = await _getById(orgId);
37964
38842
  if (!org)
37965
38843
  throw new BadRequestError90("Org not found");
37966
- const vehicle = await _getVehicleById(id);
37967
- const plate = vehicle.plates.find((p) => p._id.toString() === id);
37968
- const _plateNumber = plate?.plateNumber;
38844
+ const vehicle = await findOne({ _id: new ObjectId72(id) });
37969
38845
  if (!vehicle) {
37970
- throw new BadRequestError90("Vehicle not found");
38846
+ throw new NotFoundError25("Vehicle not found.");
37971
38847
  }
38848
+ const _plateNumber = String(vehicle.plateNumber ?? "");
37972
38849
  const owner = vehicle.name;
38850
+ const isBlocklist = vehicle.type === "blocklist" /* BLOCKLIST */;
38851
+ const mode = listMode(vehicle.type);
37973
38852
  const hasStart = typeof vehicle.start === "string" ? vehicle.start.trim() !== "" : !!vehicle.start;
37974
38853
  const hasEnd = typeof vehicle.end === "string" ? vehicle.end.trim() !== "" : !!vehicle.end;
37975
38854
  let startDate = null;
@@ -37987,70 +38866,48 @@ function useVehicleService() {
37987
38866
  endDate.setFullYear(endDate.getFullYear() + 10);
37988
38867
  }
37989
38868
  }
37990
- const startDahua = startDate ? formatDahuaDate(startDate) : "";
37991
- const endDahua = endDate ? formatDahuaDate(endDate) : "";
38869
+ const startDahua = startDate ? startDate.toISOString() : "";
38870
+ const endDahua = endDate ? endDate.toISOString() : "";
38871
+ const journal = cameraJournal();
37992
38872
  try {
37993
38873
  session.startTransaction();
37994
- const siteCameras = [];
37995
- let page = 1;
37996
- let pages = 1;
37997
- const limit = 20;
37998
- let value = {};
37999
- do {
38000
- const siteCameraReq = await _getAllSiteCameras({
38001
- site: siteId,
38002
- type: "anpr",
38003
- direction: ["both", "entry", "residents"],
38004
- page,
38005
- limit
38006
- });
38007
- pages = siteCameraReq.pages || 1;
38008
- siteCameras.push(...siteCameraReq.items);
38009
- page++;
38010
- } while (page <= pages);
38874
+ const cameraSite = vehicle.site ?? siteId;
38875
+ const siteCameras = isBlocklist ? await blockCamerasAt(cameraSite) : await allowCamerasAt(cameraSite);
38011
38876
  if (!siteCameras.length) {
38012
38877
  throw new BadRequestError90(anprNoCameraRefusal("approved"));
38013
38878
  }
38014
- for (const camera of siteCameras) {
38015
- const { host, username, password } = camera;
38016
- const dahuaPayload = {
38017
- host,
38018
- username,
38019
- password,
38879
+ const { failures, recNo } = await writeToCameras(
38880
+ siteCameras,
38881
+ {
38020
38882
  plateNumber: _plateNumber,
38021
- mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
38883
+ mode,
38884
+ owner,
38022
38885
  start: startDahua,
38023
- end: endDahua,
38024
- owner
38025
- };
38026
- const dahuaResponse = await _addPlateNumber(dahuaPayload);
38027
- if (dahuaResponse?.statusCode != 200) {
38028
- logCameraFailure(
38029
- "approveVehicleById",
38030
- camera,
38031
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38032
- );
38033
- throw new BadRequestError90(
38034
- anprWriteFailureMessage([
38035
- {
38036
- camera: cameraLabel(camera),
38037
- reason: anprWriteReason(
38038
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38039
- )
38040
- }
38041
- ])
38042
- );
38043
- }
38044
- const responseData = dahuaResponse?.data.toString("utf-8");
38045
- value.recNo = responseData.split("=")[1]?.trim();
38886
+ end: endDahua
38887
+ },
38888
+ journal
38889
+ );
38890
+ if (failures.length) {
38891
+ throw new BadRequestError90(
38892
+ cameraFailureSentence({
38893
+ failures,
38894
+ operation: "added to",
38895
+ outcome: "approved",
38896
+ consequence: isBlocklist ? "it would still open the barrier" : "it would not open the barrier"
38897
+ })
38898
+ );
38046
38899
  }
38047
- value.status = "active" /* ACTIVE */;
38048
- if (vehicle.peopleId && value.recNo) {
38900
+ const value = {
38901
+ status: "active" /* ACTIVE */,
38902
+ // Kept for older readers only - one camera's number, never replayed.
38903
+ ...recNo ? { recNo } : {}
38904
+ };
38905
+ if (vehicle.peopleId && _plateNumber) {
38049
38906
  await _pushVehicleById(
38050
38907
  vehicle.peopleId,
38051
38908
  {
38052
38909
  plateNumber: _plateNumber,
38053
- recNo: value.recNo
38910
+ recNo
38054
38911
  },
38055
38912
  session
38056
38913
  );
@@ -38065,6 +38922,7 @@ function useVehicleService() {
38065
38922
  return "Vehicle plate number approved and added successfully.";
38066
38923
  } catch (error) {
38067
38924
  logger66.error("Error in vehicle service approving and adding:", error);
38925
+ await rollbackCameras(journal, error);
38068
38926
  await session.abortTransaction();
38069
38927
  throw error;
38070
38928
  } finally {
@@ -38073,71 +38931,38 @@ function useVehicleService() {
38073
38931
  }
38074
38932
  async function processDeletingExpiredVehicles() {
38075
38933
  const vehicles = await _getAllExpiredVehicles();
38934
+ const sweptIds = vehicles.map((vehicle) => vehicle?._id);
38935
+ const modesByVehicle = /* @__PURE__ */ new Map();
38076
38936
  const { deletableIds, skipped } = await sweepExpiredVehicles(vehicles, {
38077
- camerasForSite: async (site) => {
38078
- const cameras = [];
38079
- let page = 1;
38080
- let pages = 1;
38081
- const limit = 20;
38082
- do {
38083
- const siteCameraReq = await _getAllSiteCameras({
38084
- site,
38085
- type: "anpr",
38086
- page,
38087
- limit
38088
- });
38089
- pages = siteCameraReq.pages || 1;
38090
- cameras.push(...siteCameraReq.items);
38091
- page++;
38092
- } while (page <= pages);
38093
- return cameras;
38094
- },
38937
+ // Every ACTIVE ANPR camera at the site (an inactive one is skipped, as
38938
+ // by every add and update); which of them each list is taken off is
38939
+ // `mustRevokeFrom`.
38940
+ camerasForSite: (site) => camerasAt(site, { activeOnly: true }),
38095
38941
  revoke: async (camera, vehicle) => {
38096
- const host = camera.host;
38097
- const username = camera.username;
38098
- const password = camera.password;
38099
- const mode = vehicle?.type !== "whitelist" /* WHITELIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
38100
38942
  const plateNumber = String(vehicle?.plateNumber ?? "");
38101
- let recnoForCamera = String(vehicle?.recNo ?? "");
38102
- if (plateNumber) {
38103
- try {
38104
- const found = parseDahuaFind(
38105
- await _getPlateNumber({
38106
- host,
38107
- username,
38108
- password,
38109
- mode,
38110
- plateNumber,
38111
- requireOk: true
38112
- }) ?? ""
38113
- );
38114
- if (!found.exists)
38115
- return ANPR_NOTHING_TO_REVOKE;
38116
- if (found.recNo)
38117
- recnoForCamera = found.recNo;
38118
- } catch (error) {
38119
- const outcome2 = anprRevokeThrew(error);
38120
- logCameraFailure("expiredVehicle lookup", camera, outcome2.detail);
38121
- return outcome2;
38122
- }
38123
- }
38124
- if (!recnoForCamera)
38943
+ if (!plateNumber)
38125
38944
  return ANPR_NOTHING_TO_REVOKE;
38126
- const outcome = await _removePlateNumber({
38127
- host,
38128
- username,
38129
- password,
38130
- recno: recnoForCamera,
38131
- mode
38132
- }).catch((error) => anprRevokeThrew(error));
38133
- if (!outcome?.ok) {
38134
- logCameraFailure(
38135
- "expiredVehicle revoke",
38136
- camera,
38137
- outcome?.detail ?? "no outcome returned"
38138
- );
38945
+ const key = String(vehicle?._id ?? plateNumber);
38946
+ let modes = modesByVehicle.get(key);
38947
+ if (!modes) {
38948
+ modes = revokableModes(vehicle?.site, plateNumber, sweptIds);
38949
+ modesByVehicle.set(key, modes);
38950
+ }
38951
+ let resolved;
38952
+ try {
38953
+ resolved = await modes;
38954
+ } catch (error) {
38955
+ return anprRevokeThrew(error);
38956
+ }
38957
+ for (const mode of resolved) {
38958
+ const blocked = vehicle?.type === "blocklist" /* BLOCKLIST */;
38959
+ if (!mustRevokeFrom(camera.direction, listOf(mode), blocked))
38960
+ continue;
38961
+ const outcome = await removePlateFromCamera(camera, mode, plateNumber);
38962
+ if (!outcome.ok)
38963
+ return outcome;
38139
38964
  }
38140
- return outcome ?? anprRevokeThrew("no outcome returned");
38965
+ return ANPR_NOTHING_TO_REVOKE;
38141
38966
  },
38142
38967
  cameraLabel
38143
38968
  });
@@ -38164,90 +38989,83 @@ function useVehicleService() {
38164
38989
  "Only property management can reactivate vehicles."
38165
38990
  );
38166
38991
  }
38167
- const vehicle = await _getVehicleById(id);
38168
- const plate = vehicle.plates.find((p) => p._id.toString() === id);
38169
- const _plateNumber = plate?.plateNumber;
38992
+ const vehicle = await findOne({ _id: new ObjectId72(id) });
38170
38993
  if (!vehicle) {
38171
- throw new BadRequestError90("Vehicle not found");
38994
+ throw new NotFoundError25("Vehicle not found.");
38172
38995
  }
38996
+ const _plateNumber = String(vehicle.plateNumber ?? "");
38173
38997
  const owner = vehicle.name;
38174
- let startDate = /* @__PURE__ */ new Date();
38175
- let endDate = new Date(startDate);
38176
- endDate.setFullYear(endDate.getFullYear() + 10);
38177
- const startIso = startDate ? startDate.toISOString() : "";
38178
- const endIso = endDate ? endDate.toISOString() : "";
38179
- const startDahua = startDate ? formatDahuaDate(startDate) : "";
38180
- const endDahua = endDate ? formatDahuaDate(endDate) : "";
38998
+ const isBlocklist = vehicle.type === "blocklist" /* BLOCKLIST */;
38999
+ const mode = listMode(vehicle.type);
39000
+ const ownStart = validDate(vehicle.start);
39001
+ const ownEnd = validDate(vehicle.end);
39002
+ let startDate;
39003
+ let endDate;
39004
+ if (ownEnd) {
39005
+ if (ownEnd.getTime() <= Date.now()) {
39006
+ throw new BadRequestError90(
39007
+ `This vehicle's expiry date (${dahuaWallClock(
39008
+ ownEnd,
39009
+ await siteTimezone(vehicle.site ?? siteId)
39010
+ )}) has already passed, so it cannot be restored as it is. Add it again with new dates.`
39011
+ );
39012
+ }
39013
+ startDate = ownStart ?? /* @__PURE__ */ new Date();
39014
+ endDate = ownEnd;
39015
+ } else {
39016
+ startDate = ownStart ?? /* @__PURE__ */ new Date();
39017
+ endDate = new Date(startDate);
39018
+ endDate.setFullYear(endDate.getFullYear() + 10);
39019
+ }
39020
+ const startDahua = startDate.toISOString();
39021
+ const endDahua = endDate.toISOString();
39022
+ const journal = cameraJournal();
38181
39023
  try {
38182
39024
  session.startTransaction();
38183
- let value = {};
38184
- const siteCameras = [];
38185
- let page = 1;
38186
- let pages = 1;
38187
- const limit = 20;
38188
- do {
38189
- const siteCameraReq = await _getAllSiteCameras({
38190
- site: siteId,
38191
- type: "anpr",
38192
- direction: ["both", "entry"],
38193
- page,
38194
- limit
38195
- });
38196
- pages = siteCameraReq.pages || 1;
38197
- siteCameras.push(...siteCameraReq.items);
38198
- page++;
38199
- } while (page <= pages);
39025
+ const cameraSite = vehicle.site ?? siteId;
39026
+ const siteCameras = isBlocklist ? await blockCamerasAt(cameraSite) : await allowCamerasAt(cameraSite);
38200
39027
  if (!siteCameras.length) {
38201
39028
  throw new BadRequestError90(anprNoCameraRefusal("restored"));
38202
39029
  }
38203
- for (const camera of siteCameras) {
38204
- const { host, username, password } = camera;
38205
- const dahuaPayload = {
38206
- host,
38207
- username,
38208
- password,
39030
+ const { failures, recNo } = await writeToCameras(
39031
+ siteCameras,
39032
+ {
38209
39033
  plateNumber: _plateNumber,
38210
- mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
39034
+ mode,
39035
+ owner,
38211
39036
  start: startDahua,
38212
- end: endDahua,
38213
- owner
38214
- };
38215
- const dahuaResponse = await _addPlateNumber(dahuaPayload);
38216
- if (dahuaResponse?.statusCode !== 200) {
38217
- logCameraFailure(
38218
- "reactivateVehicleById",
38219
- camera,
38220
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38221
- );
38222
- throw new BadRequestError90(
38223
- anprRestoreFailureMessage([
38224
- {
38225
- camera: cameraLabel(camera),
38226
- reason: anprWriteReason(
38227
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38228
- )
38229
- }
38230
- ])
38231
- );
38232
- }
38233
- const responseData = dahuaResponse?.data.toString("utf-8");
38234
- value.recNo = responseData.split("=")[1]?.trim();
39037
+ end: endDahua
39038
+ },
39039
+ journal
39040
+ );
39041
+ if (failures.length) {
39042
+ throw new BadRequestError90(
39043
+ isBlocklist ? cameraFailureSentence({
39044
+ failures,
39045
+ operation: "added to",
39046
+ outcome: "restored",
39047
+ consequence: "it would still open the barrier"
39048
+ }) : anprRestoreFailureMessage(failures)
39049
+ );
38235
39050
  }
38236
- value.status = "active" /* ACTIVE */;
38237
- if (vehicle.peopleId && value.recNo) {
39051
+ if (vehicle.peopleId && _plateNumber) {
38238
39052
  await _pushVehicleById(
38239
39053
  vehicle.peopleId,
38240
39054
  {
38241
39055
  plateNumber: _plateNumber,
38242
- recNo: value.recNo
39056
+ recNo
38243
39057
  },
38244
39058
  session
38245
39059
  );
38246
39060
  }
38247
39061
  const formattedValue = {
38248
- ...value,
38249
- start: startIso,
38250
- end: endIso,
39062
+ status: "active" /* ACTIVE */,
39063
+ // Kept for older readers only - one camera's number, never replayed.
39064
+ ...recNo ? { recNo } : {},
39065
+ // Dates, not ISO strings: the expiry sweep only matches a BSON Date
39066
+ // (see `expiredVehicleSweepFilter`), so a string here never expired.
39067
+ start: startDate,
39068
+ end: endDate,
38251
39069
  deletedAt: ""
38252
39070
  };
38253
39071
  await _updateVehicleById(id, formattedValue, session);
@@ -38255,21 +39073,62 @@ function useVehicleService() {
38255
39073
  return "Vehicle reactivated successfully.";
38256
39074
  } catch (error) {
38257
39075
  logger66.error("Error in vehicle service reactivation:", error);
39076
+ await rollbackCameras(journal, error);
38258
39077
  await session.abortTransaction();
38259
39078
  throw error;
38260
39079
  } finally {
38261
39080
  session.endSession();
38262
39081
  }
38263
39082
  }
38264
- async function restorePlateOnCamera(payload) {
38265
- try {
38266
- const response = await _addPlateNumber(payload);
38267
- return (response?.statusCode ?? response?.status) === 200;
38268
- } catch (error) {
38269
- logger66.error(
38270
- `Failed to restore plate on camera ${payload.host}: ${error?.message || error}`
39083
+ async function rewriteOnCameras(vehicle, site, next, journal) {
39084
+ const oldPlate = String(vehicle.plateNumber ?? "");
39085
+ const held = listsHeldBy(vehicle);
39086
+ const targets = [];
39087
+ if (held.allow) {
39088
+ targets.push({
39089
+ mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
39090
+ cameras: await allowCamerasAt(site)
39091
+ });
39092
+ }
39093
+ if (held.block) {
39094
+ targets.push({
39095
+ mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39096
+ cameras: await blockCamerasAt(site)
39097
+ });
39098
+ }
39099
+ if (!targets.some((target) => target.cameras.length)) {
39100
+ throw new BadRequestError90(anprNoCameraRefusal("updated"));
39101
+ }
39102
+ const removeModes = next.plateNumber !== oldPlate ? await revokableModes(site, oldPlate, [vehicle._id]) : targets.map((target) => target.mode);
39103
+ const removeFailures = await removeFromCameras(
39104
+ await camerasAt(site, { activeOnly: true }),
39105
+ removeModes,
39106
+ oldPlate,
39107
+ {
39108
+ owner: String(vehicle.name ?? ""),
39109
+ ...cameraWindow(vehicle.start, vehicle.end)
39110
+ },
39111
+ journal,
39112
+ held.block
39113
+ );
39114
+ if (removeFailures.length) {
39115
+ throw new BadRequestError90(
39116
+ anprRevokeFailureMessage(removeFailures, "updated")
39117
+ );
39118
+ }
39119
+ const writeFailures = [];
39120
+ for (const target of targets) {
39121
+ const { failures } = await writeToCameras(
39122
+ target.cameras,
39123
+ { ...next, mode: target.mode },
39124
+ journal
39125
+ );
39126
+ writeFailures.push(...failures);
39127
+ }
39128
+ if (writeFailures.length) {
39129
+ throw new BadRequestError90(
39130
+ anprWriteFailureMessage(writeFailures, "updated")
38271
39131
  );
38272
- return false;
38273
39132
  }
38274
39133
  }
38275
39134
  async function updateVehicleById(_id, value) {
@@ -38277,241 +39136,75 @@ function useVehicleService() {
38277
39136
  if (!session) {
38278
39137
  throw new Error("Unable to start session for vehicle service.");
38279
39138
  }
39139
+ const journal = cameraJournal();
38280
39140
  try {
38281
39141
  session.startTransaction();
38282
- const vehicle = await _getVehicleById(_id);
38283
- const plate = vehicle.plates.find((p) => p._id.toString() === _id);
38284
- const _name = vehicle.name;
38285
- const _plateNumber = plate?.plateNumber;
38286
- const _start = vehicle.start;
38287
- const _end = vehicle.end;
38288
- const _recNo = plate.recNo;
38289
- const _type = value.type ? value.type : plate.type;
39142
+ const vehicle = await findOne({ _id: new ObjectId72(_id) });
39143
+ if (!vehicle) {
39144
+ throw new NotFoundError25("Vehicle not found.");
39145
+ }
38290
39146
  if (value.peopleId) {
38291
39147
  value.peopleId = new ObjectId72(value.peopleId);
38292
39148
  }
38293
39149
  const { name, plateNumber, start, end, recNo, type, unit, site, block, level, ...rest } = value;
38294
- if (_name != name || _plateNumber != plateNumber) {
38295
- const startDahua = value.start ? formatDahuaDate(new Date(value.start)) : formatDahuaDate(new Date(_start));
38296
- const endDahua = value.end ? formatDahuaDate(new Date(value.end)) : formatDahuaDate(new Date(_end));
38297
- if (name || plateNumber || start || end) {
38298
- const siteCameras = [];
38299
- let page = 1;
38300
- let pages = 1;
38301
- const limit = 20;
38302
- do {
38303
- const siteCameraReq = await _getAllSiteCameras({
38304
- site,
38305
- type: "anpr",
38306
- direction: ["both", "entry", "residents"],
38307
- page,
38308
- limit
38309
- });
38310
- pages = siteCameraReq.pages || 1;
38311
- siteCameras.push(...siteCameraReq.items);
38312
- page++;
38313
- } while (page <= pages);
38314
- if (!siteCameras.length) {
38315
- throw new BadRequestError90(anprNoCameraRefusal("updated"));
39150
+ const oldPlate = String(vehicle.plateNumber ?? "");
39151
+ const oldName = String(vehicle.name ?? "");
39152
+ const oldType = String(vehicle.type || "whitelist" /* WHITELIST */);
39153
+ const newPlate = plateNumber ? String(plateNumber).trim() : oldPlate;
39154
+ const newName = name ? String(name) : oldName;
39155
+ const newType = type ? String(type) : oldType;
39156
+ const cameraSite = vehicle.site ?? site;
39157
+ const startDate = validDate(start);
39158
+ const endDate = validDate(end);
39159
+ const datesChanged = !!startDate && startDate.getTime() !== validDate(vehicle.start)?.getTime() || !!endDate && endDate.getTime() !== validDate(vehicle.end)?.getTime();
39160
+ const isLive = vehicle.status === "active" /* ACTIVE */;
39161
+ let typeFields = {};
39162
+ if (newType !== oldType) {
39163
+ if (newPlate !== oldPlate) {
39164
+ throw new BadRequestError90(
39165
+ "Change the plate number and the vehicle type in two separate steps."
39166
+ );
39167
+ }
39168
+ if (newType === "blocklist" /* BLOCKLIST */) {
39169
+ if (isLive) {
39170
+ await blockOnCameras(vehicle, cameraSite, newName, journal);
38316
39171
  }
38317
- for (const camera of siteCameras) {
38318
- const { host, username, password } = camera;
38319
- if (_plateNumber != plateNumber) {
38320
- const removePlateNumber = {
38321
- host,
38322
- username,
38323
- password,
38324
- mode: plate.type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38325
- //whitelist or blocklist
38326
- recno: _recNo
38327
- };
38328
- const responseForDeletion = await _removePlateNumber(
38329
- removePlateNumber
38330
- );
38331
- if (!responseForDeletion?.ok) {
38332
- logCameraFailure(
38333
- "updateVehicleById revoke",
38334
- camera,
38335
- responseForDeletion?.detail ?? "no outcome returned"
38336
- );
38337
- throw new BadRequestError90(
38338
- anprRevokeFailureMessage(
38339
- [
38340
- {
38341
- camera: cameraLabel(camera),
38342
- reason: responseForDeletion?.reason || "the camera could not be reached"
38343
- }
38344
- ],
38345
- "updated"
38346
- )
38347
- );
38348
- }
38349
- const dahuaPayload = {
38350
- host,
38351
- username,
38352
- password,
38353
- plateNumber,
38354
- mode: value.type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38355
- //whitelist or blocklist
38356
- owner: name,
38357
- ...startDahua ? { start: startDahua } : {},
38358
- ...endDahua ? { end: endDahua } : {}
38359
- };
38360
- let dahuaResponse;
38361
- try {
38362
- dahuaResponse = await _addPlateNumber(dahuaPayload);
38363
- } catch (error) {
38364
- logCameraFailure("updateVehicleById write", camera, error);
38365
- const restored = await restorePlateOnCamera({
38366
- ...removePlateNumber,
38367
- plateNumber: _plateNumber,
38368
- owner: _name,
38369
- ...startDahua ? { start: startDahua } : {},
38370
- ...endDahua ? { end: endDahua } : {}
38371
- });
38372
- throw new BadRequestError90(
38373
- anprWriteFailureMessage(
38374
- [{ camera: cameraLabel(camera), reason: anprWriteReason(error) }],
38375
- "updated"
38376
- ) + (restored ? ` The previous plate number ${_plateNumber} is still registered, so nothing has changed.` : ` WARNING: ${_plateNumber} was taken off this camera and could not be put back - it will NOT open the barrier until someone restores it.`)
38377
- );
38378
- }
38379
- if (dahuaResponse?.statusCode !== 200) {
38380
- logCameraFailure(
38381
- "updateVehicleById write",
38382
- camera,
38383
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38384
- );
38385
- throw new BadRequestError90(
38386
- anprWriteFailureMessage(
38387
- [
38388
- {
38389
- camera: cameraLabel(camera),
38390
- reason: anprWriteReason(
38391
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38392
- )
38393
- }
38394
- ],
38395
- "updated"
38396
- )
38397
- );
38398
- }
38399
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
38400
- value.recNo = responseData.split("=")[1]?.trim();
38401
- if (value.peopleId && value.recNo) {
38402
- await _pushVehicleById(
38403
- value.peopleId,
38404
- {
38405
- plateNumber,
38406
- recNo: value.recNo
38407
- },
38408
- session
38409
- );
38410
- }
38411
- } else {
38412
- const existing = parseDahuaFind(
38413
- await _getPlateNumber({
38414
- host,
38415
- username,
38416
- password,
38417
- mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38418
- plateNumber,
38419
- requireOk: true
38420
- }) ?? ""
38421
- );
38422
- if (existing.exists) {
38423
- const outcome = await _removePlateNumber({
38424
- host,
38425
- username,
38426
- password,
38427
- mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38428
- recno: existing.recNo || _recNo
38429
- });
38430
- if (!outcome?.ok) {
38431
- logCameraFailure(
38432
- "updateVehicleById revoke",
38433
- camera,
38434
- outcome?.detail ?? "no outcome returned"
38435
- );
38436
- throw new BadRequestError90(
38437
- anprRevokeFailureMessage([
38438
- {
38439
- camera: cameraLabel(camera),
38440
- reason: outcome?.reason || "the camera could not be reached"
38441
- }
38442
- ], "updated")
38443
- );
38444
- }
38445
- }
38446
- const dahuaPayload = {
38447
- host,
38448
- username,
38449
- password,
38450
- plateNumber,
38451
- // recno: _recNo,
38452
- mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38453
- start: startDahua,
38454
- end: endDahua,
38455
- owner: name ? name : _name
38456
- };
38457
- let dahuaResponse;
38458
- try {
38459
- dahuaResponse = await _addPlateNumber(dahuaPayload);
38460
- } catch (error) {
38461
- logCameraFailure("updateVehicleById write", camera, error);
38462
- const restored = existing.exists ? await restorePlateOnCamera({
38463
- ...dahuaPayload,
38464
- owner: _name
38465
- }) : true;
38466
- throw new BadRequestError90(
38467
- anprWriteFailureMessage(
38468
- [{ camera: cameraLabel(camera), reason: anprWriteReason(error) }],
38469
- "updated"
38470
- ) + (restored ? " The vehicle is unchanged and still registered." : ` WARNING: ${plateNumber} was taken off this camera and could not be put back - it will NOT open the barrier until someone restores it.`)
38471
- );
38472
- }
38473
- if (dahuaResponse?.statusCode !== 200) {
38474
- logCameraFailure(
38475
- "updateVehicleById write",
38476
- camera,
38477
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38478
- );
38479
- throw new BadRequestError90(
38480
- anprWriteFailureMessage(
38481
- [
38482
- {
38483
- camera: cameraLabel(camera),
38484
- reason: anprWriteReason(
38485
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38486
- )
38487
- }
38488
- ],
38489
- "updated"
38490
- )
38491
- );
38492
- }
38493
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
38494
- value.recNo = responseData.split("=")[1]?.trim();
38495
- if (value.peopleId && value.recNo) {
38496
- await _pushVehicleById(
38497
- value.peopleId,
38498
- {
38499
- plateNumber,
38500
- recNo: value.recNo
38501
- },
38502
- session
38503
- );
38504
- }
38505
- }
39172
+ typeFields = {
39173
+ type: "blocklist" /* BLOCKLIST */,
39174
+ blockedFrom: oldType === "whitelist" /* WHITELIST */ ? "whitelist" /* WHITELIST */ : String(vehicle.blockedFrom ?? "")
39175
+ };
39176
+ } else {
39177
+ if (isLive) {
39178
+ await unblockOnCameras(
39179
+ vehicle,
39180
+ cameraSite,
39181
+ { restore: true, owner: newName },
39182
+ journal
39183
+ );
38506
39184
  }
39185
+ typeFields = { type: "whitelist" /* WHITELIST */, blockedFrom: "" };
38507
39186
  }
39187
+ } else if (isLive && (newPlate !== oldPlate || newName !== oldName || datesChanged)) {
39188
+ await rewriteOnCameras(
39189
+ vehicle,
39190
+ cameraSite,
39191
+ {
39192
+ plateNumber: newPlate,
39193
+ owner: newName,
39194
+ ...cameraWindow(startDate ?? vehicle.start, endDate ?? vehicle.end)
39195
+ },
39196
+ journal
39197
+ );
38508
39198
  }
38509
39199
  const formattedValue = {
38510
39200
  ...rest,
38511
39201
  ...name && { name },
38512
- ...plateNumber && { plateNumber },
38513
- ...start && { start },
38514
- ...end && { end },
39202
+ ...plateNumber && { plateNumber: newPlate },
39203
+ // Dates, not ISO strings: the expiry sweep only matches a BSON Date
39204
+ // (see `expiredVehicleSweepFilter`), so an end date edited to a string
39205
+ // never expired.
39206
+ ...startDate && { start: startDate },
39207
+ ...endDate && { end: endDate },
38515
39208
  ...block && {
38516
39209
  block: typeof block === "string" ? new ObjectId72(block) : block
38517
39210
  },
@@ -38521,13 +39214,27 @@ function useVehicleService() {
38521
39214
  ...unit && {
38522
39215
  unit: typeof unit === "string" ? new ObjectId72(unit) : unit
38523
39216
  },
38524
- ...value.recNo && { recNo: value.recNo },
38525
- ...type && { type: value.type }
39217
+ ...type && { type },
39218
+ ...typeFields
38526
39219
  };
38527
39220
  await _updateVehicleById(_id, formattedValue, session);
39221
+ const peopleId = value.peopleId || vehicle.peopleId;
39222
+ if (isLive && peopleId && newPlate !== oldPlate) {
39223
+ await _pushVehicleById(
39224
+ peopleId,
39225
+ { plateNumber: newPlate, recNo: "" },
39226
+ session
39227
+ );
39228
+ await _pullVehicleByRecNo(
39229
+ "",
39230
+ { peopleId, site: cameraSite, plateNumber: oldPlate },
39231
+ session
39232
+ );
39233
+ }
38528
39234
  await session.commitTransaction();
38529
39235
  } catch (error) {
38530
39236
  logger66.error("Error in vehicle service update:", error);
39237
+ await rollbackCameras(journal, error);
38531
39238
  await session.abortTransaction();
38532
39239
  throw error;
38533
39240
  } finally {
@@ -38563,25 +39270,12 @@ function useVehicleService() {
38563
39270
  if (sanitizedValues.length === 0) {
38564
39271
  throw new Error("No valid vehicle plate numbers found.");
38565
39272
  }
38566
- let siteCameras = [];
38567
- if (site) {
38568
- let page = 1;
38569
- let pages = 1;
38570
- const limit = 20;
38571
- do {
38572
- const siteCameraReq = await _getAllSiteCameras({
38573
- site,
38574
- type: "anpr",
38575
- direction: ["both", "entry"],
38576
- page,
38577
- limit
38578
- });
38579
- pages = siteCameraReq.pages || 1;
38580
- siteCameras.push(...siteCameraReq.items);
38581
- page++;
38582
- } while (page <= pages);
38583
- }
38584
- if (!siteCameras.length) {
39273
+ const isBlockRow = (row) => row.type !== "whitelist" /* WHITELIST */;
39274
+ const allowCameras = site ? await allowCamerasAt(site) : [];
39275
+ const blockCameras = site ? await blockCamerasAt(site) : [];
39276
+ const hasAllowRows = sanitizedValues.some((row) => !isBlockRow(row));
39277
+ const hasBlockRows = sanitizedValues.some((row) => isBlockRow(row));
39278
+ if (hasAllowRows && !allowCameras.length || hasBlockRows && !blockCameras.length) {
38585
39279
  throw new Error("No site cameras found.");
38586
39280
  }
38587
39281
  const written = /* @__PURE__ */ new Map();
@@ -38590,58 +39284,63 @@ function useVehicleService() {
38590
39284
  site,
38591
39285
  plateNumber: value.plateNumber
38592
39286
  }));
38593
- const { deletableIds: savableIds, skipped } = await sweepExpiredVehicles(
38594
- sweepRows,
38595
- {
38596
- camerasForSite: async () => siteCameras,
38597
- cameraLabel,
38598
- revoke: async (camera, row) => {
38599
- const rowId = String(row._id);
38600
- const vehicleValue = sanitizedValues[Number(rowId)];
38601
- const plateNumber = vehicleValue.plateNumber;
38602
- const { host, username, password } = camera;
38603
- const dahuaPayload = {
38604
- host,
38605
- username,
38606
- password,
38607
- plateNumber,
38608
- mode: vehicleValue.type === "whitelist" /* WHITELIST */ ? "TrafficRedList" /* TRAFFIC_REDLIST */ : "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
38609
- ...vehicleValue.start ? { start: String(vehicleValue.start) } : {},
38610
- ...vehicleValue.end ? { end: String(vehicleValue.end) } : {},
38611
- ...vehicleValue.name ? { owner: vehicleValue.name } : {},
38612
- ...vehicleValue.vehicleModel ? { vehicleType: vehicleValue.vehicleModel } : {},
38613
- ...vehicleValue.vehicleColor ? { vehicleColor: vehicleValue.vehicleColor } : {}
39287
+ const writeRow = async (camera, row) => {
39288
+ const rowId = String(row._id);
39289
+ const vehicleValue = sanitizedValues[Number(rowId)];
39290
+ const plateNumber = vehicleValue.plateNumber;
39291
+ const { host, username, password } = camera;
39292
+ const dahuaPayload = {
39293
+ host,
39294
+ username,
39295
+ password,
39296
+ plateNumber,
39297
+ mode: vehicleValue.type === "whitelist" /* WHITELIST */ ? "TrafficRedList" /* TRAFFIC_REDLIST */ : "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39298
+ ...vehicleValue.start ? { start: String(vehicleValue.start) } : {},
39299
+ ...vehicleValue.end ? { end: String(vehicleValue.end) } : {},
39300
+ ...vehicleValue.name ? { owner: vehicleValue.name } : {},
39301
+ ...vehicleValue.vehicleModel ? { vehicleType: vehicleValue.vehicleModel } : {},
39302
+ ...vehicleValue.vehicleColor ? { vehicleColor: vehicleValue.vehicleColor } : {},
39303
+ // Dates on the camera's clock, in the site's zone.
39304
+ timezone: await siteTimezone(camera.site ?? site)
39305
+ };
39306
+ try {
39307
+ const dahuaResponse = await _bulkInsertPlateNumber(dahuaPayload);
39308
+ const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
39309
+ const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
39310
+ if (responseStatus !== 200) {
39311
+ logCameraFailure(
39312
+ "bulk add",
39313
+ camera,
39314
+ `HTTP ${responseStatus}: ${responseData.slice(0, 500)}`
39315
+ );
39316
+ return {
39317
+ ok: false,
39318
+ reason: anprWriteReason(`HTTP ${responseStatus}`)
38614
39319
  };
38615
- try {
38616
- const dahuaResponse = await _bulkInsertPlateNumber(dahuaPayload);
38617
- const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
38618
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
38619
- if (responseStatus !== 200) {
38620
- logCameraFailure(
38621
- "bulk add",
38622
- camera,
38623
- `HTTP ${responseStatus}: ${responseData.slice(0, 500)}`
38624
- );
38625
- return {
38626
- ok: false,
38627
- reason: anprWriteReason(`HTTP ${responseStatus}`)
38628
- };
38629
- }
38630
- const recNo = String(responseData.split("=")[1]?.trim() ?? "");
38631
- if (recNo) {
38632
- vehicleValue.recNo = vehicleValue.recNo || recNo;
38633
- const entries = written.get(rowId) ?? [];
38634
- entries.push({ camera, recno: recNo });
38635
- written.set(rowId, entries);
38636
- }
38637
- return { ok: true, reason: "" };
38638
- } catch (error) {
38639
- logCameraFailure("bulk add", camera, error);
38640
- return { ok: false, reason: anprWriteReason(error) };
38641
- }
38642
39320
  }
39321
+ const recNo = String(responseData.split("=")[1]?.trim() ?? "");
39322
+ if (recNo) {
39323
+ vehicleValue.recNo = vehicleValue.recNo || recNo;
39324
+ const entries = written.get(rowId) ?? [];
39325
+ entries.push({ camera, recno: recNo });
39326
+ written.set(rowId, entries);
39327
+ }
39328
+ return { ok: true, reason: "" };
39329
+ } catch (error) {
39330
+ logCameraFailure("bulk add", camera, error);
39331
+ return { ok: false, reason: anprWriteReason(error) };
38643
39332
  }
39333
+ };
39334
+ const allowPass = await sweepExpiredVehicles(
39335
+ sweepRows.filter((row) => !isBlockRow(sanitizedValues[Number(row._id)])),
39336
+ { camerasForSite: async () => allowCameras, cameraLabel, revoke: writeRow }
39337
+ );
39338
+ const blockPass = await sweepExpiredVehicles(
39339
+ sweepRows.filter((row) => isBlockRow(sanitizedValues[Number(row._id)])),
39340
+ { camerasForSite: async () => blockCameras, cameraLabel, revoke: writeRow }
38644
39341
  );
39342
+ const savableIds = [...allowPass.deletableIds, ...blockPass.deletableIds];
39343
+ const skipped = [...allowPass.skipped, ...blockPass.skipped];
38645
39344
  const failedRows = [];
38646
39345
  for (const entry of skipped) {
38647
39346
  const stranded = [];
@@ -38686,123 +39385,26 @@ function useVehicleService() {
38686
39385
  if (!session) {
38687
39386
  throw new Error("Unable to start session for vehicle service.");
38688
39387
  }
39388
+ const journal = cameraJournal();
38689
39389
  try {
38690
39390
  session.startTransaction();
38691
- const vehicle = await _getVehicleById(_id);
38692
- const plate = vehicle.plates.find((p) => p._id.toString() === _id);
38693
- const _name = value.name ? value.name : vehicle.name;
38694
- const _plateNumber = plate?.plateNumber;
38695
- const _start = vehicle.start;
38696
- const _end = vehicle.end;
38697
- const _recNo = plate.recNo;
38698
- const _type = plate.type;
38699
- if (value.peopleId) {
38700
- value.peopleId = new ObjectId72(value.peopleId);
39391
+ const vehicle = await findOne({ _id: new ObjectId72(_id) });
39392
+ if (!vehicle) {
39393
+ throw new NotFoundError25("Vehicle not found.");
38701
39394
  }
38702
- const { site } = value;
38703
- const startDahua = formatDahuaDate(_start);
38704
- const endPlus24Hours = _end ? _end : new Date(new Date(_end).getTime() + 24 * 60 * 60 * 1e3);
38705
- const endDahua = formatDahuaDate(endPlus24Hours);
38706
- if (_plateNumber) {
38707
- const siteCameras = [];
38708
- let page = 1;
38709
- let pages = 1;
38710
- const limit = 20;
38711
- do {
38712
- const siteCameraReq = await _getAllSiteCameras({
38713
- site,
38714
- type: "anpr",
38715
- direction: ["both", "entry"],
38716
- page,
38717
- limit
38718
- });
38719
- pages = siteCameraReq.pages || 1;
38720
- siteCameras.push(...siteCameraReq.items);
38721
- page++;
38722
- } while (page <= pages);
38723
- if (!siteCameras.length) {
38724
- throw new BadRequestError90(anprNoCameraRefusal("blocked"));
38725
- }
38726
- for (const camera of siteCameras) {
38727
- const { host, username, password } = camera;
38728
- const removePlateNumber = {
38729
- host,
38730
- username,
38731
- password,
38732
- mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38733
- recno: _recNo
38734
- };
38735
- const responseForDeletion = await _removePlateNumber(
38736
- removePlateNumber
38737
- );
38738
- if (!responseForDeletion?.ok) {
38739
- logCameraFailure(
38740
- "blocklistVehicles revoke",
38741
- camera,
38742
- responseForDeletion?.detail ?? "no outcome returned"
38743
- );
38744
- throw new BadRequestError90(
38745
- anprRevokeFailureMessage(
38746
- [
38747
- {
38748
- camera: cameraLabel(camera),
38749
- reason: responseForDeletion?.reason || "the camera could not be reached"
38750
- }
38751
- ],
38752
- "updated"
38753
- )
38754
- );
38755
- }
38756
- const dahuaPayload = {
38757
- host,
38758
- username,
38759
- password,
38760
- plateNumber: _plateNumber,
38761
- mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
38762
- owner: _name,
38763
- ...startDahua ? { start: startDahua } : {},
38764
- ...endDahua ? { end: endDahua } : {}
38765
- };
38766
- const dahuaResponse = await _addPlateNumber(dahuaPayload);
38767
- if (dahuaResponse?.statusCode !== 200) {
38768
- logCameraFailure(
38769
- "blocklistVehicles write",
38770
- camera,
38771
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38772
- );
38773
- throw new BadRequestError90(
38774
- anprBlockFailureMessage([
38775
- {
38776
- camera: cameraLabel(camera),
38777
- reason: anprWriteReason(
38778
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38779
- )
38780
- }
38781
- ])
38782
- );
38783
- }
38784
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
38785
- value.recNo = responseData.split("=")[1]?.trim();
38786
- const normalizedPlateNumber = _plateNumber;
38787
- if (value.peopleId && value.recNo) {
38788
- await _pushVehicleById(
38789
- value.peopleId,
38790
- {
38791
- plateNumber: normalizedPlateNumber,
38792
- recNo: value.recNo
38793
- },
38794
- session
38795
- );
38796
- }
38797
- }
39395
+ const owner = String(value.name || vehicle.name || "");
39396
+ const cameraSite = vehicle.site ?? value.site;
39397
+ if (vehicle.status === "active" /* ACTIVE */) {
39398
+ await blockOnCameras(vehicle, cameraSite, owner, journal);
38798
39399
  }
38799
39400
  const formattedValue = {
38800
39401
  type: "blocklist" /* BLOCKLIST */,
38801
- recNo: value.recNo
39402
+ blockedFrom: vehicle.type === "whitelist" /* WHITELIST */ ? "whitelist" /* WHITELIST */ : String(vehicle.blockedFrom ?? "")
38802
39403
  };
38803
39404
  await _updateVehicleById(_id, formattedValue, session);
38804
39405
  await session.commitTransaction();
38805
39406
  } catch (error) {
39407
+ await rollbackCameras(journal, error);
38806
39408
  await session.abortTransaction();
38807
39409
  throw error;
38808
39410
  } finally {
@@ -38814,85 +39416,222 @@ function useVehicleService() {
38814
39416
  if (!session) {
38815
39417
  throw new Error("Unable to start session for vehicle service.");
38816
39418
  }
39419
+ const journal = cameraJournal();
38817
39420
  try {
38818
39421
  session.startTransaction();
38819
- const vehicle = await _getVehicleById(_id);
38820
- const plate = vehicle.plates.find((p) => p._id.toString() === _id);
38821
- const _plateNumber = plate?.plateNumber;
38822
- const _recNo = plate.recNo;
39422
+ const vehicle = await findOne({ _id: new ObjectId72(_id) });
39423
+ if (!vehicle) {
39424
+ throw new NotFoundError25("Vehicle not found.");
39425
+ }
39426
+ if (vehicle.type !== "blocklist" /* BLOCKLIST */) {
39427
+ throw new BadRequestError90("This vehicle is not on the blocklist.");
39428
+ }
39429
+ const _plateNumber = String(vehicle.plateNumber ?? "");
39430
+ const cameraSite = vehicle.site ?? value.site;
39431
+ const restore = vehicle.blockedFrom === "whitelist" /* WHITELIST */;
38823
39432
  if (value.peopleId) {
38824
39433
  value.peopleId = new ObjectId72(value.peopleId);
38825
39434
  }
38826
- const { site } = value;
38827
- if (_plateNumber) {
38828
- const siteCameras = [];
38829
- let page = 1;
38830
- let pages = 1;
38831
- const limit = 20;
38832
- do {
38833
- const siteCameraReq = await _getAllSiteCameras({
38834
- site,
38835
- type: "anpr",
38836
- direction: ["both", "entry"],
38837
- page,
38838
- limit
38839
- });
38840
- pages = siteCameraReq.pages || 1;
38841
- siteCameras.push(...siteCameraReq.items);
38842
- page++;
38843
- } while (page <= pages);
38844
- logger66.info(
38845
- `removeFromBlocklist resolved ${siteCameras.length} ANPR camera(s)`
39435
+ if (vehicle.status === "active" /* ACTIVE */) {
39436
+ await unblockOnCameras(
39437
+ vehicle,
39438
+ cameraSite,
39439
+ { restore, owner: String(vehicle.name ?? "") },
39440
+ journal
38846
39441
  );
38847
- for (const camera of siteCameras) {
38848
- const { host, username, password } = camera;
38849
- const removePlateNumber = {
38850
- host,
38851
- username,
38852
- password,
38853
- mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
38854
- recno: _recNo
38855
- };
38856
- const responseForDeletion = await _removePlateNumber(
38857
- removePlateNumber
38858
- );
38859
- if (!responseForDeletion?.ok) {
38860
- logCameraFailure(
38861
- "removeFromBlocklist revoke",
38862
- camera,
38863
- responseForDeletion?.detail ?? "no outcome returned"
38864
- );
38865
- throw new BadRequestError90(
38866
- anprUnblockFailureMessage([
38867
- {
38868
- camera: cameraLabel(camera),
38869
- reason: responseForDeletion?.reason || "the camera could not be reached"
38870
- }
38871
- ])
38872
- );
38873
- }
38874
- }
39442
+ }
39443
+ if (restore) {
39444
+ await _updateVehicleById(
39445
+ _id,
39446
+ { type: "whitelist" /* WHITELIST */, blockedFrom: "" },
39447
+ session
39448
+ );
39449
+ await session.commitTransaction();
39450
+ return "Vehicle unblocked and restored to the whitelist.";
38875
39451
  }
38876
39452
  await _deleteVehicle(_id, session);
38877
- if ((value.peopleId || site) && (_plateNumber || _recNo)) {
39453
+ const peopleId = value.peopleId || vehicle.peopleId;
39454
+ if ((peopleId || cameraSite) && _plateNumber) {
38878
39455
  await _pullVehicleByRecNo(
38879
- String(_recNo ?? ""),
39456
+ "",
38880
39457
  {
38881
- peopleId: value.peopleId,
38882
- site,
39458
+ peopleId,
39459
+ site: cameraSite,
38883
39460
  plateNumber: _plateNumber
38884
39461
  },
38885
39462
  session
38886
39463
  );
38887
39464
  }
38888
39465
  await session.commitTransaction();
39466
+ return "Vehicle removed from the blocklist.";
38889
39467
  } catch (error) {
39468
+ await rollbackCameras(journal, error);
38890
39469
  await session.abortTransaction();
38891
39470
  throw error;
38892
39471
  } finally {
38893
39472
  session.endSession();
38894
39473
  }
38895
39474
  }
39475
+ async function fillCameraLists(site, camera, lists) {
39476
+ let target;
39477
+ if (camera.id) {
39478
+ const stored = (await camerasAt(site)).find(
39479
+ (candidate) => String(candidate._id) === String(camera.id)
39480
+ );
39481
+ if (!stored)
39482
+ throw new NotFoundError25("Camera not found.");
39483
+ target = {
39484
+ ...stored,
39485
+ ...camera.host ? { host: camera.host } : {},
39486
+ ...camera.username ? { username: camera.username } : {},
39487
+ ...camera.password ? { password: camera.password } : {},
39488
+ ...camera.direction ? { direction: camera.direction } : {}
39489
+ };
39490
+ } else {
39491
+ target = {
39492
+ host: String(camera.host ?? ""),
39493
+ username: String(camera.username ?? ""),
39494
+ password: String(camera.password ?? ""),
39495
+ name: camera.name,
39496
+ direction: camera.direction,
39497
+ // Whose clock the season pass windows are written on.
39498
+ site
39499
+ };
39500
+ }
39501
+ const vehicles = await _getLivePlatesBySite(String(site));
39502
+ const unique = (rows) => {
39503
+ const seen = /* @__PURE__ */ new Set();
39504
+ return rows.filter((row) => {
39505
+ const key = normalisePlate(row.plateNumber);
39506
+ if (!key || seen.has(key))
39507
+ return false;
39508
+ seen.add(key);
39509
+ return true;
39510
+ });
39511
+ };
39512
+ const blocked = lists.block ? unique(vehicles.filter((vehicle) => listsHeldBy(vehicle).block)) : [];
39513
+ const allowed = lists.allow ? unique(vehicles.filter((vehicle) => listsHeldBy(vehicle).allow)) : [];
39514
+ const writes = [
39515
+ ...blocked.map((vehicle) => ({
39516
+ plateNumber: String(vehicle.plateNumber),
39517
+ mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39518
+ owner: String(vehicle.name ?? ""),
39519
+ ...blockWindow(vehicle.end)
39520
+ })),
39521
+ ...allowed.map((vehicle) => ({
39522
+ plateNumber: String(vehicle.plateNumber),
39523
+ mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
39524
+ owner: String(vehicle.name ?? ""),
39525
+ ...cameraWindow(vehicle.start, vehicle.end)
39526
+ }))
39527
+ ];
39528
+ const written = { allowed: 0, blocked: 0 };
39529
+ for (const write of writes) {
39530
+ try {
39531
+ await insertPlateOnCamera(target, write);
39532
+ } catch (error) {
39533
+ logCameraFailure("fill camera", target, error);
39534
+ throw new BadRequestError90(
39535
+ `Only ${written.allowed + written.blocked} of ${writes.length} plates could be added to ${cameraLabel(target)} before it stopped: ${anprWriteReason(error)}.`
39536
+ );
39537
+ }
39538
+ if (write.mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */)
39539
+ written.blocked++;
39540
+ else
39541
+ written.allowed++;
39542
+ }
39543
+ logger66.info(
39544
+ `fillCameraLists wrote ${written.allowed} allowed and ${written.blocked} blocked plate(s) for site ${String(site)}`
39545
+ );
39546
+ return written;
39547
+ }
39548
+ async function newCameraPlateRows(site, direction) {
39549
+ const lists = listsFilledByDirectionChange("", direction);
39550
+ if (!lists.allow && !lists.block)
39551
+ return [];
39552
+ const vehicles = await _getLivePlatesBySite(String(site));
39553
+ const rowsFor = (list) => {
39554
+ const seen = /* @__PURE__ */ new Set();
39555
+ const rows = [];
39556
+ for (const vehicle of vehicles) {
39557
+ const held = listsHeldBy(vehicle);
39558
+ if (list === "blocked" ? !held.block : !held.allow)
39559
+ continue;
39560
+ const key = normalisePlate(vehicle.plateNumber);
39561
+ if (!key || seen.has(key))
39562
+ continue;
39563
+ seen.add(key);
39564
+ rows.push({
39565
+ id: `${list}:${String(vehicle._id)}`,
39566
+ vehicle: String(vehicle._id),
39567
+ list,
39568
+ plateNumber: String(vehicle.plateNumber ?? ""),
39569
+ name: String(vehicle.name ?? ""),
39570
+ block: String(vehicle.blockName ?? ""),
39571
+ level: String(vehicle.levelName ?? ""),
39572
+ unit: String(vehicle.unitName ?? ""),
39573
+ kind: list === "blocked" ? "Blocked" : vehicle.seasonPassType ? "Season pass" : "Whitelist"
39574
+ });
39575
+ }
39576
+ return rows;
39577
+ };
39578
+ return [
39579
+ ...lists.block ? rowsFor("blocked") : [],
39580
+ ...lists.allow ? rowsFor("allowed") : []
39581
+ ];
39582
+ }
39583
+ async function writeVehicleToNewCamera(site, device, vehicleId, list, options = {}) {
39584
+ let siteId;
39585
+ let id;
39586
+ try {
39587
+ siteId = new ObjectId72(String(site));
39588
+ id = new ObjectId72(String(vehicleId));
39589
+ } catch {
39590
+ throw new BadRequestError90("Invalid vehicle or site ID.");
39591
+ }
39592
+ const vehicle = await findOne({
39593
+ _id: id,
39594
+ site: siteId,
39595
+ status: "active" /* ACTIVE */
39596
+ });
39597
+ if (!vehicle)
39598
+ throw new NotFoundError25("Vehicle not found.");
39599
+ const held = listsHeldBy(vehicle);
39600
+ if (list === "blocked" ? !held.block : !held.allow) {
39601
+ throw new BadRequestError90(
39602
+ "This vehicle no longer belongs on that list. Refresh and try again."
39603
+ );
39604
+ }
39605
+ const write = {
39606
+ plateNumber: String(vehicle.plateNumber ?? ""),
39607
+ mode: list === "blocked" ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
39608
+ owner: String(vehicle.name ?? ""),
39609
+ ...list === "blocked" ? blockWindow(vehicle.end) : cameraWindow(vehicle.start, vehicle.end)
39610
+ };
39611
+ const target = {
39612
+ host: device.host,
39613
+ username: device.username,
39614
+ password: device.password,
39615
+ name: device.name,
39616
+ direction: device.direction,
39617
+ // Whose clock the season pass window is written on.
39618
+ site: siteId
39619
+ };
39620
+ try {
39621
+ if (options.onlyIfMissing)
39622
+ await ensurePlateOnCamera(target, write);
39623
+ else
39624
+ await insertPlateOnCamera(target, write);
39625
+ return { ok: true };
39626
+ } catch (error) {
39627
+ logCameraFailure("new camera plate", target, error);
39628
+ return {
39629
+ ok: false,
39630
+ reason: anprWriteReason(error),
39631
+ kind: classifyDahuaFailure(error?.message ?? error)
39632
+ };
39633
+ }
39634
+ }
38896
39635
  return {
38897
39636
  add,
38898
39637
  deleteVehicle,
@@ -38902,7 +39641,10 @@ function useVehicleService() {
38902
39641
  updateVehicleById,
38903
39642
  bulkUpsertVehicles,
38904
39643
  blocklistVehicles,
38905
- removeFromBlocklist
39644
+ removeFromBlocklist,
39645
+ fillCameraLists,
39646
+ newCameraPlateRows,
39647
+ writeVehicleToNewCamera
38906
39648
  };
38907
39649
  }
38908
39650
 
@@ -39084,7 +39826,7 @@ import {
39084
39826
  AppError as AppError17,
39085
39827
  BadRequestError as BadRequestError92,
39086
39828
  logger as logger68,
39087
- NotFoundError as NotFoundError25,
39829
+ NotFoundError as NotFoundError26,
39088
39830
  UnauthorizedError as UnauthorizedError12
39089
39831
  } from "@7365admin1/node-server-utils";
39090
39832
 
@@ -39231,7 +39973,7 @@ function siteModulesHandlers({
39231
39973
  const actor = await requireSiteWrite2(req, siteId);
39232
39974
  const site = await _getSiteById(siteId);
39233
39975
  if (!site)
39234
- throw new NotFoundError25("Site not found.");
39976
+ throw new NotFoundError26("Site not found.");
39235
39977
  if (!ownsSite(actor, site))
39236
39978
  throw new UnauthorizedError12("Not authorized.");
39237
39979
  const org = site?.orgId ? await _getOrgById(site.orgId.toString()) : null;
@@ -40426,7 +41168,7 @@ function useBuildingRepo() {
40426
41168
  import {
40427
41169
  BadRequestError as BadRequestError98,
40428
41170
  logger as logger75,
40429
- NotFoundError as NotFoundError26,
41171
+ NotFoundError as NotFoundError27,
40430
41172
  useAtlas as useAtlas59
40431
41173
  } from "@7365admin1/node-server-utils";
40432
41174
  import { ObjectId as ObjectId77 } from "mongodb";
@@ -41039,7 +41781,7 @@ function useBuildingService() {
41039
41781
  session
41040
41782
  );
41041
41783
  if (!file) {
41042
- throw new NotFoundError26("File not found.");
41784
+ throw new NotFoundError27("File not found.");
41043
41785
  }
41044
41786
  }
41045
41787
  }
@@ -41082,7 +41824,7 @@ function useBuildingService() {
41082
41824
  session.startTransaction();
41083
41825
  const building = await _getById(id);
41084
41826
  if (!building) {
41085
- throw new NotFoundError26("Building not found.");
41827
+ throw new NotFoundError27("Building not found.");
41086
41828
  }
41087
41829
  const dataFiles = data?.buildingFiles || [];
41088
41830
  const buildingFiles = building?.buildingFiles || [];
@@ -42041,7 +42783,7 @@ import {
42041
42783
  BadRequestError as BadRequestError100,
42042
42784
  logger as logger77,
42043
42785
  useAtlas as useAtlas60,
42044
- NotFoundError as NotFoundError27
42786
+ NotFoundError as NotFoundError28
42045
42787
  } from "@7365admin1/node-server-utils";
42046
42788
  function useBuildingUnitService() {
42047
42789
  const { add: _add, updateById: _updateById, getById } = useBuildingUnitRepo();
@@ -42063,7 +42805,7 @@ function useBuildingUnitService() {
42063
42805
  session
42064
42806
  );
42065
42807
  if (!file) {
42066
- throw new NotFoundError27("File not found.");
42808
+ throw new NotFoundError28("File not found.");
42067
42809
  }
42068
42810
  }
42069
42811
  }
@@ -43124,40 +43866,42 @@ function useVehicleController() {
43124
43866
  }
43125
43867
  const { _id, ...rest } = value;
43126
43868
  await requireVehicleReach(req, _id);
43127
- await _blocklistVehicles(_id, rest);
43128
- res.json({ message: "Successfully blocked vehicle." });
43129
- return;
43130
- } catch (error) {
43131
- logger79.log({ level: "error", message: error.message });
43132
- next(error);
43133
- return;
43134
- }
43135
- }
43136
- async function removeFromBlocklist(req, res, next) {
43137
- try {
43138
- const schema2 = Joi57.object({
43139
- _id: Joi57.string().hex().length(24).required(),
43140
- site: Joi57.string().hex().length(24).required(),
43141
- peopleId: Joi57.string().hex().length(24).optional().allow(null, ""),
43142
- remarks: Joi57.string().optional().allow("", null)
43869
+ await _blocklistVehicles(_id, rest);
43870
+ res.json({ message: "Successfully blocked vehicle." });
43871
+ return;
43872
+ } catch (error) {
43873
+ logger79.log({ level: "error", message: error.message });
43874
+ next(error);
43875
+ return;
43876
+ }
43877
+ }
43878
+ async function removeFromBlocklist(req, res, next) {
43879
+ try {
43880
+ const schema2 = Joi57.object({
43881
+ _id: Joi57.string().hex().length(24).required(),
43882
+ site: Joi57.string().hex().length(24).required(),
43883
+ peopleId: Joi57.string().hex().length(24).optional().allow(null, ""),
43884
+ remarks: Joi57.string().optional().allow("", null)
43885
+ });
43886
+ const { error, value } = schema2.validate(
43887
+ {
43888
+ _id: req.params.id,
43889
+ ...req.body
43890
+ },
43891
+ { abortEarly: false }
43892
+ );
43893
+ if (error) {
43894
+ const messages = error.details.map((d) => d.message).join(", ");
43895
+ logger79.log({ level: "error", message: messages });
43896
+ next(new BadRequestError102(messages));
43897
+ return;
43898
+ }
43899
+ const { _id, ...rest } = value;
43900
+ await requireVehicleReach(req, _id);
43901
+ const message = await _removeFromBlocklist(_id, rest);
43902
+ res.json({
43903
+ message: message || "Successfully removed vehicle from blocklist."
43143
43904
  });
43144
- const { error, value } = schema2.validate(
43145
- {
43146
- _id: req.params.id,
43147
- ...req.body
43148
- },
43149
- { abortEarly: false }
43150
- );
43151
- if (error) {
43152
- const messages = error.details.map((d) => d.message).join(", ");
43153
- logger79.log({ level: "error", message: messages });
43154
- next(new BadRequestError102(messages));
43155
- return;
43156
- }
43157
- const { _id, ...rest } = value;
43158
- await requireVehicleReach(req, _id);
43159
- await _removeFromBlocklist(_id, rest);
43160
- res.json({ message: "Successfully removed vehicle from blocklist." });
43161
43905
  return;
43162
43906
  } catch (error) {
43163
43907
  logger79.log({ level: "error", message: error.message });
@@ -43187,7 +43931,9 @@ function useVehicleController() {
43187
43931
  // src/services/site-camera.service.ts
43188
43932
  import {
43189
43933
  BadRequestError as BadRequestError103,
43190
- useAtlas as useAtlas61
43934
+ NotFoundError as NotFoundError29,
43935
+ useAtlas as useAtlas61,
43936
+ logger as logger80
43191
43937
  } from "@7365admin1/node-server-utils";
43192
43938
  function useSiteCameraService() {
43193
43939
  const { add: _add, getAllCameraWithPassword: _getAllCameraWithPassword } = useSiteCameraRepo();
@@ -43260,15 +44006,74 @@ function useSiteCameraService() {
43260
44006
  session.endSession();
43261
44007
  }
43262
44008
  }
44009
+ async function clearPlateLists(site, camera, lists, action = "type-change") {
44010
+ let target;
44011
+ const id = typeof camera === "string" ? camera : "";
44012
+ if (typeof camera === "string") {
44013
+ const cameras = [];
44014
+ let page = 1;
44015
+ let pages = 1;
44016
+ const limit = 20;
44017
+ do {
44018
+ const siteCameraReq = await _getAllCameraWithPassword({
44019
+ site: String(site ?? ""),
44020
+ type: "anpr",
44021
+ page,
44022
+ limit
44023
+ });
44024
+ pages = siteCameraReq.pages || 1;
44025
+ cameras.push(...siteCameraReq.items);
44026
+ page++;
44027
+ } while (page <= pages);
44028
+ target = cameras.find((candidate) => String(candidate._id) === camera);
44029
+ if (!target)
44030
+ throw new NotFoundError29("Camera not found.");
44031
+ } else {
44032
+ target = camera;
44033
+ }
44034
+ const label = cameraLabel(target);
44035
+ const notDone = action === "add" ? "The camera has NOT been added." : action === "sync" ? "No vehicles have been added to it." : "The camera type has NOT been changed.";
44036
+ const cleared = [];
44037
+ const steps = [
44038
+ ...lists.allow ? [{ list: "allowed", mode: "TrafficRedList" /* TRAFFIC_REDLIST */ }] : [],
44039
+ ...lists.block ? [{ list: "blocked", mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */ }] : []
44040
+ ];
44041
+ for (const [index, step] of steps.entries()) {
44042
+ if (index > 0)
44043
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
44044
+ const outcome = await dahuaService.clearPlateList({
44045
+ host: target.host,
44046
+ username: target.username,
44047
+ password: target.password,
44048
+ mode: step.mode
44049
+ });
44050
+ if (!outcome?.ok) {
44051
+ logger80.error(
44052
+ `clearPlateLists could not clear ${step.mode} on camera ${id || target.host}: ${outcome?.detail ?? "no outcome"}`
44053
+ );
44054
+ const listName = step.list === "allowed" ? "allowed list" : "blocklist";
44055
+ const reason = outcome?.reason || "the camera could not be reached";
44056
+ throw new BadRequestError103(
44057
+ cleared.length ? `The allowed list on ${label} was cleared, but its blocklist could not be: ${reason}. ${notDone} Until it is sorted out, whitelist and season pass vehicles will not be let in through this camera. Please check the camera and try again.` : `The ${listName} on ${label} could not be cleared: ${reason}. Nothing on the camera has changed. ${notDone} Please check the camera and try again.`
44058
+ );
44059
+ }
44060
+ cleared.push(step.list);
44061
+ }
44062
+ logger80.info(
44063
+ `clearPlateLists cleared ${cleared.join(" and ") || "nothing"} on camera ${id || target.host}`
44064
+ );
44065
+ return cleared;
44066
+ }
43263
44067
  return {
43264
44068
  add,
43265
44069
  listenToCapturedPlateNumber,
43266
- updateById
44070
+ updateById,
44071
+ clearPlateLists
43267
44072
  };
43268
44073
  }
43269
44074
 
43270
44075
  // src/controllers/site-camera.controller.ts
43271
- import { BadRequestError as BadRequestError104, NotFoundError as NotFoundError28 } from "@7365admin1/node-server-utils";
44076
+ import { BadRequestError as BadRequestError104, NotFoundError as NotFoundError30 } from "@7365admin1/node-server-utils";
43272
44077
  import Joi58 from "joi";
43273
44078
  import { ObjectId as ObjectId78 } from "mongodb";
43274
44079
 
@@ -43399,8 +44204,29 @@ function summariseBulkCameraPlan(results) {
43399
44204
  }
43400
44205
 
43401
44206
  // src/controllers/site-camera.controller.ts
44207
+ var CLEARED_ONLY_NOTE = " The camera's plate lists were cleared.";
44208
+ function filledNote(filled, cleared = true) {
44209
+ const parts = [
44210
+ ...filled.allowed ? [`${filled.allowed} allowed`] : [],
44211
+ ...filled.blocked ? [`${filled.blocked} blocked`] : []
44212
+ ];
44213
+ const total = filled.allowed + filled.blocked;
44214
+ if (!total) {
44215
+ return cleared ? " The camera's plate lists were cleared; there were no vehicle plates to add." : " There were no vehicle plates to add to the camera.";
44216
+ }
44217
+ const added = `${parts.join(" and ")} ${total === 1 ? "plate was" : "plates were"} added`;
44218
+ return cleared ? ` The camera's plate lists were cleared and ${added} to it.` : ` ${added} to the camera.`;
44219
+ }
44220
+ function cameraListClearConfirmation(lists) {
44221
+ const what = lists.allow && lists.block ? "its allowed list and its blocklist" : lists.block ? "its blocklist" : "its allowed list";
44222
+ return `This change clears every plate on ${what}, on the camera itself - including plates added directly on the camera. Confirm the change to continue.`;
44223
+ }
43402
44224
  function useSiteCameraController() {
43403
- const { add: _add, updateById: _updateById } = useSiteCameraService();
44225
+ const {
44226
+ add: _add,
44227
+ updateById: _updateById,
44228
+ clearPlateLists: _clearPlateLists
44229
+ } = useSiteCameraService();
43404
44230
  const {
43405
44231
  add: _addOne,
43406
44232
  activeHostsBySite: _activeHostsBySite,
@@ -43422,8 +44248,8 @@ function useSiteCameraController() {
43422
44248
  permissions: cameraManagePermissions(params.type)
43423
44249
  });
43424
44250
  } catch (error) {
43425
- if (error instanceof NotFoundError28) {
43426
- throw new NotFoundError28(params.notFound);
44251
+ if (error instanceof NotFoundError30) {
44252
+ throw new NotFoundError30(params.notFound);
43427
44253
  }
43428
44254
  throw error;
43429
44255
  }
@@ -43484,6 +44310,12 @@ function useSiteCameraController() {
43484
44310
  return null;
43485
44311
  }
43486
44312
  async function add(req, res, next) {
44313
+ const clearCameraLists = req.body?.clearCameraLists === true;
44314
+ const skipPlateSync = req.body?.skipPlateSync === true;
44315
+ if (req.body && typeof req.body === "object") {
44316
+ delete req.body.clearCameraLists;
44317
+ delete req.body.skipPlateSync;
44318
+ }
43487
44319
  const data = req.body;
43488
44320
  const refusal = applyDerivedHost(data);
43489
44321
  if (refusal) {
@@ -43502,6 +44334,52 @@ function useSiteCameraController() {
43502
44334
  type: value.type,
43503
44335
  notFound: "Site not found."
43504
44336
  });
44337
+ const isActiveAnpr = !skipPlateSync && String(value.type ?? "").toLowerCase() === "anpr" && String(value.status ?? "active") === "active";
44338
+ const lists = isActiveAnpr ? listsFilledByDirectionChange("", value.direction) : { allow: false, block: false };
44339
+ const willClear = isActiveAnpr && clearCameraLists;
44340
+ let cleared = false;
44341
+ let filled = null;
44342
+ if (willClear || lists.allow || lists.block) {
44343
+ const taken = await _activeHostsBySite(value.site);
44344
+ if (taken.includes(value.host)) {
44345
+ throw new BadRequestError104("ANPR already exist.");
44346
+ }
44347
+ const device = {
44348
+ host: value.host,
44349
+ username: value.username,
44350
+ password: value.password,
44351
+ name: value.name,
44352
+ direction: value.direction
44353
+ };
44354
+ const everything = { allow: true, block: true };
44355
+ if (willClear) {
44356
+ await _clearPlateLists(value.site, device, everything, "add");
44357
+ cleared = true;
44358
+ }
44359
+ if (lists.allow || lists.block) {
44360
+ try {
44361
+ filled = await useVehicleService().fillCameraLists(
44362
+ value.site,
44363
+ device,
44364
+ lists
44365
+ );
44366
+ } catch (error2) {
44367
+ let undone = null;
44368
+ if (willClear) {
44369
+ try {
44370
+ await _clearPlateLists(value.site, device, everything, "add");
44371
+ undone = true;
44372
+ } catch {
44373
+ undone = false;
44374
+ }
44375
+ }
44376
+ if (error2 && typeof error2 === "object") {
44377
+ error2.message = `${error2.message ?? ""} The camera has NOT been added` + (undone === true ? ` and the plates written to it were cleared again.` : undone === false ? `, and the plates written to it could NOT be cleared again - clear them on the camera by hand.` : `. The plates already written to it stay on the camera.`) + ` Please check the camera and try again.`;
44378
+ }
44379
+ throw error2;
44380
+ }
44381
+ }
44382
+ }
43505
44383
  const vehicle = await _add(value);
43506
44384
  await recordConsoleAction({
43507
44385
  action: "camera.added" /* CAMERA_ADDED */,
@@ -43511,8 +44389,10 @@ function useSiteCameraController() {
43511
44389
  after: pickAuditFields("camera.added" /* CAMERA_ADDED */, value)
43512
44390
  });
43513
44391
  res.status(201).json({
43514
- message: "ANPR(s) added successfully.",
43515
- data: { vehicle }
44392
+ message: `ANPR(s) added successfully.` + (filled ? filledNote(filled, cleared) : cleared ? CLEARED_ONLY_NOTE : ""),
44393
+ data: { vehicle },
44394
+ ...filled ? { addedPlates: filled } : {},
44395
+ ...cleared ? { clearedLists: ["allowed", "blocked"] } : {}
43516
44396
  });
43517
44397
  } catch (error2) {
43518
44398
  next(error2);
@@ -43608,8 +44488,8 @@ function useSiteCameraController() {
43608
44488
  try {
43609
44489
  await entitleSite({ siteId: site, userId: callerId5(req) });
43610
44490
  } catch (error2) {
43611
- if (error2 instanceof NotFoundError28) {
43612
- throw new NotFoundError28("Site not found.");
44491
+ if (error2 instanceof NotFoundError30) {
44492
+ throw new NotFoundError30("Site not found.");
43613
44493
  }
43614
44494
  throw error2;
43615
44495
  }
@@ -43621,6 +44501,12 @@ function useSiteCameraController() {
43621
44501
  }
43622
44502
  }
43623
44503
  async function updateById(req, res, next) {
44504
+ const confirmClearCameraLists = req.body?.confirmClearCameraLists === true;
44505
+ const skipPlateSync = req.body?.skipPlateSync === true;
44506
+ if (req.body && typeof req.body === "object") {
44507
+ delete req.body.confirmClearCameraLists;
44508
+ delete req.body.skipPlateSync;
44509
+ }
43624
44510
  const value = req.body;
43625
44511
  const id = req.params.id ?? "";
43626
44512
  const refusal = applyDerivedHost(value);
@@ -43640,7 +44526,7 @@ function useSiteCameraController() {
43640
44526
  try {
43641
44527
  const camera = await loadForWrite(id);
43642
44528
  if (!camera)
43643
- throw new NotFoundError28("Camera not found.");
44529
+ throw new NotFoundError30("Camera not found.");
43644
44530
  let staffId = null;
43645
44531
  try {
43646
44532
  await authorizeWrite({
@@ -43655,6 +44541,43 @@ function useSiteCameraController() {
43655
44541
  throw error2;
43656
44542
  }
43657
44543
  const update = staffId ? staffUpdate(camera, value.status) : value;
44544
+ const lists = String(camera.type ?? "").toLowerCase() === "anpr" ? listsClearedByDirectionChange(camera.direction, update?.direction) : { allow: false, block: false };
44545
+ let clearedLists = null;
44546
+ if (lists.allow || lists.block) {
44547
+ if (!confirmClearCameraLists) {
44548
+ throw new BadRequestError104(cameraListClearConfirmation(lists));
44549
+ }
44550
+ clearedLists = await _clearPlateLists(camera.site, id, lists);
44551
+ }
44552
+ const fill = !skipPlateSync && String(camera.type ?? "").toLowerCase() === "anpr" && String(update?.status ?? camera.status ?? "active") === "active" ? listsFilledByDirectionChange(camera.direction, update?.direction) : { allow: false, block: false };
44553
+ let filled = null;
44554
+ if (fill.allow || fill.block) {
44555
+ await _clearPlateLists(camera.site, id, fill);
44556
+ try {
44557
+ filled = await useVehicleService().fillCameraLists(
44558
+ camera.site,
44559
+ {
44560
+ id,
44561
+ host: update?.host,
44562
+ username: update?.username,
44563
+ password: update?.password
44564
+ },
44565
+ fill
44566
+ );
44567
+ } catch (error2) {
44568
+ let undone = false;
44569
+ try {
44570
+ await _clearPlateLists(camera.site, id, fill);
44571
+ undone = true;
44572
+ } catch {
44573
+ undone = false;
44574
+ }
44575
+ if (error2 && typeof error2 === "object") {
44576
+ error2.message = `${error2.message ?? ""} The camera type has NOT been changed` + (undone ? ` and the plates written to it were cleared again.` : `, and the plates written to it could NOT be cleared again - clear them on the camera by hand.`) + ` Please check the camera and try again.`;
44577
+ }
44578
+ throw error2;
44579
+ }
44580
+ }
43658
44581
  const message = await _updateById(id, update);
43659
44582
  if (staffId) {
43660
44583
  await recordConsoleAction({
@@ -43676,8 +44599,11 @@ function useSiteCameraController() {
43676
44599
  after: pickAuditFields("camera.updated" /* CAMERA_UPDATED */, update)
43677
44600
  });
43678
44601
  }
44602
+ const clearedNote = clearedLists === null ? "" : clearedLists.length === 2 ? " The camera's allowed list and blocklist were cleared." : clearedLists[0] === "blocked" ? " The camera's blocklist was cleared." : " The camera's allowed list was cleared.";
43679
44603
  res.json({
43680
- message
44604
+ message: `${message}${clearedNote}${filled ? filledNote(filled) : ""}`,
44605
+ ...clearedLists === null ? {} : { clearedLists },
44606
+ ...filled ? { addedPlates: filled } : {}
43681
44607
  });
43682
44608
  } catch (error2) {
43683
44609
  next(error2);
@@ -43696,7 +44622,7 @@ function useSiteCameraController() {
43696
44622
  try {
43697
44623
  const camera = await loadForWrite(id);
43698
44624
  if (!camera)
43699
- throw new NotFoundError28("Camera not found.");
44625
+ throw new NotFoundError30("Camera not found.");
43700
44626
  await authorizeWrite({
43701
44627
  req,
43702
44628
  site: camera.site,
@@ -43718,12 +44644,216 @@ function useSiteCameraController() {
43718
44644
  next(error2);
43719
44645
  }
43720
44646
  }
44647
+ const schemaNewCameraDevice = {
44648
+ host: Joi58.string().trim().required(),
44649
+ username: Joi58.string().required(),
44650
+ password: Joi58.string().required(),
44651
+ name: Joi58.string().optional().allow("", null),
44652
+ direction: Joi58.string().required().valid("entry", "exit", "both", "visitors", "residents")
44653
+ };
44654
+ async function authorizeNewCamera(req, siteId, host) {
44655
+ await authorizeWrite({
44656
+ req,
44657
+ site: siteId,
44658
+ type: "anpr",
44659
+ notFound: "Site not found."
44660
+ });
44661
+ const taken = await _activeHostsBySite(siteId);
44662
+ if (taken.includes(host)) {
44663
+ throw new BadRequestError104("ANPR already exist.");
44664
+ }
44665
+ }
44666
+ async function newCameraPlates(req, res, next) {
44667
+ const { error, value } = Joi58.object({
44668
+ siteId: Joi58.string().hex().length(24).required(),
44669
+ host: Joi58.string().trim().required(),
44670
+ direction: schemaNewCameraDevice.direction
44671
+ }).validate({
44672
+ siteId: req.params.siteId,
44673
+ host: req.query?.host,
44674
+ direction: req.query?.direction
44675
+ });
44676
+ if (error) {
44677
+ next(new BadRequestError104(error.message));
44678
+ return;
44679
+ }
44680
+ try {
44681
+ await authorizeNewCamera(req, value.siteId, value.host);
44682
+ const items = await useVehicleService().newCameraPlateRows(
44683
+ value.siteId,
44684
+ value.direction
44685
+ );
44686
+ res.json({ items });
44687
+ } catch (error2) {
44688
+ next(error2);
44689
+ }
44690
+ }
44691
+ async function clearNewCamera(req, res, next) {
44692
+ const { error, value } = Joi58.object({
44693
+ siteId: Joi58.string().hex().length(24).required(),
44694
+ ...schemaNewCameraDevice
44695
+ }).validate({ siteId: req.params.siteId, ...req.body ?? {} });
44696
+ if (error) {
44697
+ next(new BadRequestError104(error.message));
44698
+ return;
44699
+ }
44700
+ try {
44701
+ await authorizeNewCamera(req, value.siteId, value.host);
44702
+ const { siteId, ...device } = value;
44703
+ const cleared = await _clearPlateLists(
44704
+ siteId,
44705
+ device,
44706
+ { allow: true, block: true },
44707
+ "add"
44708
+ );
44709
+ res.json({
44710
+ message: "The camera's allowed list and blocklist were cleared.",
44711
+ clearedLists: cleared
44712
+ });
44713
+ } catch (error2) {
44714
+ next(error2);
44715
+ }
44716
+ }
44717
+ async function addNewCameraPlate(req, res, next) {
44718
+ const { error, value } = Joi58.object({
44719
+ siteId: Joi58.string().hex().length(24).required(),
44720
+ ...schemaNewCameraDevice,
44721
+ vehicle: Joi58.string().hex().length(24).required(),
44722
+ list: Joi58.string().valid("allowed", "blocked").required(),
44723
+ onlyIfMissing: Joi58.boolean().optional().default(false)
44724
+ }).validate({ siteId: req.params.siteId, ...req.body ?? {} });
44725
+ if (error) {
44726
+ next(new BadRequestError104(error.message));
44727
+ return;
44728
+ }
44729
+ try {
44730
+ await authorizeNewCamera(req, value.siteId, value.host);
44731
+ const { siteId, vehicle, list, onlyIfMissing, ...device } = value;
44732
+ const outcome = await useVehicleService().writeVehicleToNewCamera(
44733
+ siteId,
44734
+ device,
44735
+ vehicle,
44736
+ list,
44737
+ { onlyIfMissing }
44738
+ );
44739
+ res.json(outcome);
44740
+ } catch (error2) {
44741
+ next(error2);
44742
+ }
44743
+ }
44744
+ async function loadForSync(req, id) {
44745
+ if (!ObjectId78.isValid(id))
44746
+ throw new NotFoundError30("Camera not found.");
44747
+ const camera = await _findOne(
44748
+ { _id: new ObjectId78(id) },
44749
+ {
44750
+ site: 1,
44751
+ type: 1,
44752
+ host: 1,
44753
+ username: 1,
44754
+ password: 1,
44755
+ name: 1,
44756
+ direction: 1,
44757
+ status: 1
44758
+ }
44759
+ );
44760
+ if (!camera)
44761
+ throw new NotFoundError30("Camera not found.");
44762
+ await authorizeWrite({
44763
+ req,
44764
+ site: camera.site,
44765
+ type: camera.type,
44766
+ notFound: "Camera not found."
44767
+ });
44768
+ if (String(camera.type ?? "").toLowerCase() !== "anpr") {
44769
+ throw new BadRequestError104("Only ANPR cameras hold vehicle plates.");
44770
+ }
44771
+ if (String(camera.status ?? "active") !== "active") {
44772
+ throw new BadRequestError104(
44773
+ "This camera is inactive. Switch it on before adding vehicles to it."
44774
+ );
44775
+ }
44776
+ const lists = listsFilledByDirectionChange("", camera.direction);
44777
+ if (!lists.allow && !lists.block) {
44778
+ throw new BadRequestError104("Exit cameras receive no vehicle plates.");
44779
+ }
44780
+ return {
44781
+ site: String(camera.site ?? ""),
44782
+ device: {
44783
+ host: String(camera.host ?? ""),
44784
+ username: String(camera.username ?? ""),
44785
+ password: String(camera.password ?? ""),
44786
+ name: camera.name ?? "",
44787
+ direction: String(camera.direction ?? "")
44788
+ }
44789
+ };
44790
+ }
44791
+ async function cameraPlates(req, res, next) {
44792
+ try {
44793
+ const { site, device } = await loadForSync(req, String(req.params.id));
44794
+ const items = await useVehicleService().newCameraPlateRows(
44795
+ site,
44796
+ device.direction
44797
+ );
44798
+ res.json({ items });
44799
+ } catch (error) {
44800
+ next(error);
44801
+ }
44802
+ }
44803
+ async function clearCamera(req, res, next) {
44804
+ try {
44805
+ const { site, device } = await loadForSync(req, String(req.params.id));
44806
+ const cleared = await _clearPlateLists(
44807
+ site,
44808
+ device,
44809
+ { allow: true, block: true },
44810
+ "sync"
44811
+ );
44812
+ res.json({
44813
+ message: "The camera's allowed list and blocklist were cleared.",
44814
+ clearedLists: cleared
44815
+ });
44816
+ } catch (error) {
44817
+ next(error);
44818
+ }
44819
+ }
44820
+ async function addCameraPlate(req, res, next) {
44821
+ const { error, value } = Joi58.object({
44822
+ vehicle: Joi58.string().hex().length(24).required(),
44823
+ list: Joi58.string().valid("allowed", "blocked").required(),
44824
+ onlyIfMissing: Joi58.boolean().optional().default(true)
44825
+ }).validate(req.body ?? {});
44826
+ if (error) {
44827
+ next(new BadRequestError104(error.message));
44828
+ return;
44829
+ }
44830
+ try {
44831
+ const { site, device } = await loadForSync(req, String(req.params.id));
44832
+ const outcome = await useVehicleService().writeVehicleToNewCamera(
44833
+ site,
44834
+ device,
44835
+ value.vehicle,
44836
+ value.list,
44837
+ // Absent means look first: a saved camera may already hold the plate.
44838
+ { onlyIfMissing: value.onlyIfMissing !== false }
44839
+ );
44840
+ res.json(outcome);
44841
+ } catch (error2) {
44842
+ next(error2);
44843
+ }
44844
+ }
43721
44845
  return {
43722
44846
  add,
43723
44847
  addBulk,
43724
44848
  getAll,
43725
44849
  updateById,
43726
- deleteById
44850
+ deleteById,
44851
+ newCameraPlates,
44852
+ clearNewCamera,
44853
+ addNewCameraPlate,
44854
+ cameraPlates,
44855
+ clearCamera,
44856
+ addCameraPlate
43727
44857
  };
43728
44858
  }
43729
44859
 
@@ -43888,7 +45018,7 @@ function useCameraViewController() {
43888
45018
  import {
43889
45019
  BadRequestError as BadRequestError106,
43890
45020
  useAtlas as useAtlas62,
43891
- NotFoundError as NotFoundError29
45021
+ NotFoundError as NotFoundError31
43892
45022
  } from "@7365admin1/node-server-utils";
43893
45023
 
43894
45024
  // src/utils/site-name.util.ts
@@ -43995,7 +45125,7 @@ function useCustomerSiteService() {
43995
45125
  const siteIdStr = siteId.toString();
43996
45126
  const orgData = await _getByOrgId(value.siteOrg);
43997
45127
  if (!orgData) {
43998
- throw new NotFoundError29("Organization not found.");
45128
+ throw new NotFoundError31("Organization not found.");
43999
45129
  }
44000
45130
  if (!orgData.defaultSite) {
44001
45131
  const updateOrgField = await _updateFieldById(
@@ -44029,7 +45159,7 @@ function useCustomerSiteService() {
44029
45159
  try {
44030
45160
  const verification = await getVerificationById(invite);
44031
45161
  if (!verification) {
44032
- throw new NotFoundError29("Invite not found.");
45162
+ throw new NotFoundError31("Invite not found.");
44033
45163
  }
44034
45164
  const siteId = verification.metadata?.siteId?.toString() ?? "";
44035
45165
  if (!siteId) {
@@ -44041,17 +45171,17 @@ function useCustomerSiteService() {
44041
45171
  }
44042
45172
  const org = await _getByOrgId(orgId);
44043
45173
  if (!org) {
44044
- throw new NotFoundError29("Organization not found.");
45174
+ throw new NotFoundError31("Organization not found.");
44045
45175
  }
44046
45176
  const site = await getSiteById(siteId);
44047
45177
  if (!site) {
44048
- throw new NotFoundError29("Site not found.");
45178
+ throw new NotFoundError31("Site not found.");
44049
45179
  }
44050
45180
  const serviceProvider = await getOrgByEmail(
44051
45181
  String(verification.email ?? "").trim().toLowerCase()
44052
45182
  );
44053
45183
  if (!serviceProvider) {
44054
- throw new NotFoundError29("Organization not found.");
45184
+ throw new NotFoundError31("Organization not found.");
44055
45185
  }
44056
45186
  const providerOrgId = serviceProvider._id?.toString() ?? "";
44057
45187
  const alreadyEngaged = await _countActiveByOrgAndSite(
@@ -44066,7 +45196,7 @@ function useCustomerSiteService() {
44066
45196
  return "This site is already on your list.";
44067
45197
  }
44068
45198
  if (verification.status === "awaiting-approval") {
44069
- throw new NotFoundError29("Invite not found.");
45199
+ throw new NotFoundError31("Invite not found.");
44070
45200
  }
44071
45201
  if (verification.status && verification.status !== "pending") {
44072
45202
  throw new BadRequestError106(
@@ -44112,7 +45242,7 @@ function useCustomerSiteService() {
44112
45242
  try {
44113
45243
  const customerSite = await _getById(customerSiteId);
44114
45244
  if (!customerSite) {
44115
- throw new NotFoundError29("Customer site not found.");
45245
+ throw new NotFoundError31("Customer site not found.");
44116
45246
  }
44117
45247
  const siteId = customerSite.site;
44118
45248
  await updateSiteById(
@@ -44442,7 +45572,7 @@ import {
44442
45572
  useCache as useCache34,
44443
45573
  logger as logger83,
44444
45574
  makeCacheKey as makeCacheKey29,
44445
- NotFoundError as NotFoundError30
45575
+ NotFoundError as NotFoundError32
44446
45576
  } from "@7365admin1/node-server-utils";
44447
45577
  function useAttendanceSettingsRepository() {
44448
45578
  const db2 = useAtlas63.getDb();
@@ -44544,7 +45674,7 @@ function useAttendanceSettingsRepository() {
44544
45674
  { session }
44545
45675
  );
44546
45676
  if (res.matchedCount === 0) {
44547
- throw new NotFoundError30("Attendance settings not found for this site.");
45677
+ throw new NotFoundError32("Attendance settings not found for this site.");
44548
45678
  }
44549
45679
  delNamespace().then(() => {
44550
45680
  logger83.info(`Cache cleared for namespace: ${namespace_collection3}`);
@@ -44729,7 +45859,7 @@ import {
44729
45859
  useCache as useCache35,
44730
45860
  logger as logger87,
44731
45861
  makeCacheKey as makeCacheKey30,
44732
- NotFoundError as NotFoundError31
45862
+ NotFoundError as NotFoundError33
44733
45863
  } from "@7365admin1/node-server-utils";
44734
45864
  function useAttendanceRepository() {
44735
45865
  const db2 = useAtlas65.getDb();
@@ -45074,7 +46204,7 @@ function useAttendanceRepository() {
45074
46204
  }
45075
46205
  ]).toArray();
45076
46206
  if (!data || data.length === 0) {
45077
- throw new NotFoundError31("Attendance not found.");
46207
+ throw new NotFoundError33("Attendance not found.");
45078
46208
  }
45079
46209
  setCache(cacheKey, data[0], 15 * 60).then(() => {
45080
46210
  logger87.info(`Cache set for key: ${cacheKey}`);
@@ -45108,7 +46238,7 @@ function useAttendanceRepository() {
45108
46238
  try {
45109
46239
  const data = await collection.aggregate([{ $match: query2 }]).toArray();
45110
46240
  if (!data || data.length === 0) {
45111
- throw new NotFoundError31("Attendance not found.");
46241
+ throw new NotFoundError33("Attendance not found.");
45112
46242
  }
45113
46243
  setCache(cacheKey, data[0], 15 * 60).then(() => {
45114
46244
  logger87.info(`Cache set for key: ${cacheKey}`);
@@ -45539,7 +46669,7 @@ import { ObjectId as ObjectId90 } from "mongodb";
45539
46669
  import {
45540
46670
  BadRequestError as BadRequestError118,
45541
46671
  logger as logger90,
45542
- NotFoundError as NotFoundError34,
46672
+ NotFoundError as NotFoundError36,
45543
46673
  useAtlas as useAtlas68,
45544
46674
  hashPassword as hashPassword3,
45545
46675
  useMailer as useMailer3,
@@ -45552,14 +46682,14 @@ import { ObjectId as ObjectId85 } from "mongodb";
45552
46682
  import {
45553
46683
  BadRequestError as BadRequestError117,
45554
46684
  InternalServerError as InternalServerError42,
45555
- NotFoundError as NotFoundError33
46685
+ NotFoundError as NotFoundError35
45556
46686
  } from "@7365admin1/node-server-utils";
45557
46687
 
45558
46688
  // src/repositories/platform-terms.repo.ts
45559
46689
  import {
45560
46690
  BadRequestError as BadRequestError116,
45561
46691
  InternalServerError as InternalServerError41,
45562
- NotFoundError as NotFoundError32,
46692
+ NotFoundError as NotFoundError34,
45563
46693
  useAtlas as useAtlas67
45564
46694
  } from "@7365admin1/node-server-utils";
45565
46695
  import { ObjectId as ObjectId84 } from "mongodb";
@@ -45658,7 +46788,7 @@ function usePlatformTermsRepo() {
45658
46788
  try {
45659
46789
  _id = new ObjectId84(id);
45660
46790
  } catch {
45661
- throw new NotFoundError32("Terms version not found.");
46791
+ throw new NotFoundError34("Terms version not found.");
45662
46792
  }
45663
46793
  let data;
45664
46794
  try {
@@ -45667,7 +46797,7 @@ function usePlatformTermsRepo() {
45667
46797
  throw new InternalServerError41("Failed to retrieve terms version.");
45668
46798
  }
45669
46799
  if (!data) {
45670
- throw new NotFoundError32("Terms version not found.");
46800
+ throw new NotFoundError34("Terms version not found.");
45671
46801
  }
45672
46802
  return data;
45673
46803
  }
@@ -45680,7 +46810,7 @@ function usePlatformTermsRepo() {
45680
46810
  throw new InternalServerError41("Failed to retrieve latest terms.");
45681
46811
  }
45682
46812
  if (!data) {
45683
- throw new NotFoundError32("No terms version found.");
46813
+ throw new NotFoundError34("No terms version found.");
45684
46814
  }
45685
46815
  return data;
45686
46816
  }
@@ -45777,14 +46907,14 @@ function usePlatformTermsService() {
45777
46907
  const latest = await _getLatest();
45778
46908
  const record = await _getAcceptedTerms(userId);
45779
46909
  if (!record) {
45780
- throw new NotFoundError33("User not found.");
46910
+ throw new NotFoundError35("User not found.");
45781
46911
  }
45782
46912
  return buildStatus(latest, record);
45783
46913
  }
45784
46914
  async function acceptByUserId(userId, termsId, session) {
45785
46915
  const latest = await assertLatestTerms(termsId);
45786
46916
  if (!await _getAcceptedTerms(userId, session)) {
45787
- throw new NotFoundError33("User not found.");
46917
+ throw new NotFoundError35("User not found.");
45788
46918
  }
45789
46919
  await _setAcceptedTerms(userId, latest._id, session);
45790
46920
  return { acceptedTerms: latest._id, version: latest.version };
@@ -45792,7 +46922,7 @@ function usePlatformTermsService() {
45792
46922
  async function resolveTermsPrincipal(personId) {
45793
46923
  const person = await _getPersonById(personId);
45794
46924
  if (!person || person.status === "deleted") {
45795
- throw new NotFoundError33("Person not found.");
46925
+ throw new NotFoundError35("Person not found.");
45796
46926
  }
45797
46927
  return person.user ? { userId: person.user } : { userId: null };
45798
46928
  }
@@ -45804,7 +46934,7 @@ function usePlatformTermsService() {
45804
46934
  const latest = await _getLatest();
45805
46935
  const record = await _getPersonAcceptedTerms(personId);
45806
46936
  if (!record) {
45807
- throw new NotFoundError33("Person not found.");
46937
+ throw new NotFoundError35("Person not found.");
45808
46938
  }
45809
46939
  return buildStatus(latest, record);
45810
46940
  }
@@ -45827,7 +46957,7 @@ function usePlatformTermsService() {
45827
46957
  if (version) {
45828
46958
  const found = await _getByVersion(version);
45829
46959
  if (!found) {
45830
- throw new NotFoundError33("Terms version not found.");
46960
+ throw new NotFoundError35("Terms version not found.");
45831
46961
  }
45832
46962
  terms = found._id;
45833
46963
  }
@@ -45920,7 +47050,7 @@ function usePersonService() {
45920
47050
  session
45921
47051
  );
45922
47052
  if (!file) {
45923
- throw new NotFoundError34("File not found.");
47053
+ throw new NotFoundError36("File not found.");
45924
47054
  }
45925
47055
  }
45926
47056
  }
@@ -46274,7 +47404,7 @@ function usePersonService() {
46274
47404
  session.startTransaction();
46275
47405
  const person = await _getByUserId(id.toString());
46276
47406
  if (!person)
46277
- throw new NotFoundError34("Person not found.");
47407
+ throw new NotFoundError36("Person not found.");
46278
47408
  await _updateById(person._id, { status: "deleted" }, session);
46279
47409
  if (person.user) {
46280
47410
  await _updateUserFieldById(
@@ -47377,7 +48507,7 @@ import axios from "axios";
47377
48507
  import {
47378
48508
  BadRequestError as BadRequestError121,
47379
48509
  InternalServerError as InternalServerError44,
47380
- NotFoundError as NotFoundError35,
48510
+ NotFoundError as NotFoundError37,
47381
48511
  UnauthorizedError as UnauthorizedError14,
47382
48512
  logger as logger93
47383
48513
  } from "@7365admin1/node-server-utils";
@@ -50932,14 +52062,14 @@ function useHidAmicoService() {
50932
52062
  try {
50933
52063
  reader = await repo.getById(readerId, options);
50934
52064
  } catch {
50935
- throw new NotFoundError35(HID_READER_NOT_FOUND);
52065
+ throw new NotFoundError37(HID_READER_NOT_FOUND);
50936
52066
  }
50937
52067
  try {
50938
52068
  await authorizeSiteForCaller(String(reader.site || ""), userId);
50939
52069
  } catch (error) {
50940
52070
  if (error instanceof UnauthorizedError14)
50941
52071
  throw error;
50942
- throw new NotFoundError35(HID_READER_NOT_FOUND);
52072
+ throw new NotFoundError37(HID_READER_NOT_FOUND);
50943
52073
  }
50944
52074
  return reader;
50945
52075
  }
@@ -50948,7 +52078,7 @@ function useHidAmicoService() {
50948
52078
  try {
50949
52079
  reader = await repo.getById(readerId);
50950
52080
  } catch {
50951
- throw new NotFoundError35(HID_READER_NOT_FOUND);
52081
+ throw new NotFoundError37(HID_READER_NOT_FOUND);
50952
52082
  }
50953
52083
  try {
50954
52084
  await useCameraViewService().entitleSite({
@@ -50956,7 +52086,7 @@ function useHidAmicoService() {
50956
52086
  userId
50957
52087
  });
50958
52088
  } catch {
50959
- throw new NotFoundError35(HID_READER_NOT_FOUND);
52089
+ throw new NotFoundError37(HID_READER_NOT_FOUND);
50960
52090
  }
50961
52091
  return reader;
50962
52092
  }
@@ -54926,7 +56056,7 @@ function useVisitorTransactionService() {
54926
56056
  import Joi69 from "joi";
54927
56057
  import moment5 from "moment-timezone";
54928
56058
  import { ObjectId as ObjectId93 } from "mongodb";
54929
- import { BadRequestError as BadRequestError124, NotFoundError as NotFoundError37, InternalServerError as InternalServerError46, UnauthorizedError as UnauthorizedError15, logger as logger97 } from "@7365admin1/node-server-utils";
56059
+ import { BadRequestError as BadRequestError124, NotFoundError as NotFoundError39, InternalServerError as InternalServerError46, UnauthorizedError as UnauthorizedError15, logger as logger97 } from "@7365admin1/node-server-utils";
54930
56060
 
54931
56061
  // src/models/visitor-invite.model.ts
54932
56062
  import Joi67 from "joi";
@@ -59181,7 +60311,7 @@ function useVisitorTransactionController() {
59181
60311
  const status = onboardingSettings?.enabled ? "approved" /* APPROVED */ : "pending" /* PENDING */;
59182
60312
  const site = await _getSiteById(siteId);
59183
60313
  if (!site) {
59184
- next(new NotFoundError37("Site not found."));
60314
+ next(new NotFoundError39("Site not found."));
59185
60315
  return;
59186
60316
  }
59187
60317
  const { error, value } = schemaSelfServiceVisitor.validate(req.body, {
@@ -59280,7 +60410,7 @@ function useVisitorTransactionController() {
59280
60410
  }
59281
60411
  const data = await _getSelfServiceVisitorPreview(id);
59282
60412
  if (!data) {
59283
- next(new NotFoundError37("Registration not found."));
60413
+ next(new NotFoundError39("Registration not found."));
59284
60414
  return;
59285
60415
  }
59286
60416
  res.status(200).json({
@@ -59337,7 +60467,7 @@ function useVisitorTransactionController() {
59337
60467
  });
59338
60468
  if (!decision.ok) {
59339
60469
  if (decision.httpStatus === 404) {
59340
- next(new NotFoundError37(decision.reason));
60470
+ next(new NotFoundError39(decision.reason));
59341
60471
  return;
59342
60472
  }
59343
60473
  if (decision.httpStatus === 429) {
@@ -60277,7 +61407,22 @@ function useGuestManagementController() {
60277
61407
  // src/controllers/person.controller.ts
60278
61408
  import Joi72 from "joi";
60279
61409
  import { BadRequestError as BadRequestError127, logger as logger100 } from "@7365admin1/node-server-utils";
60280
- import { NotFoundError as NotFoundError38, UnauthorizedError as UnauthorizedError16 } from "@7365admin1/node-server-utils";
61410
+ import { NotFoundError as NotFoundError40, UnauthorizedError as UnauthorizedError16 } from "@7365admin1/node-server-utils";
61411
+
61412
+ // src/utils/people-create-gate.util.ts
61413
+ var PEOPLE_CREATE_REACH_MODE_DEFAULT = "log";
61414
+ function peopleCreateReachMode(env2 = process.env) {
61415
+ return gateMode(
61416
+ env2.PEOPLE_CREATE_REACH_MODE,
61417
+ ["log", "enforce", "off"],
61418
+ PEOPLE_CREATE_REACH_MODE_DEFAULT
61419
+ );
61420
+ }
61421
+ function peopleCreateVerdict(params) {
61422
+ if (params.mode === "off" || params.reachable)
61423
+ return { refuse: false, record: false };
61424
+ return { refuse: params.mode === "enforce", record: true };
61425
+ }
60281
61426
 
60282
61427
  // src/utils/self-signup-files.util.ts
60283
61428
  function dropUnsentFiles(body) {
@@ -60321,7 +61466,7 @@ function usePersonController() {
60321
61466
  return actor;
60322
61467
  const person = await _getById(_id);
60323
61468
  if (!person)
60324
- throw new NotFoundError38("Person not found.");
61469
+ throw new NotFoundError40("Person not found.");
60325
61470
  const site = person.site?.toString() ?? "";
60326
61471
  if (site) {
60327
61472
  await useCameraViewService().entitleSite({ siteId: site, userId: actor.id });
@@ -60387,6 +61532,41 @@ function usePersonController() {
60387
61532
  next(new BadRequestError127(messages));
60388
61533
  return;
60389
61534
  }
61535
+ try {
61536
+ const mode = peopleCreateReachMode();
61537
+ if (mode !== "off") {
61538
+ const actor = await resolveInviteActor(callerId(req));
61539
+ let reachable = true;
61540
+ if (!staffMayBypass(actor)) {
61541
+ const site = payload?.site ? String(payload.site) : "";
61542
+ const org = payload?.org ? String(payload.org) : "";
61543
+ if (site) {
61544
+ reachable = await useCameraViewService().entitleSite({ siteId: site, userId: actor.id }).then(
61545
+ () => true,
61546
+ () => false
61547
+ );
61548
+ } else if (org) {
61549
+ reachable = actor.orgIds.includes(org);
61550
+ }
61551
+ }
61552
+ const verdict = peopleCreateVerdict({ mode, reachable });
61553
+ if (verdict.record) {
61554
+ logger100.log({
61555
+ level: "info",
61556
+ message: `[people-create] ${verdict.refuse ? "refused" : "would-refuse"} caller=${actor.id} site=${payload?.site ?? ""} org=${payload?.org ?? ""} mode=${mode}`
61557
+ });
61558
+ }
61559
+ if (verdict.refuse) {
61560
+ next(new UnauthorizedError16("Not authorized."));
61561
+ return;
61562
+ }
61563
+ }
61564
+ } catch (gateError) {
61565
+ logger100.log({
61566
+ level: "warn",
61567
+ message: `[people-create] reach check failed open: ${gateError?.message}`
61568
+ });
61569
+ }
60390
61570
  try {
60391
61571
  const data = await _add(payload);
60392
61572
  res.status(201).json({ message: data });
@@ -61714,10 +62894,10 @@ import {
61714
62894
  import { ObjectId as ObjectId101 } from "mongodb";
61715
62895
 
61716
62896
  // src/utils/update-result.util.ts
61717
- import { NotFoundError as NotFoundError39 } from "@7365admin1/node-server-utils";
62897
+ import { NotFoundError as NotFoundError41 } from "@7365admin1/node-server-utils";
61718
62898
  function assertUpdateMatched(result, message) {
61719
62899
  if (result?.matchedCount === 0) {
61720
- throw new NotFoundError39(message);
62900
+ throw new NotFoundError41(message);
61721
62901
  }
61722
62902
  return result;
61723
62903
  }
@@ -62879,7 +64059,7 @@ function usePatrolLogRepo() {
62879
64059
  import {
62880
64060
  BadRequestError as BadRequestError140,
62881
64061
  logger as logger112,
62882
- NotFoundError as NotFoundError40,
64062
+ NotFoundError as NotFoundError42,
62883
64063
  useAtlas as useAtlas78
62884
64064
  } from "@7365admin1/node-server-utils";
62885
64065
  function patrolProblem(status) {
@@ -62919,7 +64099,7 @@ function usePatrolLogService() {
62919
64099
  session
62920
64100
  );
62921
64101
  if (file.matchedCount === 0) {
62922
- throw new NotFoundError40(`File not found: ${fileId}`);
64102
+ throw new NotFoundError42(`File not found: ${fileId}`);
62923
64103
  }
62924
64104
  }
62925
64105
  }
@@ -63284,7 +64464,7 @@ import {
63284
64464
  BadRequestError as BadRequestError143,
63285
64465
  InternalServerError as InternalServerError52,
63286
64466
  logger as logger114,
63287
- NotFoundError as NotFoundError41,
64467
+ NotFoundError as NotFoundError43,
63288
64468
  paginate as paginate36,
63289
64469
  useAtlas as useAtlas79
63290
64470
  } from "@7365admin1/node-server-utils";
@@ -63474,7 +64654,7 @@ function usePatrolEmailRepo() {
63474
64654
  throw new InternalServerError52("Failed to retrieve the patrol email.");
63475
64655
  }
63476
64656
  if (!data) {
63477
- throw new NotFoundError41("Patrol email not found.");
64657
+ throw new NotFoundError43("Patrol email not found.");
63478
64658
  }
63479
64659
  const [decorated] = await withResendState([data], session);
63480
64660
  return decorated;
@@ -63489,7 +64669,7 @@ function usePatrolEmailRepo() {
63489
64669
  { session }
63490
64670
  );
63491
64671
  if (result.matchedCount === 0) {
63492
- throw new NotFoundError41("Patrol email not found.");
64672
+ throw new NotFoundError43("Patrol email not found.");
63493
64673
  }
63494
64674
  return result;
63495
64675
  } catch (error) {
@@ -63518,7 +64698,7 @@ import {
63518
64698
  compileHandlebar as compileHandlebar5,
63519
64699
  getDirectory as getDirectory5,
63520
64700
  logger as logger116,
63521
- NotFoundError as NotFoundError42,
64701
+ NotFoundError as NotFoundError44,
63522
64702
  useMailer as useMailer5
63523
64703
  } from "@7365admin1/node-server-utils";
63524
64704
 
@@ -63713,7 +64893,7 @@ function usePatrolEmailService() {
63713
64893
  for (const id of logIds) {
63714
64894
  const log = await _getPatrolLogById(id);
63715
64895
  if (!log) {
63716
- throw new NotFoundError42("Patrol log not found.");
64896
+ throw new NotFoundError44("Patrol log not found.");
63717
64897
  }
63718
64898
  if (String(log.site) !== String(site)) {
63719
64899
  throw new BadRequestError146(
@@ -63763,7 +64943,7 @@ function usePatrolEmailService() {
63763
64943
  async function claimUploadedPdf(fileId) {
63764
64944
  const file = await _getFileById(fileId);
63765
64945
  if (!file) {
63766
- throw new NotFoundError42("Uploaded PDF not found.");
64946
+ throw new NotFoundError44("Uploaded PDF not found.");
63767
64947
  }
63768
64948
  if (!isClaimableUpload(file)) {
63769
64949
  throw new BadRequestError146(
@@ -63772,7 +64952,7 @@ function usePatrolEmailService() {
63772
64952
  }
63773
64953
  const promoted = await _updateFileStatusById(fileId, { status: "active" });
63774
64954
  if (promoted.matchedCount === 0) {
63775
- throw new NotFoundError42("Uploaded PDF not found.");
64955
+ throw new NotFoundError44("Uploaded PDF not found.");
63776
64956
  }
63777
64957
  return {
63778
64958
  file: fileId,
@@ -63781,7 +64961,7 @@ function usePatrolEmailService() {
63781
64961
  };
63782
64962
  }
63783
64963
  function sanitizeError(error) {
63784
- if (error instanceof BadRequestError146 || error instanceof NotFoundError42) {
64964
+ if (error instanceof BadRequestError146 || error instanceof NotFoundError44) {
63785
64965
  return error.message;
63786
64966
  }
63787
64967
  return "Failed to send the patrol log email.";
@@ -64382,7 +65562,7 @@ import {
64382
65562
  InternalServerError as InternalServerError53,
64383
65563
  logger as logger118,
64384
65564
  makeCacheKey as makeCacheKey38,
64385
- NotFoundError as NotFoundError43,
65565
+ NotFoundError as NotFoundError45,
64386
65566
  paginate as paginate37,
64387
65567
  useAtlas as useAtlas80,
64388
65568
  useCache as useCache43
@@ -64513,7 +65693,7 @@ function useSiteFacilityRepo() {
64513
65693
  try {
64514
65694
  const data = await collection.findOne({ _id }, { session });
64515
65695
  if (!data) {
64516
- throw new NotFoundError43("Site facility not found.");
65696
+ throw new NotFoundError45("Site facility not found.");
64517
65697
  }
64518
65698
  setCache(cacheKey, data, 15 * 60).then(() => {
64519
65699
  logger118.info(`Cache set for key: ${cacheKey}`);
@@ -64638,7 +65818,7 @@ function useSiteFacilityRepo() {
64638
65818
  try {
64639
65819
  const data = await collection.findOne(query2, { session });
64640
65820
  if (!data) {
64641
- throw new NotFoundError43("Site facility not found.");
65821
+ throw new NotFoundError45("Site facility not found.");
64642
65822
  }
64643
65823
  setCache(cacheKey, data, 15 * 60).then(() => {
64644
65824
  logger118.info(`Cache set for key: ${cacheKey}`);
@@ -64779,7 +65959,7 @@ function useSiteFacilityService() {
64779
65959
  // src/controllers/site-facility.controller.ts
64780
65960
  import {
64781
65961
  BadRequestError as BadRequestError149,
64782
- NotFoundError as NotFoundError44,
65962
+ NotFoundError as NotFoundError46,
64783
65963
  UnauthorizedError as UnauthorizedError17,
64784
65964
  logger as logger119
64785
65965
  } from "@7365admin1/node-server-utils";
@@ -64799,7 +65979,7 @@ function useSiteFacilityController() {
64799
65979
  const facility = await _getSiteFacilityById(id);
64800
65980
  const { canReach } = await siteReachOf(req);
64801
65981
  if (!await canReach(facility)) {
64802
- throw new NotFoundError44("Site facility not found.");
65982
+ throw new NotFoundError46("Site facility not found.");
64803
65983
  }
64804
65984
  return facility;
64805
65985
  }
@@ -65064,7 +66244,7 @@ import {
65064
66244
  InternalServerError as InternalServerError54,
65065
66245
  logger as logger120,
65066
66246
  makeCacheKey as makeCacheKey39,
65067
- NotFoundError as NotFoundError45,
66247
+ NotFoundError as NotFoundError47,
65068
66248
  paginate as paginate38,
65069
66249
  useAtlas as useAtlas82,
65070
66250
  useCache as useCache44
@@ -65179,7 +66359,7 @@ function useServiceProviderBillingRepo() {
65179
66359
  try {
65180
66360
  const data = await collection.findOne({ _id }, { session });
65181
66361
  if (!data) {
65182
- throw new NotFoundError45("Service provider billing not found.");
66362
+ throw new NotFoundError47("Service provider billing not found.");
65183
66363
  }
65184
66364
  setCache(cacheKey, data, 15 * 60).then(() => {
65185
66365
  logger120.info(`Cache set for key: ${cacheKey}`);
@@ -65734,7 +66914,7 @@ import {
65734
66914
  InternalServerError as InternalServerError55,
65735
66915
  logger as logger122,
65736
66916
  makeCacheKey as makeCacheKey40,
65737
- NotFoundError as NotFoundError46,
66917
+ NotFoundError as NotFoundError48,
65738
66918
  paginate as paginate39,
65739
66919
  toObjectId as toObjectId16,
65740
66920
  useAtlas as useAtlas84,
@@ -65850,7 +67030,7 @@ function useSiteFacilityBookingRepo() {
65850
67030
  try {
65851
67031
  const data = await collection.findOne({ _id }, { session });
65852
67032
  if (!data) {
65853
- throw new NotFoundError46("Site facility booking not found.");
67033
+ throw new NotFoundError48("Site facility booking not found.");
65854
67034
  }
65855
67035
  setCache(cacheKey, data, 15 * 60).then(() => {
65856
67036
  logger122.info(`Cache set for key: ${cacheKey}`);
@@ -65993,7 +67173,7 @@ function useSiteFacilityBookingService() {
65993
67173
  // src/controllers/site-facility-booking.controller.ts
65994
67174
  import {
65995
67175
  BadRequestError as BadRequestError153,
65996
- NotFoundError as NotFoundError47,
67176
+ NotFoundError as NotFoundError49,
65997
67177
  logger as logger123
65998
67178
  } from "@7365admin1/node-server-utils";
65999
67179
  import Joi88 from "joi";
@@ -66011,7 +67191,7 @@ function useSiteFacilityBookingController() {
66011
67191
  const booking = await _getSiteFacilityBookingById(id);
66012
67192
  const { canReach } = await siteReachOf(req);
66013
67193
  if (!await canReach(booking)) {
66014
- throw new NotFoundError47("Site facility booking not found.");
67194
+ throw new NotFoundError49("Site facility booking not found.");
66015
67195
  }
66016
67196
  return booking;
66017
67197
  }
@@ -66303,7 +67483,7 @@ import {
66303
67483
  InternalServerError as InternalServerError56,
66304
67484
  logger as logger124,
66305
67485
  makeCacheKey as makeCacheKey41,
66306
- NotFoundError as NotFoundError48,
67486
+ NotFoundError as NotFoundError50,
66307
67487
  paginate as paginate40,
66308
67488
  useAtlas as useAtlas86,
66309
67489
  useCache as useCache46
@@ -66477,7 +67657,7 @@ function useDocumentManagementRepo() {
66477
67657
  }
66478
67658
  ]).toArray();
66479
67659
  if (!data || !data.length) {
66480
- throw new NotFoundError48("Document not found.");
67660
+ throw new NotFoundError50("Document not found.");
66481
67661
  }
66482
67662
  setCache(cacheKey, data[0], 15 * 60).then(() => {
66483
67663
  logger124.info(`Cache set for key: ${cacheKey}`);
@@ -66550,7 +67730,7 @@ function useDocumentManagementRepo() {
66550
67730
  { $set: updateValue }
66551
67731
  );
66552
67732
  if (res.matchedCount === 0) {
66553
- throw new NotFoundError48("Folder not found.");
67733
+ throw new NotFoundError50("Folder not found.");
66554
67734
  }
66555
67735
  delNamespace().then(() => {
66556
67736
  logger124.info(`Cache cleared for namespace: ${namespace_collection3}`);
@@ -66640,7 +67820,7 @@ function useDocumentManagementRepo() {
66640
67820
  session ? { session } : void 0
66641
67821
  ).toArray();
66642
67822
  if (!folders || folders.length === 0) {
66643
- throw new NotFoundError48("Folder not found.");
67823
+ throw new NotFoundError50("Folder not found.");
66644
67824
  }
66645
67825
  const ids2 = folders[0].ids;
66646
67826
  const res = await collection.updateMany(
@@ -66969,7 +68149,7 @@ function useDocumentManagementService() {
66969
68149
  import {
66970
68150
  BadRequestError as BadRequestError156,
66971
68151
  logger as logger126,
66972
- NotFoundError as NotFoundError49
68152
+ NotFoundError as NotFoundError51
66973
68153
  } from "@7365admin1/node-server-utils";
66974
68154
  import Joi90 from "joi";
66975
68155
  function useDocumentManagementController() {
@@ -66995,7 +68175,7 @@ function useDocumentManagementController() {
66995
68175
  return;
66996
68176
  const org = doc?.org?.toString() ?? "";
66997
68177
  if (!org || !actor.orgIds.includes(org)) {
66998
- throw new NotFoundError49("Document not found.");
68178
+ throw new NotFoundError51("Document not found.");
66999
68179
  }
67000
68180
  }
67001
68181
  async function add(req, res, next) {
@@ -67403,7 +68583,7 @@ import {
67403
68583
  InternalServerError as InternalServerError57,
67404
68584
  logger as logger127,
67405
68585
  makeCacheKey as makeCacheKey42,
67406
- NotFoundError as NotFoundError50,
68586
+ NotFoundError as NotFoundError52,
67407
68587
  paginate as paginate41,
67408
68588
  useAtlas as useAtlas88,
67409
68589
  useCache as useCache47
@@ -67539,7 +68719,7 @@ function useBulletinBoardRepo() {
67539
68719
  try {
67540
68720
  const data = await collection.findOne({ _id }, { session });
67541
68721
  if (!data) {
67542
- throw new NotFoundError50("Bulletin board not found.");
68722
+ throw new NotFoundError52("Bulletin board not found.");
67543
68723
  }
67544
68724
  setCache(cacheKey, data, 15 * 60).then(() => {
67545
68725
  logger127.info(`Cache set for key: ${cacheKey}`);
@@ -67638,7 +68818,7 @@ function useBulletinBoardRepo() {
67638
68818
  { session }
67639
68819
  );
67640
68820
  if (res.matchedCount === 0) {
67641
- throw new NotFoundError50("No bulletin boards found to update.");
68821
+ throw new NotFoundError52("No bulletin boards found to update.");
67642
68822
  }
67643
68823
  delNamespace().then(() => {
67644
68824
  logger127.info(
@@ -67673,7 +68853,7 @@ function useBulletinBoardRepo() {
67673
68853
  { session }
67674
68854
  );
67675
68855
  if (res.matchedCount === 0) {
67676
- throw new NotFoundError50(
68856
+ throw new NotFoundError52(
67677
68857
  "No upcoming bulletin boards found to activate."
67678
68858
  );
67679
68859
  }
@@ -67705,7 +68885,7 @@ function useBulletinBoardRepo() {
67705
68885
  }
67706
68886
 
67707
68887
  // src/services/bulletin-board.service.ts
67708
- import { useAtlas as useAtlas89, NotFoundError as NotFoundError51 } from "@7365admin1/node-server-utils";
68888
+ import { useAtlas as useAtlas89, NotFoundError as NotFoundError53 } from "@7365admin1/node-server-utils";
67709
68889
  import { ObjectId as ObjectId117 } from "mongodb";
67710
68890
 
67711
68891
  // src/utils/bulletin-service-apps.util.ts
@@ -67752,7 +68932,7 @@ function useBulletinBoardService() {
67752
68932
  session
67753
68933
  );
67754
68934
  if (!file) {
67755
- throw new NotFoundError51("File not found.");
68935
+ throw new NotFoundError53("File not found.");
67756
68936
  }
67757
68937
  }
67758
68938
  }
@@ -67901,7 +69081,7 @@ function useBulletinBoardService() {
67901
69081
  // src/controllers/bulletin-board.controller.ts
67902
69082
  import {
67903
69083
  BadRequestError as BadRequestError158,
67904
- NotFoundError as NotFoundError52,
69084
+ NotFoundError as NotFoundError54,
67905
69085
  UnauthorizedError as UnauthorizedError18,
67906
69086
  logger as logger128
67907
69087
  } from "@7365admin1/node-server-utils";
@@ -67918,7 +69098,7 @@ function useBulletinBoardController() {
67918
69098
  const bulletin = await _getBulletinBoardById(id);
67919
69099
  const { canReach } = await siteReachOf(req);
67920
69100
  if (!await canReach(scopeOf(bulletin))) {
67921
- throw new NotFoundError52("Bulletin board not found.");
69101
+ throw new NotFoundError54("Bulletin board not found.");
67922
69102
  }
67923
69103
  return bulletin;
67924
69104
  }
@@ -69498,7 +70678,7 @@ import {
69498
70678
  InternalServerError as InternalServerError60,
69499
70679
  logger as logger137,
69500
70680
  makeCacheKey as makeCacheKey45,
69501
- NotFoundError as NotFoundError55,
70681
+ NotFoundError as NotFoundError57,
69502
70682
  paginate as paginate44,
69503
70683
  useAtlas as useAtlas94,
69504
70684
  useCache as useCache50
@@ -69672,7 +70852,7 @@ function useEventManagementRepo() {
69672
70852
  try {
69673
70853
  const data = await collection.findOne({ _id }, { session });
69674
70854
  if (!data) {
69675
- throw new NotFoundError55("Event not found.");
70855
+ throw new NotFoundError57("Event not found.");
69676
70856
  }
69677
70857
  setCache(cacheKey, data, 15 * 60).then(() => {
69678
70858
  logger137.info(`Cache set for key: ${cacheKey}`);
@@ -69769,7 +70949,7 @@ function useEventManagementRepo() {
69769
70949
  { session }
69770
70950
  );
69771
70951
  if (res.matchedCount === 0) {
69772
- throw new NotFoundError55("No bulletin boards found to update.");
70952
+ throw new NotFoundError57("No bulletin boards found to update.");
69773
70953
  }
69774
70954
  delNamespace().then(() => {
69775
70955
  logger137.info(
@@ -69881,7 +71061,7 @@ function useEventManagementService() {
69881
71061
  // src/controllers/event-management.controller.ts
69882
71062
  import {
69883
71063
  BadRequestError as BadRequestError168,
69884
- NotFoundError as NotFoundError56,
71064
+ NotFoundError as NotFoundError58,
69885
71065
  logger as logger138
69886
71066
  } from "@7365admin1/node-server-utils";
69887
71067
  import Joi98 from "joi";
@@ -69896,7 +71076,7 @@ function useEventManagementController() {
69896
71076
  const event = await _getEventManagementById(id);
69897
71077
  const { canReach } = await siteReachOf(req);
69898
71078
  if (!await canReach(event)) {
69899
- throw new NotFoundError56("Event not found.");
71079
+ throw new NotFoundError58("Event not found.");
69900
71080
  }
69901
71081
  return event;
69902
71082
  }
@@ -72799,7 +73979,7 @@ import {
72799
73979
  InternalServerError as InternalServerError62,
72800
73980
  logger as logger143,
72801
73981
  makeCacheKey as makeCacheKey47,
72802
- NotFoundError as NotFoundError58,
73982
+ NotFoundError as NotFoundError60,
72803
73983
  paginate as paginate46,
72804
73984
  useAtlas as useAtlas99,
72805
73985
  useCache as useCache52
@@ -73002,7 +74182,7 @@ function useNfcPatrolTagRepo() {
73002
74182
  }
73003
74183
  const tag = await collection.findOne({ _id }, { session });
73004
74184
  if (!tag) {
73005
- throw new NotFoundError58("NFC Patrol Tag not found.");
74185
+ throw new NotFoundError60("NFC Patrol Tag not found.");
73006
74186
  }
73007
74187
  return tag;
73008
74188
  }
@@ -73419,7 +74599,7 @@ import {
73419
74599
  InternalServerError as InternalServerError63,
73420
74600
  logger as logger146,
73421
74601
  makeCacheKey as makeCacheKey48,
73422
- NotFoundError as NotFoundError59,
74602
+ NotFoundError as NotFoundError61,
73423
74603
  paginate as paginate47,
73424
74604
  toObjectId as toObjectId17,
73425
74605
  useAtlas as useAtlas101,
@@ -73563,7 +74743,7 @@ function useOccurrenceBookRepo() {
73563
74743
  try {
73564
74744
  const data = await collection.findOne({ _id }, { session });
73565
74745
  if (!data) {
73566
- throw new NotFoundError59("Occurrence book not found.");
74746
+ throw new NotFoundError61("Occurrence book not found.");
73567
74747
  }
73568
74748
  setCache(cacheKey, data, 15 * 60).then(() => {
73569
74749
  logger146.info(`Cache set for key: ${cacheKey}`);
@@ -74079,7 +75259,7 @@ import {
74079
75259
  InternalServerError as InternalServerError64,
74080
75260
  logger as logger148,
74081
75261
  makeCacheKey as makeCacheKey49,
74082
- NotFoundError as NotFoundError60,
75262
+ NotFoundError as NotFoundError62,
74083
75263
  paginate as paginate48,
74084
75264
  useAtlas as useAtlas103,
74085
75265
  useCache as useCache54
@@ -74220,7 +75400,7 @@ function useBulletinVideoRepo() {
74220
75400
  { session }
74221
75401
  );
74222
75402
  if (!data) {
74223
- throw new NotFoundError60("Bulletin video not found.");
75403
+ throw new NotFoundError62("Bulletin video not found.");
74224
75404
  }
74225
75405
  setCache(cacheKey, data, 15 * 60).then(() => {
74226
75406
  logger148.info(`Cache set for key: ${cacheKey}`);
@@ -74312,7 +75492,7 @@ function useBulletinVideoRepo() {
74312
75492
  }
74313
75493
 
74314
75494
  // src/services/bulletin-video.service.ts
74315
- import { useAtlas as useAtlas104, NotFoundError as NotFoundError61 } from "@7365admin1/node-server-utils";
75495
+ import { useAtlas as useAtlas104, NotFoundError as NotFoundError63 } from "@7365admin1/node-server-utils";
74316
75496
  function useBulletinVideoService() {
74317
75497
  const {
74318
75498
  add: _add,
@@ -74333,7 +75513,7 @@ function useBulletinVideoService() {
74333
75513
  session
74334
75514
  );
74335
75515
  if (!file) {
74336
- throw new NotFoundError61("File not found.");
75516
+ throw new NotFoundError63("File not found.");
74337
75517
  }
74338
75518
  }
74339
75519
  await _add(value, session);
@@ -74389,7 +75569,7 @@ function useBulletinVideoService() {
74389
75569
  // src/controllers/bulletin-video.controller.ts
74390
75570
  import {
74391
75571
  BadRequestError as BadRequestError181,
74392
- NotFoundError as NotFoundError62,
75572
+ NotFoundError as NotFoundError64,
74393
75573
  logger as logger149
74394
75574
  } from "@7365admin1/node-server-utils";
74395
75575
  import Joi108 from "joi";
@@ -74404,7 +75584,7 @@ function useBulletinVideoController() {
74404
75584
  const video = await _getBulletinVideoById(id);
74405
75585
  const { canReach } = await siteReachOf(req);
74406
75586
  if (!await canReach(video)) {
74407
- throw new NotFoundError62("Bulletin video not found.");
75587
+ throw new NotFoundError64("Bulletin video not found.");
74408
75588
  }
74409
75589
  return video;
74410
75590
  }
@@ -75698,7 +76878,7 @@ import {
75698
76878
  InternalServerError as InternalServerError66,
75699
76879
  logger as logger155,
75700
76880
  makeCacheKey as makeCacheKey51,
75701
- NotFoundError as NotFoundError64,
76881
+ NotFoundError as NotFoundError66,
75702
76882
  paginate as paginate50,
75703
76883
  useAtlas as useAtlas107,
75704
76884
  useCache as useCache56
@@ -75866,7 +77046,7 @@ function useEntryPassSettingsRepo() {
75866
77046
  }
75867
77047
  ]).toArray();
75868
77048
  if (!data || !data.length) {
75869
- throw new NotFoundError64("Entry Pass Settings not found.");
77049
+ throw new NotFoundError66("Entry Pass Settings not found.");
75870
77050
  }
75871
77051
  setCache(cacheKey, data[0], 15 * 60).then(() => {
75872
77052
  logger155.info(`Cache set for key: ${cacheKey}`);
@@ -76002,7 +77182,7 @@ function useEntryPassSettingsRepo() {
76002
77182
  }
76003
77183
  ]).toArray();
76004
77184
  if (!data || !data.length) {
76005
- throw new NotFoundError64("Entry Pass Settings not found.");
77185
+ throw new NotFoundError66("Entry Pass Settings not found.");
76006
77186
  }
76007
77187
  setCache(cacheKey, data[0], 15 * 60).then(() => {
76008
77188
  logger155.info(`Cache set for key: ${cacheKey}`);
@@ -76811,7 +77991,7 @@ function useNfcPatrolRouteService() {
76811
77991
  import {
76812
77992
  BadRequestError as BadRequestError193,
76813
77993
  logger as logger162,
76814
- NotFoundError as NotFoundError66
77994
+ NotFoundError as NotFoundError68
76815
77995
  } from "@7365admin1/node-server-utils";
76816
77996
  import Joi115 from "joi";
76817
77997
  function useNfcPatrolRouteController() {
@@ -76903,7 +78083,7 @@ function useNfcPatrolRouteController() {
76903
78083
  const nfcPatrolRoute = await _getById(value?.id, value?.isStart);
76904
78084
  await requireSiteReachIfSignedIn(req, nfcPatrolRoute?.site);
76905
78085
  if (!nfcPatrolRoute) {
76906
- throw new NotFoundError66("NFC Patrol Route not found.");
78086
+ throw new NotFoundError68("NFC Patrol Route not found.");
76907
78087
  }
76908
78088
  res.json({
76909
78089
  message: "Successfully retrieved nfc patrol route.",
@@ -77301,7 +78481,7 @@ function MIncidentReport(value) {
77301
78481
  import {
77302
78482
  useAtlas as useAtlas112,
77303
78483
  BadRequestError as BadRequestError195,
77304
- NotFoundError as NotFoundError68
78484
+ NotFoundError as NotFoundError70
77305
78485
  } from "@7365admin1/node-server-utils";
77306
78486
 
77307
78487
  // src/repositories/incident-report.repo.ts
@@ -77310,7 +78490,7 @@ import {
77310
78490
  InternalServerError as InternalServerError69,
77311
78491
  logger as logger163,
77312
78492
  makeCacheKey as makeCacheKey54,
77313
- NotFoundError as NotFoundError67,
78493
+ NotFoundError as NotFoundError69,
77314
78494
  paginate as paginate52,
77315
78495
  useAtlas as useAtlas111,
77316
78496
  useCache as useCache59
@@ -77568,7 +78748,7 @@ function useIncidentReportRepo() {
77568
78748
  { session }
77569
78749
  );
77570
78750
  if (!data) {
77571
- throw new NotFoundError67("Incident report not found.");
78751
+ throw new NotFoundError69("Incident report not found.");
77572
78752
  }
77573
78753
  setCache(cacheKey, data, 15 * 60).then(() => {
77574
78754
  logger163.info(`Cache set for key: ${cacheKey}`);
@@ -77851,7 +79031,7 @@ function useIncidentReportService() {
77851
79031
  session
77852
79032
  );
77853
79033
  if (!file) {
77854
- throw new NotFoundError68("File not found.");
79034
+ throw new NotFoundError70("File not found.");
77855
79035
  }
77856
79036
  }
77857
79037
  }
@@ -78334,7 +79514,7 @@ import {
78334
79514
  InternalServerError as InternalServerError70,
78335
79515
  logger as logger166,
78336
79516
  makeCacheKey as makeCacheKey55,
78337
- NotFoundError as NotFoundError69,
79517
+ NotFoundError as NotFoundError71,
78338
79518
  useAtlas as useAtlas113,
78339
79519
  useCache as useCache60
78340
79520
  } from "@7365admin1/node-server-utils";
@@ -78424,7 +79604,7 @@ function useNfcPatrolSettingsRepository() {
78424
79604
  { session }
78425
79605
  );
78426
79606
  if (res.matchedCount === 0) {
78427
- throw new NotFoundError69("NFC patrol settings not found for this site.");
79607
+ throw new NotFoundError71("NFC patrol settings not found for this site.");
78428
79608
  }
78429
79609
  delNamespace().then(() => {
78430
79610
  logger166.info(`Cache cleared for namespace: ${namespace_collection3}`);
@@ -78591,7 +79771,7 @@ import {
78591
79771
  InternalServerError as InternalServerError71,
78592
79772
  logger as logger169,
78593
79773
  makeCacheKey as makeCacheKey56,
78594
- NotFoundError as NotFoundError70,
79774
+ NotFoundError as NotFoundError72,
78595
79775
  paginate as paginate53,
78596
79776
  useAtlas as useAtlas115,
78597
79777
  useCache as useCache61
@@ -78819,7 +79999,7 @@ function useOccurrenceSubjectRepo() {
78819
79999
  try {
78820
80000
  const data = await collection.findOne({ _id }, { session });
78821
80001
  if (!data) {
78822
- throw new NotFoundError70("Occurrence subject not found.");
80002
+ throw new NotFoundError72("Occurrence subject not found.");
78823
80003
  }
78824
80004
  setCache(cacheKey, data, 15 * 60).then(() => {
78825
80005
  logger169.info(`Cache set for key: ${cacheKey}`);
@@ -79300,7 +80480,7 @@ import {
79300
80480
  InternalServerError as InternalServerError72,
79301
80481
  logger as logger171,
79302
80482
  makeCacheKey as makeCacheKey57,
79303
- NotFoundError as NotFoundError71,
80483
+ NotFoundError as NotFoundError73,
79304
80484
  paginate as paginate54,
79305
80485
  useAtlas as useAtlas117,
79306
80486
  useCache as useCache62
@@ -79446,7 +80626,7 @@ function useOnlineFormRepo() {
79446
80626
  }
79447
80627
  ]).toArray();
79448
80628
  if (!data || !data.length) {
79449
- throw new NotFoundError71("Document not found.");
80629
+ throw new NotFoundError73("Document not found.");
79450
80630
  }
79451
80631
  setCache(cacheKey, data[0], 15 * 60).then(() => {
79452
80632
  logger171.info(`Cache set for key: ${cacheKey}`);
@@ -79590,7 +80770,7 @@ function useOnlineFormRepo() {
79590
80770
  // src/controllers/online-form.controller.ts
79591
80771
  import {
79592
80772
  BadRequestError as BadRequestError204,
79593
- NotFoundError as NotFoundError72,
80773
+ NotFoundError as NotFoundError74,
79594
80774
  UnauthorizedError as UnauthorizedError21,
79595
80775
  logger as logger172
79596
80776
  } from "@7365admin1/node-server-utils";
@@ -79608,7 +80788,7 @@ function useOnlineFormController() {
79608
80788
  const form = await _getOnlineFormById(id);
79609
80789
  const { canReach } = await siteReachOf(req);
79610
80790
  if (!await canReach(form)) {
79611
- throw new NotFoundError72("Document not found.");
80791
+ throw new NotFoundError74("Document not found.");
79612
80792
  }
79613
80793
  return form;
79614
80794
  }
@@ -80187,7 +81367,7 @@ function useNfcPatrolLogRepo() {
80187
81367
  // src/services/nfc-patrol-log.service.ts
80188
81368
  import {
80189
81369
  BadRequestError as BadRequestError207,
80190
- NotFoundError as NotFoundError74,
81370
+ NotFoundError as NotFoundError76,
80191
81371
  useAtlas as useAtlas120
80192
81372
  } from "@7365admin1/node-server-utils";
80193
81373
  function useNfcPatrolLogService() {
@@ -80344,7 +81524,7 @@ function useNfcPatrolLogService() {
80344
81524
  async function sign(id, value) {
80345
81525
  const log = await getById(id);
80346
81526
  if (!log) {
80347
- throw new NotFoundError74("Patrol log not found.");
81527
+ throw new NotFoundError76("Patrol log not found.");
80348
81528
  }
80349
81529
  const canSign = log.checkPoints.every(
80350
81530
  (c) => c.status === "Completed" || c.status === "Skipped"
@@ -83553,7 +84733,7 @@ function useManpowerDesignationRepo() {
83553
84733
  // src/controllers/manpower-designations.controller.ts
83554
84734
  import {
83555
84735
  BadRequestError as BadRequestError216,
83556
- NotFoundError as NotFoundError77,
84736
+ NotFoundError as NotFoundError79,
83557
84737
  logger as logger183
83558
84738
  } from "@7365admin1/node-server-utils";
83559
84739
  import Joi131 from "joi";
@@ -83568,7 +84748,7 @@ function useManpowerDesignationCtrl() {
83568
84748
  const row = await _getManpowerDesignationsById(_id);
83569
84749
  const { canReach } = await siteReachOf(req);
83570
84750
  if (!row || !await canReach({ site: row.siteId })) {
83571
- throw new NotFoundError77("Manpower designations not found.");
84751
+ throw new NotFoundError79("Manpower designations not found.");
83572
84752
  }
83573
84753
  return row;
83574
84754
  }
@@ -83582,7 +84762,7 @@ function useManpowerDesignationCtrl() {
83582
84762
  }
83583
84763
  const { actor, canReach } = await siteReachOf(req);
83584
84764
  if (!await canReach({ site: payload.siteId })) {
83585
- throw new NotFoundError77("Site not found.");
84765
+ throw new NotFoundError79("Site not found.");
83586
84766
  }
83587
84767
  payload.createdBy = actor.id;
83588
84768
  payload.createdByName = actor.name;
@@ -86320,7 +87500,7 @@ import {
86320
87500
  useAtlas as useAtlas133,
86321
87501
  logger as logger198,
86322
87502
  BadRequestError as BadRequestError227,
86323
- NotFoundError as NotFoundError78,
87503
+ NotFoundError as NotFoundError80,
86324
87504
  AppError as AppError34,
86325
87505
  paginate as paginate65
86326
87506
  } from "@7365admin1/node-server-utils";
@@ -86378,7 +87558,7 @@ var useRedDotPaymentRepo = () => {
86378
87558
  { session }
86379
87559
  );
86380
87560
  if (!payment) {
86381
- throw new NotFoundError78("Payment not found.");
87561
+ throw new NotFoundError80("Payment not found.");
86382
87562
  }
86383
87563
  return payment;
86384
87564
  } catch (error) {
@@ -86710,7 +87890,7 @@ var useRedDotPaymentRepo = () => {
86710
87890
  { session }
86711
87891
  );
86712
87892
  if (res.matchedCount === 0) {
86713
- throw new NotFoundError78(
87893
+ throw new NotFoundError80(
86714
87894
  "Payment not found or is no longer pending."
86715
87895
  );
86716
87896
  }
@@ -87041,7 +88221,7 @@ import {
87041
88221
  logger as logger201,
87042
88222
  getDirectory as getDirectory7,
87043
88223
  BadRequestError as BadRequestError231,
87044
- NotFoundError as NotFoundError81,
88224
+ NotFoundError as NotFoundError83,
87045
88225
  InternalServerError as InternalServerError85,
87046
88226
  UnauthorizedError as UnauthorizedError22,
87047
88227
  useAtlas as useAtlas137,
@@ -87055,7 +88235,7 @@ import {
87055
88235
  BadRequestError as BadRequestError230,
87056
88236
  comparePassword as comparePassword3,
87057
88237
  InternalServerError as InternalServerError84,
87058
- NotFoundError as NotFoundError80,
88238
+ NotFoundError as NotFoundError82,
87059
88239
  useAtlas as useAtlas136,
87060
88240
  useCache as useCache73
87061
88241
  } from "@7365admin1/node-server-utils";
@@ -87069,7 +88249,7 @@ import {
87069
88249
  logger as logger200,
87070
88250
  BadRequestError as BadRequestError229,
87071
88251
  paginate as paginate66,
87072
- NotFoundError as NotFoundError79,
88252
+ NotFoundError as NotFoundError81,
87073
88253
  AppError as AppError35,
87074
88254
  useCache as useCache72,
87075
88255
  makeCacheKey as makeCacheKey65,
@@ -87202,7 +88382,7 @@ function useUserRepoV2() {
87202
88382
  ]).toArray();
87203
88383
  const data = results.length > 0 ? results[0] : null;
87204
88384
  if (!data)
87205
- throw new NotFoundError79("User not found.");
88385
+ throw new NotFoundError81("User not found.");
87206
88386
  setCache(cacheKey, data, 15 * 60).then(() => logger200.info(`Cache set for key: ${cacheKey}`)).catch(
87207
88387
  (err) => logger200.error(`Failed to set cache for key: ${cacheKey}`, err)
87208
88388
  );
@@ -87568,7 +88748,7 @@ function useAuthServiceV2() {
87568
88748
  try {
87569
88749
  const user = await getUserByEmail(email);
87570
88750
  if (!user) {
87571
- throw new NotFoundError80(
88751
+ throw new NotFoundError82(
87572
88752
  "Invalid user email. Please check your email and try again."
87573
88753
  );
87574
88754
  }
@@ -87831,7 +89011,7 @@ function useVerificationServiceV2() {
87831
89011
  session?.startTransaction();
87832
89012
  const item = await _getByVerificationCode(verificationCode);
87833
89013
  if (!item) {
87834
- throw new NotFoundError81("Verification not found.");
89014
+ throw new NotFoundError83("Verification not found.");
87835
89015
  }
87836
89016
  switch (item.status) {
87837
89017
  case "expired" /* EXPIRED */:
@@ -88093,7 +89273,7 @@ function useVerificationServiceV2() {
88093
89273
  async function resendSignUpVerification(email) {
88094
89274
  const item = await _getPendingVerificationByEmail(email);
88095
89275
  if (!item) {
88096
- throw new NotFoundError81(
89276
+ throw new NotFoundError83(
88097
89277
  "Pending verification not found."
88098
89278
  );
88099
89279
  }
@@ -88149,7 +89329,7 @@ function useVerificationServiceV2() {
88149
89329
  }
88150
89330
  async function assertInviteOpen(invite) {
88151
89331
  if (!invite) {
88152
- throw new NotFoundError81("Invitation not found.");
89332
+ throw new NotFoundError83("Invitation not found.");
88153
89333
  }
88154
89334
  if (invite.type !== "user-invite" /* USER_INVITE */ && invite.type !== "member-invite" /* MEMBER_INVITE */) {
88155
89335
  throw new BadRequestError231("This link is not a user invitation.");
@@ -88317,7 +89497,7 @@ function useVerificationServiceV2() {
88317
89497
  const email = await callerEmail(userId);
88318
89498
  const invite = await _getVerificationByIdFresh(inviteId);
88319
89499
  if (!invite) {
88320
- throw new NotFoundError81("Invitation not found.");
89500
+ throw new NotFoundError83("Invitation not found.");
88321
89501
  }
88322
89502
  if (invite.email.toLowerCase() !== email.toLowerCase()) {
88323
89503
  throw new UnauthorizedError22("Not authorized.");
@@ -89081,7 +90261,7 @@ import {
89081
90261
  comparePassword as comparePassword4,
89082
90262
  hashPassword as hashPassword5,
89083
90263
  InternalServerError as InternalServerError86,
89084
- NotFoundError as NotFoundError82,
90264
+ NotFoundError as NotFoundError84,
89085
90265
  useAtlas as useAtlas138,
89086
90266
  useS3 as useS33
89087
90267
  } from "@7365admin1/node-server-utils";
@@ -89185,14 +90365,14 @@ function useUserServiceV2() {
89185
90365
  try {
89186
90366
  const otpDoc = await _getVerificationById(id);
89187
90367
  if (!otpDoc) {
89188
- throw new NotFoundError82("You are using an invalid reset link.");
90368
+ throw new NotFoundError84("You are using an invalid reset link.");
89189
90369
  }
89190
90370
  if (otpDoc.status === "complete" /* COMPLETE */) {
89191
90371
  throw new BadRequestError234("This link has already been invalidated.");
89192
90372
  }
89193
90373
  const user = await _getUserByEmail(otpDoc.email);
89194
90374
  if (!user) {
89195
- throw new NotFoundError82("User not found.");
90375
+ throw new NotFoundError84("User not found.");
89196
90376
  }
89197
90377
  if (!user._id) {
89198
90378
  throw new InternalServerError86("Invalid user ID.");
@@ -90062,7 +91242,7 @@ function MPost(value) {
90062
91242
  import {
90063
91243
  BadRequestError as BadRequestError240,
90064
91244
  InternalServerError as InternalServerError88,
90065
- NotFoundError as NotFoundError83,
91245
+ NotFoundError as NotFoundError85,
90066
91246
  paginate as paginate67,
90067
91247
  useAtlas as useAtlas140
90068
91248
  } from "@7365admin1/node-server-utils";
@@ -90285,7 +91465,7 @@ function usePostPrelovedRepo() {
90285
91465
  ]).toArray();
90286
91466
  const data = result[0] ?? null;
90287
91467
  if (!data)
90288
- throw new NotFoundError83("Post not found.");
91468
+ throw new NotFoundError85("Post not found.");
90289
91469
  return data;
90290
91470
  } catch (error) {
90291
91471
  throw error;
@@ -90595,7 +91775,7 @@ import { useAtlas as useAtlas142 } from "@7365admin1/node-server-utils";
90595
91775
  import {
90596
91776
  BadRequestError as BadRequestError241,
90597
91777
  InternalServerError as InternalServerError89,
90598
- NotFoundError as NotFoundError84,
91778
+ NotFoundError as NotFoundError86,
90599
91779
  useAtlas as useAtlas141
90600
91780
  } from "@7365admin1/node-server-utils";
90601
91781
  import { ObjectId as ObjectId172 } from "mongodb";
@@ -90691,7 +91871,7 @@ function usePostFavoriteRepo() {
90691
91871
  }
90692
91872
  const result = await collection.findOne({ _id });
90693
91873
  if (!result)
90694
- throw new NotFoundError84("Favorite not found.");
91874
+ throw new NotFoundError86("Favorite not found.");
90695
91875
  return result;
90696
91876
  }
90697
91877
  async function getByPostId(postId) {
@@ -90702,7 +91882,7 @@ function usePostFavoriteRepo() {
90702
91882
  }
90703
91883
  const result = await collection.findOne({ postId });
90704
91884
  if (!result)
90705
- throw new NotFoundError84("Favorite not found.");
91885
+ throw new NotFoundError86("Favorite not found.");
90706
91886
  return result;
90707
91887
  }
90708
91888
  async function updateById(_id, userId, session) {
@@ -91214,7 +92394,7 @@ function MCategoryPreloved(value) {
91214
92394
  import {
91215
92395
  BadRequestError as BadRequestError244,
91216
92396
  InternalServerError as InternalServerError90,
91217
- NotFoundError as NotFoundError85,
92397
+ NotFoundError as NotFoundError87,
91218
92398
  useAtlas as useAtlas144
91219
92399
  } from "@7365admin1/node-server-utils";
91220
92400
  import { ObjectId as ObjectId175 } from "mongodb";
@@ -91257,7 +92437,7 @@ function useCategoryPrelovedRepo() {
91257
92437
  try {
91258
92438
  const data = await collection.findOne({ _id: objectId2 });
91259
92439
  if (!data) {
91260
- throw new NotFoundError85("Category not found.");
92440
+ throw new NotFoundError87("Category not found.");
91261
92441
  }
91262
92442
  return data;
91263
92443
  } catch (error) {
@@ -91286,7 +92466,7 @@ function useCategoryPrelovedRepo() {
91286
92466
  }
91287
92467
  const existing = await collection.findOne({ _id: objectId2 });
91288
92468
  if (!existing)
91289
- throw new NotFoundError85("Category not found.");
92469
+ throw new NotFoundError87("Category not found.");
91290
92470
  const res = await collection.updateOne(
91291
92471
  { _id: objectId2 },
91292
92472
  { $set: { ...value, updatedAt: (/* @__PURE__ */ new Date()).toISOString() } }
@@ -91304,7 +92484,7 @@ function useCategoryPrelovedRepo() {
91304
92484
  }
91305
92485
  const existing = await collection.findOne({ _id: objectId2 });
91306
92486
  if (!existing)
91307
- throw new NotFoundError85("Category not found.");
92487
+ throw new NotFoundError87("Category not found.");
91308
92488
  const res = await collection.deleteOne({ _id: objectId2 });
91309
92489
  if (res.deletedCount === 0)
91310
92490
  throw new InternalServerError90("Unable to delete category.");
@@ -91471,7 +92651,7 @@ function MSubcategoryPreloved(value) {
91471
92651
  import {
91472
92652
  BadRequestError as BadRequestError246,
91473
92653
  InternalServerError as InternalServerError91,
91474
- NotFoundError as NotFoundError86,
92654
+ NotFoundError as NotFoundError88,
91475
92655
  useAtlas as useAtlas145
91476
92656
  } from "@7365admin1/node-server-utils";
91477
92657
  import { ObjectId as ObjectId177 } from "mongodb";
@@ -91522,7 +92702,7 @@ function useSubcategoryPrelovedRepo() {
91522
92702
  try {
91523
92703
  const data = await collection.findOne({ _id: objectId2 });
91524
92704
  if (!data) {
91525
- throw new NotFoundError86("Subcategory not found.");
92705
+ throw new NotFoundError88("Subcategory not found.");
91526
92706
  }
91527
92707
  return data;
91528
92708
  } catch (error) {
@@ -91797,7 +92977,7 @@ function MChatPreloved(value) {
91797
92977
  import {
91798
92978
  BadRequestError as BadRequestError248,
91799
92979
  InternalServerError as InternalServerError92,
91800
- NotFoundError as NotFoundError87,
92980
+ NotFoundError as NotFoundError89,
91801
92981
  useAtlas as useAtlas146
91802
92982
  } from "@7365admin1/node-server-utils";
91803
92983
  import { ObjectId as ObjectId179 } from "mongodb";
@@ -91837,7 +93017,7 @@ function useChatPrelovedRepo() {
91837
93017
  }
91838
93018
  const existing = await collection.findOne({ _id: objectId2 });
91839
93019
  if (!existing)
91840
- throw new NotFoundError87("Chat not found.");
93020
+ throw new NotFoundError89("Chat not found.");
91841
93021
  value.edited = true;
91842
93022
  value.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
91843
93023
  const res = await collection.updateOne({ _id: objectId2 }, { $set: value });
@@ -91854,7 +93034,7 @@ function useChatPrelovedRepo() {
91854
93034
  }
91855
93035
  const existing = await collection.findOne({ _id: objectId2 });
91856
93036
  if (!existing)
91857
- throw new NotFoundError87("Chat not found.");
93037
+ throw new NotFoundError89("Chat not found.");
91858
93038
  const res = await collection.updateOne(
91859
93039
  { _id: objectId2 },
91860
93040
  { $set: { deletedAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() } }
@@ -92587,7 +93767,7 @@ function useChannelPrelovedController() {
92587
93767
  // src/repositories/bid-preloved.repo.ts
92588
93768
  import {
92589
93769
  InternalServerError as InternalServerError96,
92590
- NotFoundError as NotFoundError88,
93770
+ NotFoundError as NotFoundError90,
92591
93771
  useAtlas as useAtlas150
92592
93772
  } from "@7365admin1/node-server-utils";
92593
93773
  import { ObjectId as ObjectId183 } from "mongodb";
@@ -92609,7 +93789,7 @@ function useBidPrelovedRepo() {
92609
93789
  const objectId2 = typeof _id === "string" ? new ObjectId183(_id) : _id;
92610
93790
  const existing = await collection.findOne({ _id: objectId2 });
92611
93791
  if (!existing)
92612
- throw new NotFoundError88("Bid not found.");
93792
+ throw new NotFoundError90("Bid not found.");
92613
93793
  const res = await collection.updateOne(
92614
93794
  { _id: objectId2 },
92615
93795
  { $set: { status, updatedAt: (/* @__PURE__ */ new Date()).toISOString() } }
@@ -92623,7 +93803,7 @@ function useBidPrelovedRepo() {
92623
93803
  _id = new ObjectId183(_id);
92624
93804
  const result = await collection.findOne({ _id });
92625
93805
  if (!result)
92626
- throw new NotFoundError88("Bid not found.");
93806
+ throw new NotFoundError90("Bid not found.");
92627
93807
  return result;
92628
93808
  }
92629
93809
  return { add, getById, updateStatus };
@@ -92930,7 +94110,7 @@ var residentFormEntry = Joi163.object({
92930
94110
  import {
92931
94111
  BadRequestError as BadRequestError253,
92932
94112
  InternalServerError as InternalServerError98,
92933
- NotFoundError as NotFoundError89,
94113
+ NotFoundError as NotFoundError91,
92934
94114
  paginate as paginate70,
92935
94115
  useAtlas as useAtlas152,
92936
94116
  useCache as useCache76
@@ -93035,7 +94215,7 @@ function useFormEntryRepo() {
93035
94215
  try {
93036
94216
  const [data] = await collection.aggregate([{ $match: query2 }]).toArray();
93037
94217
  if (!data) {
93038
- throw new NotFoundError89("Document not found.");
94218
+ throw new NotFoundError91("Document not found.");
93039
94219
  }
93040
94220
  return data;
93041
94221
  } catch (error) {
@@ -93059,7 +94239,7 @@ function useFormEntryRepo() {
93059
94239
  }
93060
94240
  const onlineFormRequest = await collection.findOne({ _id });
93061
94241
  if (!onlineFormRequest) {
93062
- throw new NotFoundError89("Online form not found.");
94242
+ throw new NotFoundError91("Online form not found.");
93063
94243
  }
93064
94244
  if (onlineFormRequest.userId) {
93065
94245
  const user = await getUserById(onlineFormRequest.userId.toString());
@@ -93172,7 +94352,7 @@ function useFormEntryRepo() {
93172
94352
  // src/controllers/online-forms-v2.controller.ts
93173
94353
  import {
93174
94354
  BadRequestError as BadRequestError254,
93175
- NotFoundError as NotFoundError90,
94355
+ NotFoundError as NotFoundError92,
93176
94356
  UnauthorizedError as UnauthorizedError25,
93177
94357
  logger as logger218
93178
94358
  } from "@7365admin1/node-server-utils";
@@ -93195,7 +94375,7 @@ function useFormEntryController() {
93195
94375
  const owner = entry?.userId?.toString?.() ?? "";
93196
94376
  const isOwner = Boolean(actor.id) && owner === actor.id;
93197
94377
  if (!isOwner && !await canReach(entry)) {
93198
- throw new NotFoundError90("Document not found.");
94378
+ throw new NotFoundError92("Document not found.");
93199
94379
  }
93200
94380
  return { entry, isOwner, actor };
93201
94381
  }
@@ -95677,6 +96857,7 @@ export {
95677
96857
  cameraGrant,
95678
96858
  cameraHealthClaim,
95679
96859
  cameraHealthSummary,
96860
+ cameraListClearConfirmation,
95680
96861
  cameraManagePermissions,
95681
96862
  cameraProbeCacheKey,
95682
96863
  cameraTransports,