@7365admin1/layer-common 3.2.8-staging.202 → 3.2.8-staging.204

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.
@@ -0,0 +1,145 @@
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
+ });
@@ -0,0 +1,157 @@
1
+ /**
2
+ * WHAT A CLIENT'S SUBSCRIPTION ROW HONESTLY SAYS, IN ONE PLACE.
3
+ *
4
+ * The super-admin Client List used to print "-" in Plan Type, Billing Cycle,
5
+ * Subscription Start and Subscription End for every client that has no
6
+ * subscription document. Four dashes read as a broken screen. They are not:
7
+ * they are a real, ordinary state - nobody has set that client up yet.
8
+ *
9
+ * This file turns the row the API actually returns into words a Seven365 staff
10
+ * member can act on without being told how the data is stored underneath.
11
+ *
12
+ * THE RULE THIS FILE OBEYS: never claim more than the record proves.
13
+ * The projection behind `GET /api/organizations/v2/orgs/subscriptions`
14
+ * (core `organization.repo.ts`, `$project`) sends exactly:
15
+ *
16
+ * _id, org, type, paidSeats, currentSeats, maxSeats,
17
+ * billingCycle, nextBillingDate, createdAt, status
18
+ *
19
+ * It does NOT send a plan, a price, or - today - a charging arrangement. So a
20
+ * client whose record exists and is running is called "Active", never
21
+ * "Active - paying", because nothing in the data proves anybody is paying.
22
+ * Where two states cannot yet be told apart, the lesser claim wins.
23
+ */
24
+
25
+ /** The five states a client's subscription can be in, as a person would say them. */
26
+ export type TClientSubscriptionState =
27
+ | "none"
28
+ | "complimentary"
29
+ | "active"
30
+ | "suspended"
31
+ | "ended";
32
+
33
+ export interface TClientSubscriptionView {
34
+ state: TClientSubscriptionState;
35
+ /** The one line the "Subscription" column shows. */
36
+ label: string;
37
+ /** True when somebody has to do something about this client. */
38
+ needsAttention: boolean;
39
+ /** "Monthly" / "Yearly" / "Not set". */
40
+ billingCycle: string;
41
+ /** A date, or "Not set". */
42
+ start: string;
43
+ /** A date, or "Not set". */
44
+ end: string;
45
+ }
46
+
47
+ export const NOT_SET = "Not set";
48
+
49
+ /**
50
+ * A status the record can carry that means the arrangement is over. `canceled`
51
+ * is the spelling the hourly billing job writes, so it has to be here; the
52
+ * other spellings cost nothing and stop a one-letter difference showing a
53
+ * finished client as running.
54
+ */
55
+ const ENDED_STATUSES = new Set([
56
+ "ended",
57
+ "canceled",
58
+ "cancelled",
59
+ "expired",
60
+ "terminated",
61
+ ]);
62
+
63
+ const BILLING_CYCLE_LABELS: Record<string, string> = {
64
+ monthly: "Monthly",
65
+ yearly: "Yearly",
66
+ annually: "Yearly",
67
+ };
68
+
69
+ /** Same behaviour the screen already had, with wording instead of a dash. */
70
+ export function formatDate(value?: string | Date | null): string {
71
+ if (value == null || value === "") return NOT_SET;
72
+ const d = value instanceof Date ? value : new Date(value);
73
+ return isNaN(d.getTime()) ? NOT_SET : d.toLocaleDateString("en-US");
74
+ }
75
+
76
+ function isPast(value: unknown, now: Date): boolean {
77
+ if (value == null || value === "") return false;
78
+ const d = value instanceof Date ? value : new Date(String(value));
79
+ return !isNaN(d.getTime()) && d.getTime() < now.getTime();
80
+ }
81
+
82
+ /**
83
+ * Read one organisation row from the list endpoint and say what it means.
84
+ *
85
+ * `org.subscription` is a LEFT join: an organisation with no subscription
86
+ * document still comes back as a row, with the sub-document collapsed to `{}`.
87
+ * That empty object - specifically, the absence of `_id` - is what "nobody has
88
+ * set this client up" looks like on the wire, and it is the state most clients
89
+ * are in right now.
90
+ */
91
+ export function describeClientSubscription(
92
+ org: Record<string, any> | null | undefined,
93
+ now: Date = new Date(),
94
+ ): TClientSubscriptionView {
95
+ const sub = org?.subscription ?? {};
96
+
97
+ if (!sub._id) {
98
+ return {
99
+ state: "none",
100
+ label: "No subscription set up",
101
+ needsAttention: false,
102
+ billingCycle: NOT_SET,
103
+ start: NOT_SET,
104
+ end: NOT_SET,
105
+ };
106
+ }
107
+
108
+ const cycleKey = String(sub.billingCycle ?? "").trim().toLowerCase();
109
+
110
+ // `startDate` / `endDate` are the fields the build spec adds in Phase 2. Until
111
+ // then the endpoint sends neither, so both fall through to what it does send -
112
+ // which is what this screen has always displayed.
113
+ const view = {
114
+ billingCycle: BILLING_CYCLE_LABELS[cycleKey] ?? (cycleKey ? String(sub.billingCycle) : NOT_SET),
115
+ start: formatDate(sub.startDate ?? sub.createdAt),
116
+ end: formatDate(sub.endDate ?? sub.nextBillingDate),
117
+ };
118
+
119
+ const status = String(sub.status ?? "").trim().toLowerCase();
120
+
121
+ if (status === "suspended") {
122
+ return { ...view, state: "suspended", label: "Suspended", needsAttention: false };
123
+ }
124
+
125
+ if (ENDED_STATUSES.has(status)) {
126
+ return { ...view, state: "ended", label: "Ended", needsAttention: false };
127
+ }
128
+
129
+ // Owner decision 8: when an end date passes, the client is flagged here FIRST.
130
+ // The record still says it is running, so the honest line is that the dates
131
+ // and the status disagree and a person has to settle it. Nothing on this
132
+ // screen suspends anybody - that is the job Phase 3 builds.
133
+ if (isPast(sub.endDate ?? sub.nextBillingDate, now)) {
134
+ return { ...view, state: "ended", label: "Ended (needs attention)", needsAttention: true };
135
+ }
136
+
137
+ // ponytail: dormant until Phase 2 writes `billingMode`. Nothing sends it
138
+ // today, so today every live record falls through to plain "Active" - which
139
+ // is the honest answer, because nothing in the data proves anybody is paying.
140
+ const billingMode = String(sub.billingMode ?? "").trim().toLowerCase();
141
+
142
+ if (billingMode === "complimentary") {
143
+ return {
144
+ ...view,
145
+ state: "complimentary",
146
+ label: "Complimentary (not charged)",
147
+ needsAttention: false,
148
+ };
149
+ }
150
+
151
+ return {
152
+ ...view,
153
+ state: "active",
154
+ label: billingMode === "paid" ? "Active (paying)" : "Active",
155
+ needsAttention: false,
156
+ };
157
+ }
@@ -0,0 +1,284 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ BILLING_COMPLIMENTARY,
6
+ BILLING_PAID,
7
+ buildConsolePayload,
8
+ formValuesFrom,
9
+ formatMoney,
10
+ monthlyValue,
11
+ planOptionLabel,
12
+ readApiError,
13
+ todayISO,
14
+ validateConsoleSubscription,
15
+ } from "./subscription-form.ts";
16
+
17
+ const PLANS = [
18
+ { _id: "aaaaaaaaaaaaaaaaaaaaaaaa", name: "Standard Plan", price: 25, status: "active" },
19
+ { _id: "bbbbbbbbbbbbbbbbbbbbbbbb", name: "Retired Plan", price: 40, status: "deactive" },
20
+ { _id: "cccccccccccccccccccccccc", name: "Free Bundle", price: null, status: "active" },
21
+ ];
22
+
23
+ const STANDARD = PLANS[0]._id;
24
+
25
+ function form(overrides: Record<string, any> = {}) {
26
+ return {
27
+ plan: STANDARD,
28
+ billingMode: BILLING_COMPLIMENTARY,
29
+ startDate: "2026-09-01",
30
+ endDate: "2027-08-31",
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ /* ── WHAT A CLIENT IS WORTH PER MONTH ── */
36
+
37
+ test("a plan is priced per site, per month", () => {
38
+ assert.equal(monthlyValue(PLANS[0], 4), 100);
39
+ assert.equal(monthlyValue(PLANS[0], 1), 25);
40
+ });
41
+
42
+ test("no sites is worth nothing, and is not an error", () => {
43
+ assert.equal(monthlyValue(PLANS[0], 0), 0);
44
+ });
45
+
46
+ test("a plan with no price is worth nothing rather than NaN", () => {
47
+ assert.equal(monthlyValue(PLANS[2], 7), 0);
48
+ assert.equal(monthlyValue(undefined, 7), 0);
49
+ assert.equal(monthlyValue({ price: "not a number" } as any, 7), 0);
50
+ });
51
+
52
+ test("the monthly figure reads as Singapore money", () => {
53
+ assert.equal(formatMoney(100), "S$100.00");
54
+ assert.equal(formatMoney(0), "S$0.00");
55
+ assert.equal(formatMoney(NaN), "S$0.00");
56
+ });
57
+
58
+ test("a plan is offered with its per-site price beside its name", () => {
59
+ assert.equal(
60
+ planOptionLabel(PLANS[0]),
61
+ "Standard Plan - S$25.00 per site, per month",
62
+ );
63
+ assert.equal(planOptionLabel(PLANS[2]), "Free Bundle - no per-site price set");
64
+ });
65
+
66
+ /* ── WHAT GETS SENT ── */
67
+
68
+ test("a complimentary subscription is sent with no price at all", () => {
69
+ const payload = buildConsolePayload(form(), PLANS, 4);
70
+ assert.equal(payload.amount, 0);
71
+ assert.equal(payload.billingMode, BILLING_COMPLIMENTARY);
72
+ });
73
+
74
+ test("a paying subscription is sent the plan price times the site count", () => {
75
+ const payload = buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 4);
76
+ assert.equal(payload.amount, 100);
77
+ });
78
+
79
+ test("an empty start date is sent as today, the same default the API uses", () => {
80
+ const payload = buildConsolePayload(form({ startDate: "" }), PLANS, 1);
81
+ assert.equal(payload.startDate, todayISO());
82
+ });
83
+
84
+ test("the payload carries nothing but the five fields the API accepts", () => {
85
+ const payload = buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 2);
86
+ assert.deepEqual(Object.keys(payload).sort(), [
87
+ "amount",
88
+ "billingMode",
89
+ "endDate",
90
+ "plan",
91
+ "startDate",
92
+ ]);
93
+ });
94
+
95
+ test("no status is ever sent - suspend and reactivate are Phase 3", () => {
96
+ const payload: Record<string, any> = buildConsolePayload(form(), PLANS, 2);
97
+ assert.equal(payload.status, undefined);
98
+ });
99
+
100
+ test("nothing about a card, an invoice or a gateway is ever sent", () => {
101
+ const sent = JSON.stringify(buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 3));
102
+ for (const forbidden of ["card", "cvv", "invoice", "reddot", "payment_method"]) {
103
+ assert.equal(sent.toLowerCase().includes(forbidden), false, forbidden);
104
+ }
105
+ });
106
+
107
+ /* ── WHAT IS REFUSED, BEFORE THE REQUEST GOES OUT ── */
108
+
109
+ test("a form with everything answered is not refused", () => {
110
+ assert.equal(
111
+ validateConsoleSubscription(buildConsolePayload(form(), PLANS, 4), PLANS),
112
+ null,
113
+ );
114
+ assert.equal(
115
+ validateConsoleSubscription(
116
+ buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 4),
117
+ PLANS,
118
+ ),
119
+ null,
120
+ );
121
+ });
122
+
123
+ test("no plan chosen is refused", () => {
124
+ assert.equal(
125
+ validateConsoleSubscription(buildConsolePayload(form({ plan: "" }), PLANS, 1), PLANS),
126
+ "Choose a plan.",
127
+ );
128
+ });
129
+
130
+ test("a plan that does not exist is refused", () => {
131
+ assert.equal(
132
+ validateConsoleSubscription(
133
+ buildConsolePayload(form({ plan: "dddddddddddddddddddddddd" }), PLANS, 1),
134
+ PLANS,
135
+ ),
136
+ "That plan could not be found. Pick one from the list.",
137
+ );
138
+ });
139
+
140
+ test("a plan that is no longer active is refused, and named", () => {
141
+ assert.equal(
142
+ validateConsoleSubscription(
143
+ buildConsolePayload(form({ plan: PLANS[1]._id }), PLANS, 1),
144
+ PLANS,
145
+ ),
146
+ "The plan Retired Plan is no longer active, so it cannot be given to a client. Pick an active plan.",
147
+ );
148
+ });
149
+
150
+ test("an end date before the start date is refused", () => {
151
+ assert.equal(
152
+ validateConsoleSubscription(
153
+ buildConsolePayload(form({ startDate: "2026-09-01", endDate: "2026-08-31" }), PLANS, 1),
154
+ PLANS,
155
+ ),
156
+ "The end date must be after the start date.",
157
+ );
158
+ });
159
+
160
+ test("an end date equal to the start date is refused too - the API refuses it", () => {
161
+ assert.equal(
162
+ validateConsoleSubscription(
163
+ buildConsolePayload(form({ startDate: "2026-09-01", endDate: "2026-09-01" }), PLANS, 1),
164
+ PLANS,
165
+ ),
166
+ "The end date must be after the start date.",
167
+ );
168
+ });
169
+
170
+ test("a missing end date is refused - every client has one, free or not", () => {
171
+ assert.equal(
172
+ validateConsoleSubscription(buildConsolePayload(form({ endDate: "" }), PLANS, 1), PLANS),
173
+ "Enter an end date. Every client has one, free or not.",
174
+ );
175
+ });
176
+
177
+ test("an unreadable date is refused as a date, not as something else", () => {
178
+ assert.equal(
179
+ validateConsoleSubscription(
180
+ buildConsolePayload(form({ endDate: "31/08/2027" }), PLANS, 1),
181
+ PLANS,
182
+ ),
183
+ "The end date is not a valid date.",
184
+ );
185
+ assert.equal(
186
+ validateConsoleSubscription(
187
+ buildConsolePayload(form({ startDate: "not-a-date" }), PLANS, 1),
188
+ PLANS,
189
+ ),
190
+ "The start date is not a valid date.",
191
+ );
192
+ });
193
+
194
+ test("a complimentary subscription carrying a price is refused, not silently zeroed", () => {
195
+ // The form can only produce this by disagreeing with itself - which is
196
+ // exactly what this rule is here to catch before it reaches the API.
197
+ const payload = {
198
+ ...buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 4),
199
+ billingMode: BILLING_COMPLIMENTARY,
200
+ };
201
+ assert.equal(payload.amount, 100);
202
+ assert.equal(
203
+ validateConsoleSubscription(payload, PLANS),
204
+ "A complimentary subscription is not charged, so it cannot carry a price. Set the price to 0, or change this client to paying.",
205
+ );
206
+ });
207
+
208
+ test("a charging arrangement that is neither is refused", () => {
209
+ assert.equal(
210
+ validateConsoleSubscription(buildConsolePayload(form({ billingMode: "" }), PLANS, 1), PLANS),
211
+ "Choose whether this client is complimentary or paying.",
212
+ );
213
+ });
214
+
215
+ test("a complimentary client with no sites is allowed - most clients are exactly this", () => {
216
+ assert.equal(
217
+ validateConsoleSubscription(buildConsolePayload(form(), PLANS, 0), PLANS),
218
+ null,
219
+ );
220
+ });
221
+
222
+ /* ── PRE-FILLING THE FORM ── */
223
+
224
+ test("a client with no subscription opens complimentary, starting today, with no end date", () => {
225
+ const values = formValuesFrom(null, new Date("2026-08-20T10:00:00"));
226
+ assert.equal(values.plan, "");
227
+ assert.equal(values.billingMode, BILLING_COMPLIMENTARY);
228
+ assert.equal(values.startDate, todayISO(new Date("2026-08-20T10:00:00")));
229
+ assert.equal(values.endDate, "");
230
+ });
231
+
232
+ test("an existing subscription opens with its own plan, mode and dates", () => {
233
+ const values = formValuesFrom({
234
+ _id: "eeeeeeeeeeeeeeeeeeeeeeee",
235
+ plan: STANDARD,
236
+ billingMode: "complimentary",
237
+ startDate: "2026-01-15T00:00:00.000Z",
238
+ endDate: "2026-12-31T00:00:00.000Z",
239
+ });
240
+ assert.equal(values.plan, STANDARD);
241
+ assert.equal(values.billingMode, BILLING_COMPLIMENTARY);
242
+ assert.equal(values.startDate, "2026-01-15");
243
+ assert.equal(values.endDate, "2026-12-31");
244
+ });
245
+
246
+ test("a record with no billingMode opens as paying - every one today came from the paid checkout", () => {
247
+ const values = formValuesFrom({
248
+ _id: "eeeeeeeeeeeeeeeeeeeeeeee",
249
+ createdAt: "2025-03-04T00:00:00.000Z",
250
+ nextBillingDate: "2026-04-05T00:00:00.000Z",
251
+ });
252
+ assert.equal(values.billingMode, BILLING_PAID);
253
+ // The dates fall back to what the record does carry, exactly as the list does.
254
+ assert.equal(values.startDate, "2025-03-04");
255
+ assert.equal(values.endDate, "2026-04-05");
256
+ });
257
+
258
+ test("a complimentary record has no nextBillingDate, so the end date comes from endDate alone", () => {
259
+ const values = formValuesFrom({
260
+ _id: "eeeeeeeeeeeeeeeeeeeeeeee",
261
+ billingMode: "complimentary",
262
+ createdAt: "2026-02-01T00:00:00.000Z",
263
+ endDate: "2027-02-01T00:00:00.000Z",
264
+ });
265
+ assert.equal(values.endDate, "2027-02-01");
266
+ });
267
+
268
+ /* ── WHAT A REFUSAL FROM THE SERVER SAYS ── */
269
+
270
+ test("the API's own wording is what the person is shown", () => {
271
+ assert.equal(
272
+ readApiError(
273
+ { data: { status: "error", message: "This client already has a subscription. Edit the existing one instead of creating another." } },
274
+ "fallback",
275
+ ),
276
+ "This client already has a subscription. Edit the existing one instead of creating another.",
277
+ );
278
+ });
279
+
280
+ test("a message that says nothing to a person falls back to one that does", () => {
281
+ assert.equal(readApiError(new Error("fetch failed"), "Try again."), "Try again.");
282
+ assert.equal(readApiError(undefined, "Try again."), "Try again.");
283
+ assert.equal(readApiError({ data: { message: " " } }, "Try again."), "Try again.");
284
+ });