@7365admin1/core 3.68.0 → 3.68.2

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