@7365admin1/core 3.52.3-staging.248 → 3.52.4

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,109 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.52.4
4
+
5
+ ### Patch Changes
6
+
7
+ - c533662: Stop silently discarding a bulletin whose recipients include a service app with no users.
8
+
9
+ `POST /api/bulletin-boards` inserts the announcement inside a Mongo transaction and then
10
+ notifies each recipient group. The five per-service-app blocks each carried a bare `return;` for
11
+ "this site has no id" and "this app has no registered users". Those returns left the enclosing
12
+ `try` before `commitTransaction()`, so the session was ended with the transaction still open and
13
+ the announcement was rolled back — while the controller still answered `201 Created`. Selecting
14
+ Cleaning, M&E, Pest Control, Landscaping or Pool Maintenance at an estate with no users of that
15
+ app therefore made the announcement vanish with no error anywhere.
16
+
17
+ The five blocks are now one loop over a recipient-to-app-slug map, and a missing push audience
18
+ skips one notification instead of ending the request. Same apps, same order, same payloads.
19
+
20
+ - ad056fb: Stop the bulletin-board expiry sweep from overwriting deleted announcements
21
+
22
+ `processExpiredBulletinBoards` runs hourly and matched on the dates alone:
23
+ `{ noExpiration: false, endDate: { $lt: now } }`. A soft delete only rewrites
24
+ `status` and `deletedAt` - the row keeps its start and end dates - so every
25
+ deleted announcement whose end date had passed matched the sweep, and its
26
+ `status: "deleted"` was overwritten with `status: "expired"`. The announcement
27
+ then reappeared on the Expired tab, and the user who deleted it saw it come
28
+ back.
29
+
30
+ Measured read-only on staging: of 63 rows carrying a real `deletedAt`, only 33
31
+ still read `deleted` - 30 (48%) had been flipped to `expired` by the sweep.
32
+
33
+ The query now excludes `status: "deleted"`. It lives in the model next to
34
+ `deriveBulletinStatus` as `expiredBulletinSweepFilter`, so the rule the sweep
35
+ uses is the same one create and update derive from, and it is covered by tests.
36
+
37
+ Existing wrongly-`expired` rows are NOT repaired by this change; a backfill is
38
+ a separate operation for the leads to run after this ships.
39
+
40
+ - 245c047: Derive a bulletin board's status from its start date on CREATE, not only on update
41
+
42
+ Creating an announcement that starts later the same day published it immediately.
43
+ Create and update each carried their own copy of the rule and both compared the
44
+ start date against the end of today rather than against now, so an announcement
45
+ created at 16:50 to start at 22:00 was written `active` and the Upcoming tab
46
+ stayed empty. The two cron sweeps compared against now, so all three disagreed.
47
+
48
+ The rule is now one function, `deriveBulletinStatus`, shared by create and
49
+ update and using the same clock as the sweeps. An announcement whose window has
50
+ already closed is written `expired` rather than published and retracted.
51
+
52
+ - 516f55d: Read the season pass CSV/Excel expiry cell in the site's time zone
53
+
54
+ A spreadsheet cell carries a wall clock and no zone, but every step before the
55
+ import read it as UTC: `Joi.date().iso()` converted the text before the
56
+ controller saw it, ExcelJS turns a date-formatted cell into UTC midnight, and
57
+ `new Date()` on a UTC container did the same. A pass imported as `2026-12-05`
58
+ came back to a Singapore reader as 8am on the 5th, and `2026-12-05T18:45`
59
+ rolled over into the 6th.
60
+
61
+ `parseExpiryDate` now moves to `src/utils/vehicle-import.util.ts` and reads the
62
+ cell in `Asia/Singapore` before storing the instant — the same convention
63
+ `date-format.util.ts`, the patrol logs and the attendance reports already use
64
+ server-side, and the server-side equivalent of the `v-model:utc` binding the
65
+ season pass form uses in the browser.
66
+
67
+ - 60cd128: Default the visitor-invite `type` to `guest` so already-installed resident app builds can invite again.
68
+
69
+ `POST /api/visitor-transactions/invite/:inviterId` made `type` a required field when the
70
+ contractor invite was added. Every resident app build already in the stores sends no `type`, so
71
+ those users get a 400 — `"type" is required` — on a form that has no Type field. `type` now
72
+ defaults to `guest`, which is exactly what the endpoint did before the contractor work.
73
+ Contractor invites are unchanged: `contractorType` is still required when `type` is `contractor`,
74
+ and an unrecognised `type` is still refused.
75
+
76
+ ## 3.52.3
77
+
78
+ ### Patch Changes
79
+
80
+ - 57365e1: Write camera failures in words an operator can act on, and stop offering plates whose vehicle has been deleted.
81
+
82
+ Camera failure messages no longer carry a camera's `_id`, its host, an IP, a port or a raw driver error. A camera is named by its display name and direction ("North Gate (entry)", or "the entry camera" when it has no name), and the reason is plain language ("the camera could not be reached"). The full technical detail — record id, host, and the device's or driver's own text — goes to the server log instead. The same treatment is applied to create, approve, restore, block, unblock, edit and delete; `blocklistVehicles`, `removeFromBlocklist` and `reactivateVehicleById` previously said only "Failed to delete/update/add plate number to ANPR", which named no camera and said nothing about what happened to the record. Blocking now states the opposite consequence to saving, because a failed blacklist write leaves the car able to get in.
83
+
84
+ `site.people.plates[]` no longer keeps a plate after its vehicle is deleted. The pull matched on `recNo`, which Dahua numbers per device, so the copy on the vehicle and the copy on the person diverged and the update silently matched nothing; it now matches on the plate number, and leaves the plate alone when a second live vehicle still holds it. Unblocking, which soft-deletes the record, now pulls the plate too. `GET /api/people/unit/:unit` and `GET /api/people` also drop any plate whose vehicle records are all deleted, so rows that had already drifted are not offered either. A plate the resident merely declared, with no vehicle record at all, is unchanged.
85
+
86
+ Unblocking a vehicle at a site with no ANPR camera now succeeds instead of failing with "No site cameras found." — there is nothing to revoke, which is the state the unblock is trying to reach. Approve, Restore, Block and Edit still refuse in that state, now saying why and what did not happen.
87
+
88
+ - 3b4d5c9: `GET /api/people/plateNumber/:plateNumber` now hides plates whose vehicle has been deleted, like the other person reads.
89
+
90
+ Its own filter is `status: { $ne: "deleted" }` on the **person**, which is unrelated to the vehicle — reading that line as "deleted plates are excluded" is why this endpoint was missed when the other two were fixed. It has no caller in the estate today, but it is routed and behind `requireAuth`, so an authenticated caller gets the resident's whole `plates[]` array back. The filter runs on read, after the cache, because whether a plate's vehicle is deleted can change inside the 15-minute TTL.
91
+
92
+ `getPersonByPlateNumber` (no caller, no route) and `getApprovedPersonByPlateNumber` (a record-matching path inside `person.service.ts`, not a display path) are deliberately left alone.
93
+
94
+ - b1400ac: Hide deleted plates on every person read, not only on the two browse paths.
95
+
96
+ A vehicle's plate number is copied onto the resident's record as `site.people.plates[]`. Deleting the vehicle is meant to take it back out, and where that copy had already drifted the read paths filter it against the vehicle record. That filter was only on `GET /api/people` and `GET /api/people/unit/:unit`, so four other endpoints still handed back a plate whose only vehicle is deleted:
97
+
98
+ - `GET /api/people/plateNumber/:plateNumber`
99
+ - `GET /api/people/nric/:nric` (the MA app's NRIC lookup, and layer-common's `findPersonByNRIC`)
100
+ - `GET /api/people/contact/:contact`
101
+ - `GET /api/people/all-nric` (the MA app's NRIC search)
102
+
103
+ Each of the four now applies the same filter. `status: { $ne: "deleted" }` on three of those queries is the PERSON's status and has nothing to do with the vehicle — reading it as "deleted plates are excluded" is how they were missed. The filter runs on the way out, after the Redis read, so a plate deleted inside the 15-minute cache window disappears immediately rather than at the next expiry.
104
+
105
+ Unchanged: a plate the resident merely declared, with no vehicle record at all, is still listed; a plate a second live vehicle still holds is still listed.
106
+
3
107
  ## 3.52.2
4
108
 
5
109
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.52.3-staging.248",
4
+ "version": "3.52.4",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -1,16 +0,0 @@
1
- ---
2
- "@7365admin1/core": patch
3
- ---
4
-
5
- Stop silently discarding a bulletin whose recipients include a service app with no users.
6
-
7
- `POST /api/bulletin-boards` inserts the announcement inside a Mongo transaction and then
8
- notifies each recipient group. The five per-service-app blocks each carried a bare `return;` for
9
- "this site has no id" and "this app has no registered users". Those returns left the enclosing
10
- `try` before `commitTransaction()`, so the session was ended with the transaction still open and
11
- the announcement was rolled back — while the controller still answered `201 Created`. Selecting
12
- Cleaning, M&E, Pest Control, Landscaping or Pool Maintenance at an estate with no users of that
13
- app therefore made the announcement vanish with no error anywhere.
14
-
15
- The five blocks are now one loop over a recipient-to-app-slug map, and a missing push audience
16
- skips one notification instead of ending the request. Same apps, same order, same payloads.
@@ -1,23 +0,0 @@
1
- ---
2
- "@7365admin1/core": patch
3
- ---
4
-
5
- Stop the bulletin-board expiry sweep from overwriting deleted announcements
6
-
7
- `processExpiredBulletinBoards` runs hourly and matched on the dates alone:
8
- `{ noExpiration: false, endDate: { $lt: now } }`. A soft delete only rewrites
9
- `status` and `deletedAt` - the row keeps its start and end dates - so every
10
- deleted announcement whose end date had passed matched the sweep, and its
11
- `status: "deleted"` was overwritten with `status: "expired"`. The announcement
12
- then reappeared on the Expired tab, and the user who deleted it saw it come
13
- back.
14
-
15
- Measured read-only on staging: of 63 rows carrying a real `deletedAt`, only 33
16
- still read `deleted` - 30 (48%) had been flipped to `expired` by the sweep.
17
-
18
- The query now excludes `status: "deleted"`. It lives in the model next to
19
- `deriveBulletinStatus` as `expiredBulletinSweepFilter`, so the rule the sweep
20
- uses is the same one create and update derive from, and it is covered by tests.
21
-
22
- Existing wrongly-`expired` rows are NOT repaired by this change; a backfill is
23
- a separate operation for the leads to run after this ships.
@@ -1,15 +0,0 @@
1
- ---
2
- "@7365admin1/core": patch
3
- ---
4
-
5
- Derive a bulletin board's status from its start date on CREATE, not only on update
6
-
7
- Creating an announcement that starts later the same day published it immediately.
8
- Create and update each carried their own copy of the rule and both compared the
9
- start date against the end of today rather than against now, so an announcement
10
- created at 16:50 to start at 22:00 was written `active` and the Upcoming tab
11
- stayed empty. The two cron sweeps compared against now, so all three disagreed.
12
-
13
- The rule is now one function, `deriveBulletinStatus`, shared by create and
14
- update and using the same clock as the sweeps. An announcement whose window has
15
- already closed is written `expired` rather than published and retracted.
@@ -1,11 +0,0 @@
1
- ---
2
- "@7365admin1/core": patch
3
- ---
4
-
5
- Write camera failures in words an operator can act on, and stop offering plates whose vehicle has been deleted.
6
-
7
- Camera failure messages no longer carry a camera's `_id`, its host, an IP, a port or a raw driver error. A camera is named by its display name and direction ("North Gate (entry)", or "the entry camera" when it has no name), and the reason is plain language ("the camera could not be reached"). The full technical detail — record id, host, and the device's or driver's own text — goes to the server log instead. The same treatment is applied to create, approve, restore, block, unblock, edit and delete; `blocklistVehicles`, `removeFromBlocklist` and `reactivateVehicleById` previously said only "Failed to delete/update/add plate number to ANPR", which named no camera and said nothing about what happened to the record. Blocking now states the opposite consequence to saving, because a failed blacklist write leaves the car able to get in.
8
-
9
- `site.people.plates[]` no longer keeps a plate after its vehicle is deleted. The pull matched on `recNo`, which Dahua numbers per device, so the copy on the vehicle and the copy on the person diverged and the update silently matched nothing; it now matches on the plate number, and leaves the plate alone when a second live vehicle still holds it. Unblocking, which soft-deletes the record, now pulls the plate too. `GET /api/people/unit/:unit` and `GET /api/people` also drop any plate whose vehicle records are all deleted, so rows that had already drifted are not offered either. A plate the resident merely declared, with no vehicle record at all, is unchanged.
10
-
11
- Unblocking a vehicle at a site with no ANPR camera now succeeds instead of failing with "No site cameras found." — there is nothing to revoke, which is the state the unblock is trying to reach. Approve, Restore, Block and Edit still refuse in that state, now saying why and what did not happen.
@@ -1,18 +0,0 @@
1
- ---
2
- "@7365admin1/core": patch
3
- ---
4
-
5
- Read the season pass CSV/Excel expiry cell in the site's time zone
6
-
7
- A spreadsheet cell carries a wall clock and no zone, but every step before the
8
- import read it as UTC: `Joi.date().iso()` converted the text before the
9
- controller saw it, ExcelJS turns a date-formatted cell into UTC midnight, and
10
- `new Date()` on a UTC container did the same. A pass imported as `2026-12-05`
11
- came back to a Singapore reader as 8am on the 5th, and `2026-12-05T18:45`
12
- rolled over into the 6th.
13
-
14
- `parseExpiryDate` now moves to `src/utils/vehicle-import.util.ts` and reads the
15
- cell in `Asia/Singapore` before storing the instant — the same convention
16
- `date-format.util.ts`, the patrol logs and the attendance reports already use
17
- server-side, and the server-side equivalent of the `v-model:utc` binding the
18
- season pass form uses in the browser.
@@ -1,9 +0,0 @@
1
- ---
2
- "@7365admin1/core": patch
3
- ---
4
-
5
- `GET /api/people/plateNumber/:plateNumber` now hides plates whose vehicle has been deleted, like the other person reads.
6
-
7
- Its own filter is `status: { $ne: "deleted" }` on the **person**, which is unrelated to the vehicle — reading that line as "deleted plates are excluded" is why this endpoint was missed when the other two were fixed. It has no caller in the estate today, but it is routed and behind `requireAuth`, so an authenticated caller gets the resident's whole `plates[]` array back. The filter runs on read, after the cache, because whether a plate's vehicle is deleted can change inside the 15-minute TTL.
8
-
9
- `getPersonByPlateNumber` (no caller, no route) and `getApprovedPersonByPlateNumber` (a record-matching path inside `person.service.ts`, not a display path) are deliberately left alone.
@@ -1,16 +0,0 @@
1
- ---
2
- "@7365admin1/core": patch
3
- ---
4
-
5
- Hide deleted plates on every person read, not only on the two browse paths.
6
-
7
- A vehicle's plate number is copied onto the resident's record as `site.people.plates[]`. Deleting the vehicle is meant to take it back out, and where that copy had already drifted the read paths filter it against the vehicle record. That filter was only on `GET /api/people` and `GET /api/people/unit/:unit`, so four other endpoints still handed back a plate whose only vehicle is deleted:
8
-
9
- - `GET /api/people/plateNumber/:plateNumber`
10
- - `GET /api/people/nric/:nric` (the MA app's NRIC lookup, and layer-common's `findPersonByNRIC`)
11
- - `GET /api/people/contact/:contact`
12
- - `GET /api/people/all-nric` (the MA app's NRIC search)
13
-
14
- Each of the four now applies the same filter. `status: { $ne: "deleted" }` on three of those queries is the PERSON's status and has nothing to do with the vehicle — reading it as "deleted plates are excluded" is how they were missed. The filter runs on the way out, after the Redis read, so a plate deleted inside the 15-minute cache window disappears immediately rather than at the next expiry.
15
-
16
- Unchanged: a plate the resident merely declared, with no vehicle record at all, is still listed; a plate a second live vehicle still holds is still listed.
@@ -1,12 +0,0 @@
1
- ---
2
- "@7365admin1/core": patch
3
- ---
4
-
5
- Default the visitor-invite `type` to `guest` so already-installed resident app builds can invite again.
6
-
7
- `POST /api/visitor-transactions/invite/:inviterId` made `type` a required field when the
8
- contractor invite was added. Every resident app build already in the stores sends no `type`, so
9
- those users get a 400 — `"type" is required` — on a form that has no Type field. `type` now
10
- defaults to `guest`, which is exactly what the endpoint did before the contractor work.
11
- Contractor invites are unchanged: `contractorType` is still required when `type` is `contractor`,
12
- and an unrecognised `type` is still refused.