@7365admin1/core 3.68.0 → 3.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6145,6 +6145,7 @@ __export(src_exports, {
6145
6145
  cameraGrant: () => cameraGrant,
6146
6146
  cameraHealthClaim: () => cameraHealthClaim,
6147
6147
  cameraHealthSummary: () => cameraHealthSummary,
6148
+ cameraListClearConfirmation: () => cameraListClearConfirmation,
6148
6149
  cameraManagePermissions: () => cameraManagePermissions,
6149
6150
  cameraProbeCacheKey: () => cameraProbeCacheKey,
6150
6151
  cameraTransports: () => cameraTransports,
@@ -21860,6 +21861,7 @@ var import_fs2 = require("fs");
21860
21861
  var import_joi26 = __toESM(require("joi"));
21861
21862
  var path2 = __toESM(require("path"));
21862
21863
  var import_urllib = require("urllib");
21864
+ var import_crypto2 = require("crypto");
21863
21865
 
21864
21866
  // src/repositories/visitor-transaction.repo.ts
21865
21867
  var import_node_server_utils56 = require("@7365admin1/node-server-utils");
@@ -24360,6 +24362,69 @@ function useVehicleRepo() {
24360
24362
  throw error;
24361
24363
  }
24362
24364
  }
24365
+ async function getLivePlatesBySite(site) {
24366
+ let siteId;
24367
+ try {
24368
+ siteId = new import_mongodb47.ObjectId(site);
24369
+ } catch (error) {
24370
+ throw new import_node_server_utils58.BadRequestError("Invalid site ID format.");
24371
+ }
24372
+ try {
24373
+ return await collection.aggregate([
24374
+ {
24375
+ $match: {
24376
+ site: siteId,
24377
+ status: "active" /* ACTIVE */,
24378
+ plateNumber: { $nin: [null, ""] }
24379
+ }
24380
+ },
24381
+ { $sort: { name: 1, plateNumber: 1 } },
24382
+ {
24383
+ $lookup: {
24384
+ from: "buildings",
24385
+ localField: "block",
24386
+ foreignField: "_id",
24387
+ pipeline: [{ $project: { name: 1 } }],
24388
+ as: "blockDoc"
24389
+ }
24390
+ },
24391
+ {
24392
+ $lookup: {
24393
+ from: "building-levels",
24394
+ localField: "level",
24395
+ foreignField: "_id",
24396
+ pipeline: [{ $project: { name: 1 } }],
24397
+ as: "levelDoc"
24398
+ }
24399
+ },
24400
+ {
24401
+ $lookup: {
24402
+ from: "building-units",
24403
+ localField: "unit",
24404
+ foreignField: "_id",
24405
+ pipeline: [{ $project: { name: 1 } }],
24406
+ as: "unitDoc"
24407
+ }
24408
+ },
24409
+ {
24410
+ $project: {
24411
+ plateNumber: 1,
24412
+ name: 1,
24413
+ start: 1,
24414
+ end: 1,
24415
+ type: 1,
24416
+ blockedFrom: 1,
24417
+ seasonPassType: 1,
24418
+ blockName: { $ifNull: [{ $arrayElemAt: ["$blockDoc.name", 0] }, ""] },
24419
+ levelName: { $ifNull: [{ $arrayElemAt: ["$levelDoc.name", 0] }, ""] },
24420
+ unitName: { $ifNull: [{ $arrayElemAt: ["$unitDoc.name", 0] }, ""] }
24421
+ }
24422
+ }
24423
+ ]).toArray();
24424
+ } catch (error) {
24425
+ throw new import_node_server_utils58.InternalServerError("Failed to list the site's vehicles.");
24426
+ }
24427
+ }
24363
24428
  async function getAllExpiredVehicles() {
24364
24429
  try {
24365
24430
  const items = await collection.find(expiredVehicleSweepFilter()).toArray();
@@ -24564,6 +24629,7 @@ function useVehicleRepo() {
24564
24629
  getVehicleByPlateNumber,
24565
24630
  getVehiclesByNRIC,
24566
24631
  deleteExpiredVehicles,
24632
+ getLivePlatesBySite,
24567
24633
  getAllVehiclesByUnitId,
24568
24634
  getAllExpiredVehicles,
24569
24635
  bulkUpsertVehicles,
@@ -24763,6 +24829,73 @@ function classifyDahuaFailure(message) {
24763
24829
  }
24764
24830
  return "transient";
24765
24831
  }
24832
+ var DAHUA_COMMAND_ATTEMPTS = 10;
24833
+ var DAHUA_COMMAND_DEADLINE_MS = 45e3;
24834
+ var DAHUA_LOGIN_PROVEN_FOR_MS = 10 * 6e4;
24835
+ var COMMAND_RETRY_DELAYS_MS = [1e3, 2e3, 3e3, 4e3, 5e3];
24836
+ function dahuaCommandRetryDelayMs(failedAttempt) {
24837
+ const index = Math.min(
24838
+ Math.max(Math.floor(failedAttempt), 1),
24839
+ COMMAND_RETRY_DELAYS_MS.length
24840
+ );
24841
+ return COMMAND_RETRY_DELAYS_MS[index - 1];
24842
+ }
24843
+ var NOT_SENT_PATTERN = /ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|ENOTFOUND|UND_ERR_CONNECT_TIMEOUT|connect ?timeout|ConnectTimeout/i;
24844
+ function classifyDahuaAttempt(attempt) {
24845
+ if (attempt.error !== void 0 && attempt.error !== null) {
24846
+ const error = attempt.error;
24847
+ const message = [error?.name, error?.code, error?.cause?.code, error?.message ?? error].filter(Boolean).join(" ");
24848
+ if (/invalid authority|\b401\b|unauthori[sz]ed/i.test(message)) {
24849
+ return "login-refused";
24850
+ }
24851
+ if (/\b403\b|forbidden/i.test(message))
24852
+ return "forbidden";
24853
+ if (NOT_SENT_PATTERN.test(message))
24854
+ return "not-sent";
24855
+ return "maybe-sent";
24856
+ }
24857
+ const status = attempt.statusCode;
24858
+ if (status === 401 || /invalid authority/i.test(String(attempt.text ?? ""))) {
24859
+ return "login-refused";
24860
+ }
24861
+ if (status === 403)
24862
+ return "forbidden";
24863
+ if (status === 502 || status === 503 || status === 504)
24864
+ return "busy";
24865
+ if (status === void 0)
24866
+ return "maybe-sent";
24867
+ return "answered";
24868
+ }
24869
+ function shouldRetryDahuaCommand(options) {
24870
+ const {
24871
+ kind,
24872
+ attempt,
24873
+ loginFailures,
24874
+ loginProven,
24875
+ repeatable,
24876
+ elapsedMs,
24877
+ nextDelayMs,
24878
+ maxAttempts = DAHUA_COMMAND_ATTEMPTS,
24879
+ deadlineMs = DAHUA_COMMAND_DEADLINE_MS
24880
+ } = options;
24881
+ if (kind === "answered" || kind === "forbidden")
24882
+ return false;
24883
+ if (attempt >= maxAttempts)
24884
+ return false;
24885
+ if (elapsedMs + nextDelayMs > deadlineMs)
24886
+ return false;
24887
+ switch (kind) {
24888
+ case "login-refused":
24889
+ return loginProven || loginFailures < DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT;
24890
+ case "busy":
24891
+ case "not-sent":
24892
+ return true;
24893
+ case "maybe-sent":
24894
+ return repeatable;
24895
+ default:
24896
+ return false;
24897
+ }
24898
+ }
24766
24899
  var enc = (value) => encodeURIComponent(String(value));
24767
24900
  function buildPlateUpdateEndpoint(value) {
24768
24901
  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)}`;
@@ -24773,6 +24906,13 @@ function buildPlateInsertEndpoint(value) {
24773
24906
  function buildPlateRemoveEndpoint(value) {
24774
24907
  return `/cgi-bin/recordUpdater.cgi?action=remove&recno=${enc(value.recno)}&name=${enc(value.mode)}`;
24775
24908
  }
24909
+ var CLEARABLE_PLATE_LISTS = ["TrafficRedList", "TrafficBlackList"];
24910
+ function buildPlateClearEndpoint(value) {
24911
+ if (!CLEARABLE_PLATE_LISTS.includes(value.mode)) {
24912
+ throw new Error(`Refusing to clear record set ${JSON.stringify(value.mode)}.`);
24913
+ }
24914
+ return `/cgi-bin/recordUpdater.cgi?action=clear&name=${enc(value.mode)}`;
24915
+ }
24776
24916
 
24777
24917
  // src/utils/anpr-revoke.util.ts
24778
24918
  function anprRevokeOutcome(response) {
@@ -24898,6 +25038,148 @@ function anprNoCameraRefusal(outcome) {
24898
25038
  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.`;
24899
25039
  }
24900
25040
 
25041
+ // src/utils/anpr-plate.util.ts
25042
+ var ALLOW_CAMERA_DIRECTIONS = ["entry", "both", "residents"];
25043
+ var BLOCK_CAMERA_DIRECTIONS = ["entry", "both", "residents", "visitors"];
25044
+ function isAllowCameraDirection(direction) {
25045
+ return ALLOW_CAMERA_DIRECTIONS.includes(
25046
+ String(direction ?? "").trim().toLowerCase()
25047
+ );
25048
+ }
25049
+ function isBlockCameraDirection(direction) {
25050
+ return BLOCK_CAMERA_DIRECTIONS.includes(
25051
+ String(direction ?? "").trim().toLowerCase()
25052
+ );
25053
+ }
25054
+ function listsFilledByDirectionChange(from, to) {
25055
+ const before = String(from ?? "").trim().toLowerCase();
25056
+ const after = String(to ?? "").trim().toLowerCase();
25057
+ if (!after || after === before)
25058
+ return { allow: false, block: false };
25059
+ return {
25060
+ allow: isAllowCameraDirection(after) && !isAllowCameraDirection(before),
25061
+ block: isBlockCameraDirection(after) && !isBlockCameraDirection(before)
25062
+ };
25063
+ }
25064
+ function listsClearedByDirectionChange(from, to) {
25065
+ const before = String(from ?? "").trim().toLowerCase();
25066
+ const after = String(to ?? "").trim().toLowerCase();
25067
+ if (!after || after === before)
25068
+ return { allow: false, block: false };
25069
+ if (after === "exit")
25070
+ return { allow: true, block: true };
25071
+ return {
25072
+ allow: isAllowCameraDirection(before) && !isAllowCameraDirection(after),
25073
+ block: false
25074
+ };
25075
+ }
25076
+ function mustRevokeFrom(direction, list, vehicleIsBlocked = false) {
25077
+ if (list === "allowed")
25078
+ return isAllowCameraDirection(direction);
25079
+ if (isBlockCameraDirection(direction))
25080
+ return true;
25081
+ return vehicleIsBlocked;
25082
+ }
25083
+ function normalisePlate(plate) {
25084
+ return String(plate ?? "").replace(/\s+/g, "").toUpperCase();
25085
+ }
25086
+ function samePlate(a, b) {
25087
+ const left = normalisePlate(a);
25088
+ return left !== "" && left === normalisePlate(b);
25089
+ }
25090
+ function parseDahuaFindRecords(text2) {
25091
+ const body = String(text2 ?? "");
25092
+ const found = Number(body.match(/found=(\d+)/i)?.[1] || 0);
25093
+ const byIndex = /* @__PURE__ */ new Map();
25094
+ const pattern = /records\[(\d+)\]\.(RecNo|PlateNumber|BeginTime|CancelTime|MasterOfCar)=([^\r\n]*)/gi;
25095
+ for (const match of body.matchAll(pattern)) {
25096
+ const index = Number(match[1]);
25097
+ const key = match[2].toLowerCase();
25098
+ const value = match[3].trim();
25099
+ const record = byIndex.get(index) ?? { recNo: "", plateNumber: "" };
25100
+ if (key === "recno")
25101
+ record.recNo = value;
25102
+ else if (key === "platenumber")
25103
+ record.plateNumber = value;
25104
+ else if (key === "begintime")
25105
+ record.beginTime = value;
25106
+ else if (key === "canceltime")
25107
+ record.cancelTime = value;
25108
+ else
25109
+ record.owner = value;
25110
+ byIndex.set(index, record);
25111
+ }
25112
+ const records = Array.from(byIndex.entries()).sort(([a], [b]) => a - b).map(([, record]) => record).filter((record) => record.recNo !== "");
25113
+ return { found, records };
25114
+ }
25115
+ function matchingPlateRecords(text2, plateNumber) {
25116
+ return parseDahuaFindRecords(text2).records.filter(
25117
+ (record) => record.plateNumber === "" || samePlate(record.plateNumber, plateNumber)
25118
+ );
25119
+ }
25120
+ function listsHeldBy(vehicle) {
25121
+ const type = String(vehicle?.type ?? "");
25122
+ return {
25123
+ allow: type === "whitelist" || String(vehicle?.blockedFrom ?? "") === "whitelist",
25124
+ block: type === "blocklist"
25125
+ };
25126
+ }
25127
+ function listsToRevoke(othersStillLive) {
25128
+ const others = (othersStillLive ?? []).map(listsHeldBy);
25129
+ return {
25130
+ allow: !others.some((held) => held.allow),
25131
+ block: !others.some((held) => held.block)
25132
+ };
25133
+ }
25134
+ var DEFAULT_CAMERA_TIMEZONE = "Asia/Singapore";
25135
+ function usableTimezone(timezone) {
25136
+ const zone = String(timezone ?? "").trim();
25137
+ if (!zone)
25138
+ return DEFAULT_CAMERA_TIMEZONE;
25139
+ try {
25140
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
25141
+ return zone;
25142
+ } catch {
25143
+ return DEFAULT_CAMERA_TIMEZONE;
25144
+ }
25145
+ }
25146
+ var WALL_CLOCK = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(?::(\d{2}))?$/;
25147
+ function dahuaWallClock(value, timezone) {
25148
+ if (value === null || value === void 0 || value === "")
25149
+ return null;
25150
+ if (typeof value === "string") {
25151
+ const wall = value.trim().match(WALL_CLOCK);
25152
+ if (wall) {
25153
+ const [, y, mo, d, h, mi, se] = wall;
25154
+ return `${y}-${mo}-${d} ${h}:${mi}:${se ?? "00"}`;
25155
+ }
25156
+ }
25157
+ const date = value instanceof Date ? value : new Date(value);
25158
+ if (Number.isNaN(date.getTime()))
25159
+ return null;
25160
+ const parts = new Intl.DateTimeFormat("en-GB", {
25161
+ timeZone: usableTimezone(timezone),
25162
+ year: "numeric",
25163
+ month: "2-digit",
25164
+ day: "2-digit",
25165
+ hour: "2-digit",
25166
+ minute: "2-digit",
25167
+ second: "2-digit",
25168
+ hourCycle: "h23"
25169
+ }).formatToParts(date);
25170
+ const part = (type) => parts.find((entry) => entry.type === type)?.value ?? "00";
25171
+ return `${part("year")}-${part("month")}-${part("day")} ${part("hour")}:${part("minute")}:${part("second")}`;
25172
+ }
25173
+ function plateWindowMatches(record, wanted) {
25174
+ const same = (held, want) => {
25175
+ const a = dahuaWallClock(held);
25176
+ return a !== null && a === dahuaWallClock(want);
25177
+ };
25178
+ if (!wanted.start || !wanted.end)
25179
+ return false;
25180
+ return same(record.beginTime, wanted.start) && same(record.cancelTime, wanted.end);
25181
+ }
25182
+
24901
25183
  // src/services/dahua.service.ts
24902
25184
  function notifyCameraFault(camera, message) {
24903
25185
  NotificationService.cameraFault({
@@ -24969,12 +25251,14 @@ async function useDahuaDigestWithRetry({
24969
25251
  const statusCode = getDahuaStatusCode(response);
24970
25252
  if (streaming) {
24971
25253
  if (statusCode === 200) {
25254
+ markLoginProven(host, username, password);
24972
25255
  return response;
24973
25256
  }
24974
25257
  } else {
24975
25258
  const text2 = getDahuaResponseText(response);
24976
25259
  const isOk = statusCode === 200 && text2.includes("OK");
24977
25260
  if (isOk) {
25261
+ markLoginProven(host, username, password);
24978
25262
  return response;
24979
25263
  }
24980
25264
  }
@@ -25008,6 +25292,123 @@ async function useDahuaDigestWithRetry({
25008
25292
  }
25009
25293
  throw lastError;
25010
25294
  }
25295
+ var provenLogins = /* @__PURE__ */ new Map();
25296
+ function loginKey(host, username, password) {
25297
+ return (0, import_crypto2.createHash)("sha256").update(`${host ?? ""}
25298
+ ${username ?? ""}
25299
+ ${password ?? ""}`).digest("hex");
25300
+ }
25301
+ function markLoginProven(host, username, password) {
25302
+ provenLogins.set(loginKey(host, username, password), Date.now());
25303
+ }
25304
+ function forgetLogin(host, username, password) {
25305
+ provenLogins.delete(loginKey(host, username, password));
25306
+ }
25307
+ function isLoginProven(host, username, password) {
25308
+ const at2 = provenLogins.get(loginKey(host, username, password));
25309
+ return at2 !== void 0 && Date.now() - at2 < DAHUA_LOGIN_PROVEN_FOR_MS;
25310
+ }
25311
+ var ATTEMPT_WORDS = {
25312
+ "login-refused": "login refused",
25313
+ forbidden: "credentials refused",
25314
+ busy: "camera busy",
25315
+ "not-sent": "could not connect",
25316
+ "maybe-sent": "connection lost"
25317
+ };
25318
+ async function sendCameraCommand({
25319
+ host,
25320
+ username,
25321
+ password,
25322
+ endpoint,
25323
+ timeout = 1e4,
25324
+ repeatable = true,
25325
+ label = "command",
25326
+ deadlineMs = DAHUA_COMMAND_DEADLINE_MS
25327
+ }) {
25328
+ const startedAt = Date.now();
25329
+ let loginFailures = 0;
25330
+ for (let attempt = 1; ; attempt++) {
25331
+ let response;
25332
+ let error;
25333
+ try {
25334
+ response = await useDahuaDigest({
25335
+ host,
25336
+ username,
25337
+ password,
25338
+ endpoint,
25339
+ headers: { Connection: "close" },
25340
+ timeout
25341
+ });
25342
+ } catch (thrown) {
25343
+ error = thrown ?? new Error("the request failed with no error");
25344
+ }
25345
+ const kind = classifyDahuaAttempt(
25346
+ error ? { error } : {
25347
+ statusCode: getDahuaStatusCode(response),
25348
+ text: getDahuaResponseText(response)
25349
+ }
25350
+ );
25351
+ if (kind === "answered") {
25352
+ markLoginProven(host, username, password);
25353
+ if (attempt > 1) {
25354
+ loggerDahua.info(`[${host}] ${label}: went through on try ${attempt}`);
25355
+ }
25356
+ return response;
25357
+ }
25358
+ if (kind === "login-refused")
25359
+ loginFailures++;
25360
+ if (kind === "forbidden")
25361
+ forgetLogin(host, username, password);
25362
+ const nextDelayMs = dahuaCommandRetryDelayMs(attempt);
25363
+ const retry = shouldRetryDahuaCommand({
25364
+ kind,
25365
+ attempt,
25366
+ loginFailures,
25367
+ loginProven: isLoginProven(host, username, password),
25368
+ repeatable,
25369
+ elapsedMs: Date.now() - startedAt,
25370
+ nextDelayMs,
25371
+ deadlineMs
25372
+ });
25373
+ const why = ATTEMPT_WORDS[kind] ?? kind;
25374
+ if (!retry) {
25375
+ if (attempt > 1 || kind !== "forbidden") {
25376
+ loggerDahua.warn(
25377
+ `[${host}] ${label}: ${why} on try ${attempt}/${DAHUA_COMMAND_ATTEMPTS}; giving up`
25378
+ );
25379
+ }
25380
+ if (error)
25381
+ throw error;
25382
+ return response;
25383
+ }
25384
+ loggerDahua.warn(
25385
+ `[${host}] ${label}: ${why} on try ${attempt}/${DAHUA_COMMAND_ATTEMPTS}; trying again in ${nextDelayMs} ms`
25386
+ );
25387
+ await sleep(nextDelayMs);
25388
+ }
25389
+ }
25390
+ async function sendCameraCommandExpectingOk(options) {
25391
+ const response = await sendCameraCommand(options);
25392
+ const statusCode = getDahuaStatusCode(response);
25393
+ const text2 = getDahuaResponseText(response);
25394
+ if (statusCode === 200 && text2.includes("OK"))
25395
+ return response;
25396
+ const error = new Error(`Dahua response: ${text2 || `HTTP ${statusCode}`}`);
25397
+ error.answeredByCamera = true;
25398
+ throw error;
25399
+ }
25400
+ function anprOwnerName(owner) {
25401
+ return String(owner ?? "").replace(/['"]/g, "").replace(/[\/\\]/g, " - ").substring(0, 15).trim() || "unknown";
25402
+ }
25403
+ function plateWindowOnCamera(value) {
25404
+ const tenMinutesAgo = new Date(Date.now() - 10 * 6e4);
25405
+ const tenYearsOn = new Date(tenMinutesAgo);
25406
+ tenYearsOn.setFullYear(tenYearsOn.getFullYear() + 10);
25407
+ return {
25408
+ start: dahuaWallClock(value.start, value.timezone) ?? dahuaWallClock(tenMinutesAgo, value.timezone),
25409
+ end: dahuaWallClock(value.end, value.timezone) ?? dahuaWallClock(tenYearsOn, value.timezone)
25410
+ };
25411
+ }
25011
25412
  function createFileNameFromEvent(utcSeconds, utcMilliseconds) {
25012
25413
  const date = new Date(utcSeconds * 1e3 + utcMilliseconds);
25013
25414
  const year = date.getUTCFullYear();
@@ -25609,111 +26010,115 @@ function useDahuaService() {
25609
26010
  start: import_joi26.default.string().isoDate().optional().allow("", null),
25610
26011
  end: import_joi26.default.string().isoDate().optional().allow("", null),
25611
26012
  owner: import_joi26.default.string().optional().allow("", null),
25612
- isOpenGate: import_joi26.default.boolean().optional().allow(null)
26013
+ isOpenGate: import_joi26.default.boolean().optional().allow(null),
26014
+ timezone: import_joi26.default.string().optional().allow("", null)
25613
26015
  });
25614
26016
  const { error: validationError } = validation.validate(value);
25615
26017
  if (validationError) {
25616
26018
  throw new import_node_server_utils60.BadRequestError(`Validation error: ${validationError.message}`);
25617
26019
  }
25618
26020
  try {
25619
- value.owner = String(value.owner ?? "").replace(/['"]/g, "").replace(/[\/\\]/g, " - ").substring(0, 15).trim() || "unknown";
25620
- const formatDahuaDate3 = (dateStr, fallbackYearsAhead = 0) => {
25621
- const date = dateStr ? new Date(dateStr) : /* @__PURE__ */ new Date();
25622
- if (!dateStr) {
25623
- date.setMinutes(date.getMinutes() - 10);
25624
- }
25625
- if (fallbackYearsAhead > 0 && !dateStr) {
25626
- date.setFullYear(date.getFullYear() + fallbackYearsAhead);
25627
- }
25628
- const pad = (num) => String(num).padStart(2, "0");
25629
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
25630
- };
25631
- const formattedStart = formatDahuaDate3(value.start);
25632
- const formattedEnd = formatDahuaDate3(value.end, 10);
26021
+ value.owner = anprOwnerName(value.owner);
26022
+ const { start: formattedStart, end: formattedEnd } = plateWindowOnCamera(value);
25633
26023
  const beginTime = encodeURIComponent(formattedStart);
25634
26024
  const cancelTime = encodeURIComponent(formattedEnd);
25635
26025
  const plateNumber = encodeURIComponent(value.plateNumber);
25636
26026
  const ownerName = encodeURIComponent(value.owner);
25637
26027
  const endpoint = `/cgi-bin/recordUpdater.cgi?action=insert&name=${value.mode}&PlateNumber=${plateNumber}&BeginTime=${beginTime}&CancelTime=${cancelTime}&MasterOfCar=${ownerName}`;
25638
- try {
25639
- const insertResponse = await useDahuaDigestWithRetry({
25640
- host: value.host,
25641
- username: value.username,
25642
- password: value.password,
25643
- endpoint,
25644
- retries: 10,
25645
- retryDelayMs: 0
25646
- });
25647
- const insertText = getDahuaResponseText(insertResponse);
25648
- const insertedId = insertText.match(/\d+/)?.[0] || "unknown";
25649
- return {
25650
- ...insertResponse,
25651
- statusCode: 200,
25652
- data: Buffer.from(`recno=${insertedId}`)
25653
- };
25654
- } catch (insertError) {
25655
- const errorMessage = insertError?.message || String(insertError);
25656
- if (/recno=/i.test(errorMessage)) {
25657
- loggerDahua.info(`[${value.host}] Insert confirmed successfully via raw RecNo response text.`);
25658
- const insertedId = errorMessage.match(/\d+/)?.[0] || "unknown";
26028
+ const INSERT_ROUNDS = 3;
26029
+ for (let round = 1; ; round++) {
26030
+ try {
26031
+ const insertResponse = await sendCameraCommandExpectingOk({
26032
+ host: value.host,
26033
+ username: value.username,
26034
+ password: value.password,
26035
+ endpoint,
26036
+ repeatable: false,
26037
+ label: `add plate to ${value.mode}`
26038
+ });
26039
+ const insertText = getDahuaResponseText(insertResponse);
26040
+ const insertedId = insertText.match(/\d+/)?.[0] || "unknown";
25659
26041
  return {
26042
+ ...insertResponse,
25660
26043
  statusCode: 200,
25661
26044
  data: Buffer.from(`recno=${insertedId}`)
25662
26045
  };
25663
- }
25664
- if (errorMessage.includes("Bad Request") || errorMessage.includes("Dahua response: Error")) {
25665
- loggerDahua.info(`[${value.host}] Insert failed (duplicate). Checking if plate ${value.plateNumber} already exists...`);
25666
- const findEndpoint = `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${plateNumber}`;
25667
- try {
25668
- const findResponse = await useDahuaDigestWithRetry({
25669
- host: value.host,
25670
- username: value.username,
25671
- password: value.password,
25672
- endpoint: findEndpoint,
25673
- retries: 5
25674
- });
25675
- const textData = getDahuaResponseText(findResponse);
25676
- recno = extractRecNo(textData);
25677
- } catch (findError) {
25678
- const findErrorMessage = findError?.message || String(findError);
25679
- recno = extractRecNo(findErrorMessage);
25680
- if (!recno) {
25681
- loggerDahua.error(`[${value.host}] Failed handling existing duplicate plate flow`, findError);
25682
- }
26046
+ } catch (insertError) {
26047
+ const errorMessage = insertError?.message || String(insertError);
26048
+ if (/recno=/i.test(errorMessage)) {
26049
+ loggerDahua.info(`[${value.host}] Insert confirmed successfully via raw RecNo response text.`);
26050
+ const insertedId = errorMessage.match(/\d+/)?.[0] || "unknown";
26051
+ return {
26052
+ statusCode: 200,
26053
+ data: Buffer.from(`recno=${insertedId}`)
26054
+ };
25683
26055
  }
25684
- if (recno) {
25685
- loggerDahua.info(`[${value.host}] Found existing record ID: ${recno}. Updating registration instead...`);
25686
- const updateEndpoint = `/cgi-bin/recordUpdater.cgi?action=update&name=${value.mode}&recno=${recno}&PlateNumber=${plateNumber}&BeginTime=${beginTime}&CancelTime=${cancelTime}&MasterOfCar=${ownerName}`;
26056
+ const duplicate = errorMessage.includes("Bad Request") || errorMessage.includes("Dahua response: Error");
26057
+ const lostInFlight = !insertError?.answeredByCamera && classifyDahuaAttempt({ error: insertError }) === "maybe-sent";
26058
+ if (duplicate || lostInFlight) {
26059
+ loggerDahua.info(
26060
+ `[${value.host}] Insert ${duplicate ? "failed (duplicate)" : "lost its connection"}. Checking if the plate is already on ${value.mode}...`
26061
+ );
26062
+ const findEndpoint = `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${plateNumber}`;
25687
26063
  try {
25688
- const updateResponse = await useDahuaDigestWithRetry({
26064
+ const findResponse = await sendCameraCommand({
25689
26065
  host: value.host,
25690
26066
  username: value.username,
25691
26067
  password: value.password,
25692
- endpoint: updateEndpoint,
25693
- retries: 5
26068
+ endpoint: findEndpoint,
26069
+ label: `find plate on ${value.mode}`
25694
26070
  });
25695
- return {
25696
- ...updateResponse,
25697
- statusCode: 200,
25698
- data: Buffer.from(`recno=${recno}`)
25699
- };
25700
- } catch (updateError) {
25701
- const updateErrorMessage = updateError?.message || String(updateError);
25702
- if (/recno=/i.test(updateErrorMessage) || /ok/i.test(updateErrorMessage)) {
25703
- loggerDahua.info(`[${value.host}] Update confirmed successfully despite wrapper omission.`);
25704
- if (!recno) {
25705
- recno = extractRecNo(updateErrorMessage);
25706
- }
26071
+ if (getDahuaStatusCode(findResponse) === 200) {
26072
+ recno = extractRecNo(getDahuaResponseText(findResponse));
26073
+ } else {
26074
+ loggerDahua.error(
26075
+ `[${value.host}] Plate lookup after a failed insert answered HTTP ${getDahuaStatusCode(findResponse)}`
26076
+ );
26077
+ }
26078
+ } catch (findError) {
26079
+ loggerDahua.error(`[${value.host}] Failed handling existing duplicate plate flow`, findError);
26080
+ }
26081
+ if (recno) {
26082
+ loggerDahua.info(`[${value.host}] Found existing record ID: ${recno}. Updating registration instead...`);
26083
+ const updateEndpoint = `/cgi-bin/recordUpdater.cgi?action=update&name=${value.mode}&recno=${recno}&PlateNumber=${plateNumber}&BeginTime=${beginTime}&CancelTime=${cancelTime}&MasterOfCar=${ownerName}`;
26084
+ try {
26085
+ const updateResponse = await sendCameraCommandExpectingOk({
26086
+ host: value.host,
26087
+ username: value.username,
26088
+ password: value.password,
26089
+ endpoint: updateEndpoint,
26090
+ label: `update plate on ${value.mode}`
26091
+ });
25707
26092
  return {
26093
+ ...updateResponse,
25708
26094
  statusCode: 200,
25709
26095
  data: Buffer.from(`recno=${recno}`)
25710
26096
  };
26097
+ } catch (updateError) {
26098
+ const updateErrorMessage = updateError?.message || String(updateError);
26099
+ if (/recno=/i.test(updateErrorMessage) || /ok/i.test(updateErrorMessage)) {
26100
+ loggerDahua.info(`[${value.host}] Update confirmed successfully despite wrapper omission.`);
26101
+ if (!recno) {
26102
+ recno = extractRecNo(updateErrorMessage);
26103
+ }
26104
+ return {
26105
+ statusCode: 200,
26106
+ data: Buffer.from(`recno=${recno}`)
26107
+ };
26108
+ }
26109
+ throw updateError;
25711
26110
  }
25712
- throw updateError;
26111
+ }
26112
+ if (lostInFlight && round < INSERT_ROUNDS) {
26113
+ loggerDahua.warn(
26114
+ `[${value.host}] Plate not found after a lost insert; sending the insert again (round ${round + 1}/${INSERT_ROUNDS})`
26115
+ );
26116
+ await sleep(dahuaCommandRetryDelayMs(round));
26117
+ continue;
25713
26118
  }
25714
26119
  }
26120
+ throw insertError;
25715
26121
  }
25716
- throw insertError;
25717
26122
  }
25718
26123
  } catch (finalError) {
25719
26124
  loggerDahua.error(
@@ -25733,7 +26138,8 @@ function useDahuaService() {
25733
26138
  start: import_joi26.default.string().isoDate().optional().allow("", null),
25734
26139
  end: import_joi26.default.string().isoDate().optional().allow("", null),
25735
26140
  owner: import_joi26.default.string().optional().allow("", null),
25736
- isOpenGate: import_joi26.default.boolean().optional()
26141
+ isOpenGate: import_joi26.default.boolean().optional(),
26142
+ timezone: import_joi26.default.string().optional().allow("", null)
25737
26143
  });
25738
26144
  const { error } = validation.validate(value);
25739
26145
  if (error) {
@@ -25749,17 +26155,20 @@ function useDahuaService() {
25749
26155
  mode: value.mode,
25750
26156
  recno: value.recno,
25751
26157
  plateNumber: value.plateNumber,
25752
- start: value.start,
25753
- end: value.end,
26158
+ // On the camera's clock (see `dahuaWallClock`); a time already written
26159
+ // for the camera passes through unchanged.
26160
+ start: dahuaWallClock(value.start, value.timezone) ?? value.start,
26161
+ end: dahuaWallClock(value.end, value.timezone) ?? value.end,
25754
26162
  owner: value.owner,
25755
26163
  openGate: _isOpenGate
25756
26164
  });
25757
26165
  try {
25758
- const response = await useDahuaDigest({
26166
+ const response = await sendCameraCommand({
25759
26167
  host: value.host,
25760
26168
  username: value.username,
25761
26169
  password: value.password,
25762
- endpoint
26170
+ endpoint,
26171
+ label: `update plate on ${value.mode}`
25763
26172
  });
25764
26173
  return response;
25765
26174
  } catch (error2) {
@@ -25782,14 +26191,16 @@ function useDahuaService() {
25782
26191
  throw new import_node_server_utils60.BadRequestError(`Validation error: ${error.message}`);
25783
26192
  }
25784
26193
  try {
25785
- const response = await useDahuaDigest({
26194
+ const response = await sendCameraCommand({
25786
26195
  host: value.host,
25787
26196
  username: value.username,
25788
26197
  password: value.password,
25789
26198
  endpoint: buildPlateRemoveEndpoint({
25790
26199
  recno: value.recno,
25791
26200
  mode: value.mode
25792
- })
26201
+ }),
26202
+ repeatable: false,
26203
+ label: `remove plate from ${value.mode}`
25793
26204
  });
25794
26205
  return anprRevokeOutcome(response);
25795
26206
  } catch (error2) {
@@ -25811,11 +26222,18 @@ function useDahuaService() {
25811
26222
  throw new import_node_server_utils60.BadRequestError(`Validation error: ${error.message}`);
25812
26223
  }
25813
26224
  try {
25814
- const response = await useDahuaDigest({
26225
+ const findEndpoint = `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${encodeURIComponent(value.plateNumber)}`;
26226
+ const response = value.requireOk ? await sendCameraCommand({
25815
26227
  host: value.host,
25816
26228
  username: value.username,
25817
26229
  password: value.password,
25818
- endpoint: `/cgi-bin/recordFinder.cgi?action=find&name=${value.mode}&condition.PlateNumber=${encodeURIComponent(value.plateNumber)}`
26230
+ endpoint: findEndpoint,
26231
+ label: `find plate on ${value.mode}`
26232
+ }) : await useDahuaDigest({
26233
+ host: value.host,
26234
+ username: value.username,
26235
+ password: value.password,
26236
+ endpoint: findEndpoint
25819
26237
  });
25820
26238
  if (value.requireOk) {
25821
26239
  const statusCode = getDahuaStatusCode(response);
@@ -25842,7 +26260,8 @@ function useDahuaService() {
25842
26260
  owner: import_joi26.default.string().optional().allow("", null),
25843
26261
  isOpenGate: import_joi26.default.boolean().optional().allow(null),
25844
26262
  vehicleType: import_joi26.default.string().optional().allow("", null),
25845
- vehicleColor: import_joi26.default.string().optional().allow("", null)
26263
+ vehicleColor: import_joi26.default.string().optional().allow("", null),
26264
+ timezone: import_joi26.default.string().optional().allow("", null)
25846
26265
  });
25847
26266
  const { error } = validation.validate(value);
25848
26267
  if (error) {
@@ -25858,17 +26277,20 @@ function useDahuaService() {
25858
26277
  plateNumber: value.plateNumber,
25859
26278
  vehicleType: value.vehicleType,
25860
26279
  vehicleColor: value.vehicleColor,
25861
- start: value.start,
25862
- end: value.end,
26280
+ // On the camera's clock (see `dahuaWallClock`).
26281
+ start: dahuaWallClock(value.start, value.timezone) ?? value.start,
26282
+ end: dahuaWallClock(value.end, value.timezone) ?? value.end,
25863
26283
  owner: value.owner,
25864
26284
  openGate: isOpenGateString
25865
26285
  });
25866
26286
  try {
25867
- const response = await useDahuaDigest({
26287
+ const response = await sendCameraCommand({
25868
26288
  host: value.host,
25869
26289
  username: value.username,
25870
26290
  password: value.password,
25871
- endpoint
26291
+ endpoint,
26292
+ repeatable: false,
26293
+ label: `bulk add plate to ${value.mode}`
25872
26294
  });
25873
26295
  return response;
25874
26296
  } catch (error2) {
@@ -25922,11 +26344,91 @@ function useDahuaService() {
25922
26344
  throw error2;
25923
26345
  }
25924
26346
  }
26347
+ async function setPlateWindow(value) {
26348
+ const validation = import_joi26.default.object({
26349
+ host: import_joi26.default.string().required(),
26350
+ username: import_joi26.default.string().required(),
26351
+ password: import_joi26.default.string().required(),
26352
+ mode: import_joi26.default.string().valid(...Object.values(ANPRMode)).required(),
26353
+ recno: import_joi26.default.string().required(),
26354
+ plateNumber: import_joi26.default.string().required(),
26355
+ owner: import_joi26.default.string().optional().allow("", null),
26356
+ start: import_joi26.default.string().isoDate().optional().allow("", null),
26357
+ end: import_joi26.default.string().isoDate().optional().allow("", null),
26358
+ timezone: import_joi26.default.string().optional().allow("", null)
26359
+ });
26360
+ const { error } = validation.validate(value);
26361
+ if (error) {
26362
+ throw new import_node_server_utils60.BadRequestError(`Validation error: ${error.message}`);
26363
+ }
26364
+ const window = plateWindowOnCamera(value);
26365
+ 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))}`;
26366
+ try {
26367
+ return await sendCameraCommandExpectingOk({
26368
+ host: value.host,
26369
+ username: value.username,
26370
+ password: value.password,
26371
+ endpoint,
26372
+ label: `set plate window on ${value.mode}`
26373
+ });
26374
+ } catch (error2) {
26375
+ loggerDahua.error(
26376
+ `[${value.host}] Error setting plate window: ${error2?.message || error2}`
26377
+ );
26378
+ throw new import_node_server_utils60.BadRequestError(anprWriteReason(error2));
26379
+ }
26380
+ }
26381
+ async function clearPlateList(value) {
26382
+ const validation = import_joi26.default.object({
26383
+ host: import_joi26.default.string().required(),
26384
+ username: import_joi26.default.string().required(),
26385
+ password: import_joi26.default.string().required(),
26386
+ mode: import_joi26.default.string().valid(...Object.values(ANPRMode)).required()
26387
+ });
26388
+ const { error } = validation.validate(value);
26389
+ if (error) {
26390
+ return anprRevokeThrew(`Validation error: ${error.message}`);
26391
+ }
26392
+ loggerDahua.info(`clearPlateList called for host ${value.host} mode ${value.mode}`);
26393
+ try {
26394
+ const response = await sendCameraCommand({
26395
+ host: value.host,
26396
+ username: value.username,
26397
+ password: value.password,
26398
+ endpoint: buildPlateClearEndpoint({ mode: value.mode }),
26399
+ // A long list takes the device a while to empty.
26400
+ timeout: 6e4,
26401
+ deadlineMs: 9e4,
26402
+ label: `clear ${value.mode}`
26403
+ });
26404
+ const outcome = anprRevokeOutcome(response);
26405
+ if (!outcome.ok)
26406
+ return outcome;
26407
+ const text2 = getDahuaResponseText(response);
26408
+ if (!/\bOK\b/i.test(text2)) {
26409
+ loggerDahua.error(
26410
+ `[${value.host}] clearPlateList ${value.mode} answered 200 without OK: ${text2.slice(0, 200)}`
26411
+ );
26412
+ return {
26413
+ ok: false,
26414
+ statusCode: 200,
26415
+ reason: "the camera did not accept the command to clear its list",
26416
+ detail: `the camera answered 200 without OK: ${text2.slice(0, 200)}`
26417
+ };
26418
+ }
26419
+ return outcome;
26420
+ } catch (error2) {
26421
+ loggerDahua.error(`[${value.host}] Error clearing ${value.mode}:`, error2);
26422
+ return anprRevokeThrew(error2);
26423
+ }
26424
+ }
25925
26425
  return {
25926
26426
  getSnapshot,
25927
26427
  listenToCamera,
25928
26428
  addPlateNumber,
25929
26429
  removePlateNumber,
26430
+ clearPlateList,
26431
+ setPlateWindow,
25930
26432
  updatePlateNumber,
25931
26433
  getPlateNumber,
25932
26434
  bulkInsertPlateNumber,
@@ -26368,6 +26870,11 @@ function useSiteCameraRepo() {
26368
26870
  {
26369
26871
  $match: query2
26370
26872
  },
26873
+ // A fixed order, so paging through a site's cameras 20 at a time
26874
+ // cannot skip one camera and return another twice.
26875
+ {
26876
+ $sort: { _id: 1 }
26877
+ },
26371
26878
  {
26372
26879
  $skip: value.page * value.limit
26373
26880
  },
@@ -38087,6 +38594,7 @@ function useVehicleService() {
38087
38594
  getVehicleByPlateNumber: _getVehicleByPlateNumber,
38088
38595
  getAllExpiredVehicles: _getAllExpiredVehicles,
38089
38596
  bulkUpsertVehicles: _bulkUpsertVehicles,
38597
+ getLivePlatesBySite: _getLivePlatesBySite,
38090
38598
  findOne
38091
38599
  } = useVehicleRepo();
38092
38600
  const {
@@ -38097,12 +38605,441 @@ function useVehicleService() {
38097
38605
  bulkInsertPlateNumber: _bulkInsertPlateNumber
38098
38606
  } = useDahuaService();
38099
38607
  const { getAllCameraWithPassword: _getAllSiteCameras } = useSiteCameraRepo();
38608
+ const { setPlateWindow: _setPlateWindow } = useDahuaService();
38100
38609
  const { getById: _getById } = useOrgRepo();
38101
38610
  const {
38102
38611
  pushVehicleById: _pushVehicleById,
38103
38612
  pullVehicleByRecNo: _pullVehicleByRecNo
38104
38613
  } = usePersonRepo();
38105
38614
  const { getUnitByBlockLevelUnitNumber: _getUnitByBlockLevelUnitNumber } = useBuildingUnitRepo();
38615
+ async function camerasAt(site, options = {}) {
38616
+ const siteId = String(site ?? "");
38617
+ if (!siteId)
38618
+ return [];
38619
+ const cameras = [];
38620
+ let page = 1;
38621
+ let pages = 1;
38622
+ const limit = 20;
38623
+ do {
38624
+ const siteCameraReq = await _getAllSiteCameras({
38625
+ site: siteId,
38626
+ type: "anpr",
38627
+ ...options.directions ? { direction: options.directions } : {},
38628
+ ...options.activeOnly ? { status: "active" } : {},
38629
+ page,
38630
+ limit
38631
+ });
38632
+ pages = siteCameraReq.pages || 1;
38633
+ cameras.push(...siteCameraReq.items);
38634
+ page++;
38635
+ } while (page <= pages);
38636
+ return cameras;
38637
+ }
38638
+ function allowCamerasAt(site) {
38639
+ return camerasAt(site, {
38640
+ directions: ALLOW_CAMERA_DIRECTIONS,
38641
+ activeOnly: true
38642
+ });
38643
+ }
38644
+ function blockCamerasAt(site) {
38645
+ return camerasAt(site, {
38646
+ directions: BLOCK_CAMERA_DIRECTIONS,
38647
+ activeOnly: true
38648
+ });
38649
+ }
38650
+ function listOf(mode) {
38651
+ return mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */ ? "blocked" : "allowed";
38652
+ }
38653
+ function listMode(type) {
38654
+ return type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
38655
+ }
38656
+ function validDate(value) {
38657
+ if (value === null || value === void 0 || value === "")
38658
+ return null;
38659
+ const date = new Date(value);
38660
+ return Number.isNaN(date.getTime()) ? null : date;
38661
+ }
38662
+ function cameraWindow(start, end) {
38663
+ const startDate = validDate(start);
38664
+ const endDate = validDate(end);
38665
+ return {
38666
+ ...startDate ? { start: startDate.toISOString() } : {},
38667
+ ...endDate ? { end: endDate.toISOString() } : {}
38668
+ };
38669
+ }
38670
+ const siteZones = /* @__PURE__ */ new Map();
38671
+ async function siteTimezone(site) {
38672
+ const key = String(site ?? "");
38673
+ const cached = siteZones.get(key);
38674
+ if (cached && Date.now() - cached.at < 5 * 6e4)
38675
+ return cached.zone;
38676
+ let zone = resolveSiteTimezone(null);
38677
+ if (key) {
38678
+ try {
38679
+ const found = await useSiteRepo().getSiteById(key);
38680
+ zone = resolveSiteTimezone(found);
38681
+ } catch {
38682
+ zone = resolveSiteTimezone(null);
38683
+ }
38684
+ }
38685
+ siteZones.set(key, { zone, at: Date.now() });
38686
+ return zone;
38687
+ }
38688
+ function blockWindow(vehicleEnd) {
38689
+ const now = /* @__PURE__ */ new Date();
38690
+ let end = validDate(vehicleEnd);
38691
+ if (!end || end.getTime() <= now.getTime()) {
38692
+ end = new Date(now);
38693
+ end.setFullYear(end.getFullYear() + 10);
38694
+ }
38695
+ return { start: now.toISOString(), end: end.toISOString() };
38696
+ }
38697
+ async function findPlateOnCamera(camera, mode, plateNumber) {
38698
+ const text2 = await _getPlateNumber({
38699
+ host: camera.host,
38700
+ username: camera.username,
38701
+ password: camera.password,
38702
+ mode,
38703
+ plateNumber,
38704
+ requireOk: true
38705
+ });
38706
+ return matchingPlateRecords(text2 ?? "", plateNumber);
38707
+ }
38708
+ async function removePlateFromCamera(camera, mode, plateNumber) {
38709
+ if (!normalisePlate(plateNumber)) {
38710
+ return { ...ANPR_NOTHING_TO_REVOKE, changed: false };
38711
+ }
38712
+ let changed = false;
38713
+ const MAX_PASSES = 3;
38714
+ for (let pass = 0; pass <= MAX_PASSES; pass++) {
38715
+ let records;
38716
+ try {
38717
+ records = await findPlateOnCamera(camera, mode, plateNumber);
38718
+ } catch (error) {
38719
+ const outcome = anprRevokeThrew(error);
38720
+ logCameraFailure("plate lookup", camera, outcome.detail);
38721
+ return { ...outcome, changed };
38722
+ }
38723
+ if (!records.length)
38724
+ return { ok: true, reason: "", changed };
38725
+ if (pass === MAX_PASSES)
38726
+ break;
38727
+ for (const record of records) {
38728
+ const outcome = await _removePlateNumber({
38729
+ host: camera.host,
38730
+ username: camera.username,
38731
+ password: camera.password,
38732
+ recno: record.recNo,
38733
+ mode
38734
+ }).catch((error) => anprRevokeThrew(error));
38735
+ if (!outcome?.ok) {
38736
+ logCameraFailure(
38737
+ "plate remove",
38738
+ camera,
38739
+ outcome?.detail ?? "no outcome returned"
38740
+ );
38741
+ return {
38742
+ ...outcome ?? anprRevokeThrew("no outcome returned"),
38743
+ changed
38744
+ };
38745
+ }
38746
+ changed = true;
38747
+ }
38748
+ }
38749
+ logCameraFailure(
38750
+ "plate remove",
38751
+ camera,
38752
+ `plate still listed after ${MAX_PASSES} rounds of removal`
38753
+ );
38754
+ return {
38755
+ ok: false,
38756
+ reason: "the camera did not remove the plate number",
38757
+ detail: `plate still listed after ${MAX_PASSES} rounds of removal`,
38758
+ changed
38759
+ };
38760
+ }
38761
+ async function insertPlateOnCamera(camera, write) {
38762
+ const response = await _addPlateNumber({
38763
+ host: camera.host,
38764
+ username: camera.username,
38765
+ password: camera.password,
38766
+ plateNumber: write.plateNumber,
38767
+ mode: write.mode,
38768
+ owner: write.owner,
38769
+ ...write.start ? { start: write.start } : {},
38770
+ ...write.end ? { end: write.end } : {},
38771
+ timezone: await siteTimezone(camera.site)
38772
+ });
38773
+ const status = response?.statusCode ?? response?.status ?? response?.res?.status;
38774
+ if (status !== 200)
38775
+ throw new Error(`HTTP ${status ?? "none"}`);
38776
+ return String(
38777
+ response?.data?.toString("utf-8")?.split("=")[1]?.trim() ?? ""
38778
+ );
38779
+ }
38780
+ async function ensurePlateOnCamera(camera, write) {
38781
+ const existing = await findPlateOnCamera(
38782
+ camera,
38783
+ write.mode,
38784
+ write.plateNumber
38785
+ );
38786
+ if (!existing.length) {
38787
+ await insertPlateOnCamera(camera, write);
38788
+ return { inserted: true };
38789
+ }
38790
+ const timezone = await siteTimezone(camera.site);
38791
+ const [keep, ...extra] = existing;
38792
+ const now = dahuaWallClock(/* @__PURE__ */ new Date(), timezone);
38793
+ const wanted = {
38794
+ start: dahuaWallClock(write.start, timezone),
38795
+ end: dahuaWallClock(write.end, timezone)
38796
+ };
38797
+ const windowIsRight = wanted.start && wanted.end ? plateWindowMatches(keep, wanted) : (
38798
+ // No dates of its own: any copy that lets it in now will do.
38799
+ Boolean(
38800
+ keep.beginTime && keep.cancelTime && now && (dahuaWallClock(keep.beginTime) ?? "") <= now && (dahuaWallClock(keep.cancelTime) ?? "") > now
38801
+ )
38802
+ );
38803
+ if (!windowIsRight) {
38804
+ await _setPlateWindow({
38805
+ host: camera.host,
38806
+ username: camera.username,
38807
+ password: camera.password,
38808
+ mode: write.mode,
38809
+ recno: keep.recNo,
38810
+ plateNumber: write.plateNumber,
38811
+ owner: write.owner,
38812
+ ...write.start ? { start: write.start } : {},
38813
+ ...write.end ? { end: write.end } : {},
38814
+ timezone
38815
+ });
38816
+ }
38817
+ for (const copy of extra) {
38818
+ const outcome = await _removePlateNumber({
38819
+ host: camera.host,
38820
+ username: camera.username,
38821
+ password: camera.password,
38822
+ recno: copy.recNo,
38823
+ mode: write.mode
38824
+ }).catch((error) => anprRevokeThrew(error));
38825
+ if (!outcome?.ok) {
38826
+ throw new Error(
38827
+ outcome?.reason || "the camera did not remove an old copy of the plate"
38828
+ );
38829
+ }
38830
+ }
38831
+ return { inserted: false };
38832
+ }
38833
+ function cameraJournal() {
38834
+ const steps = [];
38835
+ return {
38836
+ /** A plate was inserted; undo = take it off again. */
38837
+ inserted(camera, mode, plateNumber) {
38838
+ steps.push({
38839
+ camera,
38840
+ undo: () => removePlateFromCamera(camera, mode, plateNumber).then(
38841
+ (outcome) => outcome.ok,
38842
+ () => false
38843
+ )
38844
+ });
38845
+ },
38846
+ /** A plate was removed; undo = put it back as it was. */
38847
+ removed(camera, previous) {
38848
+ steps.push({
38849
+ camera,
38850
+ undo: () => insertPlateOnCamera(camera, previous).then(
38851
+ () => true,
38852
+ () => false
38853
+ )
38854
+ });
38855
+ },
38856
+ /**
38857
+ * Undo everything, newest first. Never throws. Returns the cameras that
38858
+ * could not be put back, so the operator can be told which barrier is
38859
+ * now wrong.
38860
+ */
38861
+ async rollback() {
38862
+ const stranded = [];
38863
+ for (const step of steps.reverse()) {
38864
+ if (!await step.undo())
38865
+ stranded.push(cameraLabel(step.camera));
38866
+ }
38867
+ steps.length = 0;
38868
+ return Array.from(new Set(stranded));
38869
+ }
38870
+ };
38871
+ }
38872
+ function strandedWarning(stranded) {
38873
+ if (!stranded.length)
38874
+ return "";
38875
+ 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.`;
38876
+ }
38877
+ async function rollbackCameras(journal, error) {
38878
+ const stranded = await journal.rollback().catch(() => []);
38879
+ if (stranded.length) {
38880
+ import_node_server_utils106.logger.error(
38881
+ `vehicle camera rollback could not restore: ${stranded.join(", ")}`
38882
+ );
38883
+ if (error && typeof error === "object") {
38884
+ error.message = `${error.message ?? ""}${strandedWarning(stranded)}`;
38885
+ }
38886
+ }
38887
+ }
38888
+ async function revokableModes(site, plateNumber, exclude) {
38889
+ const others = [];
38890
+ let siteId = null;
38891
+ try {
38892
+ siteId = new import_mongodb72.ObjectId(String(site));
38893
+ } catch {
38894
+ siteId = null;
38895
+ }
38896
+ if (siteId && plateNumber) {
38897
+ const excludeIds = exclude.map((id) => {
38898
+ try {
38899
+ return new import_mongodb72.ObjectId(String(id));
38900
+ } catch {
38901
+ return null;
38902
+ }
38903
+ }).filter((id) => id !== null);
38904
+ const base = {
38905
+ site: siteId,
38906
+ plateNumber,
38907
+ status: "active" /* ACTIVE */,
38908
+ _id: { $nin: excludeIds }
38909
+ };
38910
+ const [allowHolder, blockHolder] = await Promise.all([
38911
+ findOne({
38912
+ ...base,
38913
+ $or: [
38914
+ { type: "whitelist" /* WHITELIST */ },
38915
+ { blockedFrom: "whitelist" /* WHITELIST */ }
38916
+ ]
38917
+ }),
38918
+ findOne({ ...base, type: "blocklist" /* BLOCKLIST */ })
38919
+ ]);
38920
+ if (allowHolder)
38921
+ others.push(allowHolder);
38922
+ if (blockHolder)
38923
+ others.push(blockHolder);
38924
+ }
38925
+ const lists = listsToRevoke(others);
38926
+ return [
38927
+ ...lists.allow ? ["TrafficRedList" /* TRAFFIC_REDLIST */] : [],
38928
+ ...lists.block ? ["TrafficBlackList" /* TRAFFIC_BLACKLIST */] : []
38929
+ ];
38930
+ }
38931
+ async function removeFromCameras(cameras, modes, plateNumber, previous, journal, vehicleIsBlocked = false) {
38932
+ const failures = [];
38933
+ for (const camera of cameras) {
38934
+ for (const mode of modes) {
38935
+ if (!mustRevokeFrom(camera.direction, listOf(mode), vehicleIsBlocked)) {
38936
+ continue;
38937
+ }
38938
+ const outcome = await removePlateFromCamera(camera, mode, plateNumber);
38939
+ if (outcome.changed) {
38940
+ journal.removed(camera, { ...previous, plateNumber, mode });
38941
+ }
38942
+ if (!outcome.ok) {
38943
+ failures.push({
38944
+ camera: cameraLabel(camera),
38945
+ reason: outcome.reason || "the camera could not be reached"
38946
+ });
38947
+ }
38948
+ }
38949
+ }
38950
+ return failures;
38951
+ }
38952
+ async function writeToCameras(cameras, write, journal, options = {}) {
38953
+ const failures = [];
38954
+ let recNo = "";
38955
+ for (const camera of cameras) {
38956
+ try {
38957
+ if (options.onlyIfMissing) {
38958
+ const { inserted } = await ensurePlateOnCamera(camera, write);
38959
+ if (inserted)
38960
+ journal.inserted(camera, write.mode, write.plateNumber);
38961
+ } else {
38962
+ recNo = await insertPlateOnCamera(camera, write) || recNo;
38963
+ journal.inserted(camera, write.mode, write.plateNumber);
38964
+ }
38965
+ } catch (error) {
38966
+ logCameraFailure(`write ${write.mode}`, camera, error);
38967
+ failures.push({
38968
+ camera: cameraLabel(camera),
38969
+ reason: anprWriteReason(error)
38970
+ });
38971
+ }
38972
+ }
38973
+ return { failures, recNo };
38974
+ }
38975
+ async function blockOnCameras(vehicle, site, owner, journal) {
38976
+ const plateNumber = String(vehicle.plateNumber ?? "");
38977
+ if (!plateNumber)
38978
+ return;
38979
+ const cameras = await blockCamerasAt(site);
38980
+ if (!cameras.length) {
38981
+ throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("blocked"));
38982
+ }
38983
+ const { failures } = await writeToCameras(
38984
+ cameras,
38985
+ {
38986
+ plateNumber,
38987
+ mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
38988
+ owner,
38989
+ ...blockWindow(vehicle.end)
38990
+ },
38991
+ journal,
38992
+ { onlyIfMissing: true }
38993
+ );
38994
+ if (failures.length) {
38995
+ throw new import_node_server_utils106.BadRequestError(anprBlockFailureMessage(failures));
38996
+ }
38997
+ }
38998
+ async function unblockOnCameras(vehicle, site, options, journal) {
38999
+ const plateNumber = String(vehicle.plateNumber ?? "");
39000
+ if (!plateNumber)
39001
+ return;
39002
+ const window = cameraWindow(vehicle.start, vehicle.end);
39003
+ if (options.restore) {
39004
+ const allowCameras = await allowCamerasAt(site);
39005
+ const { failures: failures2 } = await writeToCameras(
39006
+ allowCameras,
39007
+ {
39008
+ plateNumber,
39009
+ mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
39010
+ owner: options.owner,
39011
+ ...window
39012
+ },
39013
+ journal,
39014
+ { onlyIfMissing: true }
39015
+ );
39016
+ if (failures2.length) {
39017
+ throw new import_node_server_utils106.BadRequestError(
39018
+ cameraFailureSentence({
39019
+ failures: failures2,
39020
+ operation: "added to",
39021
+ outcome: "unblocked",
39022
+ consequence: "it is still blocked at the barrier"
39023
+ })
39024
+ );
39025
+ }
39026
+ }
39027
+ const revokable = await revokableModes(site, plateNumber, [vehicle._id]);
39028
+ const modes = options.restore ? revokable.filter((mode) => mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */) : revokable;
39029
+ if (!modes.length)
39030
+ return;
39031
+ const failures = await removeFromCameras(
39032
+ await camerasAt(site, { activeOnly: true }),
39033
+ modes,
39034
+ plateNumber,
39035
+ { owner: options.owner, ...window },
39036
+ journal,
39037
+ true
39038
+ );
39039
+ if (failures.length) {
39040
+ throw new import_node_server_utils106.BadRequestError(anprUnblockFailureMessage(failures));
39041
+ }
39042
+ }
38106
39043
  async function add(value, session, user) {
38107
39044
  const isExternalSession = !!session;
38108
39045
  if (!session) {
@@ -38157,18 +39094,18 @@ function useVehicleService() {
38157
39094
  endDate.setFullYear(endDate.getFullYear() + 10);
38158
39095
  start = startDate.toISOString();
38159
39096
  end = endDate.toISOString();
38160
- startDateDahua = formatDahuaDate(startDate);
38161
- endDateDahua = formatDahuaDate(endDate);
39097
+ startDateDahua = start;
39098
+ endDateDahua = end;
38162
39099
  } else {
38163
39100
  if (hasStart) {
38164
39101
  startDate = new Date(value.start);
38165
39102
  start = startDate.toISOString();
38166
- startDateDahua = formatDahuaDate(startDate);
39103
+ startDateDahua = start;
38167
39104
  }
38168
39105
  if (hasEnd) {
38169
39106
  endDate = new Date(value.end);
38170
39107
  end = endDate.toISOString();
38171
- endDateDahua = formatDahuaDate(endDate);
39108
+ endDateDahua = end;
38172
39109
  }
38173
39110
  }
38174
39111
  const owner = value.name;
@@ -38181,22 +39118,7 @@ function useVehicleService() {
38181
39118
  let message = "Vehicle plate number needs approval from property management.";
38182
39119
  let siteCameras = [];
38183
39120
  if (value.status && value.status !== "pending" /* PENDING */) {
38184
- let page = 1;
38185
- let pages = 1;
38186
- const limit = 20;
38187
- do {
38188
- const siteCameraReq = await _getAllSiteCameras({
38189
- site: value.site,
38190
- type: "anpr",
38191
- direction: ["both", "entry", "residents"],
38192
- page,
38193
- limit,
38194
- status: "active"
38195
- });
38196
- pages = siteCameraReq.pages || 1;
38197
- siteCameras.push(...siteCameraReq.items);
38198
- page++;
38199
- } while (page <= pages);
39121
+ siteCameras = isBlocklist ? await blockCamerasAt(value.site) : await allowCamerasAt(value.site);
38200
39122
  import_node_server_utils106.logger.info(
38201
39123
  `addVehicle resolved ${siteCameras.length} site camera(s) for site ${value?.site}`
38202
39124
  );
@@ -38231,7 +39153,10 @@ function useVehicleService() {
38231
39153
  mode: _mode,
38232
39154
  owner,
38233
39155
  ...startDateDahua ? { start: startDateDahua } : {},
38234
- ...endDateDahua ? { end: endDateDahua } : {}
39156
+ ...endDateDahua ? { end: endDateDahua } : {},
39157
+ // The camera enforces the season pass window on its own clock,
39158
+ // in the site's zone.
39159
+ timezone: await siteTimezone(camera.site ?? value.site)
38235
39160
  };
38236
39161
  try {
38237
39162
  const dahuaResponse = await _addPlateNumber(dahuaPayload);
@@ -38251,8 +39176,7 @@ function useVehicleService() {
38251
39176
  }
38252
39177
  const recNo = String(responseData.split("=")[1]?.trim() ?? "");
38253
39178
  vehicleValue.recNo = recNo;
38254
- if (recNo)
38255
- written.push({ camera, recno: recNo, mode: _mode });
39179
+ written.push({ camera, recno: recNo, mode: _mode, plateNumber: plateNumber2 });
38256
39180
  if (value.peopleId && recNo) {
38257
39181
  await _pushVehicleById(
38258
39182
  value.peopleId,
@@ -38282,13 +39206,18 @@ function useVehicleService() {
38282
39206
  } catch (error) {
38283
39207
  const stranded = [];
38284
39208
  for (const entry of written) {
38285
- const outcome = await _removePlateNumber({
39209
+ const usableId = entry.recno && /^\d+$/.test(entry.recno);
39210
+ const outcome = usableId ? await _removePlateNumber({
38286
39211
  host: entry.camera.host,
38287
39212
  username: entry.camera.username,
38288
39213
  password: entry.camera.password,
38289
39214
  recno: entry.recno,
38290
39215
  mode: entry.mode
38291
- }).catch((removeError) => anprRevokeThrew(removeError));
39216
+ }).catch((removeError) => anprRevokeThrew(removeError)) : await removePlateFromCamera(
39217
+ entry.camera,
39218
+ entry.mode,
39219
+ entry.plateNumber
39220
+ );
38292
39221
  if (!outcome?.ok)
38293
39222
  stranded.push(cameraLabel(entry.camera));
38294
39223
  }
@@ -38314,80 +39243,28 @@ function useVehicleService() {
38314
39243
  if (!session) {
38315
39244
  throw new Error("Unable to start session for vehicle service.");
38316
39245
  }
38317
- const _mode = type !== "whitelist" /* WHITELIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
39246
+ const journal = cameraJournal();
38318
39247
  try {
38319
39248
  session.startTransaction();
38320
- const siteCameras = [];
38321
- let page = 1;
38322
- let pages = 1;
38323
- const limit = 20;
38324
- do {
38325
- const siteCameraReq = await _getAllSiteCameras({
38326
- site,
38327
- type: "anpr",
38328
- page,
38329
- limit
38330
- });
38331
- pages = siteCameraReq.pages || 1;
38332
- siteCameras.push(...siteCameraReq.items);
38333
- page++;
38334
- } while (page <= pages);
38335
- import_node_server_utils106.logger.info(
38336
- `deleteVehicle resolved ${siteCameras.length} ANPR camera(s) for site ${site}`
38337
- );
38338
39249
  const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(_id) });
38339
39250
  const plateNumber = String(vehicle?.plateNumber ?? "");
38340
- const failures = [];
38341
- for (const camera of siteCameras) {
38342
- const host = camera.host;
38343
- const username = camera.username;
38344
- const password = camera.password;
38345
- const label = cameraLabel(camera);
38346
- let recnoForCamera = recno;
38347
- if (plateNumber) {
38348
- try {
38349
- const found = parseDahuaFind(
38350
- await _getPlateNumber({
38351
- host,
38352
- username,
38353
- password,
38354
- mode: _mode,
38355
- plateNumber,
38356
- requireOk: true
38357
- }) ?? ""
38358
- );
38359
- if (!found.exists)
38360
- continue;
38361
- if (found.recNo)
38362
- recnoForCamera = found.recNo;
38363
- } catch (error) {
38364
- const outcome2 = anprRevokeThrew(error);
38365
- logCameraFailure("deleteVehicle lookup", camera, outcome2.detail);
38366
- failures.push({ camera: label, reason: outcome2.reason });
38367
- continue;
38368
- }
38369
- }
38370
- if (!recnoForCamera)
38371
- continue;
38372
- const outcome = await _removePlateNumber({
38373
- host,
38374
- username,
38375
- password,
38376
- recno: recnoForCamera,
38377
- mode: _mode
38378
- });
38379
- if (!outcome?.ok) {
38380
- logCameraFailure(
38381
- "deleteVehicle revoke",
38382
- camera,
38383
- outcome?.detail ?? "no outcome returned"
38384
- );
38385
- failures.push({
38386
- camera: label,
38387
- reason: outcome?.reason || "the camera could not be reached"
38388
- });
38389
- }
38390
- }
39251
+ const cameraSite = vehicle?.site ?? site;
39252
+ const siteCameras = await camerasAt(cameraSite, { activeOnly: true });
39253
+ import_node_server_utils106.logger.info(
39254
+ `deleteVehicle resolved ${siteCameras.length} ANPR camera(s) for site ${cameraSite}`
39255
+ );
39256
+ const modes = plateNumber ? await revokableModes(cameraSite, plateNumber, [_id]) : [];
39257
+ const failures = await removeFromCameras(
39258
+ siteCameras,
39259
+ modes,
39260
+ plateNumber,
39261
+ {
39262
+ owner: String(vehicle?.name ?? ""),
39263
+ ...cameraWindow(vehicle?.start, vehicle?.end)
39264
+ },
39265
+ journal,
39266
+ vehicle?.type === "blocklist" /* BLOCKLIST */
39267
+ );
38391
39268
  if (failures.length) {
38392
39269
  const message = anprRevokeFailureMessage(failures);
38393
39270
  if (!bypass)
@@ -38397,13 +39274,14 @@ function useVehicleService() {
38397
39274
  await _deleteVehicle(_id, session);
38398
39275
  await _pullVehicleByRecNo(
38399
39276
  recno,
38400
- { peopleId: vehicle?.peopleId, site, plateNumber },
39277
+ { peopleId: vehicle?.peopleId, site: cameraSite, plateNumber },
38401
39278
  session
38402
39279
  );
38403
39280
  await session.commitTransaction();
38404
39281
  return `Vehicle plate number deleted from ${type} record successfully.`;
38405
39282
  } catch (error) {
38406
39283
  import_node_server_utils106.logger.error("Error in vehicle service delete:", error);
39284
+ await rollbackCameras(journal, error);
38407
39285
  await session.abortTransaction();
38408
39286
  throw error;
38409
39287
  } finally {
@@ -38418,13 +39296,14 @@ function useVehicleService() {
38418
39296
  const org = await _getById(orgId);
38419
39297
  if (!org)
38420
39298
  throw new import_node_server_utils106.BadRequestError("Org not found");
38421
- const vehicle = await _getVehicleById(id);
38422
- const plate = vehicle.plates.find((p) => p._id.toString() === id);
38423
- const _plateNumber = plate?.plateNumber;
39299
+ const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(id) });
38424
39300
  if (!vehicle) {
38425
- throw new import_node_server_utils106.BadRequestError("Vehicle not found");
39301
+ throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
38426
39302
  }
39303
+ const _plateNumber = String(vehicle.plateNumber ?? "");
38427
39304
  const owner = vehicle.name;
39305
+ const isBlocklist = vehicle.type === "blocklist" /* BLOCKLIST */;
39306
+ const mode = listMode(vehicle.type);
38428
39307
  const hasStart = typeof vehicle.start === "string" ? vehicle.start.trim() !== "" : !!vehicle.start;
38429
39308
  const hasEnd = typeof vehicle.end === "string" ? vehicle.end.trim() !== "" : !!vehicle.end;
38430
39309
  let startDate = null;
@@ -38442,70 +39321,48 @@ function useVehicleService() {
38442
39321
  endDate.setFullYear(endDate.getFullYear() + 10);
38443
39322
  }
38444
39323
  }
38445
- const startDahua = startDate ? formatDahuaDate(startDate) : "";
38446
- const endDahua = endDate ? formatDahuaDate(endDate) : "";
39324
+ const startDahua = startDate ? startDate.toISOString() : "";
39325
+ const endDahua = endDate ? endDate.toISOString() : "";
39326
+ const journal = cameraJournal();
38447
39327
  try {
38448
39328
  session.startTransaction();
38449
- const siteCameras = [];
38450
- let page = 1;
38451
- let pages = 1;
38452
- const limit = 20;
38453
- let value = {};
38454
- do {
38455
- const siteCameraReq = await _getAllSiteCameras({
38456
- site: siteId,
38457
- type: "anpr",
38458
- direction: ["both", "entry", "residents"],
38459
- page,
38460
- limit
38461
- });
38462
- pages = siteCameraReq.pages || 1;
38463
- siteCameras.push(...siteCameraReq.items);
38464
- page++;
38465
- } while (page <= pages);
39329
+ const cameraSite = vehicle.site ?? siteId;
39330
+ const siteCameras = isBlocklist ? await blockCamerasAt(cameraSite) : await allowCamerasAt(cameraSite);
38466
39331
  if (!siteCameras.length) {
38467
39332
  throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("approved"));
38468
39333
  }
38469
- for (const camera of siteCameras) {
38470
- const { host, username, password } = camera;
38471
- const dahuaPayload = {
38472
- host,
38473
- username,
38474
- password,
39334
+ const { failures, recNo } = await writeToCameras(
39335
+ siteCameras,
39336
+ {
38475
39337
  plateNumber: _plateNumber,
38476
- mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
39338
+ mode,
39339
+ owner,
38477
39340
  start: startDahua,
38478
- end: endDahua,
38479
- owner
38480
- };
38481
- const dahuaResponse = await _addPlateNumber(dahuaPayload);
38482
- if (dahuaResponse?.statusCode != 200) {
38483
- logCameraFailure(
38484
- "approveVehicleById",
38485
- camera,
38486
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38487
- );
38488
- throw new import_node_server_utils106.BadRequestError(
38489
- anprWriteFailureMessage([
38490
- {
38491
- camera: cameraLabel(camera),
38492
- reason: anprWriteReason(
38493
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38494
- )
38495
- }
38496
- ])
38497
- );
38498
- }
38499
- const responseData = dahuaResponse?.data.toString("utf-8");
38500
- value.recNo = responseData.split("=")[1]?.trim();
39341
+ end: endDahua
39342
+ },
39343
+ journal
39344
+ );
39345
+ if (failures.length) {
39346
+ throw new import_node_server_utils106.BadRequestError(
39347
+ cameraFailureSentence({
39348
+ failures,
39349
+ operation: "added to",
39350
+ outcome: "approved",
39351
+ consequence: isBlocklist ? "it would still open the barrier" : "it would not open the barrier"
39352
+ })
39353
+ );
38501
39354
  }
38502
- value.status = "active" /* ACTIVE */;
38503
- if (vehicle.peopleId && value.recNo) {
39355
+ const value = {
39356
+ status: "active" /* ACTIVE */,
39357
+ // Kept for older readers only - one camera's number, never replayed.
39358
+ ...recNo ? { recNo } : {}
39359
+ };
39360
+ if (vehicle.peopleId && _plateNumber) {
38504
39361
  await _pushVehicleById(
38505
39362
  vehicle.peopleId,
38506
39363
  {
38507
39364
  plateNumber: _plateNumber,
38508
- recNo: value.recNo
39365
+ recNo
38509
39366
  },
38510
39367
  session
38511
39368
  );
@@ -38520,6 +39377,7 @@ function useVehicleService() {
38520
39377
  return "Vehicle plate number approved and added successfully.";
38521
39378
  } catch (error) {
38522
39379
  import_node_server_utils106.logger.error("Error in vehicle service approving and adding:", error);
39380
+ await rollbackCameras(journal, error);
38523
39381
  await session.abortTransaction();
38524
39382
  throw error;
38525
39383
  } finally {
@@ -38528,71 +39386,38 @@ function useVehicleService() {
38528
39386
  }
38529
39387
  async function processDeletingExpiredVehicles() {
38530
39388
  const vehicles = await _getAllExpiredVehicles();
39389
+ const sweptIds = vehicles.map((vehicle) => vehicle?._id);
39390
+ const modesByVehicle = /* @__PURE__ */ new Map();
38531
39391
  const { deletableIds, skipped } = await sweepExpiredVehicles(vehicles, {
38532
- camerasForSite: async (site) => {
38533
- const cameras = [];
38534
- let page = 1;
38535
- let pages = 1;
38536
- const limit = 20;
38537
- do {
38538
- const siteCameraReq = await _getAllSiteCameras({
38539
- site,
38540
- type: "anpr",
38541
- page,
38542
- limit
38543
- });
38544
- pages = siteCameraReq.pages || 1;
38545
- cameras.push(...siteCameraReq.items);
38546
- page++;
38547
- } while (page <= pages);
38548
- return cameras;
38549
- },
39392
+ // Every ACTIVE ANPR camera at the site (an inactive one is skipped, as
39393
+ // by every add and update); which of them each list is taken off is
39394
+ // `mustRevokeFrom`.
39395
+ camerasForSite: (site) => camerasAt(site, { activeOnly: true }),
38550
39396
  revoke: async (camera, vehicle) => {
38551
- const host = camera.host;
38552
- const username = camera.username;
38553
- const password = camera.password;
38554
- const mode = vehicle?.type !== "whitelist" /* WHITELIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
38555
39397
  const plateNumber = String(vehicle?.plateNumber ?? "");
38556
- let recnoForCamera = String(vehicle?.recNo ?? "");
38557
- if (plateNumber) {
38558
- try {
38559
- const found = parseDahuaFind(
38560
- await _getPlateNumber({
38561
- host,
38562
- username,
38563
- password,
38564
- mode,
38565
- plateNumber,
38566
- requireOk: true
38567
- }) ?? ""
38568
- );
38569
- if (!found.exists)
38570
- return ANPR_NOTHING_TO_REVOKE;
38571
- if (found.recNo)
38572
- recnoForCamera = found.recNo;
38573
- } catch (error) {
38574
- const outcome2 = anprRevokeThrew(error);
38575
- logCameraFailure("expiredVehicle lookup", camera, outcome2.detail);
38576
- return outcome2;
38577
- }
38578
- }
38579
- if (!recnoForCamera)
39398
+ if (!plateNumber)
38580
39399
  return ANPR_NOTHING_TO_REVOKE;
38581
- const outcome = await _removePlateNumber({
38582
- host,
38583
- username,
38584
- password,
38585
- recno: recnoForCamera,
38586
- mode
38587
- }).catch((error) => anprRevokeThrew(error));
38588
- if (!outcome?.ok) {
38589
- logCameraFailure(
38590
- "expiredVehicle revoke",
38591
- camera,
38592
- outcome?.detail ?? "no outcome returned"
38593
- );
39400
+ const key = String(vehicle?._id ?? plateNumber);
39401
+ let modes = modesByVehicle.get(key);
39402
+ if (!modes) {
39403
+ modes = revokableModes(vehicle?.site, plateNumber, sweptIds);
39404
+ modesByVehicle.set(key, modes);
39405
+ }
39406
+ let resolved;
39407
+ try {
39408
+ resolved = await modes;
39409
+ } catch (error) {
39410
+ return anprRevokeThrew(error);
38594
39411
  }
38595
- return outcome ?? anprRevokeThrew("no outcome returned");
39412
+ for (const mode of resolved) {
39413
+ const blocked = vehicle?.type === "blocklist" /* BLOCKLIST */;
39414
+ if (!mustRevokeFrom(camera.direction, listOf(mode), blocked))
39415
+ continue;
39416
+ const outcome = await removePlateFromCamera(camera, mode, plateNumber);
39417
+ if (!outcome.ok)
39418
+ return outcome;
39419
+ }
39420
+ return ANPR_NOTHING_TO_REVOKE;
38596
39421
  },
38597
39422
  cameraLabel
38598
39423
  });
@@ -38619,90 +39444,83 @@ function useVehicleService() {
38619
39444
  "Only property management can reactivate vehicles."
38620
39445
  );
38621
39446
  }
38622
- const vehicle = await _getVehicleById(id);
38623
- const plate = vehicle.plates.find((p) => p._id.toString() === id);
38624
- const _plateNumber = plate?.plateNumber;
39447
+ const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(id) });
38625
39448
  if (!vehicle) {
38626
- throw new import_node_server_utils106.BadRequestError("Vehicle not found");
39449
+ throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
38627
39450
  }
39451
+ const _plateNumber = String(vehicle.plateNumber ?? "");
38628
39452
  const owner = vehicle.name;
38629
- let startDate = /* @__PURE__ */ new Date();
38630
- let endDate = new Date(startDate);
38631
- endDate.setFullYear(endDate.getFullYear() + 10);
38632
- const startIso = startDate ? startDate.toISOString() : "";
38633
- const endIso = endDate ? endDate.toISOString() : "";
38634
- const startDahua = startDate ? formatDahuaDate(startDate) : "";
38635
- const endDahua = endDate ? formatDahuaDate(endDate) : "";
39453
+ const isBlocklist = vehicle.type === "blocklist" /* BLOCKLIST */;
39454
+ const mode = listMode(vehicle.type);
39455
+ const ownStart = validDate(vehicle.start);
39456
+ const ownEnd = validDate(vehicle.end);
39457
+ let startDate;
39458
+ let endDate;
39459
+ if (ownEnd) {
39460
+ if (ownEnd.getTime() <= Date.now()) {
39461
+ throw new import_node_server_utils106.BadRequestError(
39462
+ `This vehicle's expiry date (${dahuaWallClock(
39463
+ ownEnd,
39464
+ await siteTimezone(vehicle.site ?? siteId)
39465
+ )}) has already passed, so it cannot be restored as it is. Add it again with new dates.`
39466
+ );
39467
+ }
39468
+ startDate = ownStart ?? /* @__PURE__ */ new Date();
39469
+ endDate = ownEnd;
39470
+ } else {
39471
+ startDate = ownStart ?? /* @__PURE__ */ new Date();
39472
+ endDate = new Date(startDate);
39473
+ endDate.setFullYear(endDate.getFullYear() + 10);
39474
+ }
39475
+ const startDahua = startDate.toISOString();
39476
+ const endDahua = endDate.toISOString();
39477
+ const journal = cameraJournal();
38636
39478
  try {
38637
39479
  session.startTransaction();
38638
- let value = {};
38639
- const siteCameras = [];
38640
- let page = 1;
38641
- let pages = 1;
38642
- const limit = 20;
38643
- do {
38644
- const siteCameraReq = await _getAllSiteCameras({
38645
- site: siteId,
38646
- type: "anpr",
38647
- direction: ["both", "entry"],
38648
- page,
38649
- limit
38650
- });
38651
- pages = siteCameraReq.pages || 1;
38652
- siteCameras.push(...siteCameraReq.items);
38653
- page++;
38654
- } while (page <= pages);
39480
+ const cameraSite = vehicle.site ?? siteId;
39481
+ const siteCameras = isBlocklist ? await blockCamerasAt(cameraSite) : await allowCamerasAt(cameraSite);
38655
39482
  if (!siteCameras.length) {
38656
39483
  throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("restored"));
38657
39484
  }
38658
- for (const camera of siteCameras) {
38659
- const { host, username, password } = camera;
38660
- const dahuaPayload = {
38661
- host,
38662
- username,
38663
- password,
39485
+ const { failures, recNo } = await writeToCameras(
39486
+ siteCameras,
39487
+ {
38664
39488
  plateNumber: _plateNumber,
38665
- mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
39489
+ mode,
39490
+ owner,
38666
39491
  start: startDahua,
38667
- end: endDahua,
38668
- owner
38669
- };
38670
- const dahuaResponse = await _addPlateNumber(dahuaPayload);
38671
- if (dahuaResponse?.statusCode !== 200) {
38672
- logCameraFailure(
38673
- "reactivateVehicleById",
38674
- camera,
38675
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38676
- );
38677
- throw new import_node_server_utils106.BadRequestError(
38678
- anprRestoreFailureMessage([
38679
- {
38680
- camera: cameraLabel(camera),
38681
- reason: anprWriteReason(
38682
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38683
- )
38684
- }
38685
- ])
38686
- );
38687
- }
38688
- const responseData = dahuaResponse?.data.toString("utf-8");
38689
- value.recNo = responseData.split("=")[1]?.trim();
39492
+ end: endDahua
39493
+ },
39494
+ journal
39495
+ );
39496
+ if (failures.length) {
39497
+ throw new import_node_server_utils106.BadRequestError(
39498
+ isBlocklist ? cameraFailureSentence({
39499
+ failures,
39500
+ operation: "added to",
39501
+ outcome: "restored",
39502
+ consequence: "it would still open the barrier"
39503
+ }) : anprRestoreFailureMessage(failures)
39504
+ );
38690
39505
  }
38691
- value.status = "active" /* ACTIVE */;
38692
- if (vehicle.peopleId && value.recNo) {
39506
+ if (vehicle.peopleId && _plateNumber) {
38693
39507
  await _pushVehicleById(
38694
39508
  vehicle.peopleId,
38695
39509
  {
38696
39510
  plateNumber: _plateNumber,
38697
- recNo: value.recNo
39511
+ recNo
38698
39512
  },
38699
39513
  session
38700
39514
  );
38701
39515
  }
38702
39516
  const formattedValue = {
38703
- ...value,
38704
- start: startIso,
38705
- end: endIso,
39517
+ status: "active" /* ACTIVE */,
39518
+ // Kept for older readers only - one camera's number, never replayed.
39519
+ ...recNo ? { recNo } : {},
39520
+ // Dates, not ISO strings: the expiry sweep only matches a BSON Date
39521
+ // (see `expiredVehicleSweepFilter`), so a string here never expired.
39522
+ start: startDate,
39523
+ end: endDate,
38706
39524
  deletedAt: ""
38707
39525
  };
38708
39526
  await _updateVehicleById(id, formattedValue, session);
@@ -38710,21 +39528,62 @@ function useVehicleService() {
38710
39528
  return "Vehicle reactivated successfully.";
38711
39529
  } catch (error) {
38712
39530
  import_node_server_utils106.logger.error("Error in vehicle service reactivation:", error);
39531
+ await rollbackCameras(journal, error);
38713
39532
  await session.abortTransaction();
38714
39533
  throw error;
38715
39534
  } finally {
38716
39535
  session.endSession();
38717
39536
  }
38718
39537
  }
38719
- async function restorePlateOnCamera(payload) {
38720
- try {
38721
- const response = await _addPlateNumber(payload);
38722
- return (response?.statusCode ?? response?.status) === 200;
38723
- } catch (error) {
38724
- import_node_server_utils106.logger.error(
38725
- `Failed to restore plate on camera ${payload.host}: ${error?.message || error}`
39538
+ async function rewriteOnCameras(vehicle, site, next, journal) {
39539
+ const oldPlate = String(vehicle.plateNumber ?? "");
39540
+ const held = listsHeldBy(vehicle);
39541
+ const targets = [];
39542
+ if (held.allow) {
39543
+ targets.push({
39544
+ mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
39545
+ cameras: await allowCamerasAt(site)
39546
+ });
39547
+ }
39548
+ if (held.block) {
39549
+ targets.push({
39550
+ mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39551
+ cameras: await blockCamerasAt(site)
39552
+ });
39553
+ }
39554
+ if (!targets.some((target) => target.cameras.length)) {
39555
+ throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("updated"));
39556
+ }
39557
+ const removeModes = next.plateNumber !== oldPlate ? await revokableModes(site, oldPlate, [vehicle._id]) : targets.map((target) => target.mode);
39558
+ const removeFailures = await removeFromCameras(
39559
+ await camerasAt(site, { activeOnly: true }),
39560
+ removeModes,
39561
+ oldPlate,
39562
+ {
39563
+ owner: String(vehicle.name ?? ""),
39564
+ ...cameraWindow(vehicle.start, vehicle.end)
39565
+ },
39566
+ journal,
39567
+ held.block
39568
+ );
39569
+ if (removeFailures.length) {
39570
+ throw new import_node_server_utils106.BadRequestError(
39571
+ anprRevokeFailureMessage(removeFailures, "updated")
39572
+ );
39573
+ }
39574
+ const writeFailures = [];
39575
+ for (const target of targets) {
39576
+ const { failures } = await writeToCameras(
39577
+ target.cameras,
39578
+ { ...next, mode: target.mode },
39579
+ journal
39580
+ );
39581
+ writeFailures.push(...failures);
39582
+ }
39583
+ if (writeFailures.length) {
39584
+ throw new import_node_server_utils106.BadRequestError(
39585
+ anprWriteFailureMessage(writeFailures, "updated")
38726
39586
  );
38727
- return false;
38728
39587
  }
38729
39588
  }
38730
39589
  async function updateVehicleById(_id, value) {
@@ -38732,241 +39591,75 @@ function useVehicleService() {
38732
39591
  if (!session) {
38733
39592
  throw new Error("Unable to start session for vehicle service.");
38734
39593
  }
39594
+ const journal = cameraJournal();
38735
39595
  try {
38736
39596
  session.startTransaction();
38737
- const vehicle = await _getVehicleById(_id);
38738
- const plate = vehicle.plates.find((p) => p._id.toString() === _id);
38739
- const _name = vehicle.name;
38740
- const _plateNumber = plate?.plateNumber;
38741
- const _start = vehicle.start;
38742
- const _end = vehicle.end;
38743
- const _recNo = plate.recNo;
38744
- const _type = value.type ? value.type : plate.type;
39597
+ const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(_id) });
39598
+ if (!vehicle) {
39599
+ throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
39600
+ }
38745
39601
  if (value.peopleId) {
38746
39602
  value.peopleId = new import_mongodb72.ObjectId(value.peopleId);
38747
39603
  }
38748
39604
  const { name, plateNumber, start, end, recNo, type, unit, site, block, level, ...rest } = value;
38749
- if (_name != name || _plateNumber != plateNumber) {
38750
- const startDahua = value.start ? formatDahuaDate(new Date(value.start)) : formatDahuaDate(new Date(_start));
38751
- const endDahua = value.end ? formatDahuaDate(new Date(value.end)) : formatDahuaDate(new Date(_end));
38752
- if (name || plateNumber || start || end) {
38753
- const siteCameras = [];
38754
- let page = 1;
38755
- let pages = 1;
38756
- const limit = 20;
38757
- do {
38758
- const siteCameraReq = await _getAllSiteCameras({
38759
- site,
38760
- type: "anpr",
38761
- direction: ["both", "entry", "residents"],
38762
- page,
38763
- limit
38764
- });
38765
- pages = siteCameraReq.pages || 1;
38766
- siteCameras.push(...siteCameraReq.items);
38767
- page++;
38768
- } while (page <= pages);
38769
- if (!siteCameras.length) {
38770
- throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("updated"));
39605
+ const oldPlate = String(vehicle.plateNumber ?? "");
39606
+ const oldName = String(vehicle.name ?? "");
39607
+ const oldType = String(vehicle.type || "whitelist" /* WHITELIST */);
39608
+ const newPlate = plateNumber ? String(plateNumber).trim() : oldPlate;
39609
+ const newName = name ? String(name) : oldName;
39610
+ const newType = type ? String(type) : oldType;
39611
+ const cameraSite = vehicle.site ?? site;
39612
+ const startDate = validDate(start);
39613
+ const endDate = validDate(end);
39614
+ const datesChanged = !!startDate && startDate.getTime() !== validDate(vehicle.start)?.getTime() || !!endDate && endDate.getTime() !== validDate(vehicle.end)?.getTime();
39615
+ const isLive = vehicle.status === "active" /* ACTIVE */;
39616
+ let typeFields = {};
39617
+ if (newType !== oldType) {
39618
+ if (newPlate !== oldPlate) {
39619
+ throw new import_node_server_utils106.BadRequestError(
39620
+ "Change the plate number and the vehicle type in two separate steps."
39621
+ );
39622
+ }
39623
+ if (newType === "blocklist" /* BLOCKLIST */) {
39624
+ if (isLive) {
39625
+ await blockOnCameras(vehicle, cameraSite, newName, journal);
38771
39626
  }
38772
- for (const camera of siteCameras) {
38773
- const { host, username, password } = camera;
38774
- if (_plateNumber != plateNumber) {
38775
- const removePlateNumber = {
38776
- host,
38777
- username,
38778
- password,
38779
- mode: plate.type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38780
- //whitelist or blocklist
38781
- recno: _recNo
38782
- };
38783
- const responseForDeletion = await _removePlateNumber(
38784
- removePlateNumber
38785
- );
38786
- if (!responseForDeletion?.ok) {
38787
- logCameraFailure(
38788
- "updateVehicleById revoke",
38789
- camera,
38790
- responseForDeletion?.detail ?? "no outcome returned"
38791
- );
38792
- throw new import_node_server_utils106.BadRequestError(
38793
- anprRevokeFailureMessage(
38794
- [
38795
- {
38796
- camera: cameraLabel(camera),
38797
- reason: responseForDeletion?.reason || "the camera could not be reached"
38798
- }
38799
- ],
38800
- "updated"
38801
- )
38802
- );
38803
- }
38804
- const dahuaPayload = {
38805
- host,
38806
- username,
38807
- password,
38808
- plateNumber,
38809
- mode: value.type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38810
- //whitelist or blocklist
38811
- owner: name,
38812
- ...startDahua ? { start: startDahua } : {},
38813
- ...endDahua ? { end: endDahua } : {}
38814
- };
38815
- let dahuaResponse;
38816
- try {
38817
- dahuaResponse = await _addPlateNumber(dahuaPayload);
38818
- } catch (error) {
38819
- logCameraFailure("updateVehicleById write", camera, error);
38820
- const restored = await restorePlateOnCamera({
38821
- ...removePlateNumber,
38822
- plateNumber: _plateNumber,
38823
- owner: _name,
38824
- ...startDahua ? { start: startDahua } : {},
38825
- ...endDahua ? { end: endDahua } : {}
38826
- });
38827
- throw new import_node_server_utils106.BadRequestError(
38828
- anprWriteFailureMessage(
38829
- [{ camera: cameraLabel(camera), reason: anprWriteReason(error) }],
38830
- "updated"
38831
- ) + (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.`)
38832
- );
38833
- }
38834
- if (dahuaResponse?.statusCode !== 200) {
38835
- logCameraFailure(
38836
- "updateVehicleById write",
38837
- camera,
38838
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38839
- );
38840
- throw new import_node_server_utils106.BadRequestError(
38841
- anprWriteFailureMessage(
38842
- [
38843
- {
38844
- camera: cameraLabel(camera),
38845
- reason: anprWriteReason(
38846
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38847
- )
38848
- }
38849
- ],
38850
- "updated"
38851
- )
38852
- );
38853
- }
38854
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
38855
- value.recNo = responseData.split("=")[1]?.trim();
38856
- if (value.peopleId && value.recNo) {
38857
- await _pushVehicleById(
38858
- value.peopleId,
38859
- {
38860
- plateNumber,
38861
- recNo: value.recNo
38862
- },
38863
- session
38864
- );
38865
- }
38866
- } else {
38867
- const existing = parseDahuaFind(
38868
- await _getPlateNumber({
38869
- host,
38870
- username,
38871
- password,
38872
- mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38873
- plateNumber,
38874
- requireOk: true
38875
- }) ?? ""
38876
- );
38877
- if (existing.exists) {
38878
- const outcome = await _removePlateNumber({
38879
- host,
38880
- username,
38881
- password,
38882
- mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38883
- recno: existing.recNo || _recNo
38884
- });
38885
- if (!outcome?.ok) {
38886
- logCameraFailure(
38887
- "updateVehicleById revoke",
38888
- camera,
38889
- outcome?.detail ?? "no outcome returned"
38890
- );
38891
- throw new import_node_server_utils106.BadRequestError(
38892
- anprRevokeFailureMessage([
38893
- {
38894
- camera: cameraLabel(camera),
38895
- reason: outcome?.reason || "the camera could not be reached"
38896
- }
38897
- ], "updated")
38898
- );
38899
- }
38900
- }
38901
- const dahuaPayload = {
38902
- host,
38903
- username,
38904
- password,
38905
- plateNumber,
38906
- // recno: _recNo,
38907
- mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
38908
- start: startDahua,
38909
- end: endDahua,
38910
- owner: name ? name : _name
38911
- };
38912
- let dahuaResponse;
38913
- try {
38914
- dahuaResponse = await _addPlateNumber(dahuaPayload);
38915
- } catch (error) {
38916
- logCameraFailure("updateVehicleById write", camera, error);
38917
- const restored = existing.exists ? await restorePlateOnCamera({
38918
- ...dahuaPayload,
38919
- owner: _name
38920
- }) : true;
38921
- throw new import_node_server_utils106.BadRequestError(
38922
- anprWriteFailureMessage(
38923
- [{ camera: cameraLabel(camera), reason: anprWriteReason(error) }],
38924
- "updated"
38925
- ) + (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.`)
38926
- );
38927
- }
38928
- if (dahuaResponse?.statusCode !== 200) {
38929
- logCameraFailure(
38930
- "updateVehicleById write",
38931
- camera,
38932
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38933
- );
38934
- throw new import_node_server_utils106.BadRequestError(
38935
- anprWriteFailureMessage(
38936
- [
38937
- {
38938
- camera: cameraLabel(camera),
38939
- reason: anprWriteReason(
38940
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
38941
- )
38942
- }
38943
- ],
38944
- "updated"
38945
- )
38946
- );
38947
- }
38948
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
38949
- value.recNo = responseData.split("=")[1]?.trim();
38950
- if (value.peopleId && value.recNo) {
38951
- await _pushVehicleById(
38952
- value.peopleId,
38953
- {
38954
- plateNumber,
38955
- recNo: value.recNo
38956
- },
38957
- session
38958
- );
38959
- }
38960
- }
39627
+ typeFields = {
39628
+ type: "blocklist" /* BLOCKLIST */,
39629
+ blockedFrom: oldType === "whitelist" /* WHITELIST */ ? "whitelist" /* WHITELIST */ : String(vehicle.blockedFrom ?? "")
39630
+ };
39631
+ } else {
39632
+ if (isLive) {
39633
+ await unblockOnCameras(
39634
+ vehicle,
39635
+ cameraSite,
39636
+ { restore: true, owner: newName },
39637
+ journal
39638
+ );
38961
39639
  }
39640
+ typeFields = { type: "whitelist" /* WHITELIST */, blockedFrom: "" };
38962
39641
  }
39642
+ } else if (isLive && (newPlate !== oldPlate || newName !== oldName || datesChanged)) {
39643
+ await rewriteOnCameras(
39644
+ vehicle,
39645
+ cameraSite,
39646
+ {
39647
+ plateNumber: newPlate,
39648
+ owner: newName,
39649
+ ...cameraWindow(startDate ?? vehicle.start, endDate ?? vehicle.end)
39650
+ },
39651
+ journal
39652
+ );
38963
39653
  }
38964
39654
  const formattedValue = {
38965
39655
  ...rest,
38966
39656
  ...name && { name },
38967
- ...plateNumber && { plateNumber },
38968
- ...start && { start },
38969
- ...end && { end },
39657
+ ...plateNumber && { plateNumber: newPlate },
39658
+ // Dates, not ISO strings: the expiry sweep only matches a BSON Date
39659
+ // (see `expiredVehicleSweepFilter`), so an end date edited to a string
39660
+ // never expired.
39661
+ ...startDate && { start: startDate },
39662
+ ...endDate && { end: endDate },
38970
39663
  ...block && {
38971
39664
  block: typeof block === "string" ? new import_mongodb72.ObjectId(block) : block
38972
39665
  },
@@ -38976,13 +39669,27 @@ function useVehicleService() {
38976
39669
  ...unit && {
38977
39670
  unit: typeof unit === "string" ? new import_mongodb72.ObjectId(unit) : unit
38978
39671
  },
38979
- ...value.recNo && { recNo: value.recNo },
38980
- ...type && { type: value.type }
39672
+ ...type && { type },
39673
+ ...typeFields
38981
39674
  };
38982
39675
  await _updateVehicleById(_id, formattedValue, session);
39676
+ const peopleId = value.peopleId || vehicle.peopleId;
39677
+ if (isLive && peopleId && newPlate !== oldPlate) {
39678
+ await _pushVehicleById(
39679
+ peopleId,
39680
+ { plateNumber: newPlate, recNo: "" },
39681
+ session
39682
+ );
39683
+ await _pullVehicleByRecNo(
39684
+ "",
39685
+ { peopleId, site: cameraSite, plateNumber: oldPlate },
39686
+ session
39687
+ );
39688
+ }
38983
39689
  await session.commitTransaction();
38984
39690
  } catch (error) {
38985
39691
  import_node_server_utils106.logger.error("Error in vehicle service update:", error);
39692
+ await rollbackCameras(journal, error);
38986
39693
  await session.abortTransaction();
38987
39694
  throw error;
38988
39695
  } finally {
@@ -39018,25 +39725,12 @@ function useVehicleService() {
39018
39725
  if (sanitizedValues.length === 0) {
39019
39726
  throw new Error("No valid vehicle plate numbers found.");
39020
39727
  }
39021
- let siteCameras = [];
39022
- if (site) {
39023
- let page = 1;
39024
- let pages = 1;
39025
- const limit = 20;
39026
- do {
39027
- const siteCameraReq = await _getAllSiteCameras({
39028
- site,
39029
- type: "anpr",
39030
- direction: ["both", "entry"],
39031
- page,
39032
- limit
39033
- });
39034
- pages = siteCameraReq.pages || 1;
39035
- siteCameras.push(...siteCameraReq.items);
39036
- page++;
39037
- } while (page <= pages);
39038
- }
39039
- if (!siteCameras.length) {
39728
+ const isBlockRow = (row) => row.type !== "whitelist" /* WHITELIST */;
39729
+ const allowCameras = site ? await allowCamerasAt(site) : [];
39730
+ const blockCameras = site ? await blockCamerasAt(site) : [];
39731
+ const hasAllowRows = sanitizedValues.some((row) => !isBlockRow(row));
39732
+ const hasBlockRows = sanitizedValues.some((row) => isBlockRow(row));
39733
+ if (hasAllowRows && !allowCameras.length || hasBlockRows && !blockCameras.length) {
39040
39734
  throw new Error("No site cameras found.");
39041
39735
  }
39042
39736
  const written = /* @__PURE__ */ new Map();
@@ -39045,58 +39739,63 @@ function useVehicleService() {
39045
39739
  site,
39046
39740
  plateNumber: value.plateNumber
39047
39741
  }));
39048
- const { deletableIds: savableIds, skipped } = await sweepExpiredVehicles(
39049
- sweepRows,
39050
- {
39051
- camerasForSite: async () => siteCameras,
39052
- cameraLabel,
39053
- revoke: async (camera, row) => {
39054
- const rowId = String(row._id);
39055
- const vehicleValue = sanitizedValues[Number(rowId)];
39056
- const plateNumber = vehicleValue.plateNumber;
39057
- const { host, username, password } = camera;
39058
- const dahuaPayload = {
39059
- host,
39060
- username,
39061
- password,
39062
- plateNumber,
39063
- mode: vehicleValue.type === "whitelist" /* WHITELIST */ ? "TrafficRedList" /* TRAFFIC_REDLIST */ : "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39064
- ...vehicleValue.start ? { start: String(vehicleValue.start) } : {},
39065
- ...vehicleValue.end ? { end: String(vehicleValue.end) } : {},
39066
- ...vehicleValue.name ? { owner: vehicleValue.name } : {},
39067
- ...vehicleValue.vehicleModel ? { vehicleType: vehicleValue.vehicleModel } : {},
39068
- ...vehicleValue.vehicleColor ? { vehicleColor: vehicleValue.vehicleColor } : {}
39742
+ const writeRow = async (camera, row) => {
39743
+ const rowId = String(row._id);
39744
+ const vehicleValue = sanitizedValues[Number(rowId)];
39745
+ const plateNumber = vehicleValue.plateNumber;
39746
+ const { host, username, password } = camera;
39747
+ const dahuaPayload = {
39748
+ host,
39749
+ username,
39750
+ password,
39751
+ plateNumber,
39752
+ mode: vehicleValue.type === "whitelist" /* WHITELIST */ ? "TrafficRedList" /* TRAFFIC_REDLIST */ : "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39753
+ ...vehicleValue.start ? { start: String(vehicleValue.start) } : {},
39754
+ ...vehicleValue.end ? { end: String(vehicleValue.end) } : {},
39755
+ ...vehicleValue.name ? { owner: vehicleValue.name } : {},
39756
+ ...vehicleValue.vehicleModel ? { vehicleType: vehicleValue.vehicleModel } : {},
39757
+ ...vehicleValue.vehicleColor ? { vehicleColor: vehicleValue.vehicleColor } : {},
39758
+ // Dates on the camera's clock, in the site's zone.
39759
+ timezone: await siteTimezone(camera.site ?? site)
39760
+ };
39761
+ try {
39762
+ const dahuaResponse = await _bulkInsertPlateNumber(dahuaPayload);
39763
+ const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
39764
+ const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
39765
+ if (responseStatus !== 200) {
39766
+ logCameraFailure(
39767
+ "bulk add",
39768
+ camera,
39769
+ `HTTP ${responseStatus}: ${responseData.slice(0, 500)}`
39770
+ );
39771
+ return {
39772
+ ok: false,
39773
+ reason: anprWriteReason(`HTTP ${responseStatus}`)
39069
39774
  };
39070
- try {
39071
- const dahuaResponse = await _bulkInsertPlateNumber(dahuaPayload);
39072
- const responseStatus = dahuaResponse?.statusCode || dahuaResponse?.status || dahuaResponse?.res?.status || "unknown";
39073
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
39074
- if (responseStatus !== 200) {
39075
- logCameraFailure(
39076
- "bulk add",
39077
- camera,
39078
- `HTTP ${responseStatus}: ${responseData.slice(0, 500)}`
39079
- );
39080
- return {
39081
- ok: false,
39082
- reason: anprWriteReason(`HTTP ${responseStatus}`)
39083
- };
39084
- }
39085
- const recNo = String(responseData.split("=")[1]?.trim() ?? "");
39086
- if (recNo) {
39087
- vehicleValue.recNo = vehicleValue.recNo || recNo;
39088
- const entries = written.get(rowId) ?? [];
39089
- entries.push({ camera, recno: recNo });
39090
- written.set(rowId, entries);
39091
- }
39092
- return { ok: true, reason: "" };
39093
- } catch (error) {
39094
- logCameraFailure("bulk add", camera, error);
39095
- return { ok: false, reason: anprWriteReason(error) };
39096
- }
39097
39775
  }
39776
+ const recNo = String(responseData.split("=")[1]?.trim() ?? "");
39777
+ if (recNo) {
39778
+ vehicleValue.recNo = vehicleValue.recNo || recNo;
39779
+ const entries = written.get(rowId) ?? [];
39780
+ entries.push({ camera, recno: recNo });
39781
+ written.set(rowId, entries);
39782
+ }
39783
+ return { ok: true, reason: "" };
39784
+ } catch (error) {
39785
+ logCameraFailure("bulk add", camera, error);
39786
+ return { ok: false, reason: anprWriteReason(error) };
39098
39787
  }
39788
+ };
39789
+ const allowPass = await sweepExpiredVehicles(
39790
+ sweepRows.filter((row) => !isBlockRow(sanitizedValues[Number(row._id)])),
39791
+ { camerasForSite: async () => allowCameras, cameraLabel, revoke: writeRow }
39792
+ );
39793
+ const blockPass = await sweepExpiredVehicles(
39794
+ sweepRows.filter((row) => isBlockRow(sanitizedValues[Number(row._id)])),
39795
+ { camerasForSite: async () => blockCameras, cameraLabel, revoke: writeRow }
39099
39796
  );
39797
+ const savableIds = [...allowPass.deletableIds, ...blockPass.deletableIds];
39798
+ const skipped = [...allowPass.skipped, ...blockPass.skipped];
39100
39799
  const failedRows = [];
39101
39800
  for (const entry of skipped) {
39102
39801
  const stranded = [];
@@ -39141,123 +39840,26 @@ function useVehicleService() {
39141
39840
  if (!session) {
39142
39841
  throw new Error("Unable to start session for vehicle service.");
39143
39842
  }
39843
+ const journal = cameraJournal();
39144
39844
  try {
39145
39845
  session.startTransaction();
39146
- const vehicle = await _getVehicleById(_id);
39147
- const plate = vehicle.plates.find((p) => p._id.toString() === _id);
39148
- const _name = value.name ? value.name : vehicle.name;
39149
- const _plateNumber = plate?.plateNumber;
39150
- const _start = vehicle.start;
39151
- const _end = vehicle.end;
39152
- const _recNo = plate.recNo;
39153
- const _type = plate.type;
39154
- if (value.peopleId) {
39155
- value.peopleId = new import_mongodb72.ObjectId(value.peopleId);
39846
+ const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(_id) });
39847
+ if (!vehicle) {
39848
+ throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
39156
39849
  }
39157
- const { site } = value;
39158
- const startDahua = formatDahuaDate(_start);
39159
- const endPlus24Hours = _end ? _end : new Date(new Date(_end).getTime() + 24 * 60 * 60 * 1e3);
39160
- const endDahua = formatDahuaDate(endPlus24Hours);
39161
- if (_plateNumber) {
39162
- const siteCameras = [];
39163
- let page = 1;
39164
- let pages = 1;
39165
- const limit = 20;
39166
- do {
39167
- const siteCameraReq = await _getAllSiteCameras({
39168
- site,
39169
- type: "anpr",
39170
- direction: ["both", "entry"],
39171
- page,
39172
- limit
39173
- });
39174
- pages = siteCameraReq.pages || 1;
39175
- siteCameras.push(...siteCameraReq.items);
39176
- page++;
39177
- } while (page <= pages);
39178
- if (!siteCameras.length) {
39179
- throw new import_node_server_utils106.BadRequestError(anprNoCameraRefusal("blocked"));
39180
- }
39181
- for (const camera of siteCameras) {
39182
- const { host, username, password } = camera;
39183
- const removePlateNumber = {
39184
- host,
39185
- username,
39186
- password,
39187
- mode: _type === "blocklist" /* BLOCKLIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
39188
- recno: _recNo
39189
- };
39190
- const responseForDeletion = await _removePlateNumber(
39191
- removePlateNumber
39192
- );
39193
- if (!responseForDeletion?.ok) {
39194
- logCameraFailure(
39195
- "blocklistVehicles revoke",
39196
- camera,
39197
- responseForDeletion?.detail ?? "no outcome returned"
39198
- );
39199
- throw new import_node_server_utils106.BadRequestError(
39200
- anprRevokeFailureMessage(
39201
- [
39202
- {
39203
- camera: cameraLabel(camera),
39204
- reason: responseForDeletion?.reason || "the camera could not be reached"
39205
- }
39206
- ],
39207
- "updated"
39208
- )
39209
- );
39210
- }
39211
- const dahuaPayload = {
39212
- host,
39213
- username,
39214
- password,
39215
- plateNumber: _plateNumber,
39216
- mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39217
- owner: _name,
39218
- ...startDahua ? { start: startDahua } : {},
39219
- ...endDahua ? { end: endDahua } : {}
39220
- };
39221
- const dahuaResponse = await _addPlateNumber(dahuaPayload);
39222
- if (dahuaResponse?.statusCode !== 200) {
39223
- logCameraFailure(
39224
- "blocklistVehicles write",
39225
- camera,
39226
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
39227
- );
39228
- throw new import_node_server_utils106.BadRequestError(
39229
- anprBlockFailureMessage([
39230
- {
39231
- camera: cameraLabel(camera),
39232
- reason: anprWriteReason(
39233
- `HTTP ${dahuaResponse?.statusCode ?? "none"}`
39234
- )
39235
- }
39236
- ])
39237
- );
39238
- }
39239
- const responseData = dahuaResponse?.data?.toString("utf-8") ?? "";
39240
- value.recNo = responseData.split("=")[1]?.trim();
39241
- const normalizedPlateNumber = _plateNumber;
39242
- if (value.peopleId && value.recNo) {
39243
- await _pushVehicleById(
39244
- value.peopleId,
39245
- {
39246
- plateNumber: normalizedPlateNumber,
39247
- recNo: value.recNo
39248
- },
39249
- session
39250
- );
39251
- }
39252
- }
39850
+ const owner = String(value.name || vehicle.name || "");
39851
+ const cameraSite = vehicle.site ?? value.site;
39852
+ if (vehicle.status === "active" /* ACTIVE */) {
39853
+ await blockOnCameras(vehicle, cameraSite, owner, journal);
39253
39854
  }
39254
39855
  const formattedValue = {
39255
39856
  type: "blocklist" /* BLOCKLIST */,
39256
- recNo: value.recNo
39857
+ blockedFrom: vehicle.type === "whitelist" /* WHITELIST */ ? "whitelist" /* WHITELIST */ : String(vehicle.blockedFrom ?? "")
39257
39858
  };
39258
39859
  await _updateVehicleById(_id, formattedValue, session);
39259
39860
  await session.commitTransaction();
39260
39861
  } catch (error) {
39862
+ await rollbackCameras(journal, error);
39261
39863
  await session.abortTransaction();
39262
39864
  throw error;
39263
39865
  } finally {
@@ -39269,85 +39871,222 @@ function useVehicleService() {
39269
39871
  if (!session) {
39270
39872
  throw new Error("Unable to start session for vehicle service.");
39271
39873
  }
39874
+ const journal = cameraJournal();
39272
39875
  try {
39273
39876
  session.startTransaction();
39274
- const vehicle = await _getVehicleById(_id);
39275
- const plate = vehicle.plates.find((p) => p._id.toString() === _id);
39276
- const _plateNumber = plate?.plateNumber;
39277
- const _recNo = plate.recNo;
39877
+ const vehicle = await findOne({ _id: new import_mongodb72.ObjectId(_id) });
39878
+ if (!vehicle) {
39879
+ throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
39880
+ }
39881
+ if (vehicle.type !== "blocklist" /* BLOCKLIST */) {
39882
+ throw new import_node_server_utils106.BadRequestError("This vehicle is not on the blocklist.");
39883
+ }
39884
+ const _plateNumber = String(vehicle.plateNumber ?? "");
39885
+ const cameraSite = vehicle.site ?? value.site;
39886
+ const restore = vehicle.blockedFrom === "whitelist" /* WHITELIST */;
39278
39887
  if (value.peopleId) {
39279
39888
  value.peopleId = new import_mongodb72.ObjectId(value.peopleId);
39280
39889
  }
39281
- const { site } = value;
39282
- if (_plateNumber) {
39283
- const siteCameras = [];
39284
- let page = 1;
39285
- let pages = 1;
39286
- const limit = 20;
39287
- do {
39288
- const siteCameraReq = await _getAllSiteCameras({
39289
- site,
39290
- type: "anpr",
39291
- direction: ["both", "entry"],
39292
- page,
39293
- limit
39294
- });
39295
- pages = siteCameraReq.pages || 1;
39296
- siteCameras.push(...siteCameraReq.items);
39297
- page++;
39298
- } while (page <= pages);
39299
- import_node_server_utils106.logger.info(
39300
- `removeFromBlocklist resolved ${siteCameras.length} ANPR camera(s)`
39890
+ if (vehicle.status === "active" /* ACTIVE */) {
39891
+ await unblockOnCameras(
39892
+ vehicle,
39893
+ cameraSite,
39894
+ { restore, owner: String(vehicle.name ?? "") },
39895
+ journal
39301
39896
  );
39302
- for (const camera of siteCameras) {
39303
- const { host, username, password } = camera;
39304
- const removePlateNumber = {
39305
- host,
39306
- username,
39307
- password,
39308
- mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39309
- recno: _recNo
39310
- };
39311
- const responseForDeletion = await _removePlateNumber(
39312
- removePlateNumber
39313
- );
39314
- if (!responseForDeletion?.ok) {
39315
- logCameraFailure(
39316
- "removeFromBlocklist revoke",
39317
- camera,
39318
- responseForDeletion?.detail ?? "no outcome returned"
39319
- );
39320
- throw new import_node_server_utils106.BadRequestError(
39321
- anprUnblockFailureMessage([
39322
- {
39323
- camera: cameraLabel(camera),
39324
- reason: responseForDeletion?.reason || "the camera could not be reached"
39325
- }
39326
- ])
39327
- );
39328
- }
39329
- }
39897
+ }
39898
+ if (restore) {
39899
+ await _updateVehicleById(
39900
+ _id,
39901
+ { type: "whitelist" /* WHITELIST */, blockedFrom: "" },
39902
+ session
39903
+ );
39904
+ await session.commitTransaction();
39905
+ return "Vehicle unblocked and restored to the whitelist.";
39330
39906
  }
39331
39907
  await _deleteVehicle(_id, session);
39332
- if ((value.peopleId || site) && (_plateNumber || _recNo)) {
39908
+ const peopleId = value.peopleId || vehicle.peopleId;
39909
+ if ((peopleId || cameraSite) && _plateNumber) {
39333
39910
  await _pullVehicleByRecNo(
39334
- String(_recNo ?? ""),
39911
+ "",
39335
39912
  {
39336
- peopleId: value.peopleId,
39337
- site,
39913
+ peopleId,
39914
+ site: cameraSite,
39338
39915
  plateNumber: _plateNumber
39339
39916
  },
39340
39917
  session
39341
39918
  );
39342
39919
  }
39343
39920
  await session.commitTransaction();
39921
+ return "Vehicle removed from the blocklist.";
39344
39922
  } catch (error) {
39923
+ await rollbackCameras(journal, error);
39345
39924
  await session.abortTransaction();
39346
39925
  throw error;
39347
39926
  } finally {
39348
39927
  session.endSession();
39349
39928
  }
39350
39929
  }
39930
+ async function fillCameraLists(site, camera, lists) {
39931
+ let target;
39932
+ if (camera.id) {
39933
+ const stored = (await camerasAt(site)).find(
39934
+ (candidate) => String(candidate._id) === String(camera.id)
39935
+ );
39936
+ if (!stored)
39937
+ throw new import_node_server_utils106.NotFoundError("Camera not found.");
39938
+ target = {
39939
+ ...stored,
39940
+ ...camera.host ? { host: camera.host } : {},
39941
+ ...camera.username ? { username: camera.username } : {},
39942
+ ...camera.password ? { password: camera.password } : {},
39943
+ ...camera.direction ? { direction: camera.direction } : {}
39944
+ };
39945
+ } else {
39946
+ target = {
39947
+ host: String(camera.host ?? ""),
39948
+ username: String(camera.username ?? ""),
39949
+ password: String(camera.password ?? ""),
39950
+ name: camera.name,
39951
+ direction: camera.direction,
39952
+ // Whose clock the season pass windows are written on.
39953
+ site
39954
+ };
39955
+ }
39956
+ const vehicles = await _getLivePlatesBySite(String(site));
39957
+ const unique = (rows) => {
39958
+ const seen = /* @__PURE__ */ new Set();
39959
+ return rows.filter((row) => {
39960
+ const key = normalisePlate(row.plateNumber);
39961
+ if (!key || seen.has(key))
39962
+ return false;
39963
+ seen.add(key);
39964
+ return true;
39965
+ });
39966
+ };
39967
+ const blocked = lists.block ? unique(vehicles.filter((vehicle) => listsHeldBy(vehicle).block)) : [];
39968
+ const allowed = lists.allow ? unique(vehicles.filter((vehicle) => listsHeldBy(vehicle).allow)) : [];
39969
+ const writes = [
39970
+ ...blocked.map((vehicle) => ({
39971
+ plateNumber: String(vehicle.plateNumber),
39972
+ mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */,
39973
+ owner: String(vehicle.name ?? ""),
39974
+ ...blockWindow(vehicle.end)
39975
+ })),
39976
+ ...allowed.map((vehicle) => ({
39977
+ plateNumber: String(vehicle.plateNumber),
39978
+ mode: "TrafficRedList" /* TRAFFIC_REDLIST */,
39979
+ owner: String(vehicle.name ?? ""),
39980
+ ...cameraWindow(vehicle.start, vehicle.end)
39981
+ }))
39982
+ ];
39983
+ const written = { allowed: 0, blocked: 0 };
39984
+ for (const write of writes) {
39985
+ try {
39986
+ await insertPlateOnCamera(target, write);
39987
+ } catch (error) {
39988
+ logCameraFailure("fill camera", target, error);
39989
+ throw new import_node_server_utils106.BadRequestError(
39990
+ `Only ${written.allowed + written.blocked} of ${writes.length} plates could be added to ${cameraLabel(target)} before it stopped: ${anprWriteReason(error)}.`
39991
+ );
39992
+ }
39993
+ if (write.mode === "TrafficBlackList" /* TRAFFIC_BLACKLIST */)
39994
+ written.blocked++;
39995
+ else
39996
+ written.allowed++;
39997
+ }
39998
+ import_node_server_utils106.logger.info(
39999
+ `fillCameraLists wrote ${written.allowed} allowed and ${written.blocked} blocked plate(s) for site ${String(site)}`
40000
+ );
40001
+ return written;
40002
+ }
40003
+ async function newCameraPlateRows(site, direction) {
40004
+ const lists = listsFilledByDirectionChange("", direction);
40005
+ if (!lists.allow && !lists.block)
40006
+ return [];
40007
+ const vehicles = await _getLivePlatesBySite(String(site));
40008
+ const rowsFor = (list) => {
40009
+ const seen = /* @__PURE__ */ new Set();
40010
+ const rows = [];
40011
+ for (const vehicle of vehicles) {
40012
+ const held = listsHeldBy(vehicle);
40013
+ if (list === "blocked" ? !held.block : !held.allow)
40014
+ continue;
40015
+ const key = normalisePlate(vehicle.plateNumber);
40016
+ if (!key || seen.has(key))
40017
+ continue;
40018
+ seen.add(key);
40019
+ rows.push({
40020
+ id: `${list}:${String(vehicle._id)}`,
40021
+ vehicle: String(vehicle._id),
40022
+ list,
40023
+ plateNumber: String(vehicle.plateNumber ?? ""),
40024
+ name: String(vehicle.name ?? ""),
40025
+ block: String(vehicle.blockName ?? ""),
40026
+ level: String(vehicle.levelName ?? ""),
40027
+ unit: String(vehicle.unitName ?? ""),
40028
+ kind: list === "blocked" ? "Blocked" : vehicle.seasonPassType ? "Season pass" : "Whitelist"
40029
+ });
40030
+ }
40031
+ return rows;
40032
+ };
40033
+ return [
40034
+ ...lists.block ? rowsFor("blocked") : [],
40035
+ ...lists.allow ? rowsFor("allowed") : []
40036
+ ];
40037
+ }
40038
+ async function writeVehicleToNewCamera(site, device, vehicleId, list, options = {}) {
40039
+ let siteId;
40040
+ let id;
40041
+ try {
40042
+ siteId = new import_mongodb72.ObjectId(String(site));
40043
+ id = new import_mongodb72.ObjectId(String(vehicleId));
40044
+ } catch {
40045
+ throw new import_node_server_utils106.BadRequestError("Invalid vehicle or site ID.");
40046
+ }
40047
+ const vehicle = await findOne({
40048
+ _id: id,
40049
+ site: siteId,
40050
+ status: "active" /* ACTIVE */
40051
+ });
40052
+ if (!vehicle)
40053
+ throw new import_node_server_utils106.NotFoundError("Vehicle not found.");
40054
+ const held = listsHeldBy(vehicle);
40055
+ if (list === "blocked" ? !held.block : !held.allow) {
40056
+ throw new import_node_server_utils106.BadRequestError(
40057
+ "This vehicle no longer belongs on that list. Refresh and try again."
40058
+ );
40059
+ }
40060
+ const write = {
40061
+ plateNumber: String(vehicle.plateNumber ?? ""),
40062
+ mode: list === "blocked" ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
40063
+ owner: String(vehicle.name ?? ""),
40064
+ ...list === "blocked" ? blockWindow(vehicle.end) : cameraWindow(vehicle.start, vehicle.end)
40065
+ };
40066
+ const target = {
40067
+ host: device.host,
40068
+ username: device.username,
40069
+ password: device.password,
40070
+ name: device.name,
40071
+ direction: device.direction,
40072
+ // Whose clock the season pass window is written on.
40073
+ site: siteId
40074
+ };
40075
+ try {
40076
+ if (options.onlyIfMissing)
40077
+ await ensurePlateOnCamera(target, write);
40078
+ else
40079
+ await insertPlateOnCamera(target, write);
40080
+ return { ok: true };
40081
+ } catch (error) {
40082
+ logCameraFailure("new camera plate", target, error);
40083
+ return {
40084
+ ok: false,
40085
+ reason: anprWriteReason(error),
40086
+ kind: classifyDahuaFailure(error?.message ?? error)
40087
+ };
40088
+ }
40089
+ }
39351
40090
  return {
39352
40091
  add,
39353
40092
  deleteVehicle,
@@ -39357,7 +40096,10 @@ function useVehicleService() {
39357
40096
  updateVehicleById,
39358
40097
  bulkUpsertVehicles,
39359
40098
  blocklistVehicles,
39360
- removeFromBlocklist
40099
+ removeFromBlocklist,
40100
+ fillCameraLists,
40101
+ newCameraPlateRows,
40102
+ writeVehicleToNewCamera
39361
40103
  };
39362
40104
  }
39363
40105
 
@@ -43571,8 +44313,10 @@ function useVehicleController() {
43571
44313
  }
43572
44314
  const { _id, ...rest } = value;
43573
44315
  await requireVehicleReach(req, _id);
43574
- await _removeFromBlocklist(_id, rest);
43575
- res.json({ message: "Successfully removed vehicle from blocklist." });
44316
+ const message = await _removeFromBlocklist(_id, rest);
44317
+ res.json({
44318
+ message: message || "Successfully removed vehicle from blocklist."
44319
+ });
43576
44320
  return;
43577
44321
  } catch (error) {
43578
44322
  import_node_server_utils120.logger.log({ level: "error", message: error.message });
@@ -43672,10 +44416,69 @@ function useSiteCameraService() {
43672
44416
  session.endSession();
43673
44417
  }
43674
44418
  }
44419
+ async function clearPlateLists(site, camera, lists, action = "type-change") {
44420
+ let target;
44421
+ const id = typeof camera === "string" ? camera : "";
44422
+ if (typeof camera === "string") {
44423
+ const cameras = [];
44424
+ let page = 1;
44425
+ let pages = 1;
44426
+ const limit = 20;
44427
+ do {
44428
+ const siteCameraReq = await _getAllCameraWithPassword({
44429
+ site: String(site ?? ""),
44430
+ type: "anpr",
44431
+ page,
44432
+ limit
44433
+ });
44434
+ pages = siteCameraReq.pages || 1;
44435
+ cameras.push(...siteCameraReq.items);
44436
+ page++;
44437
+ } while (page <= pages);
44438
+ target = cameras.find((candidate) => String(candidate._id) === camera);
44439
+ if (!target)
44440
+ throw new import_node_server_utils121.NotFoundError("Camera not found.");
44441
+ } else {
44442
+ target = camera;
44443
+ }
44444
+ const label = cameraLabel(target);
44445
+ 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.";
44446
+ const cleared = [];
44447
+ const steps = [
44448
+ ...lists.allow ? [{ list: "allowed", mode: "TrafficRedList" /* TRAFFIC_REDLIST */ }] : [],
44449
+ ...lists.block ? [{ list: "blocked", mode: "TrafficBlackList" /* TRAFFIC_BLACKLIST */ }] : []
44450
+ ];
44451
+ for (const [index, step] of steps.entries()) {
44452
+ if (index > 0)
44453
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
44454
+ const outcome = await dahuaService.clearPlateList({
44455
+ host: target.host,
44456
+ username: target.username,
44457
+ password: target.password,
44458
+ mode: step.mode
44459
+ });
44460
+ if (!outcome?.ok) {
44461
+ import_node_server_utils121.logger.error(
44462
+ `clearPlateLists could not clear ${step.mode} on camera ${id || target.host}: ${outcome?.detail ?? "no outcome"}`
44463
+ );
44464
+ const listName = step.list === "allowed" ? "allowed list" : "blocklist";
44465
+ const reason = outcome?.reason || "the camera could not be reached";
44466
+ throw new import_node_server_utils121.BadRequestError(
44467
+ 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.`
44468
+ );
44469
+ }
44470
+ cleared.push(step.list);
44471
+ }
44472
+ import_node_server_utils121.logger.info(
44473
+ `clearPlateLists cleared ${cleared.join(" and ") || "nothing"} on camera ${id || target.host}`
44474
+ );
44475
+ return cleared;
44476
+ }
43675
44477
  return {
43676
44478
  add,
43677
44479
  listenToCapturedPlateNumber,
43678
- updateById
44480
+ updateById,
44481
+ clearPlateLists
43679
44482
  };
43680
44483
  }
43681
44484
 
@@ -43811,8 +44614,29 @@ function summariseBulkCameraPlan(results) {
43811
44614
  }
43812
44615
 
43813
44616
  // src/controllers/site-camera.controller.ts
44617
+ var CLEARED_ONLY_NOTE = " The camera's plate lists were cleared.";
44618
+ function filledNote(filled, cleared = true) {
44619
+ const parts = [
44620
+ ...filled.allowed ? [`${filled.allowed} allowed`] : [],
44621
+ ...filled.blocked ? [`${filled.blocked} blocked`] : []
44622
+ ];
44623
+ const total = filled.allowed + filled.blocked;
44624
+ if (!total) {
44625
+ 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.";
44626
+ }
44627
+ const added = `${parts.join(" and ")} ${total === 1 ? "plate was" : "plates were"} added`;
44628
+ return cleared ? ` The camera's plate lists were cleared and ${added} to it.` : ` ${added} to the camera.`;
44629
+ }
44630
+ function cameraListClearConfirmation(lists) {
44631
+ const what = lists.allow && lists.block ? "its allowed list and its blocklist" : lists.block ? "its blocklist" : "its allowed list";
44632
+ return `This change clears every plate on ${what}, on the camera itself - including plates added directly on the camera. Confirm the change to continue.`;
44633
+ }
43814
44634
  function useSiteCameraController() {
43815
- const { add: _add, updateById: _updateById } = useSiteCameraService();
44635
+ const {
44636
+ add: _add,
44637
+ updateById: _updateById,
44638
+ clearPlateLists: _clearPlateLists
44639
+ } = useSiteCameraService();
43816
44640
  const {
43817
44641
  add: _addOne,
43818
44642
  activeHostsBySite: _activeHostsBySite,
@@ -43896,6 +44720,12 @@ function useSiteCameraController() {
43896
44720
  return null;
43897
44721
  }
43898
44722
  async function add(req, res, next) {
44723
+ const clearCameraLists = req.body?.clearCameraLists === true;
44724
+ const skipPlateSync = req.body?.skipPlateSync === true;
44725
+ if (req.body && typeof req.body === "object") {
44726
+ delete req.body.clearCameraLists;
44727
+ delete req.body.skipPlateSync;
44728
+ }
43899
44729
  const data = req.body;
43900
44730
  const refusal = applyDerivedHost(data);
43901
44731
  if (refusal) {
@@ -43914,6 +44744,52 @@ function useSiteCameraController() {
43914
44744
  type: value.type,
43915
44745
  notFound: "Site not found."
43916
44746
  });
44747
+ const isActiveAnpr = !skipPlateSync && String(value.type ?? "").toLowerCase() === "anpr" && String(value.status ?? "active") === "active";
44748
+ const lists = isActiveAnpr ? listsFilledByDirectionChange("", value.direction) : { allow: false, block: false };
44749
+ const willClear = isActiveAnpr && clearCameraLists;
44750
+ let cleared = false;
44751
+ let filled = null;
44752
+ if (willClear || lists.allow || lists.block) {
44753
+ const taken = await _activeHostsBySite(value.site);
44754
+ if (taken.includes(value.host)) {
44755
+ throw new import_node_server_utils122.BadRequestError("ANPR already exist.");
44756
+ }
44757
+ const device = {
44758
+ host: value.host,
44759
+ username: value.username,
44760
+ password: value.password,
44761
+ name: value.name,
44762
+ direction: value.direction
44763
+ };
44764
+ const everything = { allow: true, block: true };
44765
+ if (willClear) {
44766
+ await _clearPlateLists(value.site, device, everything, "add");
44767
+ cleared = true;
44768
+ }
44769
+ if (lists.allow || lists.block) {
44770
+ try {
44771
+ filled = await useVehicleService().fillCameraLists(
44772
+ value.site,
44773
+ device,
44774
+ lists
44775
+ );
44776
+ } catch (error2) {
44777
+ let undone = null;
44778
+ if (willClear) {
44779
+ try {
44780
+ await _clearPlateLists(value.site, device, everything, "add");
44781
+ undone = true;
44782
+ } catch {
44783
+ undone = false;
44784
+ }
44785
+ }
44786
+ if (error2 && typeof error2 === "object") {
44787
+ 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.`;
44788
+ }
44789
+ throw error2;
44790
+ }
44791
+ }
44792
+ }
43917
44793
  const vehicle = await _add(value);
43918
44794
  await recordConsoleAction({
43919
44795
  action: "camera.added" /* CAMERA_ADDED */,
@@ -43923,8 +44799,10 @@ function useSiteCameraController() {
43923
44799
  after: pickAuditFields("camera.added" /* CAMERA_ADDED */, value)
43924
44800
  });
43925
44801
  res.status(201).json({
43926
- message: "ANPR(s) added successfully.",
43927
- data: { vehicle }
44802
+ message: `ANPR(s) added successfully.` + (filled ? filledNote(filled, cleared) : cleared ? CLEARED_ONLY_NOTE : ""),
44803
+ data: { vehicle },
44804
+ ...filled ? { addedPlates: filled } : {},
44805
+ ...cleared ? { clearedLists: ["allowed", "blocked"] } : {}
43928
44806
  });
43929
44807
  } catch (error2) {
43930
44808
  next(error2);
@@ -44033,6 +44911,12 @@ function useSiteCameraController() {
44033
44911
  }
44034
44912
  }
44035
44913
  async function updateById(req, res, next) {
44914
+ const confirmClearCameraLists = req.body?.confirmClearCameraLists === true;
44915
+ const skipPlateSync = req.body?.skipPlateSync === true;
44916
+ if (req.body && typeof req.body === "object") {
44917
+ delete req.body.confirmClearCameraLists;
44918
+ delete req.body.skipPlateSync;
44919
+ }
44036
44920
  const value = req.body;
44037
44921
  const id = req.params.id ?? "";
44038
44922
  const refusal = applyDerivedHost(value);
@@ -44067,6 +44951,43 @@ function useSiteCameraController() {
44067
44951
  throw error2;
44068
44952
  }
44069
44953
  const update = staffId ? staffUpdate(camera, value.status) : value;
44954
+ const lists = String(camera.type ?? "").toLowerCase() === "anpr" ? listsClearedByDirectionChange(camera.direction, update?.direction) : { allow: false, block: false };
44955
+ let clearedLists = null;
44956
+ if (lists.allow || lists.block) {
44957
+ if (!confirmClearCameraLists) {
44958
+ throw new import_node_server_utils122.BadRequestError(cameraListClearConfirmation(lists));
44959
+ }
44960
+ clearedLists = await _clearPlateLists(camera.site, id, lists);
44961
+ }
44962
+ const fill = !skipPlateSync && String(camera.type ?? "").toLowerCase() === "anpr" && String(update?.status ?? camera.status ?? "active") === "active" ? listsFilledByDirectionChange(camera.direction, update?.direction) : { allow: false, block: false };
44963
+ let filled = null;
44964
+ if (fill.allow || fill.block) {
44965
+ await _clearPlateLists(camera.site, id, fill);
44966
+ try {
44967
+ filled = await useVehicleService().fillCameraLists(
44968
+ camera.site,
44969
+ {
44970
+ id,
44971
+ host: update?.host,
44972
+ username: update?.username,
44973
+ password: update?.password
44974
+ },
44975
+ fill
44976
+ );
44977
+ } catch (error2) {
44978
+ let undone = false;
44979
+ try {
44980
+ await _clearPlateLists(camera.site, id, fill);
44981
+ undone = true;
44982
+ } catch {
44983
+ undone = false;
44984
+ }
44985
+ if (error2 && typeof error2 === "object") {
44986
+ 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.`;
44987
+ }
44988
+ throw error2;
44989
+ }
44990
+ }
44070
44991
  const message = await _updateById(id, update);
44071
44992
  if (staffId) {
44072
44993
  await recordConsoleAction({
@@ -44088,8 +45009,11 @@ function useSiteCameraController() {
44088
45009
  after: pickAuditFields("camera.updated" /* CAMERA_UPDATED */, update)
44089
45010
  });
44090
45011
  }
45012
+ 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.";
44091
45013
  res.json({
44092
- message
45014
+ message: `${message}${clearedNote}${filled ? filledNote(filled) : ""}`,
45015
+ ...clearedLists === null ? {} : { clearedLists },
45016
+ ...filled ? { addedPlates: filled } : {}
44093
45017
  });
44094
45018
  } catch (error2) {
44095
45019
  next(error2);
@@ -44130,12 +45054,216 @@ function useSiteCameraController() {
44130
45054
  next(error2);
44131
45055
  }
44132
45056
  }
45057
+ const schemaNewCameraDevice = {
45058
+ host: import_joi58.default.string().trim().required(),
45059
+ username: import_joi58.default.string().required(),
45060
+ password: import_joi58.default.string().required(),
45061
+ name: import_joi58.default.string().optional().allow("", null),
45062
+ direction: import_joi58.default.string().required().valid("entry", "exit", "both", "visitors", "residents")
45063
+ };
45064
+ async function authorizeNewCamera(req, siteId, host) {
45065
+ await authorizeWrite({
45066
+ req,
45067
+ site: siteId,
45068
+ type: "anpr",
45069
+ notFound: "Site not found."
45070
+ });
45071
+ const taken = await _activeHostsBySite(siteId);
45072
+ if (taken.includes(host)) {
45073
+ throw new import_node_server_utils122.BadRequestError("ANPR already exist.");
45074
+ }
45075
+ }
45076
+ async function newCameraPlates(req, res, next) {
45077
+ const { error, value } = import_joi58.default.object({
45078
+ siteId: import_joi58.default.string().hex().length(24).required(),
45079
+ host: import_joi58.default.string().trim().required(),
45080
+ direction: schemaNewCameraDevice.direction
45081
+ }).validate({
45082
+ siteId: req.params.siteId,
45083
+ host: req.query?.host,
45084
+ direction: req.query?.direction
45085
+ });
45086
+ if (error) {
45087
+ next(new import_node_server_utils122.BadRequestError(error.message));
45088
+ return;
45089
+ }
45090
+ try {
45091
+ await authorizeNewCamera(req, value.siteId, value.host);
45092
+ const items = await useVehicleService().newCameraPlateRows(
45093
+ value.siteId,
45094
+ value.direction
45095
+ );
45096
+ res.json({ items });
45097
+ } catch (error2) {
45098
+ next(error2);
45099
+ }
45100
+ }
45101
+ async function clearNewCamera(req, res, next) {
45102
+ const { error, value } = import_joi58.default.object({
45103
+ siteId: import_joi58.default.string().hex().length(24).required(),
45104
+ ...schemaNewCameraDevice
45105
+ }).validate({ siteId: req.params.siteId, ...req.body ?? {} });
45106
+ if (error) {
45107
+ next(new import_node_server_utils122.BadRequestError(error.message));
45108
+ return;
45109
+ }
45110
+ try {
45111
+ await authorizeNewCamera(req, value.siteId, value.host);
45112
+ const { siteId, ...device } = value;
45113
+ const cleared = await _clearPlateLists(
45114
+ siteId,
45115
+ device,
45116
+ { allow: true, block: true },
45117
+ "add"
45118
+ );
45119
+ res.json({
45120
+ message: "The camera's allowed list and blocklist were cleared.",
45121
+ clearedLists: cleared
45122
+ });
45123
+ } catch (error2) {
45124
+ next(error2);
45125
+ }
45126
+ }
45127
+ async function addNewCameraPlate(req, res, next) {
45128
+ const { error, value } = import_joi58.default.object({
45129
+ siteId: import_joi58.default.string().hex().length(24).required(),
45130
+ ...schemaNewCameraDevice,
45131
+ vehicle: import_joi58.default.string().hex().length(24).required(),
45132
+ list: import_joi58.default.string().valid("allowed", "blocked").required(),
45133
+ onlyIfMissing: import_joi58.default.boolean().optional().default(false)
45134
+ }).validate({ siteId: req.params.siteId, ...req.body ?? {} });
45135
+ if (error) {
45136
+ next(new import_node_server_utils122.BadRequestError(error.message));
45137
+ return;
45138
+ }
45139
+ try {
45140
+ await authorizeNewCamera(req, value.siteId, value.host);
45141
+ const { siteId, vehicle, list, onlyIfMissing, ...device } = value;
45142
+ const outcome = await useVehicleService().writeVehicleToNewCamera(
45143
+ siteId,
45144
+ device,
45145
+ vehicle,
45146
+ list,
45147
+ { onlyIfMissing }
45148
+ );
45149
+ res.json(outcome);
45150
+ } catch (error2) {
45151
+ next(error2);
45152
+ }
45153
+ }
45154
+ async function loadForSync(req, id) {
45155
+ if (!import_mongodb78.ObjectId.isValid(id))
45156
+ throw new import_node_server_utils122.NotFoundError("Camera not found.");
45157
+ const camera = await _findOne(
45158
+ { _id: new import_mongodb78.ObjectId(id) },
45159
+ {
45160
+ site: 1,
45161
+ type: 1,
45162
+ host: 1,
45163
+ username: 1,
45164
+ password: 1,
45165
+ name: 1,
45166
+ direction: 1,
45167
+ status: 1
45168
+ }
45169
+ );
45170
+ if (!camera)
45171
+ throw new import_node_server_utils122.NotFoundError("Camera not found.");
45172
+ await authorizeWrite({
45173
+ req,
45174
+ site: camera.site,
45175
+ type: camera.type,
45176
+ notFound: "Camera not found."
45177
+ });
45178
+ if (String(camera.type ?? "").toLowerCase() !== "anpr") {
45179
+ throw new import_node_server_utils122.BadRequestError("Only ANPR cameras hold vehicle plates.");
45180
+ }
45181
+ if (String(camera.status ?? "active") !== "active") {
45182
+ throw new import_node_server_utils122.BadRequestError(
45183
+ "This camera is inactive. Switch it on before adding vehicles to it."
45184
+ );
45185
+ }
45186
+ const lists = listsFilledByDirectionChange("", camera.direction);
45187
+ if (!lists.allow && !lists.block) {
45188
+ throw new import_node_server_utils122.BadRequestError("Exit cameras receive no vehicle plates.");
45189
+ }
45190
+ return {
45191
+ site: String(camera.site ?? ""),
45192
+ device: {
45193
+ host: String(camera.host ?? ""),
45194
+ username: String(camera.username ?? ""),
45195
+ password: String(camera.password ?? ""),
45196
+ name: camera.name ?? "",
45197
+ direction: String(camera.direction ?? "")
45198
+ }
45199
+ };
45200
+ }
45201
+ async function cameraPlates(req, res, next) {
45202
+ try {
45203
+ const { site, device } = await loadForSync(req, String(req.params.id));
45204
+ const items = await useVehicleService().newCameraPlateRows(
45205
+ site,
45206
+ device.direction
45207
+ );
45208
+ res.json({ items });
45209
+ } catch (error) {
45210
+ next(error);
45211
+ }
45212
+ }
45213
+ async function clearCamera(req, res, next) {
45214
+ try {
45215
+ const { site, device } = await loadForSync(req, String(req.params.id));
45216
+ const cleared = await _clearPlateLists(
45217
+ site,
45218
+ device,
45219
+ { allow: true, block: true },
45220
+ "sync"
45221
+ );
45222
+ res.json({
45223
+ message: "The camera's allowed list and blocklist were cleared.",
45224
+ clearedLists: cleared
45225
+ });
45226
+ } catch (error) {
45227
+ next(error);
45228
+ }
45229
+ }
45230
+ async function addCameraPlate(req, res, next) {
45231
+ const { error, value } = import_joi58.default.object({
45232
+ vehicle: import_joi58.default.string().hex().length(24).required(),
45233
+ list: import_joi58.default.string().valid("allowed", "blocked").required(),
45234
+ onlyIfMissing: import_joi58.default.boolean().optional().default(true)
45235
+ }).validate(req.body ?? {});
45236
+ if (error) {
45237
+ next(new import_node_server_utils122.BadRequestError(error.message));
45238
+ return;
45239
+ }
45240
+ try {
45241
+ const { site, device } = await loadForSync(req, String(req.params.id));
45242
+ const outcome = await useVehicleService().writeVehicleToNewCamera(
45243
+ site,
45244
+ device,
45245
+ value.vehicle,
45246
+ value.list,
45247
+ // Absent means look first: a saved camera may already hold the plate.
45248
+ { onlyIfMissing: value.onlyIfMissing !== false }
45249
+ );
45250
+ res.json(outcome);
45251
+ } catch (error2) {
45252
+ next(error2);
45253
+ }
45254
+ }
44133
45255
  return {
44134
45256
  add,
44135
45257
  addBulk,
44136
45258
  getAll,
44137
45259
  updateById,
44138
- deleteById
45260
+ deleteById,
45261
+ newCameraPlates,
45262
+ clearNewCamera,
45263
+ addNewCameraPlate,
45264
+ cameraPlates,
45265
+ clearCamera,
45266
+ addCameraPlate
44139
45267
  };
44140
45268
  }
44141
45269
 
@@ -47732,7 +48860,7 @@ var VALID_STATUSES = /* @__PURE__ */ new Set([
47732
48860
  ]);
47733
48861
 
47734
48862
  // src/services/hid-amico.service.ts
47735
- var import_crypto3 = __toESM(require("crypto"));
48863
+ var import_crypto4 = __toESM(require("crypto"));
47736
48864
  var import_axios = __toESM(require("axios"));
47737
48865
  var import_node_server_utils140 = require("@7365admin1/node-server-utils");
47738
48866
 
@@ -49687,7 +50815,7 @@ function stringifyHidJson(value) {
49687
50815
  }
49688
50816
 
49689
50817
  // src/utils/hid-secret.util.ts
49690
- var import_crypto2 = __toESM(require("crypto"));
50818
+ var import_crypto3 = __toESM(require("crypto"));
49691
50819
  var HID_SECRET_ENV = "HID_AMICO_SECRET";
49692
50820
  var HID_LEGACY_SECRET_ENV = "ACCESS_TOKEN_SECRET";
49693
50821
  var HID_SECRET_MISSING_FOR_WRITE = `${HID_SECRET_ENV} is not configured, so a new HID credential cannot be encrypted. Set ${HID_SECRET_ENV} on this service and try again. It is deliberately NOT falling back to ${HID_LEGACY_SECRET_ENV}: that key is rotated independently, and anything encrypted with it becomes permanently unreadable the moment it is.`;
@@ -49708,7 +50836,7 @@ function resolveHidSecretKey(purpose, env2 = process.env) {
49708
50836
  return { key: deriveKey(legacy), usedLegacyFallback: true };
49709
50837
  }
49710
50838
  function deriveKey(secret) {
49711
- return import_crypto2.default.createHash("sha256").update(secret).digest();
50839
+ return import_crypto3.default.createHash("sha256").update(secret).digest();
49712
50840
  }
49713
50841
 
49714
50842
  // src/services/hid-amico.service.ts
@@ -49912,8 +51040,8 @@ function getPublicMonitorConfiguration(monitor, callbackToken) {
49912
51040
  };
49913
51041
  }
49914
51042
  function encryptSecret(value) {
49915
- const iv = import_crypto3.default.randomBytes(12);
49916
- const cipher = import_crypto3.default.createCipheriv("aes-256-gcm", getSecretKey("encrypt"), iv);
51043
+ const iv = import_crypto4.default.randomBytes(12);
51044
+ const cipher = import_crypto4.default.createCipheriv("aes-256-gcm", getSecretKey("encrypt"), iv);
49917
51045
  const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
49918
51046
  const tag = cipher.getAuthTag();
49919
51047
  return [PASSWORD_PREFIX, iv.toString("hex"), tag.toString("hex"), encrypted.toString("hex")].join(":");
@@ -49925,7 +51053,7 @@ function decryptSecret(value) {
49925
51053
  const key = getSecretKey("decrypt");
49926
51054
  try {
49927
51055
  const [, ivHex, tagHex, encryptedHex] = value.split(":");
49928
- const decipher = import_crypto3.default.createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
51056
+ const decipher = import_crypto4.default.createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
49929
51057
  decipher.setAuthTag(Buffer.from(tagHex, "hex"));
49930
51058
  return Buffer.concat([
49931
51059
  decipher.update(Buffer.from(encryptedHex, "hex")),
@@ -49965,7 +51093,7 @@ function normalizeHidReaderBaseUrl(value) {
49965
51093
  return endpoint.origin;
49966
51094
  }
49967
51095
  function createStableHidId(seed) {
49968
- const bytes = import_crypto3.default.createHash("sha256").update(seed).digest().subarray(0, 6);
51096
+ const bytes = import_crypto4.default.createHash("sha256").update(seed).digest().subarray(0, 6);
49969
51097
  return Number.parseInt(bytes.toString("hex"), 16) || 1;
49970
51098
  }
49971
51099
  function isUnknownRecord2(value) {
@@ -50040,7 +51168,7 @@ function resolveProfileIdentityType(memberType, userType) {
50040
51168
  var VISITOR_QR_FORMAT = "0";
50041
51169
  function createVisitorQrCredential(visitorId, readerId, qrValue) {
50042
51170
  const credentialId = createStableHidId(`visitor-qr:${readerId}:${visitorId}`);
50043
- const token = qrValue || `VIS${import_crypto3.default.randomBytes(12).toString("hex").toUpperCase()}`;
51171
+ const token = qrValue || `VIS${import_crypto4.default.randomBytes(12).toString("hex").toUpperCase()}`;
50044
51172
  return { credentialId, object: "qrcodes", value: token, qrValue: token };
50045
51173
  }
50046
51174
  async function configureVisitorQrReader(client) {
@@ -50544,7 +51672,7 @@ function getSipProvisioningConfig() {
50544
51672
  return { provisionerUrl, provisionerToken, websocketUrl, domain };
50545
51673
  }
50546
51674
  function createSipPassword() {
50547
- return import_crypto3.default.randomBytes(12).toString("base64url");
51675
+ return import_crypto4.default.randomBytes(12).toString("base64url");
50548
51676
  }
50549
51677
  function sipAccountResponse(account) {
50550
51678
  const { websocketUrl, domain } = getSipProvisioningConfig();
@@ -53224,7 +54352,7 @@ function useHidAmicoService() {
53224
54352
  assertReaderCardProfile(reader, cardType);
53225
54353
  const hidUserId = Number(hidUserIdValue);
53226
54354
  const client = new HidAmicoClient(reader);
53227
- const lockOwner = import_crypto3.default.randomUUID();
54355
+ const lockOwner = import_crypto4.default.randomUUID();
53228
54356
  const acquired = await repo.acquireEnrollmentLock(readerId, lockOwner, timeoutSeconds + 30);
53229
54357
  if (!acquired) {
53230
54358
  throw new import_node_server_utils140.BadRequestError("Another physical HID card enrollment is already running on this reader.");
@@ -55506,17 +56634,17 @@ var import_fs5 = __toESM(require("fs"));
55506
56634
  var import_path2 = __toESM(require("path"));
55507
56635
  var import_axios2 = __toESM(require("axios"));
55508
56636
  var import_xml2js = require("xml2js");
55509
- var import_crypto4 = __toESM(require("crypto"));
56637
+ var import_crypto5 = __toESM(require("crypto"));
55510
56638
  var ALGORITHM = "aes-256-gcm";
55511
- var SECRET_KEY = import_crypto4.default.createSecretKey(
55512
- new Uint8Array(import_crypto4.default.createHash("sha256").update("acm-secret-key").digest())
56639
+ var SECRET_KEY = import_crypto5.default.createSecretKey(
56640
+ new Uint8Array(import_crypto5.default.createHash("sha256").update("acm-secret-key").digest())
55513
56641
  );
55514
56642
  function decryptAcmUrl(encryptedText) {
55515
56643
  const [ivB64, authTagB64, encryptedB64] = encryptedText.split(":");
55516
56644
  const iv = Buffer.from(ivB64, "base64");
55517
56645
  const authTag = Buffer.from(authTagB64, "base64");
55518
56646
  const encrypted = Buffer.from(encryptedB64, "base64");
55519
- const decipher = import_crypto4.default.createDecipheriv(
56647
+ const decipher = import_crypto5.default.createDecipheriv(
55520
56648
  ALGORITHM,
55521
56649
  SECRET_KEY,
55522
56650
  new Uint8Array(iv)
@@ -55659,10 +56787,10 @@ async function getTransactions(index, url) {
55659
56787
  }
55660
56788
  function encryptPayload(payloadHex) {
55661
56789
  const buffer = Buffer.from(payloadHex, "ascii");
55662
- const encrypted = import_crypto4.default.publicEncrypt(
56790
+ const encrypted = import_crypto5.default.publicEncrypt(
55663
56791
  {
55664
56792
  key: "testing",
55665
- padding: import_crypto4.default.constants.RSA_PKCS1_PADDING
56793
+ padding: import_crypto5.default.constants.RSA_PKCS1_PADDING
55666
56794
  },
55667
56795
  buffer
55668
56796
  );
@@ -55672,7 +56800,7 @@ function toHex(num, length) {
55672
56800
  return num.toString(16).padStart(length * 2, "0");
55673
56801
  }
55674
56802
  function buildPayload(cardNumber, validityMinutes) {
55675
- const randomHex = import_crypto4.default.randomBytes(2).toString("hex");
56803
+ const randomHex = import_crypto5.default.randomBytes(2).toString("hex");
55676
56804
  const cardHex = toHex(cardNumber, 4);
55677
56805
  const epochHex = toHex(Math.floor(Date.now() / 1e3), 4);
55678
56806
  const validityHex = toHex(validityMinutes, 2);
@@ -60615,6 +61743,21 @@ var import_joi72 = __toESM(require("joi"));
60615
61743
  var import_node_server_utils148 = require("@7365admin1/node-server-utils");
60616
61744
  var import_node_server_utils149 = require("@7365admin1/node-server-utils");
60617
61745
 
61746
+ // src/utils/people-create-gate.util.ts
61747
+ var PEOPLE_CREATE_REACH_MODE_DEFAULT = "log";
61748
+ function peopleCreateReachMode(env2 = process.env) {
61749
+ return gateMode(
61750
+ env2.PEOPLE_CREATE_REACH_MODE,
61751
+ ["log", "enforce", "off"],
61752
+ PEOPLE_CREATE_REACH_MODE_DEFAULT
61753
+ );
61754
+ }
61755
+ function peopleCreateVerdict(params) {
61756
+ if (params.mode === "off" || params.reachable)
61757
+ return { refuse: false, record: false };
61758
+ return { refuse: params.mode === "enforce", record: true };
61759
+ }
61760
+
60618
61761
  // src/utils/self-signup-files.util.ts
60619
61762
  function dropUnsentFiles(body) {
60620
61763
  if (!Array.isArray(body?.files))
@@ -60723,6 +61866,41 @@ function usePersonController() {
60723
61866
  next(new import_node_server_utils148.BadRequestError(messages));
60724
61867
  return;
60725
61868
  }
61869
+ try {
61870
+ const mode = peopleCreateReachMode();
61871
+ if (mode !== "off") {
61872
+ const actor = await resolveInviteActor(callerId(req));
61873
+ let reachable = true;
61874
+ if (!staffMayBypass(actor)) {
61875
+ const site = payload?.site ? String(payload.site) : "";
61876
+ const org = payload?.org ? String(payload.org) : "";
61877
+ if (site) {
61878
+ reachable = await useCameraViewService().entitleSite({ siteId: site, userId: actor.id }).then(
61879
+ () => true,
61880
+ () => false
61881
+ );
61882
+ } else if (org) {
61883
+ reachable = actor.orgIds.includes(org);
61884
+ }
61885
+ }
61886
+ const verdict = peopleCreateVerdict({ mode, reachable });
61887
+ if (verdict.record) {
61888
+ import_node_server_utils148.logger.log({
61889
+ level: "info",
61890
+ message: `[people-create] ${verdict.refuse ? "refused" : "would-refuse"} caller=${actor.id} site=${payload?.site ?? ""} org=${payload?.org ?? ""} mode=${mode}`
61891
+ });
61892
+ }
61893
+ if (verdict.refuse) {
61894
+ next(new import_node_server_utils149.UnauthorizedError("Not authorized."));
61895
+ return;
61896
+ }
61897
+ }
61898
+ } catch (gateError) {
61899
+ import_node_server_utils148.logger.log({
61900
+ level: "warn",
61901
+ message: `[people-create] reach check failed open: ${gateError?.message}`
61902
+ });
61903
+ }
60726
61904
  try {
60727
61905
  const data = await _add(payload);
60728
61906
  res.status(201).json({ message: data });
@@ -86987,7 +88165,7 @@ function useRedDotPaymentController() {
86987
88165
 
86988
88166
  // src/services/verification-v2.service.ts
86989
88167
  var import_node_server_utils271 = require("@7365admin1/node-server-utils");
86990
- var import_crypto5 = __toESM(require("crypto"));
88168
+ var import_crypto6 = __toESM(require("crypto"));
86991
88169
  var import_joi142 = __toESM(require("joi"));
86992
88170
 
86993
88171
  // src/services/auth-v2.service.ts
@@ -87685,7 +88863,7 @@ function useVerificationServiceV2() {
87685
88863
  }
87686
88864
  function generateVerificationCode(length = 8) {
87687
88865
  const chars = "0123456789";
87688
- const bytes = import_crypto5.default.randomBytes(length);
88866
+ const bytes = import_crypto6.default.randomBytes(length);
87689
88867
  let result = "";
87690
88868
  for (let i = 0; i < length; i++) {
87691
88869
  result += chars[bytes[i] % chars.length];
@@ -95518,6 +96696,7 @@ function usePersonalEmergencyChainController() {
95518
96696
  cameraGrant,
95519
96697
  cameraHealthClaim,
95520
96698
  cameraHealthSummary,
96699
+ cameraListClearConfirmation,
95521
96700
  cameraManagePermissions,
95522
96701
  cameraProbeCacheKey,
95523
96702
  cameraTransports,