@krizaka/credits 0.1.3 → 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/{chunk-34Y2HXOB.js → chunk-FPQSMNV2.js} +33 -13
- package/dist/chunk-FPQSMNV2.js.map +1 -0
- package/dist/{chunk-WDHQ7QHY.js → chunk-NKDQNDSD.js} +9 -2
- package/dist/chunk-NKDQNDSD.js.map +1 -0
- package/dist/index.js +270 -40
- package/dist/index.js.map +1 -1
- package/dist/settings.js +11 -4
- package/dist/settings.js.map +1 -1
- package/dist/testkit.js +116 -21
- package/dist/testkit.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-34Y2HXOB.js.map +0 -1
- package/dist/chunk-WDHQ7QHY.js.map +0 -1
package/dist/testkit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/testkit.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { expect, it } from \"vitest\";\n\nimport type { Credits } from \"./credits\";\nimport { type CreditsCode, ENTRY_TYPE, ON_EXHAUSTED, type Query } from \"./port\";\n\n/** What a store (through the ledger) is run against: the ledger on that store, a clock the test moves, and accounts that exist for it. */\nexport interface LedgerHarness {\n credits: Credits;\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 /** 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}\n\n/** Midday on 26 October 2026 in Toronto (16:00 UTC): the day the scenarios start on. */\nexport const T0 = new Date(\"2026-10-26T16:00:00.000Z\");\nconst plus = (instant: Date, seconds: number) => new Date(instant.valueOf() + seconds * 1000);\nconst DAY = 86_400;\n\nasync function refusal(work: Promise<unknown>): Promise<CreditsCode> {\n try {\n await work;\n } catch (error) {\n return (error as { code: CreditsCode }).code;\n }\n throw new Error(\"expected a refusal, got an answer\");\n}\n\n/**\n * What every credits store must do, behind the same ledger: derived balances, the order of draws, idempotency, the caps of the day and\n * the month in the configured zone, expiry, `stop` against `fallback_paid`, holds, refunds. `make` gives an EMPTY ledger and the clock\n * at {@link T0}, time zone `America/Toronto`, hold lifetime 900 seconds.\n */\nexport function creditsLedgerContract(name: string, make: () => Promise<LedgerHarness>) {\n const grant = (h: LedgerHarness, accountId: string, over: Partial<Parameters<Credits[\"grant\"]>[1]> = {}) =>\n h.credits.grant(h.tx, { accountId, amount: 10, expiresAt: plus(T0, 30 * DAY), onExhausted: ON_EXHAUSTED.stop, source: \"coupon:test\", idempotencyKey: randomUUID(), ...over });\n const buy = (h: LedgerHarness, accountId: string, amount: number) => h.credits.purchase(h.tx, { accountId, amount, source: \"purchase:pi_1\", idempotencyKey: randomUUID() });\n const consume = (h: LedgerHarness, accountId: string, amount: number, idempotencyKey: string = randomUUID()) => h.credits.consume(h.tx, { accountId, amount, reason: \"audit:1\", idempotencyKey });\n\n it(`${name}: derives the balance from the ledger: a grant and a purchase, the history newest first`, async () => {\n const h = await make();\n const a = await h.newAccount();\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 0, available: 0, grants: [] });\n await grant(h, a, { amount: 5 });\n await buy(h, a, 7);\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 12, granted: 5, purchased: 7, held: 0, available: 12 });\n const lines = await h.credits.history(h.tx, a);\n expect(lines.map((l) => [l.type, l.amount])).toEqual([[ENTRY_TYPE.purchase, 7], [ENTRY_TYPE.grant, 5]]);\n });\n\n it(`${name}: a grant or a purchase with a key seen before changes nothing`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const first = await grant(h, a, { idempotencyKey: \"g-1\", amount: 4 });\n const again = await grant(h, a, { idempotencyKey: \"g-1\", amount: 4 });\n expect(again).toEqual({ grantId: first.grantId, replayed: true });\n const p = await h.credits.purchase(h.tx, { accountId: a, amount: 3, source: \"purchase:x\", idempotencyKey: \"p-1\" });\n expect((await h.credits.purchase(h.tx, { accountId: a, amount: 3, source: \"purchase:x\", idempotencyKey: \"p-1\" })).grantId).toBe(p.grantId);\n expect((await h.credits.balance(h.tx, a)).total).toBe(7);\n expect(await h.credits.history(h.tx, a)).toHaveLength(2);\n });\n\n it(`${name}: refuses a key already used by another account`, async () => {\n const h = await make();\n const [a, b] = [await h.newAccount(), await h.newAccount()];\n await grant(h, a, { idempotencyKey: \"shared\" });\n expect(await refusal(grant(h, b, { idempotencyKey: \"shared\" }))).toBe(\"CREDITS_KEY_REUSED\");\n await consume(h, a, 1, \"c-shared\");\n expect(await refusal(consume(h, b, 1, \"c-shared\"))).toBe(\"CREDITS_KEY_REUSED\");\n });\n\n it(`${name}: spends from the pot that lapses first, then the next; purchased credits last`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const late = await grant(h, a, { amount: 5, expiresAt: plus(T0, 20 * DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });\n const soon = await grant(h, a, { amount: 3, expiresAt: plus(T0, 5 * DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });\n const paid = await buy(h, a, 10);\n const out = await consume(h, a, 4);\n expect(out.draws).toEqual([{ grantId: soon.grantId, amount: 3 }, { grantId: late.grantId, amount: 1 }]);\n const rest = await consume(h, a, 4 + 4 + 1);\n expect(rest.draws).toEqual([{ grantId: late.grantId, amount: 4 }, { grantId: paid.grantId, amount: 5 }]);\n });\n\n it(`${name}: a consume with a key seen before answers the first answer and spends nothing more`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 5 });\n const first = await consume(h, a, 2, \"job-1\");\n const again = await consume(h, a, 2, \"job-1\");\n expect(first.replayed).toBe(false);\n expect(again).toEqual({ draws: first.draws, replayed: true });\n expect((await h.credits.balance(h.tx, a)).total).toBe(3);\n });\n\n it(`${name}: no credits at all is a PAYWALL, and a refusal writes nothing`, async () => {\n const h = await make();\n const a = await h.newAccount();\n expect(await refusal(consume(h, a, 1))).toBe(\"PAYWALL\");\n await buy(h, a, 2);\n expect(await refusal(consume(h, a, 3))).toBe(\"PAYWALL\");\n expect((await h.credits.balance(h.tx, a)).total).toBe(2);\n expect(await h.credits.history(h.tx, a)).toHaveLength(1);\n });\n\n it(`${name}: a stop pot that is empty answers CREDITS_EXHAUSTED even if purchased credits exist; fallback_paid moves on to them`, async () => {\n const h = await make();\n const [stop, fall] = [await h.newAccount(), await h.newAccount()];\n await grant(h, stop, { amount: 2, onExhausted: ON_EXHAUSTED.stop });\n await buy(h, stop, 5);\n await consume(h, stop, 2);\n expect(await refusal(consume(h, stop, 1))).toBe(\"CREDITS_EXHAUSTED\");\n expect(await refusal(consume(h, stop, 3))).toBe(\"CREDITS_EXHAUSTED\");\n expect((await h.credits.balance(h.tx, stop)).purchased).toBe(5);\n\n await grant(h, fall, { amount: 2, onExhausted: ON_EXHAUSTED.fallbackPaid });\n await buy(h, fall, 5);\n const out = await consume(h, fall, 4);\n expect(out.draws).toHaveLength(2);\n expect(out.draws.map((d) => d.amount)).toEqual([2, 2]);\n const none = await h.newAccount();\n await grant(h, none, { amount: 1, onExhausted: ON_EXHAUSTED.fallbackPaid });\n await consume(h, none, 1);\n expect(await refusal(consume(h, none, 1))).toBe(\"PAYWALL\");\n });\n\n it(`${name}: the daily cap holds, and a new calendar day in the configured zone opens it again`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 20, capPerDay: 3 });\n await consume(h, a, 2);\n expect(await refusal(consume(h, a, 2))).toBe(\"CREDITS_CAP_DAY\");\n await consume(h, a, 1);\n // 23:30 in Toronto is still the same day; 00:30 is the next one\n h.setNow(new Date(\"2026-10-27T03:30:00.000Z\"));\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_DAY\");\n h.setNow(new Date(\"2026-10-27T04:30:00.000Z\"));\n const next = await consume(h, a, 3);\n expect(next.draws.reduce((n, d) => n + d.amount, 0)).toBe(3);\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_DAY\");\n });\n\n it(`${name}: the monthly cap holds across the month boundary of the zone (daylight saving ends that night)`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 50, capPerMonth: 4, expiresAt: plus(T0, 60 * DAY) });\n h.setNow(new Date(\"2026-10-31T10:00:00.000Z\"));\n await consume(h, a, 4);\n h.setNow(new Date(\"2026-11-01T03:59:59.000Z\")); // 23:59:59 on 31 October in Toronto\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_MONTH\");\n h.setNow(new Date(\"2026-11-01T04:00:00.000Z\")); // midnight, 1 November\n await consume(h, a, 4);\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_MONTH\");\n });\n\n it(`${name}: the cap of one pot does not stop the next pot`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const capped = await grant(h, a, { amount: 10, capPerDay: 2, expiresAt: plus(T0, 5 * DAY) });\n const open = await grant(h, a, { amount: 10, expiresAt: plus(T0, 9 * DAY) });\n const out = await consume(h, a, 5);\n expect(out.draws).toEqual([{ grantId: capped.grantId, amount: 2 }, { grantId: open.grantId, amount: 3 }]);\n });\n\n it(`${name}: a pot is not spendable after its date, and expire() writes off what is left once`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const g = await grant(h, a, { amount: 6, expiresAt: plus(T0, DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });\n await consume(h, a, 2);\n h.setNow(plus(T0, DAY));\n expect((await h.credits.balance(h.tx, a)).total).toBe(0);\n expect(await refusal(consume(h, a, 1))).toBe(\"PAYWALL\");\n expect(await h.credits.expire(h.tx, g.grantId)).toEqual({ expired: 4 });\n expect(await h.credits.expire(h.tx, g.grantId)).toEqual({ expired: 0 });\n expect((await h.credits.history(h.tx, a)).map((l) => [l.type, l.amount])).toEqual([[ENTRY_TYPE.expiry, -4], [ENTRY_TYPE.debit, -2], [ENTRY_TYPE.grant, 6]]);\n });\n\n it(`${name}: a revoked pot (expire before its date) gives nothing, and a stop pot closed this way does not block purchased credits`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const g = await grant(h, a, { amount: 3, onExhausted: ON_EXHAUSTED.stop });\n await buy(h, a, 2);\n await h.credits.expire(h.tx, g.grantId);\n expect((await h.credits.balance(h.tx, a)).total).toBe(2);\n expect((await consume(h, a, 2)).draws).toHaveLength(1);\n });\n\n it(`${name}: the sweep closes lapsed pots that still hold credits, oldest first, and skips closed ones`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 3, expiresAt: plus(T0, DAY) });\n await grant(h, a, { amount: 2, expiresAt: plus(T0, 2 * DAY) });\n await grant(h, a, { amount: 9, expiresAt: plus(T0, 10 * DAY) });\n h.setNow(plus(T0, 3 * DAY));\n expect(await h.credits.expireDue(h.tx)).toBe(2);\n expect(await h.credits.expireDue(h.tx)).toBe(0);\n expect((await h.credits.balance(h.tx, a)).total).toBe(9);\n });\n\n it(`${name}: a hold keeps credits back from others, settles for the real cost, and the rest is free again`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 5 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 4, idempotencyKey: \"h-1\" });\n expect(hold.replayed).toBe(false);\n expect((await h.credits.hold(h.tx, { accountId: a, amount: 4, idempotencyKey: \"h-1\" })).holdId).toBe(hold.holdId);\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 5, held: 4, available: 1 });\n expect(await refusal(consume(h, a, 2))).toBe(\"CREDITS_EXHAUSTED\");\n expect((await consume(h, a, 1)).draws).toHaveLength(1);\n const settled = await h.credits.settle(h.tx, hold.holdId, { amount: 3, reason: \"audit:9\" });\n expect(settled.draws.reduce((n, d) => n + d.amount, 0)).toBe(3);\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 1, held: 0 });\n expect(await h.credits.settle(h.tx, hold.holdId)).toMatchObject({ replayed: true });\n expect((await h.credits.balance(h.tx, a)).total).toBe(1);\n expect(await refusal(h.credits.release(h.tx, hold.holdId))).toBe(\"CREDITS_HOLD_CLOSED\");\n });\n\n it(`${name}: a hold counts against the caps, a hold that cannot be covered is refused, and a release frees it`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 10, capPerDay: 3 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 3, idempotencyKey: \"h-1\" });\n expect(await refusal(h.credits.hold(h.tx, { accountId: a, amount: 1, idempotencyKey: \"h-2\" }))).toBe(\"CREDITS_CAP_DAY\");\n await h.credits.release(h.tx, hold.holdId);\n await h.credits.release(h.tx, hold.holdId);\n expect(await refusal(h.credits.settle(h.tx, hold.holdId))).toBe(\"CREDITS_HOLD_CLOSED\");\n expect((await h.credits.balance(h.tx, a)).held).toBe(0);\n await h.credits.hold(h.tx, { accountId: a, amount: 3, idempotencyKey: \"h-3\" });\n });\n\n it(`${name}: a hold lapses by itself after its lifetime; settling for more than it held, or an unknown hold, is refused`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 5 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 5, ttlSeconds: 60, idempotencyKey: \"h-1\" });\n expect(await refusal(h.credits.settle(h.tx, hold.holdId, { amount: 6 }))).toBe(\"CREDITS_BAD_AMOUNT\");\n expect((await h.credits.balance(h.tx, a)).held).toBe(5);\n h.setNow(plus(T0, 61));\n expect((await h.credits.balance(h.tx, a)).held).toBe(0);\n expect((await consume(h, a, 5)).draws).toHaveLength(1);\n expect(await refusal(h.credits.settle(h.tx, randomUUID()))).toBe(\"CREDITS_UNKNOWN_HOLD\");\n expect(await refusal(h.credits.release(h.tx, randomUUID()))).toBe(\"CREDITS_UNKNOWN_HOLD\");\n });\n\n it(`${name}: a hold settled for nothing spends nothing and is released`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 5 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 2, idempotencyKey: \"h-0\" });\n expect(await h.credits.settle(h.tx, hold.holdId, { amount: 0 })).toEqual({ draws: [], replayed: false });\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 5, held: 0 });\n });\n\n it(`${name}: a refund gives back to the same pots, once, whatever the number of calls`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const g = await grant(h, a, { amount: 5, onExhausted: ON_EXHAUSTED.fallbackPaid });\n await buy(h, a, 5);\n const spent = await consume(h, a, 7, \"job-7\");\n expect(spent.draws).toHaveLength(2);\n expect(await h.credits.refund(h.tx, { accountId: a, consumed: \"job-7\", reason: \"the run failed\" })).toEqual({ refunded: 7, replayed: false });\n expect(await h.credits.refund(h.tx, { accountId: a, consumed: \"job-7\", reason: \"the run failed\" })).toEqual({ refunded: 7, replayed: true });\n const b = await h.credits.balance(h.tx, a);\n expect(b.total).toBe(10);\n expect(b.grants.find((x) => x.id === g.grantId)?.remaining).toBe(5);\n expect(await refusal(h.credits.refund(h.tx, { accountId: a, consumed: \"nope\", reason: \"x\" }))).toBe(\"CREDITS_NOTHING_TO_REFUND\");\n const other = await h.newAccount();\n expect(await refusal(h.credits.refund(h.tx, { accountId: other, consumed: \"job-7\", reason: \"x\" }))).toBe(\"CREDITS_KEY_REUSED\");\n });\n\n it(`${name}: a refund of a settled hold, and a refund counts back against the cap`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 10, capPerDay: 2 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 2, idempotencyKey: \"h-1\" });\n await h.credits.settle(h.tx, hold.holdId);\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_DAY\");\n await h.credits.refund(h.tx, { accountId: a, settledHold: hold.holdId, reason: \"failed\" });\n expect((await consume(h, a, 2)).draws).toHaveLength(1);\n });\n\n it(`${name}: an adjustment corrects a pot, never below zero, once per key`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const g = await grant(h, a, { amount: 3 });\n expect(await h.credits.adjust(h.tx, { accountId: a, grantId: g.grantId, amount: 2, reason: \"goodwill\", idempotencyKey: \"adj-1\" })).toEqual({ replayed: false });\n expect(await h.credits.adjust(h.tx, { accountId: a, grantId: g.grantId, amount: 2, reason: \"goodwill\", idempotencyKey: \"adj-1\" })).toEqual({ replayed: true });\n expect((await h.credits.balance(h.tx, a)).total).toBe(5);\n expect(await refusal(h.credits.adjust(h.tx, { accountId: a, grantId: g.grantId, amount: -6, reason: \"oops\", idempotencyKey: \"adj-2\" }))).toBe(\"CREDITS_BAD_AMOUNT\");\n await h.credits.adjust(h.tx, { accountId: a, grantId: g.grantId, amount: -5, reason: \"revoked\", idempotencyKey: \"adj-3\" });\n expect((await h.credits.balance(h.tx, a)).total).toBe(0);\n expect(await refusal(h.credits.adjust(h.tx, { accountId: a, grantId: randomUUID(), amount: 1, reason: \"x\", idempotencyKey: \"adj-4\" }))).toBe(\"CREDITS_UNKNOWN_GRANT\");\n });\n\n it(`${name}: announces a grant, and a pot running out, once each`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 2, idempotencyKey: \"g-1\" });\n await grant(h, a, { amount: 2, idempotencyKey: \"g-1\" });\n await consume(h, a, 1);\n await consume(h, a, 1);\n const types = h.announced().map((m) => m.type);\n expect(types.filter((t) => t === \"evt.credit.granted.v1\")).toHaveLength(1);\n expect(types.filter((t) => t === \"evt.credit.exhausted.v1\")).toHaveLength(1);\n expect(h.announced().find((m) => m.type === \"evt.credit.exhausted.v1\")?.payload).toMatchObject({ accountId: a, bucket: \"granted\", onExhausted: \"stop\", source: \"coupon:test\" });\n });\n\n it(`${name}: history pages back from a date`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 9 });\n h.setNow(plus(T0, 60));\n await consume(h, a, 1);\n h.setNow(plus(T0, 120));\n await consume(h, a, 1);\n expect((await h.credits.history(h.tx, a, { limit: 2 })).map((l) => l.amount)).toEqual([-1, -1]);\n expect((await h.credits.history(h.tx, a, { before: plus(T0, 100) })).map((l) => l.amount)).toEqual([-1, 9]);\n });\n}\n"],"mappings":";;;;;;AAAA,SAAS,kBAAkB;AAE3B,SAAS,QAAQ,UAAU;AAmBpB,IAAM,KAAK,oBAAI,KAAK,0BAA0B;AACrD,IAAM,OAAO,CAAC,SAAe,YAAoB,IAAI,KAAK,QAAQ,QAAQ,IAAI,UAAU,GAAI;AAC5F,IAAM,MAAM;AAEZ,eAAe,QAAQ,MAA8C;AACnE,MAAI;AACF,UAAM;AAAA,EACR,SAAS,OAAO;AACd,WAAQ,MAAgC;AAAA,EAC1C;AACA,QAAM,IAAI,MAAM,mCAAmC;AACrD;AAOO,SAAS,sBAAsB,MAAc,MAAoC;AACtF,QAAM,QAAQ,CAAC,GAAkB,WAAmB,OAAiD,CAAC,MACpG,EAAE,QAAQ,MAAM,EAAE,IAAI,EAAE,WAAW,QAAQ,IAAI,WAAW,KAAK,IAAI,KAAK,GAAG,GAAG,aAAa,aAAa,MAAM,QAAQ,eAAe,gBAAgB,WAAW,GAAG,GAAG,KAAK,CAAC;AAC9K,QAAM,MAAM,CAAC,GAAkB,WAAmB,WAAmB,EAAE,QAAQ,SAAS,EAAE,IAAI,EAAE,WAAW,QAAQ,QAAQ,iBAAiB,gBAAgB,WAAW,EAAE,CAAC;AAC1K,QAAM,UAAU,CAAC,GAAkB,WAAmB,QAAgB,iBAAyB,WAAW,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,EAAE,WAAW,QAAQ,QAAQ,WAAW,eAAe,CAAC;AAEhM,KAAG,GAAG,IAAI,2FAA2F,YAAY;AAC/G,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,WAAO,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,GAAG,WAAW,GAAG,QAAQ,CAAC,EAAE,CAAC;AAC7F,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC/B,UAAM,IAAI,GAAG,GAAG,CAAC;AACjB,WAAO,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,IAAI,SAAS,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,CAAC;AACtH,UAAM,QAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC;AAC7C,WAAO,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,WAAW,UAAU,CAAC,GAAG,CAAC,WAAW,OAAO,CAAC,CAAC,CAAC;AAAA,EACxG,CAAC;AAED,KAAG,GAAG,IAAI,kEAAkE,YAAY;AACtF,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,QAAQ,MAAM,MAAM,GAAG,GAAG,EAAE,gBAAgB,OAAO,QAAQ,EAAE,CAAC;AACpE,UAAM,QAAQ,MAAM,MAAM,GAAG,GAAG,EAAE,gBAAgB,OAAO,QAAQ,EAAE,CAAC;AACpE,WAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,MAAM,SAAS,UAAU,KAAK,CAAC;AAChE,UAAM,IAAI,MAAM,EAAE,QAAQ,SAAS,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,QAAQ,cAAc,gBAAgB,MAAM,CAAC;AACjH,YAAQ,MAAM,EAAE,QAAQ,SAAS,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,QAAQ,cAAc,gBAAgB,MAAM,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO;AACzI,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AACvD,WAAO,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,EAAE,aAAa,CAAC;AAAA,EACzD,CAAC;AAED,KAAG,GAAG,IAAI,mDAAmD,YAAY;AACvE,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,EAAE,WAAW,CAAC;AAC1D,UAAM,MAAM,GAAG,GAAG,EAAE,gBAAgB,SAAS,CAAC;AAC9C,WAAO,MAAM,QAAQ,MAAM,GAAG,GAAG,EAAE,gBAAgB,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,oBAAoB;AAC1F,UAAM,QAAQ,GAAG,GAAG,GAAG,UAAU;AACjC,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,GAAG,UAAU,CAAC,CAAC,EAAE,KAAK,oBAAoB;AAAA,EAC/E,CAAC;AAED,KAAG,GAAG,IAAI,kFAAkF,YAAY;AACtG,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,OAAO,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,WAAW,KAAK,IAAI,KAAK,GAAG,GAAG,aAAa,aAAa,aAAa,CAAC;AACnH,UAAM,OAAO,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,WAAW,KAAK,IAAI,IAAI,GAAG,GAAG,aAAa,aAAa,aAAa,CAAC;AAClH,UAAM,OAAO,MAAM,IAAI,GAAG,GAAG,EAAE;AAC/B,UAAM,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC;AACjC,WAAO,IAAI,KAAK,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,SAAS,QAAQ,EAAE,GAAG,EAAE,SAAS,KAAK,SAAS,QAAQ,EAAE,CAAC,CAAC;AACtG,UAAM,OAAO,MAAM,QAAQ,GAAG,GAAG,IAAI,IAAI,CAAC;AAC1C,WAAO,KAAK,KAAK,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,SAAS,QAAQ,EAAE,GAAG,EAAE,SAAS,KAAK,SAAS,QAAQ,EAAE,CAAC,CAAC;AAAA,EACzG,CAAC;AAED,KAAG,GAAG,IAAI,uFAAuF,YAAY;AAC3G,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC/B,UAAM,QAAQ,MAAM,QAAQ,GAAG,GAAG,GAAG,OAAO;AAC5C,UAAM,QAAQ,MAAM,QAAQ,GAAG,GAAG,GAAG,OAAO;AAC5C,WAAO,MAAM,QAAQ,EAAE,KAAK,KAAK;AACjC,WAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,MAAM,OAAO,UAAU,KAAK,CAAC;AAC5D,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AAAA,EACzD,CAAC;AAED,KAAG,GAAG,IAAI,kEAAkE,YAAY;AACtF,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS;AACtD,UAAM,IAAI,GAAG,GAAG,CAAC;AACjB,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS;AACtD,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AACvD,WAAO,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,EAAE,aAAa,CAAC;AAAA,EACzD,CAAC;AAED,KAAG,GAAG,IAAI,wHAAwH,YAAY;AAC5I,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,EAAE,WAAW,CAAC;AAChE,UAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,GAAG,aAAa,aAAa,KAAK,CAAC;AAClE,UAAM,IAAI,GAAG,MAAM,CAAC;AACpB,UAAM,QAAQ,GAAG,MAAM,CAAC;AACxB,WAAO,MAAM,QAAQ,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,mBAAmB;AACnE,WAAO,MAAM,QAAQ,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,mBAAmB;AACnE,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,IAAI,GAAG,SAAS,EAAE,KAAK,CAAC;AAE9D,UAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,GAAG,aAAa,aAAa,aAAa,CAAC;AAC1E,UAAM,IAAI,GAAG,MAAM,CAAC;AACpB,UAAM,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AACpC,WAAO,IAAI,KAAK,EAAE,aAAa,CAAC;AAChC,WAAO,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;AACrD,UAAM,OAAO,MAAM,EAAE,WAAW;AAChC,UAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,GAAG,aAAa,aAAa,aAAa,CAAC;AAC1E,UAAM,QAAQ,GAAG,MAAM,CAAC;AACxB,WAAO,MAAM,QAAQ,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS;AAAA,EAC3D,CAAC;AAED,KAAG,GAAG,IAAI,uFAAuF,YAAY;AAC3G,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,WAAW,EAAE,CAAC;AAC9C,UAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,iBAAiB;AAC9D,UAAM,QAAQ,GAAG,GAAG,CAAC;AAErB,MAAE,OAAO,oBAAI,KAAK,0BAA0B,CAAC;AAC7C,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,iBAAiB;AAC9D,MAAE,OAAO,oBAAI,KAAK,0BAA0B,CAAC;AAC7C,UAAM,OAAO,MAAM,QAAQ,GAAG,GAAG,CAAC;AAClC,WAAO,KAAK,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC;AAC3D,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,iBAAiB;AAAA,EAChE,CAAC;AAED,KAAG,GAAG,IAAI,mGAAmG,YAAY;AACvH,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,aAAa,GAAG,WAAW,KAAK,IAAI,KAAK,GAAG,EAAE,CAAC;AAC/E,MAAE,OAAO,oBAAI,KAAK,0BAA0B,CAAC;AAC7C,UAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,MAAE,OAAO,oBAAI,KAAK,0BAA0B,CAAC;AAC7C,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,mBAAmB;AAChE,MAAE,OAAO,oBAAI,KAAK,0BAA0B,CAAC;AAC7C,UAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,mBAAmB;AAAA,EAClE,CAAC;AAED,KAAG,GAAG,IAAI,mDAAmD,YAAY;AACvE,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,SAAS,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,WAAW,GAAG,WAAW,KAAK,IAAI,IAAI,GAAG,EAAE,CAAC;AAC3F,UAAM,OAAO,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,WAAW,KAAK,IAAI,IAAI,GAAG,EAAE,CAAC;AAC3E,UAAM,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC;AACjC,WAAO,IAAI,KAAK,EAAE,QAAQ,CAAC,EAAE,SAAS,OAAO,SAAS,QAAQ,EAAE,GAAG,EAAE,SAAS,KAAK,SAAS,QAAQ,EAAE,CAAC,CAAC;AAAA,EAC1G,CAAC;AAED,KAAG,GAAG,IAAI,sFAAsF,YAAY;AAC1G,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,IAAI,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,WAAW,KAAK,IAAI,GAAG,GAAG,aAAa,aAAa,aAAa,CAAC;AAC3G,UAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,MAAE,OAAO,KAAK,IAAI,GAAG,CAAC;AACtB,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AACvD,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS;AACtD,WAAO,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACtE,WAAO,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACtE,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,WAAW,QAAQ,EAAE,GAAG,CAAC,WAAW,OAAO,EAAE,GAAG,CAAC,WAAW,OAAO,CAAC,CAAC,CAAC;AAAA,EAC5J,CAAC;AAED,KAAG,GAAG,IAAI,2HAA2H,YAAY;AAC/I,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,IAAI,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,aAAa,aAAa,KAAK,CAAC;AACzE,UAAM,IAAI,GAAG,GAAG,CAAC;AACjB,UAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,OAAO;AACtC,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AACvD,YAAQ,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,EAAE,aAAa,CAAC;AAAA,EACvD,CAAC;AAED,KAAG,GAAG,IAAI,+FAA+F,YAAY;AACnH,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,WAAW,KAAK,IAAI,GAAG,EAAE,CAAC;AACzD,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,WAAW,KAAK,IAAI,IAAI,GAAG,EAAE,CAAC;AAC7D,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,WAAW,KAAK,IAAI,KAAK,GAAG,EAAE,CAAC;AAC9D,MAAE,OAAO,KAAK,IAAI,IAAI,GAAG,CAAC;AAC1B,WAAO,MAAM,EAAE,QAAQ,UAAU,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC;AAC9C,WAAO,MAAM,EAAE,QAAQ,UAAU,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC;AAC9C,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AAAA,EACzD,CAAC;AAED,KAAG,GAAG,IAAI,kGAAkG,YAAY;AACtH,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC/B,UAAM,OAAO,MAAM,EAAE,QAAQ,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,MAAM,CAAC;AAC1F,WAAO,KAAK,QAAQ,EAAE,KAAK,KAAK;AAChC,YAAQ,MAAM,EAAE,QAAQ,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,KAAK,KAAK,MAAM;AAChH,WAAO,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,EAAE,CAAC;AAC1F,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,mBAAmB;AAChE,YAAQ,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,EAAE,aAAa,CAAC;AACrD,UAAM,UAAU,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,KAAK,QAAQ,EAAE,QAAQ,GAAG,QAAQ,UAAU,CAAC;AAC1F,WAAO,QAAQ,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC;AAC9D,WAAO,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,GAAG,MAAM,EAAE,CAAC;AAC5E,WAAO,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,KAAK,MAAM,CAAC,EAAE,cAAc,EAAE,UAAU,KAAK,CAAC;AAClF,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AACvD,WAAO,MAAM,QAAQ,EAAE,QAAQ,QAAQ,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,qBAAqB;AAAA,EACxF,CAAC;AAED,KAAG,GAAG,IAAI,sGAAsG,YAAY;AAC1H,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,WAAW,EAAE,CAAC;AAC9C,UAAM,OAAO,MAAM,EAAE,QAAQ,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,MAAM,CAAC;AAC1F,WAAO,MAAM,QAAQ,EAAE,QAAQ,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,iBAAiB;AACtH,UAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,KAAK,MAAM;AACzC,UAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,KAAK,MAAM;AACzC,WAAO,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,EAAE,KAAK,qBAAqB;AACrF,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC;AACtD,UAAM,EAAE,QAAQ,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,MAAM,CAAC;AAAA,EAC/E,CAAC;AAED,KAAG,GAAG,IAAI,gHAAgH,YAAY;AACpI,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC/B,UAAM,OAAO,MAAM,EAAE,QAAQ,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,YAAY,IAAI,gBAAgB,MAAM,CAAC;AAC1G,WAAO,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,IAAI,KAAK,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,oBAAoB;AACnG,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC;AACtD,MAAE,OAAO,KAAK,IAAI,EAAE,CAAC;AACrB,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC;AACtD,YAAQ,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,EAAE,aAAa,CAAC;AACrD,WAAO,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,sBAAsB;AACvF,WAAO,MAAM,QAAQ,EAAE,QAAQ,QAAQ,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,sBAAsB;AAAA,EAC1F,CAAC;AAED,KAAG,GAAG,IAAI,+DAA+D,YAAY;AACnF,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC/B,UAAM,OAAO,MAAM,EAAE,QAAQ,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,MAAM,CAAC;AAC1F,WAAO,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,KAAK,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,UAAU,MAAM,CAAC;AACvG,WAAO,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,GAAG,MAAM,EAAE,CAAC;AAAA,EAC9E,CAAC;AAED,KAAG,GAAG,IAAI,8EAA8E,YAAY;AAClG,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,IAAI,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,aAAa,aAAa,aAAa,CAAC;AACjF,UAAM,IAAI,GAAG,GAAG,CAAC;AACjB,UAAM,QAAQ,MAAM,QAAQ,GAAG,GAAG,GAAG,OAAO;AAC5C,WAAO,MAAM,KAAK,EAAE,aAAa,CAAC;AAClC,WAAO,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,SAAS,QAAQ,iBAAiB,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,GAAG,UAAU,MAAM,CAAC;AAC5I,WAAO,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,SAAS,QAAQ,iBAAiB,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,GAAG,UAAU,KAAK,CAAC;AAC3I,UAAM,IAAI,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC;AACzC,WAAO,EAAE,KAAK,EAAE,KAAK,EAAE;AACvB,WAAO,EAAE,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,KAAK,CAAC;AAClE,WAAO,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,QAAQ,QAAQ,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,2BAA2B;AAC/H,UAAM,QAAQ,MAAM,EAAE,WAAW;AACjC,WAAO,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,OAAO,UAAU,SAAS,QAAQ,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,oBAAoB;AAAA,EAC/H,CAAC;AAED,KAAG,GAAG,IAAI,0EAA0E,YAAY;AAC9F,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,WAAW,EAAE,CAAC;AAC9C,UAAM,OAAO,MAAM,EAAE,QAAQ,KAAK,EAAE,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,MAAM,CAAC;AAC1F,UAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,KAAK,MAAM;AACxC,WAAO,MAAM,QAAQ,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,iBAAiB;AAC9D,UAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,aAAa,KAAK,QAAQ,QAAQ,SAAS,CAAC;AACzF,YAAQ,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,EAAE,aAAa,CAAC;AAAA,EACvD,CAAC;AAED,KAAG,GAAG,IAAI,kEAAkE,YAAY;AACtF,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,IAAI,MAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC;AACzC,WAAO,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,SAAS,QAAQ,GAAG,QAAQ,YAAY,gBAAgB,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,MAAM,CAAC;AAC9J,WAAO,MAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,SAAS,QAAQ,GAAG,QAAQ,YAAY,gBAAgB,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,KAAK,CAAC;AAC7J,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AACvD,WAAO,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,SAAS,QAAQ,IAAI,QAAQ,QAAQ,gBAAgB,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,oBAAoB;AAClK,UAAM,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,SAAS,QAAQ,IAAI,QAAQ,WAAW,gBAAgB,QAAQ,CAAC;AACzH,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC;AACvD,WAAO,MAAM,QAAQ,EAAE,QAAQ,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,SAAS,WAAW,GAAG,QAAQ,GAAG,QAAQ,KAAK,gBAAgB,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,uBAAuB;AAAA,EACtK,CAAC;AAED,KAAG,GAAG,IAAI,yDAAyD,YAAY;AAC7E,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,gBAAgB,MAAM,CAAC;AACtD,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,GAAG,gBAAgB,MAAM,CAAC;AACtD,UAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,UAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,UAAM,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI;AAC7C,WAAO,MAAM,OAAO,CAAC,MAAM,MAAM,uBAAuB,CAAC,EAAE,aAAa,CAAC;AACzE,WAAO,MAAM,OAAO,CAAC,MAAM,MAAM,yBAAyB,CAAC,EAAE,aAAa,CAAC;AAC3E,WAAO,EAAE,UAAU,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,yBAAyB,GAAG,OAAO,EAAE,cAAc,EAAE,WAAW,GAAG,QAAQ,WAAW,aAAa,QAAQ,QAAQ,cAAc,CAAC;AAAA,EAChL,CAAC;AAED,KAAG,GAAG,IAAI,oCAAoC,YAAY;AACxD,UAAM,IAAI,MAAM,KAAK;AACrB,UAAM,IAAI,MAAM,EAAE,WAAW;AAC7B,UAAM,MAAM,GAAG,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC/B,MAAE,OAAO,KAAK,IAAI,EAAE,CAAC;AACrB,UAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,MAAE,OAAO,KAAK,IAAI,GAAG,CAAC;AACtB,UAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,GAAG,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;AAC9F,YAAQ,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI,GAAG,EAAE,QAAQ,KAAK,IAAI,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;AAAA,EAC5G,CAAC;AACH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/testkit.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { expect, it } from \"vitest\";\n\nimport type { Credits } from \"./credits\";\nimport { type CreditsCode, ENTRY_TYPE, ON_EXHAUSTED, type Query } from \"./port\";\n\n/** What a store (through the ledger) is run against: the ledger on that store, a clock the test moves, and accounts that exist for it. */\nexport interface LedgerHarness {\n credits: Credits;\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 /** 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}\n\n/** Midday on 26 October 2026 in Toronto (16:00 UTC): the day the scenarios start on. */\nexport const T0 = new Date(\"2026-10-26T16:00:00.000Z\");\nconst plus = (instant: Date, seconds: number) => new Date(instant.valueOf() + seconds * 1000);\nconst DAY = 86_400;\n\nasync function refusal(work: Promise<unknown>): Promise<CreditsCode> {\n try {\n await work;\n } catch (error) {\n return (error as { code: CreditsCode }).code;\n }\n throw new Error(\"expected a refusal, got an answer\");\n}\n\n/**\n * What every credits store must do, behind the same ledger: derived balances, the order of draws, idempotency, the caps of the day and\n * the month in the configured zone, expiry, `stop` against `fallback_paid`, holds, refunds. `make` gives an EMPTY ledger and the clock\n * at {@link T0}, time zone `America/Toronto`, hold lifetime 900 seconds.\n */\nexport function creditsLedgerContract(name: string, make: () => Promise<LedgerHarness>) {\n const grant = (h: LedgerHarness, accountId: string, over: Partial<Parameters<Credits[\"grant\"]>[1]> = {}) =>\n h.credits.grant(h.tx, {\n accountId,\n amount: 10,\n expiresAt: plus(T0, 30 * DAY),\n onExhausted: ON_EXHAUSTED.stop,\n source: \"coupon:test\",\n idempotencyKey: randomUUID(),\n ...over,\n });\n const buy = (h: LedgerHarness, accountId: string, amount: number) =>\n h.credits.purchase(h.tx, { accountId, amount, source: \"purchase:pi_1\", idempotencyKey: randomUUID() });\n const consume = (h: LedgerHarness, accountId: string, amount: number, idempotencyKey: string = randomUUID()) =>\n h.credits.consume(h.tx, { accountId, amount, reason: \"audit:1\", idempotencyKey });\n\n it(`${name}: derives the balance from the ledger: a grant and a purchase, the history newest first`, async () => {\n const h = await make();\n const a = await h.newAccount();\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 0, available: 0, grants: [] });\n await grant(h, a, { amount: 5 });\n await buy(h, a, 7);\n expect(await h.credits.balance(h.tx, a)).toMatchObject({\n total: 12,\n granted: 5,\n purchased: 7,\n held: 0,\n available: 12,\n });\n const lines = await h.credits.history(h.tx, a);\n expect(lines.map((l) => [l.type, l.amount])).toEqual([\n [ENTRY_TYPE.purchase, 7],\n [ENTRY_TYPE.grant, 5],\n ]);\n });\n\n it(`${name}: a grant or a purchase with a key seen before changes nothing`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const first = await grant(h, a, { idempotencyKey: \"g-1\", amount: 4 });\n const again = await grant(h, a, { idempotencyKey: \"g-1\", amount: 4 });\n expect(again).toEqual({ grantId: first.grantId, replayed: true });\n const p = await h.credits.purchase(h.tx, { accountId: a, amount: 3, source: \"purchase:x\", idempotencyKey: \"p-1\" });\n expect(\n (await h.credits.purchase(h.tx, { accountId: a, amount: 3, source: \"purchase:x\", idempotencyKey: \"p-1\" }))\n .grantId,\n ).toBe(p.grantId);\n expect((await h.credits.balance(h.tx, a)).total).toBe(7);\n expect(await h.credits.history(h.tx, a)).toHaveLength(2);\n });\n\n it(`${name}: refuses a key already used by another account`, async () => {\n const h = await make();\n const [a, b] = [await h.newAccount(), await h.newAccount()];\n await grant(h, a, { idempotencyKey: \"shared\" });\n expect(await refusal(grant(h, b, { idempotencyKey: \"shared\" }))).toBe(\"CREDITS_KEY_REUSED\");\n await consume(h, a, 1, \"c-shared\");\n expect(await refusal(consume(h, b, 1, \"c-shared\"))).toBe(\"CREDITS_KEY_REUSED\");\n });\n\n it(`${name}: spends from the pot that lapses first, then the next; purchased credits last`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const late = await grant(h, a, {\n amount: 5,\n expiresAt: plus(T0, 20 * DAY),\n onExhausted: ON_EXHAUSTED.fallbackPaid,\n });\n const soon = await grant(h, a, { amount: 3, expiresAt: plus(T0, 5 * DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });\n const paid = await buy(h, a, 10);\n const out = await consume(h, a, 4);\n expect(out.draws).toEqual([\n { grantId: soon.grantId, amount: 3 },\n { grantId: late.grantId, amount: 1 },\n ]);\n const rest = await consume(h, a, 4 + 4 + 1);\n expect(rest.draws).toEqual([\n { grantId: late.grantId, amount: 4 },\n { grantId: paid.grantId, amount: 5 },\n ]);\n });\n\n it(`${name}: a consume with a key seen before answers the first answer and spends nothing more`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 5 });\n const first = await consume(h, a, 2, \"job-1\");\n const again = await consume(h, a, 2, \"job-1\");\n expect(first.replayed).toBe(false);\n expect(again).toEqual({ draws: first.draws, replayed: true });\n expect((await h.credits.balance(h.tx, a)).total).toBe(3);\n });\n\n it(`${name}: no credits at all is a PAYWALL, and a refusal writes nothing`, async () => {\n const h = await make();\n const a = await h.newAccount();\n expect(await refusal(consume(h, a, 1))).toBe(\"PAYWALL\");\n await buy(h, a, 2);\n expect(await refusal(consume(h, a, 3))).toBe(\"PAYWALL\");\n expect((await h.credits.balance(h.tx, a)).total).toBe(2);\n expect(await h.credits.history(h.tx, a)).toHaveLength(1);\n });\n\n it(`${name}: a stop pot that is empty answers CREDITS_EXHAUSTED even if purchased credits exist; fallback_paid moves on to them`, async () => {\n const h = await make();\n const [stop, fall] = [await h.newAccount(), await h.newAccount()];\n await grant(h, stop, { amount: 2, onExhausted: ON_EXHAUSTED.stop });\n await buy(h, stop, 5);\n await consume(h, stop, 2);\n expect(await refusal(consume(h, stop, 1))).toBe(\"CREDITS_EXHAUSTED\");\n expect(await refusal(consume(h, stop, 3))).toBe(\"CREDITS_EXHAUSTED\");\n expect((await h.credits.balance(h.tx, stop)).purchased).toBe(5);\n\n await grant(h, fall, { amount: 2, onExhausted: ON_EXHAUSTED.fallbackPaid });\n await buy(h, fall, 5);\n const out = await consume(h, fall, 4);\n expect(out.draws).toHaveLength(2);\n expect(out.draws.map((d) => d.amount)).toEqual([2, 2]);\n const none = await h.newAccount();\n await grant(h, none, { amount: 1, onExhausted: ON_EXHAUSTED.fallbackPaid });\n await consume(h, none, 1);\n expect(await refusal(consume(h, none, 1))).toBe(\"PAYWALL\");\n });\n\n it(`${name}: the daily cap holds, and a new calendar day in the configured zone opens it again`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 20, capPerDay: 3 });\n await consume(h, a, 2);\n expect(await refusal(consume(h, a, 2))).toBe(\"CREDITS_CAP_DAY\");\n await consume(h, a, 1);\n // 23:30 in Toronto is still the same day; 00:30 is the next one\n h.setNow(new Date(\"2026-10-27T03:30:00.000Z\"));\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_DAY\");\n h.setNow(new Date(\"2026-10-27T04:30:00.000Z\"));\n const next = await consume(h, a, 3);\n expect(next.draws.reduce((n, d) => n + d.amount, 0)).toBe(3);\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_DAY\");\n });\n\n it(`${name}: the monthly cap holds across the month boundary of the zone (daylight saving ends that night)`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 50, capPerMonth: 4, expiresAt: plus(T0, 60 * DAY) });\n h.setNow(new Date(\"2026-10-31T10:00:00.000Z\"));\n await consume(h, a, 4);\n h.setNow(new Date(\"2026-11-01T03:59:59.000Z\")); // 23:59:59 on 31 October in Toronto\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_MONTH\");\n h.setNow(new Date(\"2026-11-01T04:00:00.000Z\")); // midnight, 1 November\n await consume(h, a, 4);\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_MONTH\");\n });\n\n it(`${name}: the cap of one pot does not stop the next pot`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const capped = await grant(h, a, { amount: 10, capPerDay: 2, expiresAt: plus(T0, 5 * DAY) });\n const open = await grant(h, a, { amount: 10, expiresAt: plus(T0, 9 * DAY) });\n const out = await consume(h, a, 5);\n expect(out.draws).toEqual([\n { grantId: capped.grantId, amount: 2 },\n { grantId: open.grantId, amount: 3 },\n ]);\n });\n\n it(`${name}: a pot is not spendable after its date, and expire() writes off what is left once`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const g = await grant(h, a, { amount: 6, expiresAt: plus(T0, DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });\n await consume(h, a, 2);\n h.setNow(plus(T0, DAY));\n expect((await h.credits.balance(h.tx, a)).total).toBe(0);\n expect(await refusal(consume(h, a, 1))).toBe(\"PAYWALL\");\n expect(await h.credits.expire(h.tx, g.grantId)).toEqual({ expired: 4 });\n expect(await h.credits.expire(h.tx, g.grantId)).toEqual({ expired: 0 });\n expect((await h.credits.history(h.tx, a)).map((l) => [l.type, l.amount])).toEqual([\n [ENTRY_TYPE.expiry, -4],\n [ENTRY_TYPE.debit, -2],\n [ENTRY_TYPE.grant, 6],\n ]);\n });\n\n it(`${name}: a revoked pot (expire before its date) gives nothing, and a stop pot closed this way does not block purchased credits`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const g = await grant(h, a, { amount: 3, onExhausted: ON_EXHAUSTED.stop });\n await buy(h, a, 2);\n await h.credits.expire(h.tx, g.grantId);\n expect((await h.credits.balance(h.tx, a)).total).toBe(2);\n expect((await consume(h, a, 2)).draws).toHaveLength(1);\n });\n\n it(`${name}: the sweep closes lapsed pots that still hold credits, oldest first, and skips closed ones`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 3, expiresAt: plus(T0, DAY) });\n await grant(h, a, { amount: 2, expiresAt: plus(T0, 2 * DAY) });\n await grant(h, a, { amount: 9, expiresAt: plus(T0, 10 * DAY) });\n h.setNow(plus(T0, 3 * DAY));\n expect(await h.credits.expireDue(h.tx)).toBe(2);\n expect(await h.credits.expireDue(h.tx)).toBe(0);\n expect((await h.credits.balance(h.tx, a)).total).toBe(9);\n });\n\n it(`${name}: a hold keeps credits back from others, settles for the real cost, and the rest is free again`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 5 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 4, idempotencyKey: \"h-1\" });\n expect(hold.replayed).toBe(false);\n expect((await h.credits.hold(h.tx, { accountId: a, amount: 4, idempotencyKey: \"h-1\" })).holdId).toBe(hold.holdId);\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 5, held: 4, available: 1 });\n expect(await refusal(consume(h, a, 2))).toBe(\"CREDITS_EXHAUSTED\");\n expect((await consume(h, a, 1)).draws).toHaveLength(1);\n const settled = await h.credits.settle(h.tx, hold.holdId, { amount: 3, reason: \"audit:9\" });\n expect(settled.draws.reduce((n, d) => n + d.amount, 0)).toBe(3);\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 1, held: 0 });\n expect(await h.credits.settle(h.tx, hold.holdId)).toMatchObject({ replayed: true });\n expect((await h.credits.balance(h.tx, a)).total).toBe(1);\n expect(await refusal(h.credits.release(h.tx, hold.holdId))).toBe(\"CREDITS_HOLD_CLOSED\");\n });\n\n it(`${name}: a hold counts against the caps, a hold that cannot be covered is refused, and a release frees it`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 10, capPerDay: 3 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 3, idempotencyKey: \"h-1\" });\n expect(await refusal(h.credits.hold(h.tx, { accountId: a, amount: 1, idempotencyKey: \"h-2\" }))).toBe(\n \"CREDITS_CAP_DAY\",\n );\n await h.credits.release(h.tx, hold.holdId);\n await h.credits.release(h.tx, hold.holdId);\n expect(await refusal(h.credits.settle(h.tx, hold.holdId))).toBe(\"CREDITS_HOLD_CLOSED\");\n expect((await h.credits.balance(h.tx, a)).held).toBe(0);\n await h.credits.hold(h.tx, { accountId: a, amount: 3, idempotencyKey: \"h-3\" });\n });\n\n it(`${name}: a hold lapses by itself after its lifetime; settling for more than it held, or an unknown hold, is refused`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 5 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 5, ttlSeconds: 60, idempotencyKey: \"h-1\" });\n expect(await refusal(h.credits.settle(h.tx, hold.holdId, { amount: 6 }))).toBe(\"CREDITS_BAD_AMOUNT\");\n expect((await h.credits.balance(h.tx, a)).held).toBe(5);\n h.setNow(plus(T0, 61));\n expect((await h.credits.balance(h.tx, a)).held).toBe(0);\n expect((await consume(h, a, 5)).draws).toHaveLength(1);\n expect(await refusal(h.credits.settle(h.tx, randomUUID()))).toBe(\"CREDITS_UNKNOWN_HOLD\");\n expect(await refusal(h.credits.release(h.tx, randomUUID()))).toBe(\"CREDITS_UNKNOWN_HOLD\");\n });\n\n it(`${name}: a hold settled for nothing spends nothing and is released`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 5 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 2, idempotencyKey: \"h-0\" });\n expect(await h.credits.settle(h.tx, hold.holdId, { amount: 0 })).toEqual({ draws: [], replayed: false });\n expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 5, held: 0 });\n });\n\n it(`${name}: a refund gives back to the same pots, once, whatever the number of calls`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const g = await grant(h, a, { amount: 5, onExhausted: ON_EXHAUSTED.fallbackPaid });\n await buy(h, a, 5);\n const spent = await consume(h, a, 7, \"job-7\");\n expect(spent.draws).toHaveLength(2);\n expect(await h.credits.refund(h.tx, { accountId: a, consumed: \"job-7\", reason: \"the run failed\" })).toEqual({\n refunded: 7,\n replayed: false,\n });\n expect(await h.credits.refund(h.tx, { accountId: a, consumed: \"job-7\", reason: \"the run failed\" })).toEqual({\n refunded: 7,\n replayed: true,\n });\n const b = await h.credits.balance(h.tx, a);\n expect(b.total).toBe(10);\n expect(b.grants.find((x) => x.id === g.grantId)?.remaining).toBe(5);\n expect(await refusal(h.credits.refund(h.tx, { accountId: a, consumed: \"nope\", reason: \"x\" }))).toBe(\n \"CREDITS_NOTHING_TO_REFUND\",\n );\n const other = await h.newAccount();\n expect(await refusal(h.credits.refund(h.tx, { accountId: other, consumed: \"job-7\", reason: \"x\" }))).toBe(\n \"CREDITS_KEY_REUSED\",\n );\n });\n\n it(`${name}: a refund of a settled hold, and a refund counts back against the cap`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 10, capPerDay: 2 });\n const hold = await h.credits.hold(h.tx, { accountId: a, amount: 2, idempotencyKey: \"h-1\" });\n await h.credits.settle(h.tx, hold.holdId);\n expect(await refusal(consume(h, a, 1))).toBe(\"CREDITS_CAP_DAY\");\n await h.credits.refund(h.tx, { accountId: a, settledHold: hold.holdId, reason: \"failed\" });\n expect((await consume(h, a, 2)).draws).toHaveLength(1);\n });\n\n it(`${name}: an adjustment corrects a pot, never below zero, once per key`, async () => {\n const h = await make();\n const a = await h.newAccount();\n const g = await grant(h, a, { amount: 3 });\n expect(\n await h.credits.adjust(h.tx, {\n accountId: a,\n grantId: g.grantId,\n amount: 2,\n reason: \"goodwill\",\n idempotencyKey: \"adj-1\",\n }),\n ).toEqual({ replayed: false });\n expect(\n await h.credits.adjust(h.tx, {\n accountId: a,\n grantId: g.grantId,\n amount: 2,\n reason: \"goodwill\",\n idempotencyKey: \"adj-1\",\n }),\n ).toEqual({ replayed: true });\n expect((await h.credits.balance(h.tx, a)).total).toBe(5);\n expect(\n await refusal(\n h.credits.adjust(h.tx, {\n accountId: a,\n grantId: g.grantId,\n amount: -6,\n reason: \"oops\",\n idempotencyKey: \"adj-2\",\n }),\n ),\n ).toBe(\"CREDITS_BAD_AMOUNT\");\n await h.credits.adjust(h.tx, {\n accountId: a,\n grantId: g.grantId,\n amount: -5,\n reason: \"revoked\",\n idempotencyKey: \"adj-3\",\n });\n expect((await h.credits.balance(h.tx, a)).total).toBe(0);\n expect(\n await refusal(\n h.credits.adjust(h.tx, {\n accountId: a,\n grantId: randomUUID(),\n amount: 1,\n reason: \"x\",\n idempotencyKey: \"adj-4\",\n }),\n ),\n ).toBe(\"CREDITS_UNKNOWN_GRANT\");\n });\n\n it(`${name}: announces a grant, and a pot running out, once each`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 2, idempotencyKey: \"g-1\" });\n await grant(h, a, { amount: 2, idempotencyKey: \"g-1\" });\n await consume(h, a, 1);\n await consume(h, a, 1);\n const types = h.announced().map((m) => m.type);\n expect(types.filter((t) => t === \"evt.credit.granted.v1\")).toHaveLength(1);\n expect(types.filter((t) => t === \"evt.credit.exhausted.v1\")).toHaveLength(1);\n expect(h.announced().find((m) => m.type === \"evt.credit.exhausted.v1\")?.payload).toMatchObject({\n accountId: a,\n bucket: \"granted\",\n onExhausted: \"stop\",\n source: \"coupon:test\",\n });\n });\n\n it(`${name}: history pages back from a date`, async () => {\n const h = await make();\n const a = await h.newAccount();\n await grant(h, a, { amount: 9 });\n h.setNow(plus(T0, 60));\n await consume(h, a, 1);\n h.setNow(plus(T0, 120));\n await consume(h, a, 1);\n expect((await h.credits.history(h.tx, a, { limit: 2 })).map((l) => l.amount)).toEqual([-1, -1]);\n expect((await h.credits.history(h.tx, a, { before: plus(T0, 100) })).map((l) => l.amount)).toEqual([-1, 9]);\n });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/credits",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "One credits ledger for every Krizaka product: grants that expire with daily and monthly caps, purchases, debits, refunds, holds. Append-only; the balance is derived. Concurrent consumption never goes negative and never spends twice. No database client, no wall clock.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"krizaka",
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"typescript": "^5.9.3",
|
|
57
57
|
"vitest": "^4.1.11",
|
|
58
58
|
"zod": "^4.6.7",
|
|
59
|
-
"@krizaka/config": "^0.
|
|
59
|
+
"@krizaka/config": "^0.20.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@krizaka/intl": "^0.4.1",
|
|
63
|
+
"@krizaka/db": "^0.3.1",
|
|
63
64
|
"@krizaka/time": "^0.2.0",
|
|
64
|
-
"@krizaka/settings": "^0.
|
|
65
|
-
"@krizaka/db": "^0.3.1"
|
|
65
|
+
"@krizaka/settings": "^0.6.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"vitest": ">=4",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rules.ts"],"sourcesContent":["import { isAfter } from \"@krizaka/time\";\n\nimport { BUCKET, CREDITS_CODE, type CreditsCode, CreditsError, type CreditsSettings, type GrantState, ON_EXHAUSTED } from \"./port\";\n\n/** How much of one grant a consumption takes. */\nexport interface Draw {\n grantId: string;\n amount: number;\n}\n\nexport type Plan = { ok: true; draws: Draw[] } | { ok: false; code: CreditsCode };\n\n/** The defaults and bounds of the settings (the admin changes them without a redeploy). */\nexport const CREDITS_DEFAULTS: CreditsSettings = { timeZone: \"America/Toronto\", holdTtlSeconds: 900 };\n// @fixed: bounds on the lifetime of a hold, in seconds: 30 seconds to a day (a sanity range, not a business parameter)\nexport const HOLD_TTL_BOUNDS = [30, 86_400] as const;\n\nconst alive = (s: GrantState, now: Date) => !s.closed && (s.grant.expiresAt === null || isAfter(s.grant.expiresAt, now));\nconst byExpiry = (a: GrantState, b: GrantState) =>\n (a.grant.expiresAt?.valueOf() ?? Infinity) - (b.grant.expiresAt?.valueOf() ?? Infinity) || a.grant.createdAt.valueOf() - b.grant.createdAt.valueOf() || a.grant.id.localeCompare(b.grant.id);\n\n/** What the granted pots can give, each within its caps, and the first cap that held one back. */\nfunction drawGranted(granted: readonly GrantState[], need: number): { draws: Draw[]; left: number; capped: CreditsCode | null } {\n const draws: Draw[] = [];\n let left = need;\n let capped: CreditsCode | null = null;\n for (const s of granted) {\n if (left === 0) break;\n const dayLeft = s.grant.capPerDay === null ? Infinity : Math.max(0, s.grant.capPerDay - s.usedToday);\n const monthLeft = s.grant.capPerMonth === null ? Infinity : Math.max(0, s.grant.capPerMonth - s.usedThisMonth);\n const take = Math.min(left, s.remaining, dayLeft, monthLeft);\n if (take < Math.min(left, s.remaining)) capped ??= dayLeft <= monthLeft ? CREDITS_CODE.capDay : CREDITS_CODE.capMonth;\n if (take > 0) {\n draws.push({ grantId: s.grant.id, amount: take });\n left -= take;\n }\n }\n return { draws, left, capped };\n}\n\n/** What the purchased pots can give, the oldest first. */\nfunction drawPurchased(purchased: readonly GrantState[], need: number): { draws: Draw[]; left: number } {\n const draws: Draw[] = [];\n let left = need;\n const oldestFirst = [...purchased].sort((a, b) => a.grant.createdAt.valueOf() - b.grant.createdAt.valueOf() || a.grant.id.localeCompare(b.grant.id));\n for (const s of oldestFirst) {\n if (left === 0) break;\n const take = Math.min(left, s.remaining);\n draws.push({ grantId: s.grant.id, amount: take });\n left -= take;\n }\n return { draws, left };\n}\n\n/**\n * Plans where `need` credits come from, or says why they cannot. Pure: the same states give the same plan.\n *\n * Granted credits go first, the pot that lapses first first, each within its caps of the day and the month. If the granted pots cannot\n * give it all: a pot held back by a cap answers `CREDITS_CAP_DAY` / `CREDITS_CAP_MONTH` (the cap is a fair-use limit: purchased\n * credits do not bypass it); else a `stop` pot that is empty answers `CREDITS_EXHAUSTED`; else the purchased credits give the rest, and\n * when they cannot, `PAYWALL`. Nothing is partial: the plan covers the whole amount or refuses.\n * @param states every grant of the account with what is left (`CreditStore.grantStates`)\n * @param need credits wanted, a positive integer\n * @param now the injected clock's instant\n */\nexport function planDraws(states: readonly GrantState[], need: number, now: Date): Plan {\n const live = states.filter((s) => alive(s, now));\n const granted = live.filter((s) => s.grant.bucket === BUCKET.granted && s.remaining > 0).sort(byExpiry);\n const first = drawGranted(granted, need);\n if (first.left === 0) return { ok: true, draws: first.draws };\n if (first.capped) return { ok: false, code: first.capped };\n const taken = new Map(first.draws.map((d) => [d.grantId, d.amount]));\n const stopped = live.some((s) => s.grant.bucket === BUCKET.granted && s.grant.onExhausted === ON_EXHAUSTED.stop && s.remaining - (taken.get(s.grant.id) ?? 0) <= 0);\n if (stopped) return { ok: false, code: CREDITS_CODE.exhausted };\n const rest = drawPurchased(live.filter((s) => s.grant.bucket === BUCKET.purchased && s.remaining > 0), first.left);\n return rest.left === 0 ? { ok: true, draws: [...first.draws, ...rest.draws] } : { ok: false, code: CREDITS_CODE.paywall };\n}\n\n/** Drops the first `skip` credits of a plan (the ones already held back by active holds), keeping the order. */\nexport function skipFirst(draws: readonly Draw[], skip: number): Draw[] {\n let toSkip = skip;\n const out: Draw[] = [];\n for (const d of draws) {\n const dropped = Math.min(toSkip, d.amount);\n toSkip -= dropped;\n if (d.amount > dropped) out.push({ grantId: d.grantId, amount: d.amount - dropped });\n }\n return out;\n}\n\n/** Whether `n` is an amount of credits: a positive safe integer. @throws CreditsError `CREDITS_BAD_AMOUNT` */\nexport function checkAmount(n: number, what = \"an amount\"): number {\n if (!Number.isSafeInteger(n) || n <= 0) throw new CreditsError(CREDITS_CODE.badAmount, `${what} of credits is a positive integer, got ${n}`);\n return n;\n}\n\nconst KEY = /^[\\x21-\\x7e]{1,150}$/;\n/** An idempotency key: printable ASCII without spaces, 1 to 150 characters. @throws CreditsError `CREDITS_BAD_KEY` */\nexport function checkKey(key: string): string {\n if (!KEY.test(key) || key.includes(\":\")) throw new CreditsError(CREDITS_CODE.badKey, \"An idempotency key is 1 to 150 printable characters, without spaces or a colon\");\n return key;\n}\n\n/** Checks the settings the service was given. @throws CreditsError `CREDITS_BAD_SETTINGS` */\nexport function checkSettings(s: CreditsSettings): CreditsSettings {\n try {\n new Intl.DateTimeFormat(\"en\", { timeZone: s.timeZone });\n } catch {\n throw new CreditsError(CREDITS_CODE.badSettings, `Not an IANA time zone: ${s.timeZone}`);\n }\n if (!Number.isSafeInteger(s.holdTtlSeconds) || s.holdTtlSeconds < HOLD_TTL_BOUNDS[0] || s.holdTtlSeconds > HOLD_TTL_BOUNDS[1]) {\n throw new CreditsError(CREDITS_CODE.badSettings, `holdTtlSeconds is ${HOLD_TTL_BOUNDS[0]} to ${HOLD_TTL_BOUNDS[1]}`);\n }\n return s;\n}\n\n// @fixed: the longest source a line of the ledger carries (its CHECK)\nconst MAX_SOURCE = 200;\n\n/** The shape of what `grant` and `purchase` take, for the checks. */\ninterface NewGrantInput {\n accountId: string;\n amount: number;\n source: string;\n idempotencyKey: string;\n expiresAt?: Date;\n capPerDay?: number | null;\n capPerMonth?: number | null;\n onExhausted?: string;\n}\n\n/** The checks of a granted pot's own terms: it lapses in the future, its caps are whole numbers, it says what happens when it runs out. */\nfunction checkGrantedTerms(input: NewGrantInput, now: Date): void {\n if (!(input.expiresAt instanceof Date) || Number.isNaN(input.expiresAt.valueOf()) || input.expiresAt <= now) throw new CreditsError(CREDITS_CODE.badGrant, \"A granted pot lapses in the future\");\n for (const cap of [input.capPerDay, input.capPerMonth]) if (cap !== undefined && cap !== null) checkAmount(cap, \"a cap\");\n if (input.onExhausted !== ON_EXHAUSTED.stop && input.onExhausted !== ON_EXHAUSTED.fallbackPaid) throw new CreditsError(CREDITS_CODE.badGrant, \"onExhausted is stop or fallback_paid\");\n}\n\n/**\n * Checks what `grant` or `purchase` was given, before any store call.\n * @throws CreditsError `CREDITS_BAD_AMOUNT`, `CREDITS_BAD_KEY` or `CREDITS_BAD_GRANT`\n */\nexport function checkNewGrant(kind: \"grant\" | \"purchase\", input: NewGrantInput, now: Date): void {\n checkAmount(input.amount, \"a grant\");\n checkKey(input.idempotencyKey);\n if (!input.accountId) throw new CreditsError(CREDITS_CODE.badGrant, \"A grant has an account\");\n if (!input.source || input.source.length > MAX_SOURCE) throw new CreditsError(CREDITS_CODE.badGrant, `A source is 1 to ${MAX_SOURCE} characters`);\n if (kind === \"grant\") checkGrantedTerms(input, now);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/port.ts"],"sourcesContent":["/**\n * The ports of « credits »: ONE ledger of usage credits per account. Generic: no product import, no database client (a `Query`\n * comes in), no wall clock (a `Clock` comes in). Credits are integers (never floats). The ledger is append-only and the balance is\n * DERIVED from it: nothing stores a balance that could drift.\n *\n * Two kinds of credit live in it. A GRANT is free usage (a coupon, a monthly allowance, a demo): it expires, may be capped per day or\n * month, and says what happens when it runs out (`stop`, or `fallback_paid`: go on with what the person bought). A PURCHASE is paid\n * usage: it does not expire and is never capped.\n */\nexport type { Clock, Query } from \"@krizaka/db\";\n\nexport const BUCKET = { granted: \"granted\", purchased: \"purchased\" } as const;\nexport type Bucket = (typeof BUCKET)[keyof typeof BUCKET];\n\n/** What happens when a grant has run out: refuse (`stop`), or move on to purchased credits (`fallback_paid`). */\nexport const ON_EXHAUSTED = { stop: \"stop\", fallbackPaid: \"fallback_paid\" } as const;\nexport type OnExhausted = (typeof ON_EXHAUSTED)[keyof typeof ON_EXHAUSTED];\n\n/** The kinds of line of the ledger. Amounts are signed: credits in are positive, credits out negative. */\nexport const ENTRY_TYPE = { grant: \"GRANT\", purchase: \"PURCHASE\", debit: \"DEBIT\", refund: \"REFUND\", expiry: \"EXPIRY\", adjustment: \"ADJUSTMENT\" } as const;\nexport type EntryType = (typeof ENTRY_TYPE)[keyof typeof ENTRY_TYPE];\n\nexport const HOLD_STATUS = { active: \"active\", settled: \"settled\", released: \"released\" } as const;\nexport type HoldStatus = (typeof HOLD_STATUS)[keyof typeof HOLD_STATUS];\n\n/** A pot of credits. Immutable: what is left of it is derived from the ledger. */\nexport interface Grant {\n id: string;\n accountId: string;\n bucket: Bucket;\n /** What it started with. */\n amount: number;\n /** The most it gives in a calendar day (in the settings' time zone), or none. Granted only. */\n capPerDay: number | null;\n /** The most it gives in a calendar month, or none. Granted only. */\n capPerMonth: number | null;\n /** When it lapses. Required for a granted pot, none for a purchased one. */\n expiresAt: Date | null;\n onExhausted: OnExhausted;\n /** Where it comes from (`coupon:<redemptionId>`, `plan:monthly`, `purchase:<paymentRef>`). */\n source: string;\n createdAt: Date;\n}\n\n/** One line of the ledger. */\nexport interface Entry {\n id: string;\n accountId: string;\n grantId: string | null;\n type: EntryType;\n /** Signed. */\n amount: number;\n reason: string;\n /** The caller's key for the whole operation: a replay finds its lines. */\n operation: string;\n /** Unique: `<operation>:<n>`. */\n idempotencyKey: string;\n /** For a REFUND: the operation it gives back. */\n reverses: string | null;\n holdId: string | null;\n at: Date;\n}\n\nexport interface Hold {\n id: string;\n accountId: string;\n amount: number;\n expiresAt: Date;\n status: HoldStatus;\n /** What it settled for, when settled. */\n settledAmount: number | null;\n createdAt: Date;\n closedAt: Date | null;\n operation: string;\n}\n\n/** A grant as the planner sees it: what is left, what it gave today and this month, and whether it was closed by an expiry line. */\nexport interface GrantState {\n grant: Grant;\n remaining: number;\n usedToday: number;\n usedThisMonth: number;\n /** An EXPIRY line closed it (revoked, or swept after its date). */\n closed: boolean;\n}\n\n/** The calendar the caps are counted in: the zone, and the local day and month of « now » in it (`2026-10-26`, `2026-10`). */\nexport interface Calendar {\n timeZone: string;\n day: string;\n month: string;\n}\n\n/** Where the ledger lives. Every adapter runs `creditStoreContract` (`@krizaka/credits/testkit`). Times come from the caller. */\nexport interface CreditStore {\n /** Serialises the writers of one account until the transaction ends (an advisory lock). Call it before reading to decide. */\n lock(accountId: string): Promise<void>;\n insertGrant(grant: Grant): Promise<void>;\n grantById(grantId: string): Promise<Grant | null>;\n /** The account's grants, with what is left and what each gave in the calendar's day and month. Includes lapsed and closed ones. */\n grantStates(accountId: string, calendar: Calendar): Promise<GrantState[]>;\n /** Writes lines. A duplicate `idempotencyKey` is a bug of the caller (it checked `entriesOf` first, under the lock) and throws. */\n insertEntries(entries: Entry[]): Promise<void>;\n /** The lines of one operation, in order. */\n entriesOf(operation: string): Promise<Entry[]>;\n /** The lines of the account, newest first. */\n history(accountId: string, options: { limit: number; before?: Date }): Promise<Entry[]>;\n insertHold(hold: Hold): Promise<void>;\n holdById(holdId: string): Promise<Hold | null>;\n holdByOperation(operation: string): Promise<Hold | null>;\n /** What the account's active holds that have not expired at `now` keep back, except `exceptHoldId`. */\n heldAmount(accountId: string, now: Date, exceptHoldId?: string): Promise<number>;\n closeHold(holdId: string, status: typeof HOLD_STATUS.settled | typeof HOLD_STATUS.released, settledAmount: number | null, at: Date): Promise<void>;\n /** Granted pots past their date that still hold credits and no EXPIRY line, oldest first. */\n lapsedWithCredits(now: Date, limit: number): Promise<{ grantId: string; accountId: string }[]>;\n}\n\n/** The knobs of the ledger, read from the admin's settings (`@krizaka/credits/settings`). */\nexport interface CreditsSettings {\n /** The IANA zone that days and months are counted in. */\n timeZone: string;\n /** Seconds a hold keeps credits back when the caller names no lifetime. */\n holdTtlSeconds: number;\n}\n\n/** Stable codes of the refusals of this package. */\nexport const CREDITS_CODE = {\n capDay: \"CREDITS_CAP_DAY\",\n capMonth: \"CREDITS_CAP_MONTH\",\n exhausted: \"CREDITS_EXHAUSTED\",\n paywall: \"PAYWALL\",\n badAmount: \"CREDITS_BAD_AMOUNT\",\n badGrant: \"CREDITS_BAD_GRANT\",\n badKey: \"CREDITS_BAD_KEY\",\n badSettings: \"CREDITS_BAD_SETTINGS\",\n unknownGrant: \"CREDITS_UNKNOWN_GRANT\",\n unknownHold: \"CREDITS_UNKNOWN_HOLD\",\n holdClosed: \"CREDITS_HOLD_CLOSED\",\n keyReused: \"CREDITS_KEY_REUSED\",\n nothingToRefund: \"CREDITS_NOTHING_TO_REFUND\",\n} as const;\nexport type CreditsCode = (typeof CREDITS_CODE)[keyof typeof CREDITS_CODE];\n\n/** The HTTP status a route may answer for each refusal. */\nexport const CREDITS_STATUS: Record<CreditsCode, number> = {\n CREDITS_CAP_DAY: 429,\n CREDITS_CAP_MONTH: 429,\n CREDITS_EXHAUSTED: 402,\n PAYWALL: 402,\n CREDITS_BAD_AMOUNT: 400,\n CREDITS_BAD_GRANT: 400,\n CREDITS_BAD_KEY: 400,\n CREDITS_BAD_SETTINGS: 500,\n CREDITS_UNKNOWN_GRANT: 404,\n CREDITS_UNKNOWN_HOLD: 404,\n CREDITS_HOLD_CLOSED: 409,\n CREDITS_KEY_REUSED: 409,\n CREDITS_NOTHING_TO_REFUND: 404,\n};\n\n/** A refusal: a stable `code`, the HTTP `status` a route may answer, and a message. */\nexport class CreditsError extends Error {\n readonly status: number;\n constructor(\n readonly code: CreditsCode,\n message: string,\n ) {\n super(message);\n this.name = \"CreditsError\";\n this.status = CREDITS_STATUS[code];\n }\n}\n"],"mappings":";AAWO,IAAM,SAAS,EAAE,SAAS,WAAW,WAAW,YAAY;AAI5D,IAAM,eAAe,EAAE,MAAM,QAAQ,cAAc,gBAAgB;AAInE,IAAM,aAAa,EAAE,OAAO,SAAS,UAAU,YAAY,OAAO,SAAS,QAAQ,UAAU,QAAQ,UAAU,YAAY,aAAa;AAGxI,IAAM,cAAc,EAAE,QAAQ,UAAU,SAAS,WAAW,UAAU,WAAW;AAwGjF,IAAM,eAAe;AAAA,EAC1B,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,WAAW;AAAA,EACX,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,iBAAiB;AACnB;AAIO,IAAM,iBAA8C;AAAA,EACzD,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,qBAAqB;AAAA,EACrB,oBAAoB;AAAA,EACpB,2BAA2B;AAC7B;AAGO,IAAM,eAAN,cAA2B,MAAM;AAAA,EAEtC,YACW,MACT,SACA;AACA,UAAM,OAAO;AAHJ;AAIT,SAAK,OAAO;AACZ,SAAK,SAAS,eAAe,IAAI;AAAA,EACnC;AAAA,EANW;AAAA,EAFF;AASX;","names":[]}
|