@krizaka/coupons 0.1.3 → 0.1.5
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/{chunk-ECCGFGSV.js → chunk-V2HCEF26.js} +346 -62
- package/dist/chunk-V2HCEF26.js.map +1 -0
- package/dist/{chunk-JMQO6PTK.js → chunk-XXFDAM5G.js} +43 -13
- package/dist/chunk-XXFDAM5G.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/settings.js +1 -1
- package/dist/settings.js.map +1 -1
- package/dist/testkit.js +270 -47
- package/dist/testkit.js.map +1 -1
- package/package.json +9 -9
- package/dist/chunk-ECCGFGSV.js.map +0 -1
- package/dist/chunk-JMQO6PTK.js.map +0 -1
package/dist/testkit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-V2HCEF26.js";
|
|
2
2
|
import {
|
|
3
3
|
COUPON_DEFAULTS
|
|
4
|
-
} from "./chunk-
|
|
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 = {}) => ({
|
|
45
|
-
|
|
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 = {}) => ({
|
|
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(
|
|
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"])
|
|
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([
|
|
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, {
|
|
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({
|
|
129
|
-
|
|
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, {
|
|
138
|
-
|
|
139
|
-
|
|
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({
|
|
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, {
|
|
174
|
-
|
|
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(
|
|
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(
|
|
190
|
-
|
|
191
|
-
|
|
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({
|
|
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(
|
|
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({
|
|
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({
|
|
269
|
-
|
|
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(
|
|
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(
|
|
299
|
-
|
|
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([
|
|
304
|
-
|
|
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(
|
|
307
|
-
|
|
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(
|
|
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(
|
|
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, {
|
|
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({
|
|
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, {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
373
|
-
|
|
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, {
|
|
384
|
-
|
|
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(
|
|
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");
|