@krizaka/coupons 0.0.0-stage → 0.1.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.
@@ -0,0 +1,427 @@
1
+ import "./chunk-ECCGFGSV.js";
2
+ import {
3
+ COUPON_DEFAULTS
4
+ } from "./chunk-JMQO6PTK.js";
5
+
6
+ // src/testkit.ts
7
+ import { randomUUID } from "crypto";
8
+ import { expect, it } from "vitest";
9
+ var T0 = /* @__PURE__ */ new Date("2026-10-26T16:00:00.000Z");
10
+ var HOUR = 3600;
11
+ var at = (seconds) => new Date(T0.valueOf() + seconds * 1e3);
12
+ var ADMIN = { id: null, label: "Eric (admin)" };
13
+ var START = at(-HOUR);
14
+ var discount = (over = {}) => ({
15
+ code: `PROMO${randomUUID().replace(/-/g, "").slice(0, 8)}`,
16
+ kind: "discount",
17
+ purpose: "promo",
18
+ startsAt: START,
19
+ percentOffBp: 2e3,
20
+ duration: "repeating",
21
+ durationMonths: 3,
22
+ ...over
23
+ });
24
+ var credit = (over = {}) => ({
25
+ code: `DEMO${randomUUID().replace(/-/g, "").slice(0, 8)}`,
26
+ kind: "credit",
27
+ purpose: "demo",
28
+ startsAt: START,
29
+ credits: 20,
30
+ capPerDay: 5,
31
+ creditsValidDays: 30,
32
+ onExhausted: "stop",
33
+ ...over
34
+ });
35
+ async function code(work) {
36
+ try {
37
+ await work;
38
+ } catch (error) {
39
+ return error.code;
40
+ }
41
+ return "answered";
42
+ }
43
+ function couponsContract(name, make) {
44
+ const ctx = (accountId, over = {}) => ({ mode: "checkout", accountId, plan: "pro", amountMinor: 1e4, currency: "cad", ...over });
45
+ const apply = (h, codeTyped, accountId, over = {}) => h.coupons.apply(h.tx, { code: codeTyped, accountId, plan: "pro", amountMinor: 1e4, currency: "cad", idempotencyKey: randomUUID(), ...over });
46
+ const made = async (h, over = {}) => h.coupons.admin.create(h.tx, ADMIN, discount({ currency: "cad", ...over }));
47
+ const paid = (redemptionId, over = {}) => ({ redemptionId, paymentRef: `pi_${randomUUID().slice(0, 8)}`, amountPaidMinor: 8e3, amountDiscountMinor: 2e3, currency: "cad", ...over });
48
+ it(`${name}: creates a coupon, reads its code without case, spaces or hyphens, refuses a taken code, and audits`, async () => {
49
+ const h = await make();
50
+ const c = await h.coupons.admin.create(h.tx, ADMIN, discount({ code: "Summer-2026" }));
51
+ expect(c).toMatchObject({ code: "Summer-2026", codeKey: "SUMMER2026", status: "active", termsVersion: 1 });
52
+ expect(await code(h.coupons.admin.create(h.tx, ADMIN, discount({ code: "summer 2026" })))).toBe("COUPON_CODE_TAKEN");
53
+ const a = await h.newAccount();
54
+ for (const typed of [" summer-2026 ", "SUMMER 2026", "summer_2026"]) expect(await h.coupons.validate(h.tx, typed, ctx(a))).toMatchObject({ ok: true, coupon: { id: c.id } });
55
+ expect(h.audited().map((e) => e.action)).toEqual(["coupon.created"]);
56
+ expect(h.audited()[0]).toMatchObject({ target: { type: "coupon", id: c.id }, detail: { code: "Summer-2026" } });
57
+ });
58
+ it(`${name}: the window opens at its start and closes at its end, to the second`, async () => {
59
+ const h = await make();
60
+ const a = await h.newAccount();
61
+ const c = await made(h, { startsAt: at(HOUR), endsAt: at(2 * HOUR) });
62
+ const state = async (seconds) => {
63
+ h.setNow(at(seconds));
64
+ const v = await h.coupons.validate(h.tx, c.code, ctx(a));
65
+ return v.ok ? "ok" : v.code;
66
+ };
67
+ expect(await state(HOUR - 1)).toBe("COUPON_NOT_STARTED");
68
+ expect(await state(HOUR)).toBe("ok");
69
+ expect(await state(2 * HOUR - 1)).toBe("ok");
70
+ expect(await state(2 * HOUR)).toBe("COUPON_EXPIRED");
71
+ });
72
+ it(`${name}: pause, resume and archive; an archived coupon stays archived; each is audited`, async () => {
73
+ const h = await make();
74
+ const a = await h.newAccount();
75
+ const c = await made(h);
76
+ await h.coupons.admin.pause(h.tx, ADMIN, c.id);
77
+ expect(await h.coupons.validate(h.tx, c.code, ctx(a))).toEqual({ ok: false, code: "COUPON_PAUSED" });
78
+ await h.coupons.admin.resume(h.tx, ADMIN, c.id);
79
+ expect((await h.coupons.validate(h.tx, c.code, ctx(a))).ok).toBe(true);
80
+ await h.coupons.admin.archive(h.tx, ADMIN, c.id);
81
+ expect(await h.coupons.validate(h.tx, c.code, ctx(a))).toEqual({ ok: false, code: "COUPON_PAUSED" });
82
+ expect(await code(h.coupons.admin.resume(h.tx, ADMIN, c.id))).toBe("COUPON_BAD_INPUT");
83
+ expect(h.audited().map((e) => e.action)).toEqual(["coupon.created", "coupon.paused", "coupon.resumed", "coupon.archived"]);
84
+ });
85
+ it(`${name}: refuses with a typed code: unknown, kind, plan, minimum, currency, new customers, provider, too large, disabled`, async () => {
86
+ const h = await make();
87
+ const a = await h.newAccount();
88
+ const refusal = async (typed, over = {}) => {
89
+ const v = await h.coupons.validate(h.tx, typed, ctx(a, over));
90
+ return v.ok ? "ok" : v.code;
91
+ };
92
+ const plan = await made(h, { plans: ["pro"], minimumMinor: 5e3 });
93
+ const fresh = await made(h, { newCustomersOnly: true });
94
+ h.setSettings({ maxPercentOff: 70 });
95
+ const big = await made(h, { percentOffBp: 6e3 });
96
+ h.setSettings({ maxPercentOff: 50 });
97
+ const fixed = await made(h, { percentOffBp: null, amountOffMinor: 6e3, currency: "cad", duration: "once", durationMonths: null });
98
+ const cr = await h.coupons.admin.create(h.tx, ADMIN, credit());
99
+ expect(await refusal("NOPE")).toBe("COUPON_UNKNOWN");
100
+ expect(await refusal("???")).toBe("COUPON_UNKNOWN");
101
+ expect(await refusal(plan.code, { plan: "audit" })).toBe("COUPON_NOT_FOR_PLAN");
102
+ expect(await refusal(plan.code, { amountMinor: 4999 })).toBe("COUPON_MINIMUM");
103
+ expect(await refusal(plan.code, { currency: "usd" })).toBe("COUPON_CURRENCY");
104
+ expect(await refusal(plan.code)).toBe("ok");
105
+ expect(await refusal(fresh.code, { hasPaidBefore: true })).toBe("COUPON_NEW_CUSTOMERS_ONLY");
106
+ expect(await refusal(fresh.code, { hasPaidBefore: false })).toBe("ok");
107
+ expect(await refusal(plan.code, { providerSupportsDiscounts: false })).toBe("COUPON_PROVIDER");
108
+ expect(await refusal(big.code)).toBe("COUPON_TOO_LARGE");
109
+ expect(await refusal(fixed.code)).toBe("COUPON_TOO_LARGE");
110
+ expect(await refusal(fixed.code, { amountMinor: 12e3 })).toBe("ok");
111
+ expect(await refusal(cr.code)).toBe("COUPON_KIND");
112
+ h.setSettings({ maxPercentOff: 70 });
113
+ expect(await refusal(big.code)).toBe("ok");
114
+ h.setSettings({ enabled: false });
115
+ expect(await refusal(plan.code)).toBe("COUPON_DISABLED");
116
+ });
117
+ it(`${name}: counts an attempt before any lookup, and refuses past the limit with the time to wait`, async () => {
118
+ const h = await make();
119
+ const a = await h.newAccount();
120
+ const c = await made(h);
121
+ await h.coupons.validate(h.tx, c.code, ctx(a, { ip: "203.0.113.9" }));
122
+ await h.coupons.validate(h.tx, "WHATEVER", ctx(a));
123
+ expect(h.limiter.attempts).toEqual([
124
+ { action: "couponAttempt", ip: "203.0.113.9", subject: a },
125
+ { action: "couponAttempt", ip: void 0, subject: a }
126
+ ]);
127
+ h.limiter.refuse(42);
128
+ expect(await h.coupons.validate(h.tx, c.code, ctx(a))).toEqual({ ok: false, code: "COUPON_RATE_LIMITED", retryAfterSeconds: 42 });
129
+ await expect(apply(h, c.code, a)).rejects.toMatchObject({ code: "COUPON_RATE_LIMITED", status: 429, retryAfterSeconds: 42 });
130
+ h.limiter.refuse(null);
131
+ expect((await h.coupons.validate(h.tx, c.code, ctx(a))).ok).toBe(true);
132
+ });
133
+ it(`${name}: says what a discount gives in numbers (the provider computes the real amount and its taxes)`, async () => {
134
+ const h = await make();
135
+ const a = await h.newAccount();
136
+ const pct = await made(h);
137
+ const fixed = await made(h, { percentOffBp: null, amountOffMinor: 500, currency: "cad", duration: "once", durationMonths: null });
138
+ expect(await h.coupons.validate(h.tx, pct.code, ctx(a))).toMatchObject({ ok: true, effect: { kind: "discount", percentOffBp: 2e3, duration: "repeating", durationMonths: 3, discountMinor: 2e3, payableMinor: 8e3 } });
139
+ expect(await h.coupons.validate(h.tx, fixed.code, ctx(a, { amountMinor: 3900 }))).toMatchObject({ effect: { discountMinor: 500, payableMinor: 3400, amountOffMinor: 500 } });
140
+ });
141
+ it(`${name}: reserves a use under the counting rule: reserved uses count until their deadline, released ones never`, async () => {
142
+ const h = await make();
143
+ const [a1, a2, a3] = [await h.newAccount(), await h.newAccount(), await h.newAccount()];
144
+ const c = await made(h, { maxRedemptions: 2 });
145
+ const r1 = await apply(h, c.code, a1);
146
+ expect(await code(apply(h, c.code, a1))).toBe("COUPON_ACCOUNT_LIMIT");
147
+ await apply(h, c.code, a2);
148
+ expect(await code(apply(h, c.code, a3))).toBe("COUPON_EXHAUSTED");
149
+ expect(await h.coupons.validate(h.tx, c.code, ctx(a3))).toEqual({ ok: false, code: "COUPON_EXHAUSTED" });
150
+ expect(await h.coupons.release(h.tx, { redemptionId: r1.redemptionId })).toBe(true);
151
+ expect((await apply(h, c.code, a3)).replayed).toBe(false);
152
+ h.setNow(at(61 * 60));
153
+ expect((await h.coupons.validate(h.tx, c.code, ctx(a1))).ok).toBe(true);
154
+ });
155
+ it(`${name}: apply is idempotent by key, and a key is not shared between coupons or accounts`, async () => {
156
+ const h = await make();
157
+ const [a1, a2] = [await h.newAccount(), await h.newAccount()];
158
+ const c = await made(h);
159
+ const other = await made(h);
160
+ const first = await apply(h, c.code, a1, { idempotencyKey: "co-1" });
161
+ const again = await apply(h, c.code, a1, { idempotencyKey: "co-1" });
162
+ expect(first.replayed).toBe(false);
163
+ expect(again).toMatchObject({ redemptionId: first.redemptionId, replayed: true });
164
+ expect(await code(apply(h, c.code, a2, { idempotencyKey: "co-1" }))).toBe("COUPON_KEY_REUSED");
165
+ expect(await code(apply(h, other.code, a1, { idempotencyKey: "co-1" }))).toBe("COUPON_KEY_REUSED");
166
+ expect(first.discount).toEqual({ couponId: c.id, termsVersion: 1, percentOffBp: 2e3, duration: "repeating", durationMonths: 3 });
167
+ expect(first.effect).toMatchObject({ discountMinor: 2e3, payableMinor: 8e3 });
168
+ expect(first.reservedUntil).toEqual(at(HOUR));
169
+ });
170
+ it(`${name}: hands the provider terms for an amount-off coupon with its currency`, async () => {
171
+ const h = await make();
172
+ const a = await h.newAccount();
173
+ const c = await made(h, { percentOffBp: null, amountOffMinor: 500, currency: "cad", duration: "once", durationMonths: null });
174
+ expect((await apply(h, c.code, a)).discount).toEqual({ couponId: c.id, termsVersion: 1, amountOffMinor: 500, currency: "cad", duration: "once" });
175
+ });
176
+ it(`${name}: confirms a use once, from the checkout's metadata or from the session when the attach never ran`, async () => {
177
+ const h = await make();
178
+ const [a1, a2, a3] = [await h.newAccount(), await h.newAccount(), await h.newAccount()];
179
+ const c = await made(h, { maxRedemptions: 5 });
180
+ const r1 = await apply(h, c.code, a1);
181
+ const p1 = paid(r1.redemptionId, { paymentRef: "pi_1" });
182
+ expect(await h.coupons.redeem(h.tx, p1)).toBe("confirmed");
183
+ expect(await h.coupons.redeem(h.tx, p1)).toBe("already");
184
+ const r2 = await apply(h, c.code, a2);
185
+ await h.coupons.attachCheckout(h.tx, r2.redemptionId, "cs_2", at(2 * HOUR));
186
+ expect(await h.coupons.redeem(h.tx, { checkoutRef: "cs_2", paymentRef: "pi_2", amountPaidMinor: 8e3, amountDiscountMinor: 2e3, currency: "cad" })).toBe("confirmed");
187
+ const r3 = await apply(h, c.code, a3);
188
+ expect(await h.coupons.redeem(h.tx, paid(r3.redemptionId, { paymentRef: "pi_1" }))).toBe("already");
189
+ expect(await h.coupons.redeem(h.tx, { redemptionId: randomUUID(), checkoutRef: "cs_none", paymentRef: "pi_x", amountPaidMinor: 1, amountDiscountMinor: 0, currency: "cad" })).toBe("unknown");
190
+ const [row] = (await h.coupons.admin.redemptions(h.tx, { couponId: c.id, status: "confirmed" })).filter((r) => r.id === r1.redemptionId);
191
+ expect(row).toMatchObject({ status: "confirmed", paymentRef: "pi_1", amountPaidMinor: 8e3, amountDiscountMinor: 2e3, overLimit: false, accountId: a1 });
192
+ });
193
+ it(`${name}: attaching the session makes the reservation outlive it`, async () => {
194
+ const h = await make();
195
+ const a = await h.newAccount();
196
+ const c = await made(h);
197
+ const r = await apply(h, c.code, a);
198
+ await h.coupons.attachCheckout(h.tx, r.redemptionId, "cs_9", at(3 * HOUR));
199
+ h.setNow(at(3 * HOUR + 4 * 60));
200
+ expect(await h.coupons.release(h.tx, { checkoutRef: "cs_9" })).toBe(true);
201
+ expect(await code(h.coupons.attachCheckout(h.tx, randomUUID(), "cs_x"))).toBe("COUPON_REDEMPTION_NOT_FOUND");
202
+ });
203
+ it(`${name}: a payment after the release is honoured and flagged: released becomes confirmed with over_limit`, async () => {
204
+ const h = await make();
205
+ const [a1, a2] = [await h.newAccount(), await h.newAccount()];
206
+ const c = await made(h, { maxRedemptions: 1 });
207
+ const r1 = await apply(h, c.code, a1);
208
+ await h.coupons.release(h.tx, { redemptionId: r1.redemptionId });
209
+ const r2 = await apply(h, c.code, a2);
210
+ await h.coupons.redeem(h.tx, paid(r2.redemptionId));
211
+ expect(await h.coupons.redeem(h.tx, paid(r1.redemptionId))).toBe("confirmed");
212
+ const late = (await h.coupons.admin.redemptions(h.tx, { couponId: c.id })).find((r) => r.id === r1.redemptionId);
213
+ expect(late).toMatchObject({ status: "confirmed", overLimit: true });
214
+ expect(await h.coupons.validate(h.tx, c.code, ctx(await h.newAccount()))).toEqual({ ok: false, code: "COUPON_EXHAUSTED" });
215
+ });
216
+ it(`${name}: the sweeper releases reservations past their deadline, and a released one is not released twice`, async () => {
217
+ const h = await make();
218
+ const [a1, a2] = [await h.newAccount(), await h.newAccount()];
219
+ const c = await made(h);
220
+ const r1 = await apply(h, c.code, a1);
221
+ await apply(h, c.code, a2);
222
+ expect(await h.coupons.releaseExpired(h.tx)).toBe(0);
223
+ h.setNow(at(HOUR));
224
+ expect(await h.coupons.releaseExpired(h.tx)).toBe(2);
225
+ expect(await h.coupons.releaseExpired(h.tx)).toBe(0);
226
+ expect(await h.coupons.release(h.tx, { redemptionId: r1.redemptionId })).toBe(false);
227
+ expect(await h.coupons.release(h.tx, { redemptionId: randomUUID() })).toBe(false);
228
+ expect(h.announced().filter((m) => m.type === "evt.coupon.released.v1")).toHaveLength(2);
229
+ });
230
+ it(`${name}: a refund adds up and never changes the status; a partial one keeps the use, a full one gives it back only if the setting says so`, async () => {
231
+ const h = await make();
232
+ const [a1, a2] = [await h.newAccount(), await h.newAccount()];
233
+ const c = await made(h, { maxRedemptions: 1 });
234
+ const r1 = await apply(h, c.code, a1);
235
+ await h.coupons.redeem(h.tx, paid(r1.redemptionId, { paymentRef: "pi_a", amountPaidMinor: 8e3 }));
236
+ expect(await h.coupons.refund(h.tx, { paymentRef: "pi_a", amountRefunded: 2e3 })).toBe("refunded");
237
+ expect(await h.coupons.refund(h.tx, { paymentRef: "pi_a", amountRefunded: 2e3 })).toBe("already");
238
+ expect(await h.coupons.refund(h.tx, { paymentRef: "pi_a", amountRefunded: 1e3 })).toBe("already");
239
+ expect(await h.coupons.refund(h.tx, { paymentRef: "pi_zz", amountRefunded: 1 })).toBe("unknown");
240
+ expect(await h.coupons.refund(h.tx, { paymentRef: null, invoiceRef: null, amountRefunded: 1 })).toBe("unknown");
241
+ expect(await h.coupons.refund(h.tx, { paymentRef: "pi_a", amountRefunded: 8e3 })).toBe("refunded");
242
+ const row = (await h.coupons.admin.redemptions(h.tx, { couponId: c.id }))[0];
243
+ expect(row).toMatchObject({ status: "confirmed", amountRefundedMinor: 8e3, useRestored: false });
244
+ expect(await code(apply(h, c.code, a2))).toBe("COUPON_EXHAUSTED");
245
+ const b = await made(h, { maxRedemptions: 1 });
246
+ h.setSettings({ refundRestoresUse: true });
247
+ const rb = await apply(h, b.code, a1);
248
+ await h.coupons.redeem(h.tx, paid(rb.redemptionId, { paymentRef: "pi_b", amountPaidMinor: 8e3 }));
249
+ await h.coupons.refund(h.tx, { paymentRef: "pi_b", amountRefunded: 7999 });
250
+ expect(await code(apply(h, b.code, a2))).toBe("COUPON_EXHAUSTED");
251
+ await h.coupons.refund(h.tx, { invoiceRef: "pi_b", amountRefunded: 8e3 });
252
+ expect((await apply(h, b.code, a2)).replayed).toBe(false);
253
+ expect(h.announced().filter((m) => m.type === "evt.coupon.refunded.v1").map((m) => m.payload.useRestored)).toEqual([false, false, false, true]);
254
+ });
255
+ it(`${name}: the terms are locked after the first use; the rules stay editable; a change is audited with before and after`, async () => {
256
+ const h = await make();
257
+ const a = await h.newAccount();
258
+ const c = await made(h);
259
+ const before = await h.coupons.admin.update(h.tx, ADMIN, c.id, { percentOffBp: 1500 });
260
+ expect(before.percentOffBp).toBe(1500);
261
+ await apply(h, c.code, a);
262
+ expect(await code(h.coupons.admin.update(h.tx, ADMIN, c.id, { percentOffBp: 3e3 }))).toBe("COUPON_TERMS_LOCKED");
263
+ const after = await h.coupons.admin.update(h.tx, ADMIN, c.id, { maxRedemptions: 10, endsAt: at(5 * HOUR) });
264
+ expect(after).toMatchObject({ maxRedemptions: 10, percentOffBp: 1500 });
265
+ expect(await h.coupons.admin.update(h.tx, ADMIN, c.id, { maxRedemptions: 10 })).toMatchObject({ maxRedemptions: 10 });
266
+ expect(await code(h.coupons.admin.update(h.tx, ADMIN, c.id, { endsAt: START }))).toBe("COUPON_BAD_INPUT");
267
+ const entry = h.audited().filter((e) => e.action === "coupon.updated").at(-1);
268
+ expect(entry.detail).toMatchObject({ changed: ["maxRedemptions", "endsAt"], before: { maxRedemptions: null, endsAt: null }, after: { maxRedemptions: 10 } });
269
+ expect(await code(h.coupons.admin.update(h.tx, ADMIN, randomUUID(), { maxRedemptions: 1 }))).toBe("COUPON_NOT_FOUND");
270
+ });
271
+ it(`${name}: the admin cannot create what the rules refuse: a free discount, a fixed amount forever, a credit coupon without a cap of its own`, async () => {
272
+ const h = await make();
273
+ const bad = (over) => code(h.coupons.admin.create(h.tx, ADMIN, discount({ currency: "cad", ...over })));
274
+ expect(await bad({ percentOffBp: 1e4 })).toBe("COUPON_BAD_INPUT");
275
+ expect(await bad({ percentOffBp: 5001 })).toBe("COUPON_BAD_INPUT");
276
+ expect(await bad({ percentOffBp: null, amountOffMinor: 500, duration: "forever", durationMonths: null })).toBe("COUPON_BAD_INPUT");
277
+ expect(await bad({ percentOffBp: null, amountOffMinor: null })).toBe("COUPON_BAD_INPUT");
278
+ expect(await bad({ amountOffMinor: 5 })).toBe("COUPON_BAD_INPUT");
279
+ expect(await bad({ duration: "once" })).toBe("COUPON_BAD_INPUT");
280
+ expect(await bad({ code: "ABC" })).toBe("COUPON_BAD_INPUT");
281
+ expect(await bad({ code: "bad!code123" })).toBe("COUPON_BAD_INPUT");
282
+ expect(await bad({ endsAt: START })).toBe("COUPON_BAD_INPUT");
283
+ expect(await bad({ credits: 5 })).toBe("COUPON_BAD_INPUT");
284
+ expect(await bad({ purpose: "partner" })).toBe("COUPON_BAD_INPUT");
285
+ expect(await bad({ partnerId: randomUUID() })).toBe("COUPON_BAD_INPUT");
286
+ expect(await bad({ purpose: "partner", partnerId: randomUUID() })).toBe("COUPON_PARTNER_NOT_FOUND");
287
+ const credits = (over) => code(h.coupons.admin.create(h.tx, ADMIN, credit(over)));
288
+ expect(await credits({ credits: null })).toBe("COUPON_BAD_INPUT");
289
+ expect(await credits({ creditsValidDays: null })).toBe("COUPON_BAD_INPUT");
290
+ expect(await credits({ onExhausted: null })).toBe("COUPON_BAD_INPUT");
291
+ expect(await credits({ percentOffBp: 1e3 })).toBe("COUPON_BAD_INPUT");
292
+ expect(await credits({ capPerDay: 0 })).toBe("COUPON_BAD_INPUT");
293
+ expect(await credits({})).toBe("answered");
294
+ });
295
+ it(`${name}: a credit coupon grants capped credits that expire, once per key, and the use counts`, async () => {
296
+ const h = await make();
297
+ const [a, b] = [await h.newAccount(), await h.newAccount()];
298
+ const c = await h.coupons.admin.create(h.tx, ADMIN, credit({ capPerMonth: 12, maxRedemptions: 1, onExhausted: "fallback_paid" }));
299
+ expect(await h.coupons.validate(h.tx, c.code, { mode: "redeem", accountId: a })).toMatchObject({ ok: true, effect: { kind: "credit", credits: 20, capPerDay: 5, capPerMonth: 12, validDays: 30, onExhausted: "fallback_paid" } });
300
+ const got = await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: "rc-1" });
301
+ expect(got).toMatchObject({ credits: 20, replayed: false });
302
+ expect(got.expiresAt).toEqual(at(30 * 24 * HOUR));
303
+ expect(h.credits.granted).toEqual([{ accountId: a, amount: 20, capPerDay: 5, capPerMonth: 12, expiresAt: at(30 * 24 * HOUR), onExhausted: "fallback_paid", source: `coupon:${got.redemptionId}`, idempotencyKey: `coupon-${got.redemptionId}` }]);
304
+ expect((await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: "rc-1" })).replayed).toBe(true);
305
+ expect(h.credits.granted).toHaveLength(1);
306
+ expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: "rc-1" }))).toBe("COUPON_KEY_REUSED");
307
+ expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: "rc-2" }))).toBe("COUPON_EXHAUSTED");
308
+ const kind = await made(h);
309
+ expect(await code(h.coupons.redeemCredit(h.tx, { code: kind.code, accountId: b, idempotencyKey: "rc-3" }))).toBe("COUPON_KIND");
310
+ expect(await code(apply(h, c.code, b))).toBe("COUPON_KIND");
311
+ });
312
+ it(`${name}: revoking a credit redemption writes off what is left, keeps the use counted, and refuses what is not a credit`, async () => {
313
+ const h = await make();
314
+ const [a, b] = [await h.newAccount(), await h.newAccount()];
315
+ const c = await h.coupons.admin.create(h.tx, ADMIN, credit({ maxRedemptions: 1 }));
316
+ const got = await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: "rc-1" });
317
+ await h.coupons.revoke(h.tx, ADMIN, got.redemptionId);
318
+ await h.coupons.revoke(h.tx, ADMIN, got.redemptionId);
319
+ expect(h.credits.expired).toEqual([got.grantId]);
320
+ expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: "rc-2" }))).toBe("COUPON_EXHAUSTED");
321
+ const d = await made(h);
322
+ const r = await apply(h, d.code, a);
323
+ expect(await code(h.coupons.revoke(h.tx, ADMIN, r.redemptionId))).toBe("COUPON_NOT_REVOCABLE");
324
+ expect(await code(h.coupons.revoke(h.tx, ADMIN, randomUUID()))).toBe("COUPON_REDEMPTION_NOT_FOUND");
325
+ expect(h.audited().filter((e) => e.action === "coupon.revoked")).toHaveLength(1);
326
+ });
327
+ it(`${name}: announces the redemption with the partner and the amounts, and the last allowed use, once each`, async () => {
328
+ const h = await make();
329
+ const [a1, a2] = [await h.newAccount(), await h.newAccount()];
330
+ const partner = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "Marie Tremblay", contactEmail: "Marie@Example.com" });
331
+ const c = await made(h, { purpose: "partner", partnerId: partner.id, maxRedemptions: 2 });
332
+ for (const [i, a] of [a1, a2].entries()) {
333
+ const r = await apply(h, c.code, a);
334
+ await h.coupons.redeem(h.tx, paid(r.redemptionId, { paymentRef: `pi_${i}` }));
335
+ }
336
+ const redeemed = h.announced().filter((m) => m.type === "evt.coupon.redeemed.v1");
337
+ expect(redeemed).toHaveLength(2);
338
+ expect(redeemed[0].payload).toMatchObject({ couponId: c.id, kind: "discount", purpose: "partner", partnerId: partner.id, amountPaidMinor: 8e3, amountDiscountMinor: 2e3, currency: "cad", credits: null, overLimit: false });
339
+ expect(h.announced().filter((m) => m.type === "evt.coupon.exhausted.v1")).toHaveLength(1);
340
+ });
341
+ it(`${name}: partners: the view is net of refunds and never names a customer; erasing keeps the numbers and blanks the person`, async () => {
342
+ const h = await make();
343
+ const [a1, a2] = [await h.newAccount(), await h.newAccount()];
344
+ const partner = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "Marie Tremblay", contactEmail: "marie@example.com" });
345
+ const other = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "Paul" });
346
+ const c = await made(h, { purpose: "partner", partnerId: partner.id });
347
+ const d = await h.coupons.admin.create(h.tx, ADMIN, credit({ purpose: "partner", partnerId: partner.id }));
348
+ const r1 = await apply(h, c.code, a1);
349
+ await h.coupons.redeem(h.tx, paid(r1.redemptionId, { paymentRef: "pi_p1", amountPaidMinor: 1e4, amountDiscountMinor: 2e3 }));
350
+ await h.coupons.refund(h.tx, { paymentRef: "pi_p1", amountRefunded: 1e3 });
351
+ await h.coupons.redeemCredit(h.tx, { code: d.code, accountId: a2, idempotencyKey: "rc-p" });
352
+ const report = await h.coupons.admin.partners.report(h.tx, ADMIN, partner.id);
353
+ expect(report).toEqual(expect.arrayContaining([{ couponId: c.id, code: c.code, currency: "cad", redemptions: 1, discountMinor: 2e3, revenueMinor: 9e3, creditsGranted: 0 }, { couponId: d.id, code: d.code, currency: null, redemptions: 1, discountMinor: 0, revenueMinor: 0, creditsGranted: 20 }]));
354
+ expect(JSON.stringify(report)).not.toContain(a1);
355
+ expect(await h.coupons.admin.partners.report(h.tx, ADMIN, other.id)).toEqual([]);
356
+ expect(await h.coupons.admin.partners.report(h.tx, ADMIN, partner.id, { from: at(HOUR), to: at(2 * HOUR) })).toEqual([]);
357
+ expect(h.audited().filter((e) => e.action === "partner.report_read")).toHaveLength(3);
358
+ expect(await code(h.coupons.admin.partners.report(h.tx, ADMIN, randomUUID()))).toBe("COUPON_PARTNER_NOT_FOUND");
359
+ const exported = await h.coupons.admin.partners.export(h.tx, partner.id);
360
+ expect(exported.partner).toMatchObject({ displayName: "Marie Tremblay", contactEmail: "marie@example.com" });
361
+ const erased = await h.coupons.admin.partners.erase(h.tx, ADMIN, partner.id);
362
+ expect(erased).toMatchObject({ displayName: null, contactEmail: null, status: "archived" });
363
+ expect(erased.erasedAt).not.toBeNull();
364
+ expect((await h.coupons.admin.partners.erase(h.tx, ADMIN, partner.id)).erasedAt).toEqual(erased.erasedAt);
365
+ expect((await h.coupons.admin.partners.export(h.tx, partner.id)).rows).toHaveLength(2);
366
+ expect((await h.coupons.admin.partners.list(h.tx)).map((p) => p.id)).toEqual([other.id]);
367
+ expect((await h.coupons.admin.partners.list(h.tx, { includeArchived: true })).map((p) => p.id)).toEqual(expect.arrayContaining([partner.id, other.id]));
368
+ });
369
+ it(`${name}: partners: a name and a well-formed e-mail, archive and restore`, async () => {
370
+ const h = await make();
371
+ expect(await code(h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "" }))).toBe("COUPON_BAD_INPUT");
372
+ expect(await code(h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "x".repeat(81) }))).toBe("COUPON_BAD_INPUT");
373
+ expect(await code(h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "Marie", contactEmail: "nope" }))).toBe("COUPON_BAD_INPUT");
374
+ const p = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: " Marie " });
375
+ expect(p).toMatchObject({ displayName: "Marie", contactEmail: null, status: "active" });
376
+ expect((await h.coupons.admin.partners.archive(h.tx, ADMIN, p.id)).status).toBe("archived");
377
+ expect((await h.coupons.admin.partners.restore(h.tx, ADMIN, p.id)).status).toBe("active");
378
+ expect(await code(h.coupons.admin.partners.archive(h.tx, ADMIN, randomUUID()))).toBe("COUPON_PARTNER_NOT_FOUND");
379
+ expect(await h.coupons.admin.partners.get(h.tx, randomUUID())).toBeNull();
380
+ });
381
+ it(`${name}: a price drop pauses the fixed-amount coupons that would now be too large, and only those`, async () => {
382
+ const h = await make();
383
+ const small = await made(h, { percentOffBp: null, amountOffMinor: 1e3, currency: "cad", duration: "once", durationMonths: null, plans: ["pro"] });
384
+ const other = await made(h, { percentOffBp: null, amountOffMinor: 1e3, currency: "cad", duration: "once", durationMonths: null, plans: ["audit"] });
385
+ const percent = await made(h);
386
+ expect(await h.coupons.admin.checkPrices(h.tx, [{ plan: "pro", amountMinor: 2e3, currency: "cad" }])).toEqual([]);
387
+ expect(await h.coupons.admin.checkPrices(h.tx, [{ plan: "pro", amountMinor: 1999, currency: "cad" }, { plan: "pro", amountMinor: 100, currency: "usd" }])).toEqual([small.id]);
388
+ expect((await h.coupons.admin.get(h.tx, small.id))?.status).toBe("paused");
389
+ expect((await h.coupons.admin.get(h.tx, other.id))?.status).toBe("active");
390
+ expect((await h.coupons.admin.get(h.tx, percent.id))?.status).toBe("active");
391
+ expect(h.audited().filter((e) => e.action === "coupon.paused_by_price")).toHaveLength(1);
392
+ });
393
+ it(`${name}: lists coupons and redemptions with filters and pages`, async () => {
394
+ const h = await make();
395
+ const a = await h.newAccount();
396
+ const d = await made(h);
397
+ const c = await h.coupons.admin.create(h.tx, ADMIN, credit());
398
+ await h.coupons.admin.pause(h.tx, ADMIN, d.id);
399
+ expect((await h.coupons.admin.list(h.tx)).map((x) => x.id)).toEqual(expect.arrayContaining([d.id, c.id]));
400
+ expect((await h.coupons.admin.list(h.tx, { kind: "credit" })).map((x) => x.id)).toEqual([c.id]);
401
+ expect((await h.coupons.admin.list(h.tx, { status: "paused" })).map((x) => x.id)).toEqual([d.id]);
402
+ expect((await h.coupons.admin.list(h.tx, { purpose: "demo" })).map((x) => x.id)).toEqual([c.id]);
403
+ expect(await h.coupons.admin.list(h.tx, { limit: 1, offset: 5 })).toEqual([]);
404
+ expect(await h.coupons.admin.get(h.tx, randomUUID())).toBeNull();
405
+ await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: "rc-l" });
406
+ expect(await h.coupons.admin.redemptions(h.tx, { couponId: c.id })).toHaveLength(1);
407
+ expect(await h.coupons.admin.redemptions(h.tx, { status: "released" })).toEqual([]);
408
+ expect(await h.coupons.admin.redemptions(h.tx, { from: at(HOUR) })).toEqual([]);
409
+ expect(await h.coupons.admin.redemptions(h.tx, { to: at(-5 * HOUR) })).toEqual([]);
410
+ });
411
+ it(`${name}: generates a free code of the configured length from an alphabet without I, L, O and U`, async () => {
412
+ const h = await make();
413
+ h.setSettings({ generatedCodeLength: 12 });
414
+ const generated = await h.coupons.admin.generateCode(h.tx);
415
+ expect(generated).toMatch(/^[0-9A-HJKMNP-TV-Z]{12}$/);
416
+ const c = await h.coupons.admin.create(h.tx, ADMIN, discount({ code: generated }));
417
+ expect(c.codeKey).toBe(generated);
418
+ });
419
+ }
420
+ export {
421
+ COUPON_DEFAULTS,
422
+ T0,
423
+ couponsContract,
424
+ credit,
425
+ discount
426
+ };
427
+ //# sourceMappingURL=testkit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testkit.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { expect, it } from \"vitest\";\n\nimport type { Coupons } from \"./coupons\";\nimport { type Actor, COUPON_DEFAULTS, type CouponSettings, type CreditsPort, type NewCoupon, type Query } from \"./index\";\n\n/** What a store (through the module) is run against: the module on that store, a clock the test moves, and fakes for its neighbours. */\nexport interface CouponsHarness {\n coupons: Coupons;\n /** The query every call goes through (the transaction; a no-op for memory). */\n tx: Query;\n /** Moves the injected clock. */\n setNow(instant: Date): void;\n /** Changes the settings the module reads at each call. */\n setSettings(patch: Partial<CouponSettings>): void;\n /** A fresh account id (a row in the owner table, when the store has one). */\n newAccount(): Promise<string>;\n /** The messages appended to the outbox so far. */\n announced(): { type: string; id: string | undefined; payload: Record<string, unknown> }[];\n /** The audit actions recorded so far, with their target. */\n audited(): { action: string; target: { type: string; id: string } | null | undefined; detail: Record<string, unknown> | undefined }[];\n /** The credits ledger's calls (a fake, or the real one wrapped to record). */\n credits: { granted: Parameters<CreditsPort[\"grant\"]>[1][]; expired: string[] };\n /** The limiter: the attempts counted, and a way to refuse the next ones. */\n limiter: { attempts: { action: string; ip: string | null | undefined; subject: string | null | undefined }[]; refuse(retryAfterSeconds: number | null): void };\n}\n\n/** Midday on 26 October 2026 in Toronto: the day the scenarios start on. */\nexport const T0 = new Date(\"2026-10-26T16:00:00.000Z\");\nconst HOUR = 3600;\nconst at = (seconds: number) => new Date(T0.valueOf() + seconds * 1000);\nconst ADMIN: Actor = { id: null, label: \"Eric (admin)\" };\nconst START = at(-HOUR);\n\n/** A discount of 20 % for three months on a monthly plan. */\nexport const discount = (over: Partial<NewCoupon> = {}): NewCoupon => ({\n code: `PROMO${randomUUID().replace(/-/g, \"\").slice(0, 8)}`,\n kind: \"discount\",\n purpose: \"promo\",\n startsAt: START,\n percentOffBp: 2000,\n duration: \"repeating\",\n durationMonths: 3,\n ...over,\n});\n\n/** 20 free audits, at most 5 a day, valid 30 days. */\nexport const credit = (over: Partial<NewCoupon> = {}): NewCoupon => ({\n code: `DEMO${randomUUID().replace(/-/g, \"\").slice(0, 8)}`,\n kind: \"credit\",\n purpose: \"demo\",\n startsAt: START,\n credits: 20,\n capPerDay: 5,\n creditsValidDays: 30,\n onExhausted: \"stop\",\n ...over,\n});\n\nasync function code(work: Promise<unknown>): Promise<string> {\n try {\n await work;\n } catch (error) {\n return (error as { code: string }).code;\n }\n return \"answered\";\n}\n\n/**\n * What every coupons store must do, behind the same module: the window and the refusals, the counting rule of the uses, reservation,\n * confirmation and release, refunds, credit coupons, partners, the admin. `make` gives an EMPTY module, the clock at {@link T0},\n * `coupons.enabled` on.\n */\nexport function couponsContract(name: string, make: () => Promise<CouponsHarness>) {\n const ctx = (accountId: string, over: Record<string, unknown> = {}) => ({ mode: \"checkout\" as const, accountId, plan: \"pro\", amountMinor: 10_000, currency: \"cad\", ...over });\n const apply = (h: CouponsHarness, codeTyped: string, accountId: string, over: Record<string, unknown> = {}) =>\n h.coupons.apply(h.tx, { code: codeTyped, accountId, plan: \"pro\", amountMinor: 10_000, currency: \"cad\", idempotencyKey: randomUUID(), ...over });\n const made = async (h: CouponsHarness, over: Partial<NewCoupon> = {}) => h.coupons.admin.create(h.tx, ADMIN, discount({ currency: \"cad\", ...over }));\n const paid = (redemptionId: string, over: Record<string, unknown> = {}) => ({ redemptionId, paymentRef: `pi_${randomUUID().slice(0, 8)}`, amountPaidMinor: 8000, amountDiscountMinor: 2000, currency: \"cad\", ...over });\n\n it(`${name}: creates a coupon, reads its code without case, spaces or hyphens, refuses a taken code, and audits`, async () => {\n const h = await make();\n const c = await h.coupons.admin.create(h.tx, ADMIN, discount({ code: \"Summer-2026\" }));\n expect(c).toMatchObject({ code: \"Summer-2026\", codeKey: \"SUMMER2026\", status: \"active\", termsVersion: 1 });\n expect(await code(h.coupons.admin.create(h.tx, ADMIN, discount({ code: \"summer 2026\" })))).toBe(\"COUPON_CODE_TAKEN\");\n const a = await h.newAccount();\n for (const typed of [\" summer-2026 \", \"SUMMER 2026\", \"summer_2026\"]) expect(await h.coupons.validate(h.tx, typed, ctx(a))).toMatchObject({ ok: true, coupon: { id: c.id } });\n expect(h.audited().map((e) => e.action)).toEqual([\"coupon.created\"]);\n expect(h.audited()[0]).toMatchObject({ target: { type: \"coupon\", id: c.id }, detail: { code: \"Summer-2026\" } });\n });\n\n it(`${name}: the window opens at its start and closes at its end, to the second`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const c = await made(h, { startsAt: at(HOUR), endsAt: at(2 * HOUR) });\n const state = async (seconds: number) => {\n h.setNow(at(seconds));\n const v = await h.coupons.validate(h.tx, c.code, ctx(a));\n return v.ok ? \"ok\" : v.code;\n };\n expect(await state(HOUR - 1)).toBe(\"COUPON_NOT_STARTED\");\n expect(await state(HOUR)).toBe(\"ok\");\n expect(await state(2 * HOUR - 1)).toBe(\"ok\");\n expect(await state(2 * HOUR)).toBe(\"COUPON_EXPIRED\");\n });\n\n it(`${name}: pause, resume and archive; an archived coupon stays archived; each is audited`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const c = await made(h);\n await h.coupons.admin.pause(h.tx, ADMIN, c.id);\n expect(await h.coupons.validate(h.tx, c.code, ctx(a))).toEqual({ ok: false, code: \"COUPON_PAUSED\" });\n await h.coupons.admin.resume(h.tx, ADMIN, c.id);\n expect((await h.coupons.validate(h.tx, c.code, ctx(a))).ok).toBe(true);\n await h.coupons.admin.archive(h.tx, ADMIN, c.id);\n expect(await h.coupons.validate(h.tx, c.code, ctx(a))).toEqual({ ok: false, code: \"COUPON_PAUSED\" });\n expect(await code(h.coupons.admin.resume(h.tx, ADMIN, c.id))).toBe(\"COUPON_BAD_INPUT\");\n expect(h.audited().map((e) => e.action)).toEqual([\"coupon.created\", \"coupon.paused\", \"coupon.resumed\", \"coupon.archived\"]);\n });\n\n it(`${name}: refuses with a typed code: unknown, kind, plan, minimum, currency, new customers, provider, too large, disabled`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const refusal = async (typed: string, over: Record<string, unknown> = {}) => {\n const v = await h.coupons.validate(h.tx, typed, ctx(a, over));\n return v.ok ? \"ok\" : v.code;\n };\n const plan = await made(h, { plans: [\"pro\"], minimumMinor: 5000 });\n const fresh = await made(h, { newCustomersOnly: true });\n h.setSettings({ maxPercentOff: 70 }); // a coupon is created within the bound, then the admin lowers it\n const big = await made(h, { percentOffBp: 6000 });\n h.setSettings({ maxPercentOff: 50 });\n const fixed = await made(h, { percentOffBp: null, amountOffMinor: 6000, currency: \"cad\", duration: \"once\", durationMonths: null });\n const cr = await h.coupons.admin.create(h.tx, ADMIN, credit());\n expect(await refusal(\"NOPE\")).toBe(\"COUPON_UNKNOWN\");\n expect(await refusal(\"???\")).toBe(\"COUPON_UNKNOWN\");\n expect(await refusal(plan.code, { plan: \"audit\" })).toBe(\"COUPON_NOT_FOR_PLAN\");\n expect(await refusal(plan.code, { amountMinor: 4999 })).toBe(\"COUPON_MINIMUM\");\n expect(await refusal(plan.code, { currency: \"usd\" })).toBe(\"COUPON_CURRENCY\");\n expect(await refusal(plan.code)).toBe(\"ok\");\n expect(await refusal(fresh.code, { hasPaidBefore: true })).toBe(\"COUPON_NEW_CUSTOMERS_ONLY\");\n expect(await refusal(fresh.code, { hasPaidBefore: false })).toBe(\"ok\");\n expect(await refusal(plan.code, { providerSupportsDiscounts: false })).toBe(\"COUPON_PROVIDER\");\n expect(await refusal(big.code)).toBe(\"COUPON_TOO_LARGE\");\n expect(await refusal(fixed.code)).toBe(\"COUPON_TOO_LARGE\");\n expect(await refusal(fixed.code, { amountMinor: 12_000 })).toBe(\"ok\");\n expect(await refusal(cr.code)).toBe(\"COUPON_KIND\");\n h.setSettings({ maxPercentOff: 70 });\n expect(await refusal(big.code)).toBe(\"ok\");\n h.setSettings({ enabled: false });\n expect(await refusal(plan.code)).toBe(\"COUPON_DISABLED\");\n });\n\n it(`${name}: counts an attempt before any lookup, and refuses past the limit with the time to wait`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const c = await made(h);\n await h.coupons.validate(h.tx, c.code, ctx(a, { ip: \"203.0.113.9\" }));\n await h.coupons.validate(h.tx, \"WHATEVER\", ctx(a));\n expect(h.limiter.attempts).toEqual([\n { action: \"couponAttempt\", ip: \"203.0.113.9\", subject: a },\n { action: \"couponAttempt\", ip: undefined, subject: a },\n ]);\n h.limiter.refuse(42);\n expect(await h.coupons.validate(h.tx, c.code, ctx(a))).toEqual({ ok: false, code: \"COUPON_RATE_LIMITED\", retryAfterSeconds: 42 });\n await expect(apply(h, c.code, a)).rejects.toMatchObject({ code: \"COUPON_RATE_LIMITED\", status: 429, retryAfterSeconds: 42 });\n h.limiter.refuse(null);\n expect((await h.coupons.validate(h.tx, c.code, ctx(a))).ok).toBe(true);\n });\n\n it(`${name}: says what a discount gives in numbers (the provider computes the real amount and its taxes)`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const pct = await made(h);\n const fixed = await made(h, { percentOffBp: null, amountOffMinor: 500, currency: \"cad\", duration: \"once\", durationMonths: null });\n expect(await h.coupons.validate(h.tx, pct.code, ctx(a))).toMatchObject({ ok: true, effect: { kind: \"discount\", percentOffBp: 2000, duration: \"repeating\", durationMonths: 3, discountMinor: 2000, payableMinor: 8000 } });\n expect(await h.coupons.validate(h.tx, fixed.code, ctx(a, { amountMinor: 3900 }))).toMatchObject({ effect: { discountMinor: 500, payableMinor: 3400, amountOffMinor: 500 } });\n });\n\n it(`${name}: reserves a use under the counting rule: reserved uses count until their deadline, released ones never`, async () => {\n const h = await make();\n const [a1, a2, a3] = [await h.newAccount(), await h.newAccount(), await h.newAccount()];\n const c = await made(h, { maxRedemptions: 2 });\n const r1 = await apply(h, c.code, a1);\n expect(await code(apply(h, c.code, a1))).toBe(\"COUPON_ACCOUNT_LIMIT\");\n await apply(h, c.code, a2);\n expect(await code(apply(h, c.code, a3))).toBe(\"COUPON_EXHAUSTED\");\n expect(await h.coupons.validate(h.tx, c.code, ctx(a3))).toEqual({ ok: false, code: \"COUPON_EXHAUSTED\" });\n expect(await h.coupons.release(h.tx, { redemptionId: r1.redemptionId })).toBe(true);\n expect((await apply(h, c.code, a3)).replayed).toBe(false);\n h.setNow(at(61 * 60)); // the two reservations still held lapse after 60 minutes\n expect((await h.coupons.validate(h.tx, c.code, ctx(a1))).ok).toBe(true);\n });\n\n it(`${name}: apply is idempotent by key, and a key is not shared between coupons or accounts`, async () => {\n const h = await make();\n const [a1, a2] = [await h.newAccount(), await h.newAccount()];\n const c = await made(h);\n const other = await made(h);\n const first = await apply(h, c.code, a1, { idempotencyKey: \"co-1\" });\n const again = await apply(h, c.code, a1, { idempotencyKey: \"co-1\" });\n expect(first.replayed).toBe(false);\n expect(again).toMatchObject({ redemptionId: first.redemptionId, replayed: true });\n expect(await code(apply(h, c.code, a2, { idempotencyKey: \"co-1\" }))).toBe(\"COUPON_KEY_REUSED\");\n expect(await code(apply(h, other.code, a1, { idempotencyKey: \"co-1\" }))).toBe(\"COUPON_KEY_REUSED\");\n expect(first.discount).toEqual({ couponId: c.id, termsVersion: 1, percentOffBp: 2000, duration: \"repeating\", durationMonths: 3 });\n expect(first.effect).toMatchObject({ discountMinor: 2000, payableMinor: 8000 });\n expect(first.reservedUntil).toEqual(at(HOUR));\n });\n\n it(`${name}: hands the provider terms for an amount-off coupon with its currency`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const c = await made(h, { percentOffBp: null, amountOffMinor: 500, currency: \"cad\", duration: \"once\", durationMonths: null });\n expect((await apply(h, c.code, a)).discount).toEqual({ couponId: c.id, termsVersion: 1, amountOffMinor: 500, currency: \"cad\", duration: \"once\" });\n });\n\n it(`${name}: confirms a use once, from the checkout's metadata or from the session when the attach never ran`, async () => {\n const h = await make();\n const [a1, a2, a3] = [await h.newAccount(), await h.newAccount(), await h.newAccount()];\n const c = await made(h, { maxRedemptions: 5 });\n const r1 = await apply(h, c.code, a1);\n const p1 = paid(r1.redemptionId, { paymentRef: \"pi_1\" });\n expect(await h.coupons.redeem(h.tx, p1)).toBe(\"confirmed\");\n expect(await h.coupons.redeem(h.tx, p1)).toBe(\"already\");\n const r2 = await apply(h, c.code, a2);\n await h.coupons.attachCheckout(h.tx, r2.redemptionId, \"cs_2\", at(2 * HOUR));\n expect(await h.coupons.redeem(h.tx, { checkoutRef: \"cs_2\", paymentRef: \"pi_2\", amountPaidMinor: 8000, amountDiscountMinor: 2000, currency: \"cad\" })).toBe(\"confirmed\");\n const r3 = await apply(h, c.code, a3);\n expect(await h.coupons.redeem(h.tx, paid(r3.redemptionId, { paymentRef: \"pi_1\" }))).toBe(\"already\");\n expect(await h.coupons.redeem(h.tx, { redemptionId: randomUUID(), checkoutRef: \"cs_none\", paymentRef: \"pi_x\", amountPaidMinor: 1, amountDiscountMinor: 0, currency: \"cad\" })).toBe(\"unknown\");\n const [row] = (await h.coupons.admin.redemptions(h.tx, { couponId: c.id, status: \"confirmed\" })).filter((r) => r.id === r1.redemptionId);\n expect(row).toMatchObject({ status: \"confirmed\", paymentRef: \"pi_1\", amountPaidMinor: 8000, amountDiscountMinor: 2000, overLimit: false, accountId: a1 });\n });\n\n it(`${name}: attaching the session makes the reservation outlive it`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const c = await made(h);\n const r = await apply(h, c.code, a);\n await h.coupons.attachCheckout(h.tx, r.redemptionId, \"cs_9\", at(3 * HOUR));\n h.setNow(at(3 * HOUR + 4 * 60));\n expect(await h.coupons.release(h.tx, { checkoutRef: \"cs_9\" })).toBe(true);\n expect(await code(h.coupons.attachCheckout(h.tx, randomUUID(), \"cs_x\"))).toBe(\"COUPON_REDEMPTION_NOT_FOUND\");\n });\n\n it(`${name}: a payment after the release is honoured and flagged: released becomes confirmed with over_limit`, async () => {\n const h = await make();\n const [a1, a2] = [await h.newAccount(), await h.newAccount()];\n const c = await made(h, { maxRedemptions: 1 });\n const r1 = await apply(h, c.code, a1);\n await h.coupons.release(h.tx, { redemptionId: r1.redemptionId });\n const r2 = await apply(h, c.code, a2);\n await h.coupons.redeem(h.tx, paid(r2.redemptionId));\n expect(await h.coupons.redeem(h.tx, paid(r1.redemptionId))).toBe(\"confirmed\");\n const late = (await h.coupons.admin.redemptions(h.tx, { couponId: c.id })).find((r) => r.id === r1.redemptionId);\n expect(late).toMatchObject({ status: \"confirmed\", overLimit: true });\n expect(await h.coupons.validate(h.tx, c.code, ctx(await h.newAccount()))).toEqual({ ok: false, code: \"COUPON_EXHAUSTED\" });\n });\n\n it(`${name}: the sweeper releases reservations past their deadline, and a released one is not released twice`, async () => {\n const h = await make();\n const [a1, a2] = [await h.newAccount(), await h.newAccount()];\n const c = await made(h);\n const r1 = await apply(h, c.code, a1);\n await apply(h, c.code, a2);\n expect(await h.coupons.releaseExpired(h.tx)).toBe(0);\n h.setNow(at(HOUR));\n expect(await h.coupons.releaseExpired(h.tx)).toBe(2);\n expect(await h.coupons.releaseExpired(h.tx)).toBe(0);\n expect(await h.coupons.release(h.tx, { redemptionId: r1.redemptionId })).toBe(false);\n expect(await h.coupons.release(h.tx, { redemptionId: randomUUID() })).toBe(false);\n expect(h.announced().filter((m) => m.type === \"evt.coupon.released.v1\")).toHaveLength(2);\n });\n\n it(`${name}: a refund adds up and never changes the status; a partial one keeps the use, a full one gives it back only if the setting says so`, async () => {\n const h = await make();\n const [a1, a2] = [await h.newAccount(), await h.newAccount()];\n const c = await made(h, { maxRedemptions: 1 });\n const r1 = await apply(h, c.code, a1);\n await h.coupons.redeem(h.tx, paid(r1.redemptionId, { paymentRef: \"pi_a\", amountPaidMinor: 8000 }));\n expect(await h.coupons.refund(h.tx, { paymentRef: \"pi_a\", amountRefunded: 2000 })).toBe(\"refunded\");\n expect(await h.coupons.refund(h.tx, { paymentRef: \"pi_a\", amountRefunded: 2000 })).toBe(\"already\");\n expect(await h.coupons.refund(h.tx, { paymentRef: \"pi_a\", amountRefunded: 1000 })).toBe(\"already\");\n expect(await h.coupons.refund(h.tx, { paymentRef: \"pi_zz\", amountRefunded: 1 })).toBe(\"unknown\");\n expect(await h.coupons.refund(h.tx, { paymentRef: null, invoiceRef: null, amountRefunded: 1 })).toBe(\"unknown\");\n expect(await h.coupons.refund(h.tx, { paymentRef: \"pi_a\", amountRefunded: 8000 })).toBe(\"refunded\");\n const row = (await h.coupons.admin.redemptions(h.tx, { couponId: c.id }))[0]!;\n expect(row).toMatchObject({ status: \"confirmed\", amountRefundedMinor: 8000, useRestored: false });\n expect(await code(apply(h, c.code, a2))).toBe(\"COUPON_EXHAUSTED\");\n\n const b = await made(h, { maxRedemptions: 1 });\n h.setSettings({ refundRestoresUse: true });\n const rb = await apply(h, b.code, a1);\n await h.coupons.redeem(h.tx, paid(rb.redemptionId, { paymentRef: \"pi_b\", amountPaidMinor: 8000 }));\n await h.coupons.refund(h.tx, { paymentRef: \"pi_b\", amountRefunded: 7999 });\n expect(await code(apply(h, b.code, a2))).toBe(\"COUPON_EXHAUSTED\");\n await h.coupons.refund(h.tx, { invoiceRef: \"pi_b\", amountRefunded: 8000 });\n expect((await apply(h, b.code, a2)).replayed).toBe(false);\n expect(h.announced().filter((m) => m.type === \"evt.coupon.refunded.v1\").map((m) => m.payload.useRestored)).toEqual([false, false, false, true]);\n });\n\n it(`${name}: the terms are locked after the first use; the rules stay editable; a change is audited with before and after`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const c = await made(h);\n const before = await h.coupons.admin.update(h.tx, ADMIN, c.id, { percentOffBp: 1500 });\n expect(before.percentOffBp).toBe(1500);\n await apply(h, c.code, a);\n expect(await code(h.coupons.admin.update(h.tx, ADMIN, c.id, { percentOffBp: 3000 }))).toBe(\"COUPON_TERMS_LOCKED\");\n const after = await h.coupons.admin.update(h.tx, ADMIN, c.id, { maxRedemptions: 10, endsAt: at(5 * HOUR) });\n expect(after).toMatchObject({ maxRedemptions: 10, percentOffBp: 1500 });\n expect(await h.coupons.admin.update(h.tx, ADMIN, c.id, { maxRedemptions: 10 })).toMatchObject({ maxRedemptions: 10 });\n expect(await code(h.coupons.admin.update(h.tx, ADMIN, c.id, { endsAt: START }))).toBe(\"COUPON_BAD_INPUT\");\n const entry = h.audited().filter((e) => e.action === \"coupon.updated\").at(-1)!;\n expect(entry.detail).toMatchObject({ changed: [\"maxRedemptions\", \"endsAt\"], before: { maxRedemptions: null, endsAt: null }, after: { maxRedemptions: 10 } });\n expect(await code(h.coupons.admin.update(h.tx, ADMIN, randomUUID(), { maxRedemptions: 1 }))).toBe(\"COUPON_NOT_FOUND\");\n });\n\n it(`${name}: the admin cannot create what the rules refuse: a free discount, a fixed amount forever, a credit coupon without a cap of its own`, async () => {\n const h = await make();\n const bad = (over: Partial<NewCoupon>) => code(h.coupons.admin.create(h.tx, ADMIN, discount({ currency: \"cad\", ...over })));\n expect(await bad({ percentOffBp: 10_000 })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ percentOffBp: 5001 })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ percentOffBp: null, amountOffMinor: 500, duration: \"forever\", durationMonths: null })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ percentOffBp: null, amountOffMinor: null })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ amountOffMinor: 5 })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ duration: \"once\" })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ code: \"ABC\" })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ code: \"bad!code123\" })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ endsAt: START })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ credits: 5 })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ purpose: \"partner\" })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ partnerId: randomUUID() })).toBe(\"COUPON_BAD_INPUT\");\n expect(await bad({ purpose: \"partner\", partnerId: randomUUID() })).toBe(\"COUPON_PARTNER_NOT_FOUND\");\n const credits = (over: Partial<NewCoupon>) => code(h.coupons.admin.create(h.tx, ADMIN, credit(over)));\n expect(await credits({ credits: null })).toBe(\"COUPON_BAD_INPUT\");\n expect(await credits({ creditsValidDays: null })).toBe(\"COUPON_BAD_INPUT\");\n expect(await credits({ onExhausted: null })).toBe(\"COUPON_BAD_INPUT\");\n expect(await credits({ percentOffBp: 1000 })).toBe(\"COUPON_BAD_INPUT\");\n expect(await credits({ capPerDay: 0 })).toBe(\"COUPON_BAD_INPUT\");\n expect(await credits({})).toBe(\"answered\");\n });\n\n it(`${name}: a credit coupon grants capped credits that expire, once per key, and the use counts`, async () => {\n const h = await make();\n const [a, b] = [await h.newAccount(), await h.newAccount()];\n const c = await h.coupons.admin.create(h.tx, ADMIN, credit({ capPerMonth: 12, maxRedemptions: 1, onExhausted: \"fallback_paid\" }));\n expect(await h.coupons.validate(h.tx, c.code, { mode: \"redeem\", accountId: a })).toMatchObject({ ok: true, effect: { kind: \"credit\", credits: 20, capPerDay: 5, capPerMonth: 12, validDays: 30, onExhausted: \"fallback_paid\" } });\n const got = await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: \"rc-1\" });\n expect(got).toMatchObject({ credits: 20, replayed: false });\n expect(got.expiresAt).toEqual(at(30 * 24 * HOUR));\n expect(h.credits.granted).toEqual([{ accountId: a, amount: 20, capPerDay: 5, capPerMonth: 12, expiresAt: at(30 * 24 * HOUR), onExhausted: \"fallback_paid\", source: `coupon:${got.redemptionId}`, idempotencyKey: `coupon-${got.redemptionId}` }]);\n expect((await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: \"rc-1\" })).replayed).toBe(true);\n expect(h.credits.granted).toHaveLength(1);\n expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: \"rc-1\" }))).toBe(\"COUPON_KEY_REUSED\");\n expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: \"rc-2\" }))).toBe(\"COUPON_EXHAUSTED\");\n const kind = await made(h);\n expect(await code(h.coupons.redeemCredit(h.tx, { code: kind.code, accountId: b, idempotencyKey: \"rc-3\" }))).toBe(\"COUPON_KIND\");\n expect(await code(apply(h, c.code, b))).toBe(\"COUPON_KIND\");\n });\n\n it(`${name}: revoking a credit redemption writes off what is left, keeps the use counted, and refuses what is not a credit`, async () => {\n const h = await make();\n const [a, b] = [await h.newAccount(), await h.newAccount()];\n const c = await h.coupons.admin.create(h.tx, ADMIN, credit({ maxRedemptions: 1 }));\n const got = await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: \"rc-1\" });\n await h.coupons.revoke(h.tx, ADMIN, got.redemptionId);\n await h.coupons.revoke(h.tx, ADMIN, got.redemptionId);\n expect(h.credits.expired).toEqual([got.grantId]);\n expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: \"rc-2\" }))).toBe(\"COUPON_EXHAUSTED\");\n const d = await made(h);\n const r = await apply(h, d.code, a);\n expect(await code(h.coupons.revoke(h.tx, ADMIN, r.redemptionId))).toBe(\"COUPON_NOT_REVOCABLE\");\n expect(await code(h.coupons.revoke(h.tx, ADMIN, randomUUID()))).toBe(\"COUPON_REDEMPTION_NOT_FOUND\");\n expect(h.audited().filter((e) => e.action === \"coupon.revoked\")).toHaveLength(1);\n });\n\n it(`${name}: announces the redemption with the partner and the amounts, and the last allowed use, once each`, async () => {\n const h = await make();\n const [a1, a2] = [await h.newAccount(), await h.newAccount()];\n const partner = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: \"Marie Tremblay\", contactEmail: \"Marie@Example.com\" });\n const c = await made(h, { purpose: \"partner\", partnerId: partner.id, maxRedemptions: 2 });\n for (const [i, a] of [a1, a2].entries()) {\n const r = await apply(h, c.code, a);\n await h.coupons.redeem(h.tx, paid(r.redemptionId, { paymentRef: `pi_${i}` }));\n }\n const redeemed = h.announced().filter((m) => m.type === \"evt.coupon.redeemed.v1\");\n expect(redeemed).toHaveLength(2);\n expect(redeemed[0]!.payload).toMatchObject({ couponId: c.id, kind: \"discount\", purpose: \"partner\", partnerId: partner.id, amountPaidMinor: 8000, amountDiscountMinor: 2000, currency: \"cad\", credits: null, overLimit: false });\n expect(h.announced().filter((m) => m.type === \"evt.coupon.exhausted.v1\")).toHaveLength(1);\n });\n\n it(`${name}: partners: the view is net of refunds and never names a customer; erasing keeps the numbers and blanks the person`, async () => {\n const h = await make();\n const [a1, a2] = [await h.newAccount(), await h.newAccount()];\n const partner = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: \"Marie Tremblay\", contactEmail: \"marie@example.com\" });\n const other = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: \"Paul\" });\n const c = await made(h, { purpose: \"partner\", partnerId: partner.id });\n const d = await h.coupons.admin.create(h.tx, ADMIN, credit({ purpose: \"partner\", partnerId: partner.id }));\n const r1 = await apply(h, c.code, a1);\n await h.coupons.redeem(h.tx, paid(r1.redemptionId, { paymentRef: \"pi_p1\", amountPaidMinor: 10_000, amountDiscountMinor: 2000 }));\n await h.coupons.refund(h.tx, { paymentRef: \"pi_p1\", amountRefunded: 1000 });\n await h.coupons.redeemCredit(h.tx, { code: d.code, accountId: a2, idempotencyKey: \"rc-p\" });\n const report = await h.coupons.admin.partners.report(h.tx, ADMIN, partner.id);\n expect(report).toEqual(expect.arrayContaining([{ couponId: c.id, code: c.code, currency: \"cad\", redemptions: 1, discountMinor: 2000, revenueMinor: 9000, creditsGranted: 0 }, { couponId: d.id, code: d.code, currency: null, redemptions: 1, discountMinor: 0, revenueMinor: 0, creditsGranted: 20 }]));\n expect(JSON.stringify(report)).not.toContain(a1);\n expect(await h.coupons.admin.partners.report(h.tx, ADMIN, other.id)).toEqual([]);\n expect(await h.coupons.admin.partners.report(h.tx, ADMIN, partner.id, { from: at(HOUR), to: at(2 * HOUR) })).toEqual([]);\n expect(h.audited().filter((e) => e.action === \"partner.report_read\")).toHaveLength(3);\n expect(await code(h.coupons.admin.partners.report(h.tx, ADMIN, randomUUID()))).toBe(\"COUPON_PARTNER_NOT_FOUND\");\n\n const exported = await h.coupons.admin.partners.export(h.tx, partner.id);\n expect(exported.partner).toMatchObject({ displayName: \"Marie Tremblay\", contactEmail: \"marie@example.com\" });\n const erased = await h.coupons.admin.partners.erase(h.tx, ADMIN, partner.id);\n expect(erased).toMatchObject({ displayName: null, contactEmail: null, status: \"archived\" });\n expect(erased.erasedAt).not.toBeNull();\n expect((await h.coupons.admin.partners.erase(h.tx, ADMIN, partner.id)).erasedAt).toEqual(erased.erasedAt);\n expect((await h.coupons.admin.partners.export(h.tx, partner.id)).rows).toHaveLength(2);\n expect((await h.coupons.admin.partners.list(h.tx)).map((p) => p.id)).toEqual([other.id]);\n expect((await h.coupons.admin.partners.list(h.tx, { includeArchived: true })).map((p) => p.id)).toEqual(expect.arrayContaining([partner.id, other.id]));\n });\n\n it(`${name}: partners: a name and a well-formed e-mail, archive and restore`, async () => {\n const h = await make();\n expect(await code(h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: \"\" }))).toBe(\"COUPON_BAD_INPUT\");\n expect(await code(h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: \"x\".repeat(81) }))).toBe(\"COUPON_BAD_INPUT\");\n expect(await code(h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: \"Marie\", contactEmail: \"nope\" }))).toBe(\"COUPON_BAD_INPUT\");\n const p = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: \" Marie \" });\n expect(p).toMatchObject({ displayName: \"Marie\", contactEmail: null, status: \"active\" });\n expect((await h.coupons.admin.partners.archive(h.tx, ADMIN, p.id)).status).toBe(\"archived\");\n expect((await h.coupons.admin.partners.restore(h.tx, ADMIN, p.id)).status).toBe(\"active\");\n expect(await code(h.coupons.admin.partners.archive(h.tx, ADMIN, randomUUID()))).toBe(\"COUPON_PARTNER_NOT_FOUND\");\n expect(await h.coupons.admin.partners.get(h.tx, randomUUID())).toBeNull();\n });\n\n it(`${name}: a price drop pauses the fixed-amount coupons that would now be too large, and only those`, async () => {\n const h = await make();\n const small = await made(h, { percentOffBp: null, amountOffMinor: 1000, currency: \"cad\", duration: \"once\", durationMonths: null, plans: [\"pro\"] });\n const other = await made(h, { percentOffBp: null, amountOffMinor: 1000, currency: \"cad\", duration: \"once\", durationMonths: null, plans: [\"audit\"] });\n const percent = await made(h);\n expect(await h.coupons.admin.checkPrices(h.tx, [{ plan: \"pro\", amountMinor: 2000, currency: \"cad\" }])).toEqual([]);\n expect(await h.coupons.admin.checkPrices(h.tx, [{ plan: \"pro\", amountMinor: 1999, currency: \"cad\" }, { plan: \"pro\", amountMinor: 100, currency: \"usd\" }])).toEqual([small.id]);\n expect((await h.coupons.admin.get(h.tx, small.id))?.status).toBe(\"paused\");\n expect((await h.coupons.admin.get(h.tx, other.id))?.status).toBe(\"active\");\n expect((await h.coupons.admin.get(h.tx, percent.id))?.status).toBe(\"active\");\n expect(h.audited().filter((e) => e.action === \"coupon.paused_by_price\")).toHaveLength(1);\n });\n\n it(`${name}: lists coupons and redemptions with filters and pages`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const d = await made(h);\n const c = await h.coupons.admin.create(h.tx, ADMIN, credit());\n await h.coupons.admin.pause(h.tx, ADMIN, d.id);\n expect((await h.coupons.admin.list(h.tx)).map((x) => x.id)).toEqual(expect.arrayContaining([d.id, c.id]));\n expect((await h.coupons.admin.list(h.tx, { kind: \"credit\" })).map((x) => x.id)).toEqual([c.id]);\n expect((await h.coupons.admin.list(h.tx, { status: \"paused\" })).map((x) => x.id)).toEqual([d.id]);\n expect((await h.coupons.admin.list(h.tx, { purpose: \"demo\" })).map((x) => x.id)).toEqual([c.id]);\n expect(await h.coupons.admin.list(h.tx, { limit: 1, offset: 5 })).toEqual([]);\n expect(await h.coupons.admin.get(h.tx, randomUUID())).toBeNull();\n await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: \"rc-l\" });\n expect(await h.coupons.admin.redemptions(h.tx, { couponId: c.id })).toHaveLength(1);\n expect(await h.coupons.admin.redemptions(h.tx, { status: \"released\" })).toEqual([]);\n expect(await h.coupons.admin.redemptions(h.tx, { from: at(HOUR) })).toEqual([]);\n expect(await h.coupons.admin.redemptions(h.tx, { to: at(-5 * HOUR) })).toEqual([]);\n });\n\n it(`${name}: generates a free code of the configured length from an alphabet without I, L, O and U`, async () => {\n const h = await make();\n h.setSettings({ generatedCodeLength: 12 });\n const generated = await h.coupons.admin.generateCode(h.tx);\n expect(generated).toMatch(/^[0-9A-HJKMNP-TV-Z]{12}$/);\n const c = await h.coupons.admin.create(h.tx, ADMIN, discount({ code: generated }));\n expect(c.codeKey).toBe(generated);\n });\n}\n\nexport { COUPON_DEFAULTS };\n"],"mappings":"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","names":[]}