@7365admin1/core 3.78.0 → 3.79.0
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 +6 -0
- package/dist/index.d.ts +46 -1
- package/dist/index.js +98 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/notification-category.util.test.mjs +10 -1
- package/test/staff-platform-notifications.test.mjs +95 -0
package/package.json
CHANGED
|
@@ -20,9 +20,18 @@ import {
|
|
|
20
20
|
const keys = (result) => result.map((c) => c.key);
|
|
21
21
|
|
|
22
22
|
describe("notification categories", () => {
|
|
23
|
-
it("a wildcard role is offered everything that is rendered", () => {
|
|
23
|
+
it("a wildcard role is offered everything that is rendered, except a staff-only category", () => {
|
|
24
24
|
const offered = categoriesForPermissions({ permissions: ["*"] });
|
|
25
25
|
|
|
26
|
+
assert.deepEqual(
|
|
27
|
+
keys(offered),
|
|
28
|
+
NOTIFICATION_CATEGORIES.filter((c) => c.rendered && !c.staffOnly).map((c) => c.key),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("Seven365 staff are offered the staff-only categories too", () => {
|
|
33
|
+
const offered = categoriesForPermissions({ permissions: ["*"], isStaff: true });
|
|
34
|
+
|
|
26
35
|
assert.deepEqual(
|
|
27
36
|
keys(offered),
|
|
28
37
|
NOTIFICATION_CATEGORIES.filter((c) => c.rendered).map((c) => c.key),
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Seven365's own staff get told when a client is created and when an invited
|
|
3
|
+
* client owner accepts. Nothing addressed Seven365 staff before this, so their
|
|
4
|
+
* bell was empty by construction (owner, 2026-09-21).
|
|
5
|
+
*
|
|
6
|
+
* The category test runs the real built module. The wiring tests read the
|
|
7
|
+
* source, because `notification.service.ts` and `verification.controller.ts`
|
|
8
|
+
* pull in the whole server (mongo, mailer, expo) and are not in the suite's
|
|
9
|
+
* build list.
|
|
10
|
+
*/
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import {
|
|
15
|
+
NOTIFICATION_CATEGORIES,
|
|
16
|
+
categoriesForPermissions,
|
|
17
|
+
categorySupportsChannel,
|
|
18
|
+
} from "./.build/utils/notification-category.util.mjs";
|
|
19
|
+
|
|
20
|
+
const keys = (params) => categoriesForPermissions(params).map((c) => c.key);
|
|
21
|
+
|
|
22
|
+
test("the platform category is shown to Seven365 staff", () => {
|
|
23
|
+
assert.ok(keys({ permissions: ["*"], isStaff: true }).includes("platform"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a wildcard client role does NOT get the staff switch", () => {
|
|
27
|
+
// ~85 client roles hold `*`, and `*` opens every other category. Without the
|
|
28
|
+
// staffOnly flag every one of them would be shown a switch for messages that
|
|
29
|
+
// are never addressed to them.
|
|
30
|
+
const client = keys({ permissions: ["*"] });
|
|
31
|
+
assert.ok(!client.includes("platform"));
|
|
32
|
+
// CONTROL: the wildcard still opens the ordinary categories.
|
|
33
|
+
assert.ok(client.includes("feedback"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a resident and an ordinary role do not get it either", () => {
|
|
37
|
+
assert.ok(!keys({ isResident: true }).includes("platform"));
|
|
38
|
+
assert.ok(!keys({ permissions: { feedbacks: ["see-all-feedback"] } }).includes("platform"));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the category delivers in-app and push, and is switchable", () => {
|
|
42
|
+
const platform = NOTIFICATION_CATEGORIES.find((c) => c.key === "platform");
|
|
43
|
+
assert.equal(platform.rendered, true);
|
|
44
|
+
assert.equal(categorySupportsChannel("platform", "inApp"), true);
|
|
45
|
+
assert.equal(categorySupportsChannel("platform", "push"), true);
|
|
46
|
+
// No mailer recipient for a staff list, so no dead email switch.
|
|
47
|
+
assert.equal(categorySupportsChannel("platform", "email"), false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("the stored module tag is one core accepts", () => {
|
|
51
|
+
// `_addNotification` validates `module` against NotificationModule; an
|
|
52
|
+
// unknown tag is dropped from the feed (the inventory alerts, 2026-09-19).
|
|
53
|
+
const model = readFileSync("src/models/notification.model.ts", "utf8");
|
|
54
|
+
assert.match(model, /PLATFORM = "platform"/);
|
|
55
|
+
const service = readFileSync("src/services/notification.service.ts", "utf8");
|
|
56
|
+
assert.match(service, /module: "platform"/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("both senders resolve the staff list themselves and never throw", () => {
|
|
60
|
+
const service = readFileSync("src/services/notification.service.ts", "utf8");
|
|
61
|
+
for (const name of ["clientCreated", "clientInvitationAccepted"]) {
|
|
62
|
+
const body = service.slice(service.indexOf(`static async ${name}(`), service.indexOf(`static async ${name}(`) + 900);
|
|
63
|
+
assert.match(body, /const staff = await staffRecipients\(\);/, name);
|
|
64
|
+
assert.match(body, /if \(!staff\.length\) return;/, name);
|
|
65
|
+
// CONTROL: a caller cannot pass its own recipient list in.
|
|
66
|
+
assert.doesNotMatch(body, /userIds/, name);
|
|
67
|
+
}
|
|
68
|
+
assert.match(service, /async function staffRecipients\(\)[\s\S]{0,300}catch/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("listStaffUserIds asks the same two questions as isSuperAdmin", () => {
|
|
72
|
+
const util = readFileSync("src/utils/super-admin.util.ts", "utf8");
|
|
73
|
+
const fn = util.slice(util.indexOf("export async function listStaffUserIds"), util.indexOf("export async function isSuperAdmin"));
|
|
74
|
+
assert.match(fn, /type: "admin", status: \{ \$ne: "deleted" \}/);
|
|
75
|
+
assert.match(fn, /type: "admin", \.\.\.LIVE_ROLE/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("creating a client and accepting an invitation both notify, without blocking the request", () => {
|
|
79
|
+
const org = readFileSync("src/controllers/organization.controller.ts", "utf8");
|
|
80
|
+
const add = org.slice(org.indexOf("async function add("), org.indexOf("async function add(") + 2600);
|
|
81
|
+
assert.match(add, /void NotificationService\.clientCreated\(/);
|
|
82
|
+
assert.ok(
|
|
83
|
+
add.indexOf("NotificationService.clientCreated") > add.indexOf("seedDefaultRoles(org"),
|
|
84
|
+
"notify only after the organisation really exists",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const ver = readFileSync("src/controllers/verification.controller.ts", "utf8");
|
|
88
|
+
const upd = ver.slice(ver.indexOf("async function updateVerificationStatus("));
|
|
89
|
+
const call = upd.indexOf("NotificationService.clientInvitationAccepted");
|
|
90
|
+
assert.match(upd.slice(0, call + 60), /void NotificationService\.clientInvitationAccepted/);
|
|
91
|
+
assert.ok(call > upd.indexOf("_updateStatusById(id, status)"), "only after the status really changed");
|
|
92
|
+
// CONTROL: the already-complete early return sits above, so a replayed
|
|
93
|
+
// acceptance cannot notify twice.
|
|
94
|
+
assert.ok(upd.indexOf("if (verification.status === 'complete')") < call);
|
|
95
|
+
});
|