@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 +104 -0
- package/package.json +1 -1
- package/.changeset/bulletin-board-create-rollback.md +0 -16
- package/.changeset/bulletin-expiry-sweep-skips-deleted.md +0 -23
- package/.changeset/bulletin-status-derivation.md +0 -15
- package/.changeset/camera-error-wording-and-plate-cleanup.md +0 -11
- package/.changeset/csv-import-expiry-timezone.md +0 -18
- package/.changeset/plate-lookup-hides-deleted-plates.md +0 -9
- package/.changeset/plates-hidden-on-all-person-reads.md +0 -16
- package/.changeset/visitor-invite-type-default.md +0 -12
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,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.
|