@7365admin1/layer-common 4.0.3-staging.232 → 4.0.3-staging.234

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.
@@ -1,77 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
-
4
- /**
5
- * THE CLIENT LIST'S "NATURE" COLUMN.
6
- *
7
- * The column prints `formatNature(org.nature)`, the same helper the admin app's
8
- * Organizations list uses - so the two screens say a nature the same way while
9
- * both exist, and the console keeps saying it after that screen is retired.
10
- *
11
- * `nature` is a REQUIRED enum on the organisation model
12
- * (`iservice365-core/src/models/organization.model.ts` `allowedNatures`), so
13
- * what this asserts is that every legal value comes out as something a person
14
- * reads - not a raw `security_agency`.
15
- *
16
- * `useUtils()` is a Nuxt composable: it opens with `useState("search", ...)`,
17
- * which only exists inside a Nuxt app. One stub makes the factory constructible
18
- * here; `formatNature` itself touches nothing but its argument.
19
- */
20
- (globalThis as any).useState = (_key: string, init: () => any) => ({
21
- value: init(),
22
- });
23
-
24
- const { formatNature } = (await import("../composables/useUtils.ts")).default();
25
-
26
- /** `allowedNatures`, copied from the core model, in the model's own order. */
27
- const ALLOWED_NATURES = [
28
- "real_estate_developer",
29
- "property_management_agency",
30
- "security_agency",
31
- "cleaning_services",
32
- "mechanical_electrical_services",
33
- "landscaping_services",
34
- "pest_control_services",
35
- "pool_maintenance_services",
36
- ];
37
-
38
- test("every nature the organisation model allows prints as words", () => {
39
- assert.deepEqual(
40
- ALLOWED_NATURES.map((v) => formatNature(v)),
41
- [
42
- "Real Estate Developer",
43
- "Property Management Agency",
44
- "Security Agency",
45
- "Cleaning Services",
46
- "Mechanical Electrical Services",
47
- "Landscaping Services",
48
- "Pest Control Services",
49
- "Pool Maintenance Services",
50
- ],
51
- );
52
- });
53
-
54
- test("no allowed nature is left as its stored value", () => {
55
- for (const value of ALLOWED_NATURES) {
56
- const label = formatNature(value);
57
- assert.notEqual(label, value, `${value} was printed raw`);
58
- assert.ok(!label.includes("_"), `${value} kept an underscore`);
59
- }
60
- });
61
-
62
- test("an organisation with no nature yields nothing, so the column says so itself", () => {
63
- // The cell renders `nature ? formatNature(nature) : 'Not recorded'`. This is
64
- // the half that decides the fallback is needed: the helper answers "" rather
65
- // than a label, and a blank cell reads as a broken screen.
66
- assert.equal(formatNature(""), "");
67
- assert.equal(formatNature(undefined as unknown as string), "");
68
- assert.equal(formatNature(null as unknown as string), "");
69
- });
70
-
71
- test("a value the model does not list is still printed, not swallowed", () => {
72
- // `customer.service.ts` creates property-owner orgs with `property_owner`,
73
- // which is NOT in `allowedNatures`. Those rows appear in the Client List, and
74
- // the column showing what they are is the point - so an unknown value is
75
- // formatted like any other rather than blanked.
76
- assert.equal(formatNature("property_owner"), "Property Owner");
77
- });
@@ -1,240 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
-
4
- import {
5
- NOT_SET,
6
- describeClientSubscription,
7
- formatDate,
8
- } from "./client-subscription.ts";
9
-
10
- /** What the list endpoint really returns for an org that HAS a subscription. */
11
- function orgWithSub(sub: Record<string, any>) {
12
- return {
13
- _id: "org1",
14
- name: "A Client",
15
- status: "active",
16
- subscription: {
17
- _id: "sub1",
18
- org: "org1",
19
- type: "organization",
20
- billingCycle: "monthly",
21
- createdAt: "2026-01-01T00:00:00.000Z",
22
- nextBillingDate: "2027-01-01T00:00:00.000Z",
23
- status: "active",
24
- ...sub,
25
- },
26
- };
27
- }
28
-
29
- const NOW = new Date("2026-08-20T00:00:00.000Z");
30
-
31
- test("no subscription document is a state, not a broken row", () => {
32
- // The left join collapses a missing subscription to `{}` - this is the shape
33
- // 11 production clients are in, and it used to render as four dashes.
34
- for (const org of [{ subscription: {} }, { subscription: null }, {}, null, undefined]) {
35
- const v = describeClientSubscription(org as any, NOW);
36
- assert.equal(v.state, "none", JSON.stringify(org));
37
- assert.equal(v.label, "No subscription set up");
38
- assert.equal(v.billingCycle, NOT_SET);
39
- assert.equal(v.start, NOT_SET);
40
- assert.equal(v.end, NOT_SET);
41
- assert.equal(v.needsAttention, false);
42
- }
43
- });
44
-
45
- test("a running record with nothing else proven is plain Active, never 'paying'", () => {
46
- // Nothing the endpoint sends proves anybody is paying, so it must not say so.
47
- const v = describeClientSubscription(orgWithSub({}), NOW);
48
- assert.equal(v.state, "active");
49
- assert.equal(v.label, "Active");
50
- assert.equal(v.billingCycle, "Monthly");
51
- assert.equal(v.needsAttention, false);
52
- });
53
-
54
- test("complimentary is only claimed when the record says so", () => {
55
- const v = describeClientSubscription(
56
- orgWithSub({ billingMode: "complimentary" }),
57
- NOW,
58
- );
59
- assert.equal(v.state, "complimentary");
60
- assert.equal(v.label, "Complimentary (not charged)");
61
-
62
- // and the opposite claim needs the same proof
63
- assert.equal(
64
- describeClientSubscription(orgWithSub({ billingMode: "paid" }), NOW).label,
65
- "Active (paying)",
66
- );
67
- });
68
-
69
- test("suspended and ended come from the record's own status", () => {
70
- assert.equal(
71
- describeClientSubscription(orgWithSub({ status: "suspended" }), NOW).state,
72
- "suspended",
73
- );
74
-
75
- for (const s of ["ended", "canceled", "cancelled", "expired", "TERMINATED", " Ended "]) {
76
- const v = describeClientSubscription(orgWithSub({ status: s }), NOW);
77
- assert.equal(v.state, "ended", s);
78
- assert.equal(v.label, "Ended");
79
- assert.equal(v.needsAttention, false);
80
- }
81
- });
82
-
83
- test("owner decision 8: a passed end date is flagged, not silently left as Active", () => {
84
- const v = describeClientSubscription(
85
- orgWithSub({ nextBillingDate: "2026-08-01T00:00:00.000Z" }),
86
- NOW,
87
- );
88
- assert.equal(v.state, "ended");
89
- assert.equal(v.label, "Ended (needs attention)");
90
- assert.equal(v.needsAttention, true);
91
- });
92
-
93
- test("a suspended record is not re-labelled by its dates", () => {
94
- // Status wins: a client already suspended does not need flagging again.
95
- const v = describeClientSubscription(
96
- orgWithSub({ status: "suspended", nextBillingDate: "2026-08-01T00:00:00.000Z" }),
97
- NOW,
98
- );
99
- assert.equal(v.state, "suspended");
100
- assert.equal(v.needsAttention, false);
101
- });
102
-
103
- test("Phase 2's startDate / endDate win over the fallbacks when they arrive", () => {
104
- const v = describeClientSubscription(
105
- orgWithSub({
106
- startDate: "2026-03-05T00:00:00.000Z",
107
- endDate: "2027-03-05T00:00:00.000Z",
108
- createdAt: "2020-01-01T00:00:00.000Z",
109
- nextBillingDate: "2020-02-01T00:00:00.000Z",
110
- }),
111
- NOW,
112
- );
113
- // The stale fallbacks are both in the past; if they had won, this row would
114
- // have been flagged. It is not, so the real dates were used.
115
- assert.equal(v.state, "active");
116
- assert.equal(v.start, formatDate("2026-03-05T00:00:00.000Z"));
117
- assert.equal(v.end, formatDate("2027-03-05T00:00:00.000Z"));
118
- });
119
-
120
- test("a missing or unreadable value says 'Not set', not a dash", () => {
121
- assert.equal(formatDate(null), NOT_SET);
122
- assert.equal(formatDate(""), NOT_SET);
123
- assert.equal(formatDate("not a date"), NOT_SET);
124
- assert.notEqual(formatDate("2026-08-20T00:00:00.000Z"), NOT_SET);
125
-
126
- const v = describeClientSubscription(
127
- orgWithSub({ billingCycle: "", createdAt: null, nextBillingDate: null }),
128
- NOW,
129
- );
130
- assert.equal(v.billingCycle, NOT_SET);
131
- assert.equal(v.start, NOT_SET);
132
- assert.equal(v.end, NOT_SET);
133
- assert.equal(v.state, "active");
134
- });
135
-
136
- test("a billing cycle the screen does not know is shown, not swallowed", () => {
137
- assert.equal(
138
- describeClientSubscription(orgWithSub({ billingCycle: "quarterly" }), NOW).billingCycle,
139
- "quarterly",
140
- );
141
- assert.equal(
142
- describeClientSubscription(orgWithSub({ billingCycle: "YEARLY" }), NOW).billingCycle,
143
- "Yearly",
144
- );
145
- });
146
-
147
- /* ── SUSPEND / REACTIVATE, THE STATE THE LIST HAS TO SHOW AFTERWARDS ──────
148
- *
149
- * `PATCH /api/organizations/:id/status` (core `organization.controller.ts`
150
- * `updateStatus`) writes BOTH `organizations.status` and the organisation's
151
- * subscription status - the second one so the hourly sync job agrees rather
152
- * than undoing the decision an hour later. So after the list is re-read, a
153
- * suspended client arrives with both set, and this is what the row then says.
154
- * Nothing in the screen holds a second copy of that state.
155
- */
156
-
157
- /** Exactly what `updateStatus` leaves behind, applied to a list row. */
158
- function afterStatusChange(org: Record<string, any>, status: "active" | "suspended") {
159
- return {
160
- ...org,
161
- status,
162
- subscription: org.subscription?._id
163
- ? { ...org.subscription, status }
164
- : org.subscription,
165
- };
166
- }
167
-
168
- test("suspending a client makes the row say Suspended", () => {
169
- const before = orgWithSub({});
170
- assert.equal(describeClientSubscription(before, NOW).state, "active");
171
-
172
- const after = afterStatusChange(before, "suspended");
173
- const v = describeClientSubscription(after, NOW);
174
-
175
- assert.equal(v.state, "suspended");
176
- assert.equal(v.label, "Suspended");
177
- // Suspension is a decision somebody took, not a thing needing attention.
178
- assert.equal(v.needsAttention, false);
179
- // The dates are untouched - "data is kept" is visible, not just claimed.
180
- assert.equal(v.billingCycle, describeClientSubscription(before, NOW).billingCycle);
181
- assert.equal(v.start, describeClientSubscription(before, NOW).start);
182
- assert.equal(v.end, describeClientSubscription(before, NOW).end);
183
- });
184
-
185
- test("reactivating puts the row back exactly where it was", () => {
186
- const before = orgWithSub({});
187
- const round = afterStatusChange(afterStatusChange(before, "suspended"), "active");
188
-
189
- assert.deepEqual(
190
- describeClientSubscription(round, NOW),
191
- describeClientSubscription(before, NOW),
192
- );
193
- });
194
-
195
- test("a complimentary client suspends and reactivates the same way", () => {
196
- // All 11 production clients are complimentary, so this is the case that
197
- // actually happens - and `billingMode` must survive the round trip.
198
- const before = orgWithSub({ billingMode: "complimentary" });
199
- assert.equal(describeClientSubscription(before, NOW).state, "complimentary");
200
-
201
- assert.equal(
202
- describeClientSubscription(afterStatusChange(before, "suspended"), NOW).state,
203
- "suspended",
204
- );
205
- assert.equal(
206
- describeClientSubscription(afterStatusChange(before, "active"), NOW).state,
207
- "complimentary",
208
- );
209
- });
210
-
211
- test("a client with NO subscription document still suspends", () => {
212
- // `updateStatus` writes the organisation's status either way and only
213
- // touches a subscription if one exists. This row has none, so the
214
- // subscription column keeps saying so - the ORGANISATION's status is what
215
- // moved it to the Suspended tab, and that is the honest reading of the
216
- // record. The list is fetched per tab, so the row is on the tab that matches.
217
- const before = { _id: "o-1", name: "A Client", status: "active", subscription: {} };
218
- const after = afterStatusChange(before, "suspended");
219
-
220
- assert.equal(after.status, "suspended");
221
- assert.equal(describeClientSubscription(after, NOW).state, "none");
222
- assert.equal(describeClientSubscription(after, NOW).label, "No subscription set up");
223
- });
224
-
225
- test("suspending does not clear an end date that had already passed", () => {
226
- // Owner decision 8's flag and a suspension are different things, and the
227
- // flag is derived at read time - suspending must not hide the fact that the
228
- // subscription had run out, because reactivating brings it straight back.
229
- const overdue = orgWithSub({ nextBillingDate: "2026-01-01T00:00:00.000Z" });
230
- assert.equal(describeClientSubscription(overdue, NOW).needsAttention, true);
231
-
232
- assert.equal(
233
- describeClientSubscription(afterStatusChange(overdue, "suspended"), NOW).state,
234
- "suspended",
235
- );
236
- assert.equal(
237
- describeClientSubscription(afterStatusChange(overdue, "active"), NOW).needsAttention,
238
- true,
239
- );
240
- });
@@ -1,87 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
-
4
- import { consoleTier } from "./console-tier.ts";
5
-
6
- /** The seeded platform-staff role - `user.service.ts createDefaultUser()`. */
7
- const OWNER_ROLE = {
8
- _id: "r-owner",
9
- name: "Super Admin",
10
- type: "admin",
11
- default: true,
12
- permissions: [],
13
- };
14
-
15
- /** A role made through the admin app. `default` is not in its Joi schema. */
16
- const STAFF_ROLE = {
17
- _id: "r-staff",
18
- name: "Operations",
19
- type: "admin",
20
- permissions: ["organization:read"],
21
- };
22
-
23
- const ADMIN_MEMBER = { _id: "m-1", user: "u-1", type: "admin", role: "r-owner" };
24
-
25
- test("the owner is an admin member on an admin role marked default", () => {
26
- assert.equal(consoleTier(ADMIN_MEMBER, OWNER_ROLE), "owner");
27
- });
28
-
29
- test("ordinary Seven365 staff are admin, but their role is not the default one", () => {
30
- assert.equal(consoleTier(ADMIN_MEMBER, STAFF_ROLE), "staff");
31
- // The absence of the field, not just `false`, is the ordinary case: the admin
32
- // app's create-role form cannot send it at all.
33
- assert.equal(consoleTier(ADMIN_MEMBER, { ...STAFF_ROLE, default: false }), "staff");
34
- });
35
-
36
- test("BOTH halves are required - a role name proves nothing", () => {
37
- // Staging carries an ordinary ORGANISATION role merely NAMED "Super Admin",
38
- // and `web-app-org/pages/index.vue` gated the whole console on that name.
39
- // That row must not reach owner, or staff, on the strength of its name.
40
- const impostor = { _id: "r-x", name: "Super Admin", type: "organization", default: true };
41
- assert.equal(consoleTier(ADMIN_MEMBER, impostor), "none");
42
-
43
- // ...and an admin-typed role held through a non-admin membership is not
44
- // staff either. `isSuperAdmin` requires `members.type === "admin"` too.
45
- assert.equal(consoleTier({ ...ADMIN_MEMBER, type: "organization" }, OWNER_ROLE), "none");
46
- });
47
-
48
- test("`default` is only honoured when it is exactly true", () => {
49
- // A truthy-but-not-true value is not what `createDefaultUser` writes, and the
50
- // server compares with `===`. Drawing an owner control off `"true"` or `1`
51
- // would show a button the server then refuses.
52
- for (const value of ["true", 1, {}, "yes"]) {
53
- assert.equal(
54
- consoleTier(ADMIN_MEMBER, { ...STAFF_ROLE, default: value }),
55
- "staff",
56
- JSON.stringify(value),
57
- );
58
- }
59
- });
60
-
61
- test("anything unproven is `none` - this fails closed", () => {
62
- const unproven: Array<[any, any]> = [
63
- [null, OWNER_ROLE],
64
- [undefined, OWNER_ROLE],
65
- [{}, OWNER_ROLE],
66
- [ADMIN_MEMBER, null], // the role request failed
67
- [ADMIN_MEMBER, undefined],
68
- [ADMIN_MEMBER, {}],
69
- [null, null],
70
- // An error body answered instead of a record. `member.controller.ts`
71
- // answers `NotFoundError` as JSON, so this is a real wire shape.
72
- [{ status: "error", message: "Member not found." }, OWNER_ROLE],
73
- ];
74
-
75
- for (const [member, role] of unproven) {
76
- assert.equal(consoleTier(member, role), "none", JSON.stringify([member, role]));
77
- }
78
- });
79
-
80
- test("a deleted staff membership is not staff", () => {
81
- // `isSuperAdmin` excludes it server-side; the endpoint the browser reads does
82
- // not, so it is excluded here to keep the two answers the same.
83
- assert.equal(
84
- consoleTier({ ...ADMIN_MEMBER, status: "deleted" }, OWNER_ROLE),
85
- "none",
86
- );
87
- });
@@ -1,118 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
-
4
- import { buildWorkOrderStatus } from "./dashboard.ts";
5
-
6
- /**
7
- * THE DEFECT THIS FILE EXISTS FOR.
8
- *
9
- * The Property Management "Work Order Status" panel read
10
- * `workOrderStatusSummary` - a key no endpoint returns - and fell through to
11
- * the `openWorkOrder` KPI metric, which carries no `completed` field. So
12
- * Completed read 0 on every site on every day, and Pending was silently
13
- * recomputed as (open work orders - in progress) instead of read.
14
- *
15
- * The shape below is the one `new-dashboard.repo.ts` actually sends.
16
- */
17
- test("the server's breakdown is read, not recomputed", () => {
18
- const rows = buildWorkOrderStatus({
19
- pending: 20,
20
- inProgress: 12,
21
- completed: 55,
22
- });
23
-
24
- assert.deepEqual(
25
- rows.map((r) => [r.label, r.value]),
26
- [
27
- ["Completed", 55],
28
- ["In Progress", 12],
29
- ["Pending", 20],
30
- ]
31
- );
32
- });
33
-
34
- /**
35
- * The exact regression. Before the fix this same input drew
36
- * Completed 0 / In Progress 12 / Pending 28.
37
- */
38
- test("Completed is never zero when the server says it is not", () => {
39
- const completed = buildWorkOrderStatus({
40
- pending: 20,
41
- inProgress: 12,
42
- completed: 55,
43
- }).find((r) => r.label === "Completed");
44
-
45
- assert.equal(completed?.value, 55);
46
- });
47
-
48
- /**
49
- * The bars are share-of-largest-bucket, so the biggest is always full width
50
- * and the rest are proportional to it. This is the widths the panel draws.
51
- */
52
- test("each bar is its share of the largest bucket", () => {
53
- const rows = buildWorkOrderStatus({ pending: 25, inProgress: 50, completed: 100 });
54
-
55
- assert.deepEqual(
56
- rows.map((r) => r.percent),
57
- [100, 50, 25]
58
- );
59
- });
60
-
61
- /** Three empty buckets must draw three empty bars, not divide by zero. */
62
- test("an all-zero breakdown does not divide by zero", () => {
63
- const rows = buildWorkOrderStatus({ pending: 0, inProgress: 0, completed: 0 });
64
-
65
- assert.equal(rows.length, 3);
66
- for (const row of rows) {
67
- assert.equal(row.value, 0);
68
- assert.equal(row.percent, 0, row.label);
69
- assert.ok(Number.isFinite(row.percent), row.label);
70
- }
71
- });
72
-
73
- /** A partial payload is read for what it has and zeroed for what it does not. */
74
- test("missing buckets read as zero rather than undefined", () => {
75
- const rows = buildWorkOrderStatus({ completed: 7 });
76
-
77
- assert.deepEqual(
78
- rows.map((r) => r.value),
79
- [7, 0, 0]
80
- );
81
- });
82
-
83
- /**
84
- * NO FALLBACK, DELIBERATELY. When the breakdown is absent there is nothing
85
- * honest to draw, so the panel must show its empty state - not bars built out
86
- * of a different metric. An empty array is what triggers that.
87
- */
88
- test("no breakdown means no bars, never invented ones", () => {
89
- assert.deepEqual(buildWorkOrderStatus(undefined), []);
90
- assert.deepEqual(buildWorkOrderStatus(null), []);
91
- assert.deepEqual(buildWorkOrderStatus(42 as never), []);
92
- assert.deepEqual(buildWorkOrderStatus("55" as never), []);
93
- });
94
-
95
- /**
96
- * The tone is a token NAME, not a colour. The template writes `var(--<tone>)`,
97
- * so a tone that is not a real token silently paints nothing.
98
- */
99
- test("every row carries a real design token name", () => {
100
- const rows = buildWorkOrderStatus({ pending: 1, inProgress: 2, completed: 3 });
101
-
102
- assert.deepEqual(
103
- rows.map((r) => r.tone),
104
- ["ok", "err", "warn"]
105
- );
106
- });
107
-
108
- /**
109
- * The buckets are the whole of the server's status report - it folds every
110
- * unrecognised status into `pending` rather than dropping it - so nothing the
111
- * server counted goes missing on the way to the screen.
112
- */
113
- test("the three buckets account for the whole breakdown", () => {
114
- const src = { pending: 20, inProgress: 12, completed: 55 };
115
- const total = buildWorkOrderStatus(src).reduce((sum, r) => sum + r.value, 0);
116
-
117
- assert.equal(total, src.pending + src.inProgress + src.completed);
118
- });