@7365admin1/core 3.51.1-staging.229 → 3.52.1-staging.230

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/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.52.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bd4bd88: Add a bulk camera import on the existing guided path.
8
+
9
+ `POST /api/site-cameras/bulk` accepts a site and up to 200 rows from the
10
+ technician survey sheet, derives each camera's relay address from its recorder
11
+ and channel number, and returns a per-row result. `commit: false` previews and
12
+ writes nothing; `commit: true` writes the rows that passed. Rows that are ANPR,
13
+ that carry a hand-written address, or whose channel is outside 1-256 are refused
14
+ by name, and a camera already at the site is reported as a duplicate rather than
15
+ as a write error. Creates `type: "ip"` cameras only and needs the same write
16
+ gate as the single-camera form.
17
+
18
+ ### Patch Changes
19
+
20
+ - 5af369c: Record who added, changed or removed a camera.
21
+
22
+ "Who changed this camera's address, and when" had no answer anywhere in the system: a camera record
23
+ carries `createdAt`/`updatedAt` and no actor, and a write overwrites the previous `host` in place. At
24
+ Seventh Condominium that turned a one-line port correction into nine days.
25
+
26
+ `add`, `updateById` and `deleteById` on site cameras now append a row to the existing append-only
27
+ `console-audit` collection, with the actor from the session, the camera id, and `before`/`after`
28
+ values drawn from a per-action allow-list. No schema changes, no migration, and no new collection.
29
+
30
+ The allow-list is the boundary that matters: a camera document holds a device credential, and
31
+ `username` and `password` are not on it. `loadForWrite` still never reads them.
32
+
33
+ - 16a9105: Decouple the camera health sweep from the wall's tile ceiling.
34
+
35
+ `getSiteHealth` sliced its input to `maxTiles` (9), so a health request covering a 150-camera site
36
+ answered for the first nine cameras and said nothing at all about the other 141 — and did not report
37
+ that it had shortened the list. No client could build a site-wide "which of my cameras is down" view,
38
+ however it asked.
39
+
40
+ Health is now bounded by its own `maxHealthCameras` (`CAMERA_HEALTH_MAX_CAMERAS`, default 200), and a
41
+ request past that cap comes back with `requested` / `returned` / `truncated` instead of a quietly
42
+ partial list. The device fan-out is unchanged: `maxConcurrentProbes` still bounds probes in flight at
43
+ 4, the reachability probe is still cached per recorder, and `probeCamera` still reads a cached device
44
+ probe rather than making a fresh one — so a 150-camera sweep costs a recorder no more than a 9-camera
45
+ one after the first pass.
46
+
47
+ - 398b374: HID Amico: stop silently deriving the credential encryption key from
48
+ ACCESS_TOKEN_SECRET. Reading still accepts the legacy key so existing credentials
49
+ keep working (and now logs an error saying so), but writing a new credential
50
+ refuses with a message naming HID_AMICO_SECRET, so a later rotation of the JWT
51
+ signing secret cannot orphan credentials that were written after this.
52
+ - 8113e31: HID face registration: never retain the captured photo. Identity `metadata` is stripped of
53
+ `photo` and of any inline `data:image/...` value on every write, and reader notification
54
+ payloads (`user_image`, `access_photo`) are redacted of image data before they are persisted
55
+ as audit events. Enrollment state (`facialEnrolled`, `facialEnrolledAt`, `facialData`,
56
+ `imageTimestamp`) is unchanged.
57
+ - b097964: Constrain a site camera's `direction` to a fixed set instead of accepting any string.
58
+
59
+ `Joi.string().allow("entry", "exit", "both", "none")` reads like a whitelist but is not one — on a
60
+ validator that already accepts every non-empty string, `.allow()` only adds permitted values, so the
61
+ field rejected nothing. It is now `.valid()`.
62
+
63
+ The accepted set is `entry | exit | both | none | visitors | residents`: the four values the staging
64
+ collection actually holds (measured read-only across all 22 rows), plus the two extra options
65
+ layer-common's live `CameraForm.vue` offers for ANPR cameras. No stored camera and no reachable form
66
+ is refused. Whether `visitors` and `residents` are directions at all is a product question; dropping
67
+ them later is a zero-row migration.
68
+
69
+ - Visitor list: decide the Guests tab by "every requested status is pre-arrival"
70
+ instead of matching one specific status set, so the tab keeps filtering on the
71
+ expected arrival date for both the approved+pending request sent by
72
+ layer-common `main` and the approved+pending+rejected request sent by
73
+ layer-common `staging` (DV-0080). Extracted as `isPreCheckInStatusOnly` with
74
+ tests pinning each case.
75
+
3
76
  ## 3.51.0
4
77
 
5
78
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -4822,7 +4822,7 @@ declare function useVisitorTransactionRepo(): {
4822
4822
  checkOutBySiteAndPlate: (site: string | ObjectId, plateNumber: string, session?: ClientSession) => Promise<mongodb.WithId<bson.Document> | null>;
4823
4823
  resendEmailInvitedResident: (visitorId: string, updatedBy: string | ObjectId) => Promise<any>;
4824
4824
  getSelfServiceVisitorPreview: (id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
4825
- patchFields: (id: string | ObjectId, fields: Record<string, any>) => Promise<void>;
4825
+ patchFields: (id: string | ObjectId, fields: Record<string, any>, session?: ClientSession) => Promise<void>;
4826
4826
  updateStatusByLeadId: (leadId: string | ObjectId, status: string) => Promise<void>;
4827
4827
  };
4828
4828
 
@@ -4831,6 +4831,7 @@ declare function useVisitorTransactionService(): {
4831
4831
  updateVisitorTransactionById: (id: string | ObjectId, value: TVisitorTransaction) => Promise<string>;
4832
4832
  processTransactionDahuaStatus: () => Promise<void>;
4833
4833
  inviteVisitor: (value: TVisitorTransaction, userId: string) => Promise<ObjectId[]>;
4834
+ createInvitedMemberDocuments: (leadId: string | ObjectId, status: VisitorStatus) => Promise<ObjectId[]>;
4834
4835
  deleteVisitorTransaction: (id: string) => Promise<void>;
4835
4836
  };
4836
4837
 
package/dist/index.js CHANGED
@@ -20850,10 +20850,10 @@ function useVisitorTransactionRepo() {
20850
20850
  throw new Error(error.message);
20851
20851
  }
20852
20852
  }
20853
- async function patchFields(id, fields) {
20853
+ async function patchFields(id, fields, session) {
20854
20854
  const _id = (0, import_node_server_utils48.toObjectId)(id);
20855
20855
  try {
20856
- await collection.updateOne({ _id }, { $set: fields });
20856
+ await collection.updateOne({ _id }, { $set: fields }, { session });
20857
20857
  } catch (error) {
20858
20858
  throw new Error(error.message);
20859
20859
  }
@@ -50480,7 +50480,8 @@ function useVisitorTransactionService() {
50480
50480
  getExpiredCheckedOutTransactionsBySite: _getExpiredCheckedOutTransactionsBySite,
50481
50481
  updateManyDahuaSyncStatus: _updateManyDahuaSyncStatus,
50482
50482
  getVisitorTransactionById: _getVisitorTransactionById,
50483
- deleteVisitorTransaction: _deleteVisitorTransaction
50483
+ deleteVisitorTransaction: _deleteVisitorTransaction,
50484
+ patchFields: _patchFields
50484
50485
  } = useVisitorTransactionRepo();
50485
50486
  const { getById } = useBuildingUnitRepo();
50486
50487
  const {
@@ -51321,6 +51322,74 @@ function useVisitorTransactionService() {
51321
51322
  session?.endSession();
51322
51323
  }
51323
51324
  }
51325
+ async function createInvitedMemberDocuments(leadId, status) {
51326
+ const lead = await _getVisitorTransactionById(String(leadId));
51327
+ if (!lead?.inviterId)
51328
+ return [];
51329
+ const members = Array.isArray(lead.members) ? lead.members : [];
51330
+ if (members.length === 0)
51331
+ return [];
51332
+ if (!members.every((member) => !member._id))
51333
+ return [];
51334
+ const session = import_node_server_utils134.useAtlas.getClient()?.startSession();
51335
+ if (!session) {
51336
+ throw new Error("Unable to start session for people service.");
51337
+ }
51338
+ try {
51339
+ session.startTransaction();
51340
+ const memberPayload = {
51341
+ block: lead.block ? String(lead.block) : void 0,
51342
+ level: lead.level ? String(lead.level) : void 0,
51343
+ unit: lead.unit ? String(lead.unit) : void 0,
51344
+ unitName: lead.unitName,
51345
+ org: lead.org ? String(lead.org) : void 0,
51346
+ site: lead.site ? String(lead.site) : void 0,
51347
+ type: lead.type,
51348
+ contractorType: lead.contractorType,
51349
+ company: lead.company,
51350
+ purpose: lead.purpose,
51351
+ expectedCheckIn: lead.expectedCheckIn ? new Date(lead.expectedCheckIn).toISOString() : void 0,
51352
+ arrivalTime: lead.arrivalTime,
51353
+ duration: lead.duration,
51354
+ isOvernightParking: lead.isOvernightParking,
51355
+ numberOfPassengers: lead.numberOfPassengers,
51356
+ status,
51357
+ inviterId: String(lead.inviterId),
51358
+ leadId: String(lead._id),
51359
+ // Explicit - MVisitorTransaction defaults an undefined checkIn to now,
51360
+ // and an approved invitation hasn't arrived yet.
51361
+ checkIn: null
51362
+ };
51363
+ const insertedIds = [];
51364
+ for (const member of members) {
51365
+ const memberId = await _add(
51366
+ {
51367
+ ...memberPayload,
51368
+ name: member.name ?? "",
51369
+ nric: member.nric ?? "",
51370
+ contact: member.contact ?? "",
51371
+ email: member.email ?? "",
51372
+ plateNumber: member.plateNumber ?? ""
51373
+ },
51374
+ session
51375
+ );
51376
+ member._id = memberId;
51377
+ insertedIds.push(memberId);
51378
+ }
51379
+ await _patchFields(leadId, { members }, session);
51380
+ await session.commitTransaction();
51381
+ return insertedIds;
51382
+ } catch (error) {
51383
+ import_node_server_utils134.logger.error(
51384
+ "Error creating member documents for approved invitation:",
51385
+ error
51386
+ );
51387
+ await session.abortTransaction();
51388
+ throw error;
51389
+ } finally {
51390
+ session?.endSession();
51391
+ }
51392
+ }
51324
51393
  async function deleteVisitorTransaction(id) {
51325
51394
  const visitor = await _getVisitorTransactionById(id);
51326
51395
  if (!visitor) {
@@ -51353,6 +51422,7 @@ function useVisitorTransactionService() {
51353
51422
  updateVisitorTransactionById,
51354
51423
  processTransactionDahuaStatus,
51355
51424
  inviteVisitor,
51425
+ createInvitedMemberDocuments,
51356
51426
  deleteVisitorTransaction
51357
51427
  // changeVisitorTransactionKeysById,
51358
51428
  };
@@ -55312,6 +55382,7 @@ function useVisitorTransactionController() {
55312
55382
  add: _add,
55313
55383
  updateVisitorTransactionById: _updateVisitorTransactionById,
55314
55384
  inviteVisitor: _inviteVisitor,
55385
+ createInvitedMemberDocuments: _createInvitedMemberDocuments,
55315
55386
  deleteVisitorTransaction: _deleteVisitorTransaction
55316
55387
  // changeVisitorTransactionKeysById: _changeVisitorTransactionKeysById,
55317
55388
  } = useVisitorTransactionService();
@@ -55636,8 +55707,36 @@ function useVisitorTransactionController() {
55636
55707
  const remarks = approval?.remarks ?? req.body.remarks;
55637
55708
  const isApprovalTransition = nextStatus === "approved" /* APPROVED */ || nextStatus === "rejected" /* REJECTED */;
55638
55709
  const current = isApprovalTransition ? await _getVisitorTransactionById(_id) : null;
55639
- const updateBody = approval ? { ...req.body, status: nextStatus } : req.body;
55710
+ const needsMemberFanOut = nextStatus === "approved" /* APPROVED */ && !!current?.inviterId && Array.isArray(current.members) && current.members.length > 0 && current.members.every((member) => !member._id);
55711
+ const { members: _unusedMembers, ...bodyWithoutMembers } = req.body;
55712
+ const baseBody = needsMemberFanOut ? bodyWithoutMembers : req.body;
55713
+ const updateBody = approval ? { ...baseBody, status: nextStatus } : baseBody;
55640
55714
  await _updateVisitorTransactionById(_id, updateBody);
55715
+ let invitedMemberIds = [];
55716
+ if (needsMemberFanOut) {
55717
+ try {
55718
+ invitedMemberIds = await _createInvitedMemberDocuments(
55719
+ _id,
55720
+ nextStatus
55721
+ );
55722
+ } catch (fanOutError) {
55723
+ import_node_server_utils137.logger.log({
55724
+ level: "error",
55725
+ message: `Failed to create member documents for approved invitation ${_id}: ${fanOutError?.message}`
55726
+ });
55727
+ }
55728
+ }
55729
+ for (const insertedId of invitedMemberIds) {
55730
+ resendEmailInvitedResident(
55731
+ String(insertedId),
55732
+ String(current?.inviterId)
55733
+ ).catch((error2) => {
55734
+ import_node_server_utils137.logger.log({
55735
+ level: "error",
55736
+ message: `Failed to send invitation email: ${error2?.message}`
55737
+ });
55738
+ });
55739
+ }
55641
55740
  if (isApprovalTransition && current && current.type === "contractor" /* CONTRACTOR */) {
55642
55741
  try {
55643
55742
  await _updateStatusByLeadId(_id, nextStatus);