@krizaka/coupons 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testkit.js CHANGED
@@ -1,7 +1,7 @@
1
- import "./chunk-ECCGFGSV.js";
1
+ import "./chunk-V2HCEF26.js";
2
2
  import {
3
3
  COUPON_DEFAULTS
4
- } from "./chunk-JMQO6PTK.js";
4
+ } from "./chunk-XXFDAM5G.js";
5
5
 
6
6
  // src/testkit.ts
7
7
  import { randomUUID } from "crypto";
@@ -41,17 +41,42 @@ async function code(work) {
41
41
  return "answered";
42
42
  }
43
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 });
44
+ const ctx = (accountId, over = {}) => ({
45
+ mode: "checkout",
46
+ accountId,
47
+ plan: "pro",
48
+ amountMinor: 1e4,
49
+ currency: "cad",
50
+ ...over
51
+ });
52
+ const apply = (h, codeTyped, accountId, over = {}) => h.coupons.apply(h.tx, {
53
+ code: codeTyped,
54
+ accountId,
55
+ plan: "pro",
56
+ amountMinor: 1e4,
57
+ currency: "cad",
58
+ idempotencyKey: randomUUID(),
59
+ ...over
60
+ });
46
61
  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 });
62
+ const paid = (redemptionId, over = {}) => ({
63
+ redemptionId,
64
+ paymentRef: `pi_${randomUUID().slice(0, 8)}`,
65
+ amountPaidMinor: 8e3,
66
+ amountDiscountMinor: 2e3,
67
+ currency: "cad",
68
+ ...over
69
+ });
48
70
  it(`${name}: creates a coupon, reads its code without case, spaces or hyphens, refuses a taken code, and audits`, async () => {
49
71
  const h = await make();
50
72
  const c = await h.coupons.admin.create(h.tx, ADMIN, discount({ code: "Summer-2026" }));
51
73
  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");
74
+ expect(await code(h.coupons.admin.create(h.tx, ADMIN, discount({ code: "summer 2026" })))).toBe(
75
+ "COUPON_CODE_TAKEN"
76
+ );
53
77
  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 } });
78
+ for (const typed of [" summer-2026 ", "SUMMER 2026", "summer_2026"])
79
+ expect(await h.coupons.validate(h.tx, typed, ctx(a))).toMatchObject({ ok: true, coupon: { id: c.id } });
55
80
  expect(h.audited().map((e) => e.action)).toEqual(["coupon.created"]);
56
81
  expect(h.audited()[0]).toMatchObject({ target: { type: "coupon", id: c.id }, detail: { code: "Summer-2026" } });
57
82
  });
@@ -80,7 +105,12 @@ function couponsContract(name, make) {
80
105
  await h.coupons.admin.archive(h.tx, ADMIN, c.id);
81
106
  expect(await h.coupons.validate(h.tx, c.code, ctx(a))).toEqual({ ok: false, code: "COUPON_PAUSED" });
82
107
  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"]);
108
+ expect(h.audited().map((e) => e.action)).toEqual([
109
+ "coupon.created",
110
+ "coupon.paused",
111
+ "coupon.resumed",
112
+ "coupon.archived"
113
+ ]);
84
114
  });
85
115
  it(`${name}: refuses with a typed code: unknown, kind, plan, minimum, currency, new customers, provider, too large, disabled`, async () => {
86
116
  const h = await make();
@@ -94,7 +124,13 @@ function couponsContract(name, make) {
94
124
  h.setSettings({ maxPercentOff: 70 });
95
125
  const big = await made(h, { percentOffBp: 6e3 });
96
126
  h.setSettings({ maxPercentOff: 50 });
97
- const fixed = await made(h, { percentOffBp: null, amountOffMinor: 6e3, currency: "cad", duration: "once", durationMonths: null });
127
+ const fixed = await made(h, {
128
+ percentOffBp: null,
129
+ amountOffMinor: 6e3,
130
+ currency: "cad",
131
+ duration: "once",
132
+ durationMonths: null
133
+ });
98
134
  const cr = await h.coupons.admin.create(h.tx, ADMIN, credit());
99
135
  expect(await refusal("NOPE")).toBe("COUPON_UNKNOWN");
100
136
  expect(await refusal("???")).toBe("COUPON_UNKNOWN");
@@ -125,8 +161,16 @@ function couponsContract(name, make) {
125
161
  { action: "couponAttempt", ip: void 0, subject: a }
126
162
  ]);
127
163
  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 });
164
+ expect(await h.coupons.validate(h.tx, c.code, ctx(a))).toEqual({
165
+ ok: false,
166
+ code: "COUPON_RATE_LIMITED",
167
+ retryAfterSeconds: 42
168
+ });
169
+ await expect(apply(h, c.code, a)).rejects.toMatchObject({
170
+ code: "COUPON_RATE_LIMITED",
171
+ status: 429,
172
+ retryAfterSeconds: 42
173
+ });
130
174
  h.limiter.refuse(null);
131
175
  expect((await h.coupons.validate(h.tx, c.code, ctx(a))).ok).toBe(true);
132
176
  });
@@ -134,9 +178,27 @@ function couponsContract(name, make) {
134
178
  const h = await make();
135
179
  const a = await h.newAccount();
136
180
  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 } });
181
+ const fixed = await made(h, {
182
+ percentOffBp: null,
183
+ amountOffMinor: 500,
184
+ currency: "cad",
185
+ duration: "once",
186
+ durationMonths: null
187
+ });
188
+ expect(await h.coupons.validate(h.tx, pct.code, ctx(a))).toMatchObject({
189
+ ok: true,
190
+ effect: {
191
+ kind: "discount",
192
+ percentOffBp: 2e3,
193
+ duration: "repeating",
194
+ durationMonths: 3,
195
+ discountMinor: 2e3,
196
+ payableMinor: 8e3
197
+ }
198
+ });
199
+ expect(await h.coupons.validate(h.tx, fixed.code, ctx(a, { amountMinor: 3900 }))).toMatchObject({
200
+ effect: { discountMinor: 500, payableMinor: 3400, amountOffMinor: 500 }
201
+ });
140
202
  });
141
203
  it(`${name}: reserves a use under the counting rule: reserved uses count until their deadline, released ones never`, async () => {
142
204
  const h = await make();
@@ -163,15 +225,33 @@ function couponsContract(name, make) {
163
225
  expect(again).toMatchObject({ redemptionId: first.redemptionId, replayed: true });
164
226
  expect(await code(apply(h, c.code, a2, { idempotencyKey: "co-1" }))).toBe("COUPON_KEY_REUSED");
165
227
  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 });
228
+ expect(first.discount).toEqual({
229
+ couponId: c.id,
230
+ termsVersion: 1,
231
+ percentOffBp: 2e3,
232
+ duration: "repeating",
233
+ durationMonths: 3
234
+ });
167
235
  expect(first.effect).toMatchObject({ discountMinor: 2e3, payableMinor: 8e3 });
168
236
  expect(first.reservedUntil).toEqual(at(HOUR));
169
237
  });
170
238
  it(`${name}: hands the provider terms for an amount-off coupon with its currency`, async () => {
171
239
  const h = await make();
172
240
  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" });
241
+ const c = await made(h, {
242
+ percentOffBp: null,
243
+ amountOffMinor: 500,
244
+ currency: "cad",
245
+ duration: "once",
246
+ durationMonths: null
247
+ });
248
+ expect((await apply(h, c.code, a)).discount).toEqual({
249
+ couponId: c.id,
250
+ termsVersion: 1,
251
+ amountOffMinor: 500,
252
+ currency: "cad",
253
+ duration: "once"
254
+ });
175
255
  });
176
256
  it(`${name}: confirms a use once, from the checkout's metadata or from the session when the attach never ran`, async () => {
177
257
  const h = await make();
@@ -183,12 +263,38 @@ function couponsContract(name, make) {
183
263
  expect(await h.coupons.redeem(h.tx, p1)).toBe("already");
184
264
  const r2 = await apply(h, c.code, a2);
185
265
  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");
266
+ expect(
267
+ await h.coupons.redeem(h.tx, {
268
+ checkoutRef: "cs_2",
269
+ paymentRef: "pi_2",
270
+ amountPaidMinor: 8e3,
271
+ amountDiscountMinor: 2e3,
272
+ currency: "cad"
273
+ })
274
+ ).toBe("confirmed");
187
275
  const r3 = await apply(h, c.code, a3);
188
276
  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 });
277
+ expect(
278
+ await h.coupons.redeem(h.tx, {
279
+ redemptionId: randomUUID(),
280
+ checkoutRef: "cs_none",
281
+ paymentRef: "pi_x",
282
+ amountPaidMinor: 1,
283
+ amountDiscountMinor: 0,
284
+ currency: "cad"
285
+ })
286
+ ).toBe("unknown");
287
+ const [row] = (await h.coupons.admin.redemptions(h.tx, { couponId: c.id, status: "confirmed" })).filter(
288
+ (r) => r.id === r1.redemptionId
289
+ );
290
+ expect(row).toMatchObject({
291
+ status: "confirmed",
292
+ paymentRef: "pi_1",
293
+ amountPaidMinor: 8e3,
294
+ amountDiscountMinor: 2e3,
295
+ overLimit: false,
296
+ accountId: a1
297
+ });
192
298
  });
193
299
  it(`${name}: attaching the session makes the reservation outlive it`, async () => {
194
300
  const h = await make();
@@ -211,7 +317,10 @@ function couponsContract(name, make) {
211
317
  expect(await h.coupons.redeem(h.tx, paid(r1.redemptionId))).toBe("confirmed");
212
318
  const late = (await h.coupons.admin.redemptions(h.tx, { couponId: c.id })).find((r) => r.id === r1.redemptionId);
213
319
  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" });
320
+ expect(await h.coupons.validate(h.tx, c.code, ctx(await h.newAccount()))).toEqual({
321
+ ok: false,
322
+ code: "COUPON_EXHAUSTED"
323
+ });
215
324
  });
216
325
  it(`${name}: the sweeper releases reservations past their deadline, and a released one is not released twice`, async () => {
217
326
  const h = await make();
@@ -250,7 +359,9 @@ function couponsContract(name, make) {
250
359
  expect(await code(apply(h, b.code, a2))).toBe("COUPON_EXHAUSTED");
251
360
  await h.coupons.refund(h.tx, { invoiceRef: "pi_b", amountRefunded: 8e3 });
252
361
  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]);
362
+ expect(
363
+ h.announced().filter((m) => m.type === "evt.coupon.refunded.v1").map((m) => m.payload.useRestored)
364
+ ).toEqual([false, false, false, true]);
254
365
  });
255
366
  it(`${name}: the terms are locked after the first use; the rules stay editable; a change is audited with before and after`, async () => {
256
367
  const h = await make();
@@ -262,18 +373,28 @@ function couponsContract(name, make) {
262
373
  expect(await code(h.coupons.admin.update(h.tx, ADMIN, c.id, { percentOffBp: 3e3 }))).toBe("COUPON_TERMS_LOCKED");
263
374
  const after = await h.coupons.admin.update(h.tx, ADMIN, c.id, { maxRedemptions: 10, endsAt: at(5 * HOUR) });
264
375
  expect(after).toMatchObject({ maxRedemptions: 10, percentOffBp: 1500 });
265
- expect(await h.coupons.admin.update(h.tx, ADMIN, c.id, { maxRedemptions: 10 })).toMatchObject({ maxRedemptions: 10 });
376
+ expect(await h.coupons.admin.update(h.tx, ADMIN, c.id, { maxRedemptions: 10 })).toMatchObject({
377
+ maxRedemptions: 10
378
+ });
266
379
  expect(await code(h.coupons.admin.update(h.tx, ADMIN, c.id, { endsAt: START }))).toBe("COUPON_BAD_INPUT");
267
380
  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");
381
+ expect(entry.detail).toMatchObject({
382
+ changed: ["maxRedemptions", "endsAt"],
383
+ before: { maxRedemptions: null, endsAt: null },
384
+ after: { maxRedemptions: 10 }
385
+ });
386
+ expect(await code(h.coupons.admin.update(h.tx, ADMIN, randomUUID(), { maxRedemptions: 1 }))).toBe(
387
+ "COUPON_NOT_FOUND"
388
+ );
270
389
  });
271
390
  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
391
  const h = await make();
273
392
  const bad = (over) => code(h.coupons.admin.create(h.tx, ADMIN, discount({ currency: "cad", ...over })));
274
393
  expect(await bad({ percentOffBp: 1e4 })).toBe("COUPON_BAD_INPUT");
275
394
  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");
395
+ expect(await bad({ percentOffBp: null, amountOffMinor: 500, duration: "forever", durationMonths: null })).toBe(
396
+ "COUPON_BAD_INPUT"
397
+ );
277
398
  expect(await bad({ percentOffBp: null, amountOffMinor: null })).toBe("COUPON_BAD_INPUT");
278
399
  expect(await bad({ amountOffMinor: 5 })).toBe("COUPON_BAD_INPUT");
279
400
  expect(await bad({ duration: "once" })).toBe("COUPON_BAD_INPUT");
@@ -295,18 +416,51 @@ function couponsContract(name, make) {
295
416
  it(`${name}: a credit coupon grants capped credits that expire, once per key, and the use counts`, async () => {
296
417
  const h = await make();
297
418
  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" } });
419
+ const c = await h.coupons.admin.create(
420
+ h.tx,
421
+ ADMIN,
422
+ credit({ capPerMonth: 12, maxRedemptions: 1, onExhausted: "fallback_paid" })
423
+ );
424
+ expect(await h.coupons.validate(h.tx, c.code, { mode: "redeem", accountId: a })).toMatchObject({
425
+ ok: true,
426
+ effect: {
427
+ kind: "credit",
428
+ credits: 20,
429
+ capPerDay: 5,
430
+ capPerMonth: 12,
431
+ validDays: 30,
432
+ onExhausted: "fallback_paid"
433
+ }
434
+ });
300
435
  const got = await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: "rc-1" });
301
436
  expect(got).toMatchObject({ credits: 20, replayed: false });
302
437
  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);
438
+ expect(h.credits.granted).toEqual([
439
+ {
440
+ accountId: a,
441
+ amount: 20,
442
+ capPerDay: 5,
443
+ capPerMonth: 12,
444
+ expiresAt: at(30 * 24 * HOUR),
445
+ onExhausted: "fallback_paid",
446
+ source: `coupon:${got.redemptionId}`,
447
+ idempotencyKey: `coupon-${got.redemptionId}`
448
+ }
449
+ ]);
450
+ expect((await h.coupons.redeemCredit(h.tx, { code: c.code, accountId: a, idempotencyKey: "rc-1" })).replayed).toBe(
451
+ true
452
+ );
305
453
  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");
454
+ expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: "rc-1" }))).toBe(
455
+ "COUPON_KEY_REUSED"
456
+ );
457
+ expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: "rc-2" }))).toBe(
458
+ "COUPON_EXHAUSTED"
459
+ );
308
460
  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");
461
+ expect(await code(h.coupons.redeemCredit(h.tx, { code: kind.code, accountId: b, idempotencyKey: "rc-3" }))).toBe(
462
+ "COUPON_KIND"
463
+ );
310
464
  expect(await code(apply(h, c.code, b))).toBe("COUPON_KIND");
311
465
  });
312
466
  it(`${name}: revoking a credit redemption writes off what is left, keeps the use counted, and refuses what is not a credit`, async () => {
@@ -317,7 +471,9 @@ function couponsContract(name, make) {
317
471
  await h.coupons.revoke(h.tx, ADMIN, got.redemptionId);
318
472
  await h.coupons.revoke(h.tx, ADMIN, got.redemptionId);
319
473
  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");
474
+ expect(await code(h.coupons.redeemCredit(h.tx, { code: c.code, accountId: b, idempotencyKey: "rc-2" }))).toBe(
475
+ "COUPON_EXHAUSTED"
476
+ );
321
477
  const d = await made(h);
322
478
  const r = await apply(h, d.code, a);
323
479
  expect(await code(h.coupons.revoke(h.tx, ADMIN, r.redemptionId))).toBe("COUPON_NOT_REVOCABLE");
@@ -327,7 +483,10 @@ function couponsContract(name, make) {
327
483
  it(`${name}: announces the redemption with the partner and the amounts, and the last allowed use, once each`, async () => {
328
484
  const h = await make();
329
485
  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" });
486
+ const partner = await h.coupons.admin.partners.create(h.tx, ADMIN, {
487
+ displayName: "Marie Tremblay",
488
+ contactEmail: "Marie@Example.com"
489
+ });
331
490
  const c = await made(h, { purpose: "partner", partnerId: partner.id, maxRedemptions: 2 });
332
491
  for (const [i, a] of [a1, a2].entries()) {
333
492
  const r = await apply(h, c.code, a);
@@ -335,25 +494,64 @@ function couponsContract(name, make) {
335
494
  }
336
495
  const redeemed = h.announced().filter((m) => m.type === "evt.coupon.redeemed.v1");
337
496
  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 });
497
+ expect(redeemed[0].payload).toMatchObject({
498
+ couponId: c.id,
499
+ kind: "discount",
500
+ purpose: "partner",
501
+ partnerId: partner.id,
502
+ amountPaidMinor: 8e3,
503
+ amountDiscountMinor: 2e3,
504
+ currency: "cad",
505
+ credits: null,
506
+ overLimit: false
507
+ });
339
508
  expect(h.announced().filter((m) => m.type === "evt.coupon.exhausted.v1")).toHaveLength(1);
340
509
  });
341
510
  it(`${name}: partners: the view is net of refunds and never names a customer; erasing keeps the numbers and blanks the person`, async () => {
342
511
  const h = await make();
343
512
  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" });
513
+ const partner = await h.coupons.admin.partners.create(h.tx, ADMIN, {
514
+ displayName: "Marie Tremblay",
515
+ contactEmail: "marie@example.com"
516
+ });
345
517
  const other = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "Paul" });
346
518
  const c = await made(h, { purpose: "partner", partnerId: partner.id });
347
519
  const d = await h.coupons.admin.create(h.tx, ADMIN, credit({ purpose: "partner", partnerId: partner.id }));
348
520
  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 }));
521
+ await h.coupons.redeem(
522
+ h.tx,
523
+ paid(r1.redemptionId, { paymentRef: "pi_p1", amountPaidMinor: 1e4, amountDiscountMinor: 2e3 })
524
+ );
350
525
  await h.coupons.refund(h.tx, { paymentRef: "pi_p1", amountRefunded: 1e3 });
351
526
  await h.coupons.redeemCredit(h.tx, { code: d.code, accountId: a2, idempotencyKey: "rc-p" });
352
527
  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 }]));
528
+ expect(report).toEqual(
529
+ expect.arrayContaining([
530
+ {
531
+ couponId: c.id,
532
+ code: c.code,
533
+ currency: "cad",
534
+ redemptions: 1,
535
+ discountMinor: 2e3,
536
+ revenueMinor: 9e3,
537
+ creditsGranted: 0
538
+ },
539
+ {
540
+ couponId: d.id,
541
+ code: d.code,
542
+ currency: null,
543
+ redemptions: 1,
544
+ discountMinor: 0,
545
+ revenueMinor: 0,
546
+ creditsGranted: 20
547
+ }
548
+ ])
549
+ );
354
550
  expect(JSON.stringify(report)).not.toContain(a1);
355
551
  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([]);
552
+ expect(
553
+ await h.coupons.admin.partners.report(h.tx, ADMIN, partner.id, { from: at(HOUR), to: at(2 * HOUR) })
554
+ ).toEqual([]);
357
555
  expect(h.audited().filter((e) => e.action === "partner.report_read")).toHaveLength(3);
358
556
  expect(await code(h.coupons.admin.partners.report(h.tx, ADMIN, randomUUID()))).toBe("COUPON_PARTNER_NOT_FOUND");
359
557
  const exported = await h.coupons.admin.partners.export(h.tx, partner.id);
@@ -364,13 +562,19 @@ function couponsContract(name, make) {
364
562
  expect((await h.coupons.admin.partners.erase(h.tx, ADMIN, partner.id)).erasedAt).toEqual(erased.erasedAt);
365
563
  expect((await h.coupons.admin.partners.export(h.tx, partner.id)).rows).toHaveLength(2);
366
564
  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]));
565
+ expect((await h.coupons.admin.partners.list(h.tx, { includeArchived: true })).map((p) => p.id)).toEqual(
566
+ expect.arrayContaining([partner.id, other.id])
567
+ );
368
568
  });
369
569
  it(`${name}: partners: a name and a well-formed e-mail, archive and restore`, async () => {
370
570
  const h = await make();
371
571
  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");
572
+ expect(await code(h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "x".repeat(81) }))).toBe(
573
+ "COUPON_BAD_INPUT"
574
+ );
575
+ expect(
576
+ await code(h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: "Marie", contactEmail: "nope" }))
577
+ ).toBe("COUPON_BAD_INPUT");
374
578
  const p = await h.coupons.admin.partners.create(h.tx, ADMIN, { displayName: " Marie " });
375
579
  expect(p).toMatchObject({ displayName: "Marie", contactEmail: null, status: "active" });
376
580
  expect((await h.coupons.admin.partners.archive(h.tx, ADMIN, p.id)).status).toBe("archived");
@@ -380,11 +584,30 @@ function couponsContract(name, make) {
380
584
  });
381
585
  it(`${name}: a price drop pauses the fixed-amount coupons that would now be too large, and only those`, async () => {
382
586
  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"] });
587
+ const small = await made(h, {
588
+ percentOffBp: null,
589
+ amountOffMinor: 1e3,
590
+ currency: "cad",
591
+ duration: "once",
592
+ durationMonths: null,
593
+ plans: ["pro"]
594
+ });
595
+ const other = await made(h, {
596
+ percentOffBp: null,
597
+ amountOffMinor: 1e3,
598
+ currency: "cad",
599
+ duration: "once",
600
+ durationMonths: null,
601
+ plans: ["audit"]
602
+ });
385
603
  const percent = await made(h);
386
604
  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]);
605
+ expect(
606
+ await h.coupons.admin.checkPrices(h.tx, [
607
+ { plan: "pro", amountMinor: 1999, currency: "cad" },
608
+ { plan: "pro", amountMinor: 100, currency: "usd" }
609
+ ])
610
+ ).toEqual([small.id]);
388
611
  expect((await h.coupons.admin.get(h.tx, small.id))?.status).toBe("paused");
389
612
  expect((await h.coupons.admin.get(h.tx, other.id))?.status).toBe("active");
390
613
  expect((await h.coupons.admin.get(h.tx, percent.id))?.status).toBe("active");