@krizaka/credits 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +49 -2
- package/dist/chunk-34Y2HXOB.js +109 -0
- package/dist/chunk-34Y2HXOB.js.map +1 -0
- package/dist/chunk-WDHQ7QHY.js +56 -0
- package/dist/chunk-WDHQ7QHY.js.map +1 -0
- package/dist/credits-Do0guD2C.d.ts +258 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +524 -0
- package/dist/index.js.map +1 -0
- package/dist/port-C0Qw77Bp.d.ts +163 -0
- package/dist/settings.d.ts +18 -0
- package/dist/settings.js +23 -0
- package/dist/settings.js.map +1 -0
- package/dist/testkit.d.ts +30 -0
- package/dist/testkit.js +284 -0
- package/dist/testkit.js.map +1 -0
- package/package.json +83 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ports of « credits »: ONE ledger of usage credits per account. Generic: no product import, no database client (a `Query`
|
|
3
|
+
* comes in), no wall clock (a `Clock` comes in). Credits are integers (never floats). The ledger is append-only and the balance is
|
|
4
|
+
* DERIVED from it: nothing stores a balance that could drift.
|
|
5
|
+
*
|
|
6
|
+
* 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
|
|
7
|
+
* month, and says what happens when it runs out (`stop`, or `fallback_paid`: go on with what the person bought). A PURCHASE is paid
|
|
8
|
+
* usage: it does not expire and is never capped.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare const BUCKET: {
|
|
12
|
+
readonly granted: "granted";
|
|
13
|
+
readonly purchased: "purchased";
|
|
14
|
+
};
|
|
15
|
+
type Bucket = (typeof BUCKET)[keyof typeof BUCKET];
|
|
16
|
+
/** What happens when a grant has run out: refuse (`stop`), or move on to purchased credits (`fallback_paid`). */
|
|
17
|
+
declare const ON_EXHAUSTED: {
|
|
18
|
+
readonly stop: "stop";
|
|
19
|
+
readonly fallbackPaid: "fallback_paid";
|
|
20
|
+
};
|
|
21
|
+
type OnExhausted = (typeof ON_EXHAUSTED)[keyof typeof ON_EXHAUSTED];
|
|
22
|
+
/** The kinds of line of the ledger. Amounts are signed: credits in are positive, credits out negative. */
|
|
23
|
+
declare const ENTRY_TYPE: {
|
|
24
|
+
readonly grant: "GRANT";
|
|
25
|
+
readonly purchase: "PURCHASE";
|
|
26
|
+
readonly debit: "DEBIT";
|
|
27
|
+
readonly refund: "REFUND";
|
|
28
|
+
readonly expiry: "EXPIRY";
|
|
29
|
+
readonly adjustment: "ADJUSTMENT";
|
|
30
|
+
};
|
|
31
|
+
type EntryType = (typeof ENTRY_TYPE)[keyof typeof ENTRY_TYPE];
|
|
32
|
+
declare const HOLD_STATUS: {
|
|
33
|
+
readonly active: "active";
|
|
34
|
+
readonly settled: "settled";
|
|
35
|
+
readonly released: "released";
|
|
36
|
+
};
|
|
37
|
+
type HoldStatus = (typeof HOLD_STATUS)[keyof typeof HOLD_STATUS];
|
|
38
|
+
/** A pot of credits. Immutable: what is left of it is derived from the ledger. */
|
|
39
|
+
interface Grant {
|
|
40
|
+
id: string;
|
|
41
|
+
accountId: string;
|
|
42
|
+
bucket: Bucket;
|
|
43
|
+
/** What it started with. */
|
|
44
|
+
amount: number;
|
|
45
|
+
/** The most it gives in a calendar day (in the settings' time zone), or none. Granted only. */
|
|
46
|
+
capPerDay: number | null;
|
|
47
|
+
/** The most it gives in a calendar month, or none. Granted only. */
|
|
48
|
+
capPerMonth: number | null;
|
|
49
|
+
/** When it lapses. Required for a granted pot, none for a purchased one. */
|
|
50
|
+
expiresAt: Date | null;
|
|
51
|
+
onExhausted: OnExhausted;
|
|
52
|
+
/** Where it comes from (`coupon:<redemptionId>`, `plan:monthly`, `purchase:<paymentRef>`). */
|
|
53
|
+
source: string;
|
|
54
|
+
createdAt: Date;
|
|
55
|
+
}
|
|
56
|
+
/** One line of the ledger. */
|
|
57
|
+
interface Entry {
|
|
58
|
+
id: string;
|
|
59
|
+
accountId: string;
|
|
60
|
+
grantId: string | null;
|
|
61
|
+
type: EntryType;
|
|
62
|
+
/** Signed. */
|
|
63
|
+
amount: number;
|
|
64
|
+
reason: string;
|
|
65
|
+
/** The caller's key for the whole operation: a replay finds its lines. */
|
|
66
|
+
operation: string;
|
|
67
|
+
/** Unique: `<operation>:<n>`. */
|
|
68
|
+
idempotencyKey: string;
|
|
69
|
+
/** For a REFUND: the operation it gives back. */
|
|
70
|
+
reverses: string | null;
|
|
71
|
+
holdId: string | null;
|
|
72
|
+
at: Date;
|
|
73
|
+
}
|
|
74
|
+
interface Hold {
|
|
75
|
+
id: string;
|
|
76
|
+
accountId: string;
|
|
77
|
+
amount: number;
|
|
78
|
+
expiresAt: Date;
|
|
79
|
+
status: HoldStatus;
|
|
80
|
+
/** What it settled for, when settled. */
|
|
81
|
+
settledAmount: number | null;
|
|
82
|
+
createdAt: Date;
|
|
83
|
+
closedAt: Date | null;
|
|
84
|
+
operation: string;
|
|
85
|
+
}
|
|
86
|
+
/** 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. */
|
|
87
|
+
interface GrantState {
|
|
88
|
+
grant: Grant;
|
|
89
|
+
remaining: number;
|
|
90
|
+
usedToday: number;
|
|
91
|
+
usedThisMonth: number;
|
|
92
|
+
/** An EXPIRY line closed it (revoked, or swept after its date). */
|
|
93
|
+
closed: boolean;
|
|
94
|
+
}
|
|
95
|
+
/** The calendar the caps are counted in: the zone, and the local day and month of « now » in it (`2026-10-26`, `2026-10`). */
|
|
96
|
+
interface Calendar {
|
|
97
|
+
timeZone: string;
|
|
98
|
+
day: string;
|
|
99
|
+
month: string;
|
|
100
|
+
}
|
|
101
|
+
/** Where the ledger lives. Every adapter runs `creditStoreContract` (`@krizaka/credits/testkit`). Times come from the caller. */
|
|
102
|
+
interface CreditStore {
|
|
103
|
+
/** Serialises the writers of one account until the transaction ends (an advisory lock). Call it before reading to decide. */
|
|
104
|
+
lock(accountId: string): Promise<void>;
|
|
105
|
+
insertGrant(grant: Grant): Promise<void>;
|
|
106
|
+
grantById(grantId: string): Promise<Grant | null>;
|
|
107
|
+
/** The account's grants, with what is left and what each gave in the calendar's day and month. Includes lapsed and closed ones. */
|
|
108
|
+
grantStates(accountId: string, calendar: Calendar): Promise<GrantState[]>;
|
|
109
|
+
/** Writes lines. A duplicate `idempotencyKey` is a bug of the caller (it checked `entriesOf` first, under the lock) and throws. */
|
|
110
|
+
insertEntries(entries: Entry[]): Promise<void>;
|
|
111
|
+
/** The lines of one operation, in order. */
|
|
112
|
+
entriesOf(operation: string): Promise<Entry[]>;
|
|
113
|
+
/** The lines of the account, newest first. */
|
|
114
|
+
history(accountId: string, options: {
|
|
115
|
+
limit: number;
|
|
116
|
+
before?: Date;
|
|
117
|
+
}): Promise<Entry[]>;
|
|
118
|
+
insertHold(hold: Hold): Promise<void>;
|
|
119
|
+
holdById(holdId: string): Promise<Hold | null>;
|
|
120
|
+
holdByOperation(operation: string): Promise<Hold | null>;
|
|
121
|
+
/** What the account's active holds that have not expired at `now` keep back, except `exceptHoldId`. */
|
|
122
|
+
heldAmount(accountId: string, now: Date, exceptHoldId?: string): Promise<number>;
|
|
123
|
+
closeHold(holdId: string, status: typeof HOLD_STATUS.settled | typeof HOLD_STATUS.released, settledAmount: number | null, at: Date): Promise<void>;
|
|
124
|
+
/** Granted pots past their date that still hold credits and no EXPIRY line, oldest first. */
|
|
125
|
+
lapsedWithCredits(now: Date, limit: number): Promise<{
|
|
126
|
+
grantId: string;
|
|
127
|
+
accountId: string;
|
|
128
|
+
}[]>;
|
|
129
|
+
}
|
|
130
|
+
/** The knobs of the ledger, read from the admin's settings (`@krizaka/credits/settings`). */
|
|
131
|
+
interface CreditsSettings {
|
|
132
|
+
/** The IANA zone that days and months are counted in. */
|
|
133
|
+
timeZone: string;
|
|
134
|
+
/** Seconds a hold keeps credits back when the caller names no lifetime. */
|
|
135
|
+
holdTtlSeconds: number;
|
|
136
|
+
}
|
|
137
|
+
/** Stable codes of the refusals of this package. */
|
|
138
|
+
declare const CREDITS_CODE: {
|
|
139
|
+
readonly capDay: "CREDITS_CAP_DAY";
|
|
140
|
+
readonly capMonth: "CREDITS_CAP_MONTH";
|
|
141
|
+
readonly exhausted: "CREDITS_EXHAUSTED";
|
|
142
|
+
readonly paywall: "PAYWALL";
|
|
143
|
+
readonly badAmount: "CREDITS_BAD_AMOUNT";
|
|
144
|
+
readonly badGrant: "CREDITS_BAD_GRANT";
|
|
145
|
+
readonly badKey: "CREDITS_BAD_KEY";
|
|
146
|
+
readonly badSettings: "CREDITS_BAD_SETTINGS";
|
|
147
|
+
readonly unknownGrant: "CREDITS_UNKNOWN_GRANT";
|
|
148
|
+
readonly unknownHold: "CREDITS_UNKNOWN_HOLD";
|
|
149
|
+
readonly holdClosed: "CREDITS_HOLD_CLOSED";
|
|
150
|
+
readonly keyReused: "CREDITS_KEY_REUSED";
|
|
151
|
+
readonly nothingToRefund: "CREDITS_NOTHING_TO_REFUND";
|
|
152
|
+
};
|
|
153
|
+
type CreditsCode = (typeof CREDITS_CODE)[keyof typeof CREDITS_CODE];
|
|
154
|
+
/** The HTTP status a route may answer for each refusal. */
|
|
155
|
+
declare const CREDITS_STATUS: Record<CreditsCode, number>;
|
|
156
|
+
/** A refusal: a stable `code`, the HTTP `status` a route may answer, and a message. */
|
|
157
|
+
declare class CreditsError extends Error {
|
|
158
|
+
readonly code: CreditsCode;
|
|
159
|
+
readonly status: number;
|
|
160
|
+
constructor(code: CreditsCode, message: string);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { BUCKET as B, type CreditStore as C, type Entry as E, type Grant as G, HOLD_STATUS as H, ON_EXHAUSTED as O, type Bucket as a, CREDITS_CODE as b, CREDITS_STATUS as c, type Calendar as d, type CreditsCode as e, CreditsError as f, type CreditsSettings as g, ENTRY_TYPE as h, type EntryType as i, type GrantState as j, type Hold as k, type HoldStatus as l, type OnExhausted as m };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as _krizaka_settings from '@krizaka/settings';
|
|
2
|
+
import { g as CreditsSettings } from './port-C0Qw77Bp.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The settings of « credits », a fragment the product adds to its registry (`@krizaka/credits/settings`): the admin changes the zone
|
|
6
|
+
* the caps are counted in and how long a hold keeps credits back, without a redeploy. Global scope only.
|
|
7
|
+
*/
|
|
8
|
+
declare const creditsSettings: {
|
|
9
|
+
readonly "credits.timeZone": _krizaka_settings.Def<string>;
|
|
10
|
+
readonly "credits.holdTtlSeconds": _krizaka_settings.Def<number>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The ledger's settings as `createCredits` takes them, from the product's accessor (`(key) => service.get(key, ctx)`).
|
|
14
|
+
* A key the accessor does not know falls back to the default.
|
|
15
|
+
*/
|
|
16
|
+
declare function readCreditsSettings(read: (key: keyof typeof creditsSettings) => string | number | undefined | Promise<string | number | undefined>): Promise<CreditsSettings>;
|
|
17
|
+
|
|
18
|
+
export { creditsSettings, readCreditsSettings };
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CREDITS_DEFAULTS,
|
|
3
|
+
HOLD_TTL_BOUNDS
|
|
4
|
+
} from "./chunk-34Y2HXOB.js";
|
|
5
|
+
import "./chunk-WDHQ7QHY.js";
|
|
6
|
+
|
|
7
|
+
// src/settings.ts
|
|
8
|
+
import { choice, integer } from "@krizaka/settings";
|
|
9
|
+
var ZONES = Intl.supportedValuesOf("timeZone");
|
|
10
|
+
var creditsSettings = {
|
|
11
|
+
"credits.timeZone": choice("credits", ZONES, CREDITS_DEFAULTS.timeZone),
|
|
12
|
+
"credits.holdTtlSeconds": integer("credits", CREDITS_DEFAULTS.holdTtlSeconds, { min: HOLD_TTL_BOUNDS[0], max: HOLD_TTL_BOUNDS[1], unit: "seconds" })
|
|
13
|
+
};
|
|
14
|
+
async function readCreditsSettings(read) {
|
|
15
|
+
const zone = await read("credits.timeZone");
|
|
16
|
+
const ttl = await read("credits.holdTtlSeconds");
|
|
17
|
+
return { timeZone: typeof zone === "string" ? zone : CREDITS_DEFAULTS.timeZone, holdTtlSeconds: typeof ttl === "number" ? ttl : CREDITS_DEFAULTS.holdTtlSeconds };
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
creditsSettings,
|
|
21
|
+
readCreditsSettings
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/settings.ts"],"sourcesContent":["import { choice, integer, type Registry } from \"@krizaka/settings\";\n\nimport type { CreditsSettings } from \"./port\";\nimport { CREDITS_DEFAULTS, HOLD_TTL_BOUNDS } from \"./rules\";\n\n/** Every IANA zone the runtime knows (the admin picks one; days and months are counted in it). */\nconst ZONES = Intl.supportedValuesOf(\"timeZone\") as unknown as readonly [string, ...string[]];\n\n/**\n * The settings of « credits », a fragment the product adds to its registry (`@krizaka/credits/settings`): the admin changes the zone\n * the caps are counted in and how long a hold keeps credits back, without a redeploy. Global scope only.\n */\nexport const creditsSettings = {\n \"credits.timeZone\": choice(\"credits\", ZONES, CREDITS_DEFAULTS.timeZone),\n \"credits.holdTtlSeconds\": integer(\"credits\", CREDITS_DEFAULTS.holdTtlSeconds, { min: HOLD_TTL_BOUNDS[0], max: HOLD_TTL_BOUNDS[1], unit: \"seconds\" }),\n} as const satisfies Registry;\n\n/**\n * The ledger's settings as `createCredits` takes them, from the product's accessor (`(key) => service.get(key, ctx)`).\n * A key the accessor does not know falls back to the default.\n */\nexport async function readCreditsSettings(read: (key: keyof typeof creditsSettings) => string | number | undefined | Promise<string | number | undefined>): Promise<CreditsSettings> {\n const zone = await read(\"credits.timeZone\");\n const ttl = await read(\"credits.holdTtlSeconds\");\n return { timeZone: typeof zone === \"string\" ? zone : CREDITS_DEFAULTS.timeZone, holdTtlSeconds: typeof ttl === \"number\" ? ttl : CREDITS_DEFAULTS.holdTtlSeconds };\n}\n"],"mappings":";;;;;;;AAAA,SAAS,QAAQ,eAA8B;AAM/C,IAAM,QAAQ,KAAK,kBAAkB,UAAU;AAMxC,IAAM,kBAAkB;AAAA,EAC7B,oBAAoB,OAAO,WAAW,OAAO,iBAAiB,QAAQ;AAAA,EACtE,0BAA0B,QAAQ,WAAW,iBAAiB,gBAAgB,EAAE,KAAK,gBAAgB,CAAC,GAAG,KAAK,gBAAgB,CAAC,GAAG,MAAM,UAAU,CAAC;AACrJ;AAMA,eAAsB,oBAAoB,MAA2I;AACnL,QAAM,OAAO,MAAM,KAAK,kBAAkB;AAC1C,QAAM,MAAM,MAAM,KAAK,wBAAwB;AAC/C,SAAO,EAAE,UAAU,OAAO,SAAS,WAAW,OAAO,iBAAiB,UAAU,gBAAgB,OAAO,QAAQ,WAAW,MAAM,iBAAiB,eAAe;AAClK;","names":[]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { f as Credits } from './credits-Do0guD2C.js';
|
|
2
|
+
import { Query } from '@krizaka/db';
|
|
3
|
+
import './port-C0Qw77Bp.js';
|
|
4
|
+
|
|
5
|
+
/** 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. */
|
|
6
|
+
interface LedgerHarness {
|
|
7
|
+
credits: Credits;
|
|
8
|
+
/** The query every call goes through (the transaction; a no-op for memory). */
|
|
9
|
+
tx: Query;
|
|
10
|
+
/** Moves the injected clock. */
|
|
11
|
+
setNow(instant: Date): void;
|
|
12
|
+
/** A fresh account id (a row in the owner table, when the store has one). */
|
|
13
|
+
newAccount(): Promise<string>;
|
|
14
|
+
/** The messages appended to the outbox so far. */
|
|
15
|
+
announced(): {
|
|
16
|
+
type: string;
|
|
17
|
+
id: string | undefined;
|
|
18
|
+
payload: Record<string, unknown>;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
/** Midday on 26 October 2026 in Toronto (16:00 UTC): the day the scenarios start on. */
|
|
22
|
+
declare const T0: Date;
|
|
23
|
+
/**
|
|
24
|
+
* What every credits store must do, behind the same ledger: derived balances, the order of draws, idempotency, the caps of the day and
|
|
25
|
+
* the month in the configured zone, expiry, `stop` against `fallback_paid`, holds, refunds. `make` gives an EMPTY ledger and the clock
|
|
26
|
+
* at {@link T0}, time zone `America/Toronto`, hold lifetime 900 seconds.
|
|
27
|
+
*/
|
|
28
|
+
declare function creditsLedgerContract(name: string, make: () => Promise<LedgerHarness>): void;
|
|
29
|
+
|
|
30
|
+
export { type LedgerHarness, T0, creditsLedgerContract };
|
package/dist/testkit.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ENTRY_TYPE,
|
|
3
|
+
ON_EXHAUSTED
|
|
4
|
+
} from "./chunk-WDHQ7QHY.js";
|
|
5
|
+
|
|
6
|
+
// src/testkit.ts
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
import { expect, it } from "vitest";
|
|
9
|
+
var T0 = /* @__PURE__ */ new Date("2026-10-26T16:00:00.000Z");
|
|
10
|
+
var plus = (instant, seconds) => new Date(instant.valueOf() + seconds * 1e3);
|
|
11
|
+
var DAY = 86400;
|
|
12
|
+
async function refusal(work) {
|
|
13
|
+
try {
|
|
14
|
+
await work;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return error.code;
|
|
17
|
+
}
|
|
18
|
+
throw new Error("expected a refusal, got an answer");
|
|
19
|
+
}
|
|
20
|
+
function creditsLedgerContract(name, make) {
|
|
21
|
+
const grant = (h, accountId, over = {}) => h.credits.grant(h.tx, { accountId, amount: 10, expiresAt: plus(T0, 30 * DAY), onExhausted: ON_EXHAUSTED.stop, source: "coupon:test", idempotencyKey: randomUUID(), ...over });
|
|
22
|
+
const buy = (h, accountId, amount) => h.credits.purchase(h.tx, { accountId, amount, source: "purchase:pi_1", idempotencyKey: randomUUID() });
|
|
23
|
+
const consume = (h, accountId, amount, idempotencyKey = randomUUID()) => h.credits.consume(h.tx, { accountId, amount, reason: "audit:1", idempotencyKey });
|
|
24
|
+
it(`${name}: derives the balance from the ledger: a grant and a purchase, the history newest first`, async () => {
|
|
25
|
+
const h = await make();
|
|
26
|
+
const a = await h.newAccount();
|
|
27
|
+
expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 0, available: 0, grants: [] });
|
|
28
|
+
await grant(h, a, { amount: 5 });
|
|
29
|
+
await buy(h, a, 7);
|
|
30
|
+
expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 12, granted: 5, purchased: 7, held: 0, available: 12 });
|
|
31
|
+
const lines = await h.credits.history(h.tx, a);
|
|
32
|
+
expect(lines.map((l) => [l.type, l.amount])).toEqual([[ENTRY_TYPE.purchase, 7], [ENTRY_TYPE.grant, 5]]);
|
|
33
|
+
});
|
|
34
|
+
it(`${name}: a grant or a purchase with a key seen before changes nothing`, async () => {
|
|
35
|
+
const h = await make();
|
|
36
|
+
const a = await h.newAccount();
|
|
37
|
+
const first = await grant(h, a, { idempotencyKey: "g-1", amount: 4 });
|
|
38
|
+
const again = await grant(h, a, { idempotencyKey: "g-1", amount: 4 });
|
|
39
|
+
expect(again).toEqual({ grantId: first.grantId, replayed: true });
|
|
40
|
+
const p = await h.credits.purchase(h.tx, { accountId: a, amount: 3, source: "purchase:x", idempotencyKey: "p-1" });
|
|
41
|
+
expect((await h.credits.purchase(h.tx, { accountId: a, amount: 3, source: "purchase:x", idempotencyKey: "p-1" })).grantId).toBe(p.grantId);
|
|
42
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(7);
|
|
43
|
+
expect(await h.credits.history(h.tx, a)).toHaveLength(2);
|
|
44
|
+
});
|
|
45
|
+
it(`${name}: refuses a key already used by another account`, async () => {
|
|
46
|
+
const h = await make();
|
|
47
|
+
const [a, b] = [await h.newAccount(), await h.newAccount()];
|
|
48
|
+
await grant(h, a, { idempotencyKey: "shared" });
|
|
49
|
+
expect(await refusal(grant(h, b, { idempotencyKey: "shared" }))).toBe("CREDITS_KEY_REUSED");
|
|
50
|
+
await consume(h, a, 1, "c-shared");
|
|
51
|
+
expect(await refusal(consume(h, b, 1, "c-shared"))).toBe("CREDITS_KEY_REUSED");
|
|
52
|
+
});
|
|
53
|
+
it(`${name}: spends from the pot that lapses first, then the next; purchased credits last`, async () => {
|
|
54
|
+
const h = await make();
|
|
55
|
+
const a = await h.newAccount();
|
|
56
|
+
const late = await grant(h, a, { amount: 5, expiresAt: plus(T0, 20 * DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });
|
|
57
|
+
const soon = await grant(h, a, { amount: 3, expiresAt: plus(T0, 5 * DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });
|
|
58
|
+
const paid = await buy(h, a, 10);
|
|
59
|
+
const out = await consume(h, a, 4);
|
|
60
|
+
expect(out.draws).toEqual([{ grantId: soon.grantId, amount: 3 }, { grantId: late.grantId, amount: 1 }]);
|
|
61
|
+
const rest = await consume(h, a, 4 + 4 + 1);
|
|
62
|
+
expect(rest.draws).toEqual([{ grantId: late.grantId, amount: 4 }, { grantId: paid.grantId, amount: 5 }]);
|
|
63
|
+
});
|
|
64
|
+
it(`${name}: a consume with a key seen before answers the first answer and spends nothing more`, async () => {
|
|
65
|
+
const h = await make();
|
|
66
|
+
const a = await h.newAccount();
|
|
67
|
+
await grant(h, a, { amount: 5 });
|
|
68
|
+
const first = await consume(h, a, 2, "job-1");
|
|
69
|
+
const again = await consume(h, a, 2, "job-1");
|
|
70
|
+
expect(first.replayed).toBe(false);
|
|
71
|
+
expect(again).toEqual({ draws: first.draws, replayed: true });
|
|
72
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(3);
|
|
73
|
+
});
|
|
74
|
+
it(`${name}: no credits at all is a PAYWALL, and a refusal writes nothing`, async () => {
|
|
75
|
+
const h = await make();
|
|
76
|
+
const a = await h.newAccount();
|
|
77
|
+
expect(await refusal(consume(h, a, 1))).toBe("PAYWALL");
|
|
78
|
+
await buy(h, a, 2);
|
|
79
|
+
expect(await refusal(consume(h, a, 3))).toBe("PAYWALL");
|
|
80
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(2);
|
|
81
|
+
expect(await h.credits.history(h.tx, a)).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
it(`${name}: a stop pot that is empty answers CREDITS_EXHAUSTED even if purchased credits exist; fallback_paid moves on to them`, async () => {
|
|
84
|
+
const h = await make();
|
|
85
|
+
const [stop, fall] = [await h.newAccount(), await h.newAccount()];
|
|
86
|
+
await grant(h, stop, { amount: 2, onExhausted: ON_EXHAUSTED.stop });
|
|
87
|
+
await buy(h, stop, 5);
|
|
88
|
+
await consume(h, stop, 2);
|
|
89
|
+
expect(await refusal(consume(h, stop, 1))).toBe("CREDITS_EXHAUSTED");
|
|
90
|
+
expect(await refusal(consume(h, stop, 3))).toBe("CREDITS_EXHAUSTED");
|
|
91
|
+
expect((await h.credits.balance(h.tx, stop)).purchased).toBe(5);
|
|
92
|
+
await grant(h, fall, { amount: 2, onExhausted: ON_EXHAUSTED.fallbackPaid });
|
|
93
|
+
await buy(h, fall, 5);
|
|
94
|
+
const out = await consume(h, fall, 4);
|
|
95
|
+
expect(out.draws).toHaveLength(2);
|
|
96
|
+
expect(out.draws.map((d) => d.amount)).toEqual([2, 2]);
|
|
97
|
+
const none = await h.newAccount();
|
|
98
|
+
await grant(h, none, { amount: 1, onExhausted: ON_EXHAUSTED.fallbackPaid });
|
|
99
|
+
await consume(h, none, 1);
|
|
100
|
+
expect(await refusal(consume(h, none, 1))).toBe("PAYWALL");
|
|
101
|
+
});
|
|
102
|
+
it(`${name}: the daily cap holds, and a new calendar day in the configured zone opens it again`, async () => {
|
|
103
|
+
const h = await make();
|
|
104
|
+
const a = await h.newAccount();
|
|
105
|
+
await grant(h, a, { amount: 20, capPerDay: 3 });
|
|
106
|
+
await consume(h, a, 2);
|
|
107
|
+
expect(await refusal(consume(h, a, 2))).toBe("CREDITS_CAP_DAY");
|
|
108
|
+
await consume(h, a, 1);
|
|
109
|
+
h.setNow(/* @__PURE__ */ new Date("2026-10-27T03:30:00.000Z"));
|
|
110
|
+
expect(await refusal(consume(h, a, 1))).toBe("CREDITS_CAP_DAY");
|
|
111
|
+
h.setNow(/* @__PURE__ */ new Date("2026-10-27T04:30:00.000Z"));
|
|
112
|
+
const next = await consume(h, a, 3);
|
|
113
|
+
expect(next.draws.reduce((n, d) => n + d.amount, 0)).toBe(3);
|
|
114
|
+
expect(await refusal(consume(h, a, 1))).toBe("CREDITS_CAP_DAY");
|
|
115
|
+
});
|
|
116
|
+
it(`${name}: the monthly cap holds across the month boundary of the zone (daylight saving ends that night)`, async () => {
|
|
117
|
+
const h = await make();
|
|
118
|
+
const a = await h.newAccount();
|
|
119
|
+
await grant(h, a, { amount: 50, capPerMonth: 4, expiresAt: plus(T0, 60 * DAY) });
|
|
120
|
+
h.setNow(/* @__PURE__ */ new Date("2026-10-31T10:00:00.000Z"));
|
|
121
|
+
await consume(h, a, 4);
|
|
122
|
+
h.setNow(/* @__PURE__ */ new Date("2026-11-01T03:59:59.000Z"));
|
|
123
|
+
expect(await refusal(consume(h, a, 1))).toBe("CREDITS_CAP_MONTH");
|
|
124
|
+
h.setNow(/* @__PURE__ */ new Date("2026-11-01T04:00:00.000Z"));
|
|
125
|
+
await consume(h, a, 4);
|
|
126
|
+
expect(await refusal(consume(h, a, 1))).toBe("CREDITS_CAP_MONTH");
|
|
127
|
+
});
|
|
128
|
+
it(`${name}: the cap of one pot does not stop the next pot`, async () => {
|
|
129
|
+
const h = await make();
|
|
130
|
+
const a = await h.newAccount();
|
|
131
|
+
const capped = await grant(h, a, { amount: 10, capPerDay: 2, expiresAt: plus(T0, 5 * DAY) });
|
|
132
|
+
const open = await grant(h, a, { amount: 10, expiresAt: plus(T0, 9 * DAY) });
|
|
133
|
+
const out = await consume(h, a, 5);
|
|
134
|
+
expect(out.draws).toEqual([{ grantId: capped.grantId, amount: 2 }, { grantId: open.grantId, amount: 3 }]);
|
|
135
|
+
});
|
|
136
|
+
it(`${name}: a pot is not spendable after its date, and expire() writes off what is left once`, async () => {
|
|
137
|
+
const h = await make();
|
|
138
|
+
const a = await h.newAccount();
|
|
139
|
+
const g = await grant(h, a, { amount: 6, expiresAt: plus(T0, DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });
|
|
140
|
+
await consume(h, a, 2);
|
|
141
|
+
h.setNow(plus(T0, DAY));
|
|
142
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(0);
|
|
143
|
+
expect(await refusal(consume(h, a, 1))).toBe("PAYWALL");
|
|
144
|
+
expect(await h.credits.expire(h.tx, g.grantId)).toEqual({ expired: 4 });
|
|
145
|
+
expect(await h.credits.expire(h.tx, g.grantId)).toEqual({ expired: 0 });
|
|
146
|
+
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]]);
|
|
147
|
+
});
|
|
148
|
+
it(`${name}: a revoked pot (expire before its date) gives nothing, and a stop pot closed this way does not block purchased credits`, async () => {
|
|
149
|
+
const h = await make();
|
|
150
|
+
const a = await h.newAccount();
|
|
151
|
+
const g = await grant(h, a, { amount: 3, onExhausted: ON_EXHAUSTED.stop });
|
|
152
|
+
await buy(h, a, 2);
|
|
153
|
+
await h.credits.expire(h.tx, g.grantId);
|
|
154
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(2);
|
|
155
|
+
expect((await consume(h, a, 2)).draws).toHaveLength(1);
|
|
156
|
+
});
|
|
157
|
+
it(`${name}: the sweep closes lapsed pots that still hold credits, oldest first, and skips closed ones`, async () => {
|
|
158
|
+
const h = await make();
|
|
159
|
+
const a = await h.newAccount();
|
|
160
|
+
await grant(h, a, { amount: 3, expiresAt: plus(T0, DAY) });
|
|
161
|
+
await grant(h, a, { amount: 2, expiresAt: plus(T0, 2 * DAY) });
|
|
162
|
+
await grant(h, a, { amount: 9, expiresAt: plus(T0, 10 * DAY) });
|
|
163
|
+
h.setNow(plus(T0, 3 * DAY));
|
|
164
|
+
expect(await h.credits.expireDue(h.tx)).toBe(2);
|
|
165
|
+
expect(await h.credits.expireDue(h.tx)).toBe(0);
|
|
166
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(9);
|
|
167
|
+
});
|
|
168
|
+
it(`${name}: a hold keeps credits back from others, settles for the real cost, and the rest is free again`, async () => {
|
|
169
|
+
const h = await make();
|
|
170
|
+
const a = await h.newAccount();
|
|
171
|
+
await grant(h, a, { amount: 5 });
|
|
172
|
+
const hold = await h.credits.hold(h.tx, { accountId: a, amount: 4, idempotencyKey: "h-1" });
|
|
173
|
+
expect(hold.replayed).toBe(false);
|
|
174
|
+
expect((await h.credits.hold(h.tx, { accountId: a, amount: 4, idempotencyKey: "h-1" })).holdId).toBe(hold.holdId);
|
|
175
|
+
expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 5, held: 4, available: 1 });
|
|
176
|
+
expect(await refusal(consume(h, a, 2))).toBe("CREDITS_EXHAUSTED");
|
|
177
|
+
expect((await consume(h, a, 1)).draws).toHaveLength(1);
|
|
178
|
+
const settled = await h.credits.settle(h.tx, hold.holdId, { amount: 3, reason: "audit:9" });
|
|
179
|
+
expect(settled.draws.reduce((n, d) => n + d.amount, 0)).toBe(3);
|
|
180
|
+
expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 1, held: 0 });
|
|
181
|
+
expect(await h.credits.settle(h.tx, hold.holdId)).toMatchObject({ replayed: true });
|
|
182
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(1);
|
|
183
|
+
expect(await refusal(h.credits.release(h.tx, hold.holdId))).toBe("CREDITS_HOLD_CLOSED");
|
|
184
|
+
});
|
|
185
|
+
it(`${name}: a hold counts against the caps, a hold that cannot be covered is refused, and a release frees it`, async () => {
|
|
186
|
+
const h = await make();
|
|
187
|
+
const a = await h.newAccount();
|
|
188
|
+
await grant(h, a, { amount: 10, capPerDay: 3 });
|
|
189
|
+
const hold = await h.credits.hold(h.tx, { accountId: a, amount: 3, idempotencyKey: "h-1" });
|
|
190
|
+
expect(await refusal(h.credits.hold(h.tx, { accountId: a, amount: 1, idempotencyKey: "h-2" }))).toBe("CREDITS_CAP_DAY");
|
|
191
|
+
await h.credits.release(h.tx, hold.holdId);
|
|
192
|
+
await h.credits.release(h.tx, hold.holdId);
|
|
193
|
+
expect(await refusal(h.credits.settle(h.tx, hold.holdId))).toBe("CREDITS_HOLD_CLOSED");
|
|
194
|
+
expect((await h.credits.balance(h.tx, a)).held).toBe(0);
|
|
195
|
+
await h.credits.hold(h.tx, { accountId: a, amount: 3, idempotencyKey: "h-3" });
|
|
196
|
+
});
|
|
197
|
+
it(`${name}: a hold lapses by itself after its lifetime; settling for more than it held, or an unknown hold, is refused`, async () => {
|
|
198
|
+
const h = await make();
|
|
199
|
+
const a = await h.newAccount();
|
|
200
|
+
await grant(h, a, { amount: 5 });
|
|
201
|
+
const hold = await h.credits.hold(h.tx, { accountId: a, amount: 5, ttlSeconds: 60, idempotencyKey: "h-1" });
|
|
202
|
+
expect(await refusal(h.credits.settle(h.tx, hold.holdId, { amount: 6 }))).toBe("CREDITS_BAD_AMOUNT");
|
|
203
|
+
expect((await h.credits.balance(h.tx, a)).held).toBe(5);
|
|
204
|
+
h.setNow(plus(T0, 61));
|
|
205
|
+
expect((await h.credits.balance(h.tx, a)).held).toBe(0);
|
|
206
|
+
expect((await consume(h, a, 5)).draws).toHaveLength(1);
|
|
207
|
+
expect(await refusal(h.credits.settle(h.tx, randomUUID()))).toBe("CREDITS_UNKNOWN_HOLD");
|
|
208
|
+
expect(await refusal(h.credits.release(h.tx, randomUUID()))).toBe("CREDITS_UNKNOWN_HOLD");
|
|
209
|
+
});
|
|
210
|
+
it(`${name}: a hold settled for nothing spends nothing and is released`, async () => {
|
|
211
|
+
const h = await make();
|
|
212
|
+
const a = await h.newAccount();
|
|
213
|
+
await grant(h, a, { amount: 5 });
|
|
214
|
+
const hold = await h.credits.hold(h.tx, { accountId: a, amount: 2, idempotencyKey: "h-0" });
|
|
215
|
+
expect(await h.credits.settle(h.tx, hold.holdId, { amount: 0 })).toEqual({ draws: [], replayed: false });
|
|
216
|
+
expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 5, held: 0 });
|
|
217
|
+
});
|
|
218
|
+
it(`${name}: a refund gives back to the same pots, once, whatever the number of calls`, async () => {
|
|
219
|
+
const h = await make();
|
|
220
|
+
const a = await h.newAccount();
|
|
221
|
+
const g = await grant(h, a, { amount: 5, onExhausted: ON_EXHAUSTED.fallbackPaid });
|
|
222
|
+
await buy(h, a, 5);
|
|
223
|
+
const spent = await consume(h, a, 7, "job-7");
|
|
224
|
+
expect(spent.draws).toHaveLength(2);
|
|
225
|
+
expect(await h.credits.refund(h.tx, { accountId: a, consumed: "job-7", reason: "the run failed" })).toEqual({ refunded: 7, replayed: false });
|
|
226
|
+
expect(await h.credits.refund(h.tx, { accountId: a, consumed: "job-7", reason: "the run failed" })).toEqual({ refunded: 7, replayed: true });
|
|
227
|
+
const b = await h.credits.balance(h.tx, a);
|
|
228
|
+
expect(b.total).toBe(10);
|
|
229
|
+
expect(b.grants.find((x) => x.id === g.grantId)?.remaining).toBe(5);
|
|
230
|
+
expect(await refusal(h.credits.refund(h.tx, { accountId: a, consumed: "nope", reason: "x" }))).toBe("CREDITS_NOTHING_TO_REFUND");
|
|
231
|
+
const other = await h.newAccount();
|
|
232
|
+
expect(await refusal(h.credits.refund(h.tx, { accountId: other, consumed: "job-7", reason: "x" }))).toBe("CREDITS_KEY_REUSED");
|
|
233
|
+
});
|
|
234
|
+
it(`${name}: a refund of a settled hold, and a refund counts back against the cap`, async () => {
|
|
235
|
+
const h = await make();
|
|
236
|
+
const a = await h.newAccount();
|
|
237
|
+
await grant(h, a, { amount: 10, capPerDay: 2 });
|
|
238
|
+
const hold = await h.credits.hold(h.tx, { accountId: a, amount: 2, idempotencyKey: "h-1" });
|
|
239
|
+
await h.credits.settle(h.tx, hold.holdId);
|
|
240
|
+
expect(await refusal(consume(h, a, 1))).toBe("CREDITS_CAP_DAY");
|
|
241
|
+
await h.credits.refund(h.tx, { accountId: a, settledHold: hold.holdId, reason: "failed" });
|
|
242
|
+
expect((await consume(h, a, 2)).draws).toHaveLength(1);
|
|
243
|
+
});
|
|
244
|
+
it(`${name}: an adjustment corrects a pot, never below zero, once per key`, async () => {
|
|
245
|
+
const h = await make();
|
|
246
|
+
const a = await h.newAccount();
|
|
247
|
+
const g = await grant(h, a, { amount: 3 });
|
|
248
|
+
expect(await h.credits.adjust(h.tx, { accountId: a, grantId: g.grantId, amount: 2, reason: "goodwill", idempotencyKey: "adj-1" })).toEqual({ replayed: false });
|
|
249
|
+
expect(await h.credits.adjust(h.tx, { accountId: a, grantId: g.grantId, amount: 2, reason: "goodwill", idempotencyKey: "adj-1" })).toEqual({ replayed: true });
|
|
250
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(5);
|
|
251
|
+
expect(await refusal(h.credits.adjust(h.tx, { accountId: a, grantId: g.grantId, amount: -6, reason: "oops", idempotencyKey: "adj-2" }))).toBe("CREDITS_BAD_AMOUNT");
|
|
252
|
+
await h.credits.adjust(h.tx, { accountId: a, grantId: g.grantId, amount: -5, reason: "revoked", idempotencyKey: "adj-3" });
|
|
253
|
+
expect((await h.credits.balance(h.tx, a)).total).toBe(0);
|
|
254
|
+
expect(await refusal(h.credits.adjust(h.tx, { accountId: a, grantId: randomUUID(), amount: 1, reason: "x", idempotencyKey: "adj-4" }))).toBe("CREDITS_UNKNOWN_GRANT");
|
|
255
|
+
});
|
|
256
|
+
it(`${name}: announces a grant, and a pot running out, once each`, async () => {
|
|
257
|
+
const h = await make();
|
|
258
|
+
const a = await h.newAccount();
|
|
259
|
+
await grant(h, a, { amount: 2, idempotencyKey: "g-1" });
|
|
260
|
+
await grant(h, a, { amount: 2, idempotencyKey: "g-1" });
|
|
261
|
+
await consume(h, a, 1);
|
|
262
|
+
await consume(h, a, 1);
|
|
263
|
+
const types = h.announced().map((m) => m.type);
|
|
264
|
+
expect(types.filter((t) => t === "evt.credit.granted.v1")).toHaveLength(1);
|
|
265
|
+
expect(types.filter((t) => t === "evt.credit.exhausted.v1")).toHaveLength(1);
|
|
266
|
+
expect(h.announced().find((m) => m.type === "evt.credit.exhausted.v1")?.payload).toMatchObject({ accountId: a, bucket: "granted", onExhausted: "stop", source: "coupon:test" });
|
|
267
|
+
});
|
|
268
|
+
it(`${name}: history pages back from a date`, async () => {
|
|
269
|
+
const h = await make();
|
|
270
|
+
const a = await h.newAccount();
|
|
271
|
+
await grant(h, a, { amount: 9 });
|
|
272
|
+
h.setNow(plus(T0, 60));
|
|
273
|
+
await consume(h, a, 1);
|
|
274
|
+
h.setNow(plus(T0, 120));
|
|
275
|
+
await consume(h, a, 1);
|
|
276
|
+
expect((await h.credits.history(h.tx, a, { limit: 2 })).map((l) => l.amount)).toEqual([-1, -1]);
|
|
277
|
+
expect((await h.credits.history(h.tx, a, { before: plus(T0, 100) })).map((l) => l.amount)).toEqual([-1, 9]);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export {
|
|
281
|
+
T0,
|
|
282
|
+
creditsLedgerContract
|
|
283
|
+
};
|
|
284
|
+
//# sourceMappingURL=testkit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testkit.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { expect, it } from \"vitest\";\n\nimport type { 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":[]}
|