@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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/credits.ts","../src/events.ts","../src/persistence.ts","../src/memory.ts","../src/schema.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { localDay } from \"@krizaka/intl/date\";\nimport { addSeconds } from \"@krizaka/time\";\n\nimport { CREDIT_EVENT, CREDITS_TOPIC, type OutboxPort, uuidOf } from \"./events\";\nimport { dbCreditStore } from \"./persistence\";\nimport {\n BUCKET,\n type Calendar,\n type Clock,\n CREDITS_CODE,\n CreditsError,\n type CreditsSettings,\n type CreditStore,\n type Entry,\n ENTRY_TYPE,\n type Grant,\n type GrantState,\n HOLD_STATUS,\n ON_EXHAUSTED,\n type OnExhausted,\n type Query,\n} from \"./port\";\nimport { checkAmount, checkKey, checkNewGrant, checkSettings, type Draw, planDraws, skipFirst } from \"./rules\";\n\nexport interface CreditsDeps {\n clock: Clock;\n /** The settings, read at each call so the admin's change applies at once: an object, or a function over the settings service. */\n settings: CreditsSettings | (() => CreditsSettings | Promise<CreditsSettings>);\n /** The store over a query (a pool or a transaction). Default: the PostgreSQL store on schema `credits`. */\n storeFor?: (query: Query) => CreditStore;\n /** The Postgres schema, when the product renamed the module's (used by the default store). */\n schema?: string;\n /** Announces `evt.credit.granted.v1` and `evt.credit.exhausted.v1` in the caller's transaction (`createOutbox(...)` of `@krizaka/outbox`). */\n outbox?: OutboxPort;\n /** Fresh ids; default random UUIDs. */\n newId?: () => string;\n}\n\nexport interface GrantInput {\n accountId: string;\n /** Credits, a positive integer. */\n amount: number;\n /** When the pot lapses. Required, in the future. */\n expiresAt: Date;\n capPerDay?: number | null;\n capPerMonth?: number | null;\n onExhausted: OnExhausted;\n /** `coupon:<redemptionId>`, `plan:monthly`… */\n source: string;\n /** One key per grant: a replay answers the same grant and writes nothing. */\n idempotencyKey: string;\n}\n\nexport interface PurchaseInput {\n accountId: string;\n amount: number;\n /** `purchase:<paymentRef>`. */\n source: string;\n idempotencyKey: string;\n}\n\nexport interface ConsumeInput {\n accountId: string;\n amount: number;\n /** What it was spent on (`audit:<id>`): kept on the line. */\n reason: string;\n idempotencyKey: string;\n}\n\nexport interface Consumed {\n draws: Draw[];\n /** True when the key was seen before: the first answer is repeated and nothing was written. */\n replayed: boolean;\n}\n\nexport interface Balance {\n /** What can be spent now (not lapsed, not closed), holds not subtracted. */\n total: number;\n granted: number;\n purchased: number;\n /** Kept back by active holds. */\n held: number;\n /** `total - held`, never negative. */\n available: number;\n grants: { id: string; bucket: Grant[\"bucket\"]; remaining: number; expiresAt: Date | null; source: string; capPerDay: number | null; capPerMonth: number | null; onExhausted: OnExhausted }[];\n}\n\n/** The ledger as a product uses it. Every function takes the `Query` it works through: pass `tx` to join a unit of work. */\nexport interface Credits {\n /** Free usage that lapses, with optional caps. Idempotent by key. */\n grant(tx: Query, input: GrantInput): Promise<{ grantId: string; replayed: boolean }>;\n /** Paid usage: it does not lapse and has no cap. Idempotent by key. */\n purchase(tx: Query, input: PurchaseInput): Promise<{ grantId: string; replayed: boolean }>;\n /** Spends credits from the pots that lapse first, within their caps; refuses with `CREDITS_CAP_DAY`, `CREDITS_CAP_MONTH`, `CREDITS_EXHAUSTED` or `PAYWALL`. Nothing is partial. */\n consume(tx: Query, input: ConsumeInput): Promise<Consumed>;\n /** Keeps credits back for work that has not finished; the reserve counts against caps and balance. */\n hold(tx: Query, input: { accountId: string; amount: number; ttlSeconds?: number; idempotencyKey: string }): Promise<{ holdId: string; expiresAt: Date; replayed: boolean }>;\n /** Spends what the work cost (at most the hold's amount; default all of it) and frees the rest. Once. */\n settle(tx: Query, holdId: string, options?: { amount?: number; reason?: string }): Promise<Consumed>;\n /** Frees a hold without spending. Idempotent. */\n release(tx: Query, holdId: string): Promise<void>;\n /** Gives back what an earlier consume or settle spent, to the same pots, once. */\n refund(tx: Query, input: { accountId: string; consumed?: string; settledHold?: string; reason: string }): Promise<{ refunded: number; replayed: boolean }>;\n /** Closes a pot: what is left is written off by an EXPIRY line. Credits already spent are never taken back. Idempotent. */\n expire(tx: Query, grantId: string): Promise<{ expired: number }>;\n /** Closes the granted pots past their date that still hold credits (a job's sweep). */\n expireDue(tx: Query, options?: { limit?: number }): Promise<number>;\n /** A signed correction by an administrator, never taking a pot below zero. */\n adjust(tx: Query, input: { accountId: string; grantId: string; amount: number; reason: string; idempotencyKey: string }): Promise<{ replayed: boolean }>;\n balance(query: Query, accountId: string): Promise<Balance>;\n history(query: Query, accountId: string, options?: { limit?: number; before?: Date }): Promise<Entry[]>;\n}\n\nconst MAX_REASON = 300;\n// @fixed: the page size of the history when the caller names none (a screen's worth)\nconst DEFAULT_HISTORY = 50;\n// @fixed: the most lines the history returns in one call\nconst MAX_HISTORY = 500;\n// @fixed: the sweep's default batch\nconst DEFAULT_SWEEP = 100;\n\nconst sum = (xs: readonly number[]) => xs.reduce((a, b) => a + b, 0);\n\n/**\n * The credits ledger of a product.\n * @param deps the clock, the settings, and optionally the store, the schema, the outbox and the id source\n * @returns the ledger's functions\n */\nexport function createCredits(deps: CreditsDeps): Credits {\n const schema = deps.schema ?? \"credits\";\n const storeFor = deps.storeFor ?? ((query: Query) => dbCreditStore(query, schema));\n const settings = async () => checkSettings(typeof deps.settings === \"function\" ? await deps.settings() : deps.settings);\n const newId = deps.newId ?? randomUUID;\n\n const calendarOf = (now: Date, timeZone: string): Calendar => {\n const day = localDay(now, timeZone);\n return { timeZone, day, month: day.slice(0, 7) };\n };\n const statesOf = async (store: CreditStore, accountId: string, now: Date, timeZone: string): Promise<GrantState[]> => store.grantStates(accountId, calendarOf(now, timeZone));\n\n const entry = (over: Pick<Entry, \"accountId\" | \"grantId\" | \"type\" | \"amount\" | \"reason\" | \"operation\"> & Partial<Pick<Entry, \"reverses\" | \"holdId\">>, n: number, at: Date): Entry => ({\n id: newId(),\n reverses: null,\n holdId: null,\n ...over,\n idempotencyKey: `${over.operation}:${n}`,\n at,\n });\n\n /** A replay finds the lines of its operation. A key already used by another account is a bug of the caller, refused. */\n const replayOf = async (store: CreditStore, operation: string, accountId: string): Promise<Entry[]> => {\n const lines = await store.entriesOf(operation);\n if (lines.some((l) => l.accountId !== accountId)) throw new CreditsError(CREDITS_CODE.keyReused, \"This idempotency key was used for another account\");\n return lines;\n };\n\n const announce = async (tx: Query, type: string, seed: string, payload: Record<string, unknown>) => {\n await deps.outbox?.append(tx, { topic: CREDITS_TOPIC, type, payload, id: uuidOf(seed) });\n };\n const grantPayload = (g: Grant) => ({ accountId: g.accountId, grantId: g.id, bucket: g.bucket, amount: g.amount, expiresAt: g.expiresAt?.toISOString() ?? null, capPerDay: g.capPerDay, capPerMonth: g.capPerMonth, onExhausted: g.onExhausted, source: g.source });\n const announceExhausted = (tx: Query, states: readonly GrantState[], draws: readonly Draw[]) =>\n Promise.all(\n draws.flatMap((d) => {\n const s = states.find((x) => x.grant.id === d.grantId);\n if (!s || s.remaining - d.amount > 0) return [];\n const g = s.grant;\n return [announce(tx, CREDIT_EVENT.exhausted, `exhausted:${g.id}`, { accountId: g.accountId, grantId: g.id, bucket: g.bucket, onExhausted: g.onExhausted, source: g.source })];\n }),\n );\n\n async function create(tx: Query, kind: \"grant\" | \"purchase\", input: GrantInput | PurchaseInput): Promise<{ grantId: string; replayed: boolean }> {\n const now = deps.clock.now();\n checkNewGrant(kind, input, now);\n const granted = kind === \"grant\";\n const g = input as GrantInput;\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `${kind}.${input.idempotencyKey}`;\n const before = await replayOf(store, operation, input.accountId);\n if (before[0]?.grantId) return { grantId: before[0].grantId, replayed: true };\n const grant: Grant = {\n id: newId(),\n accountId: input.accountId,\n bucket: granted ? BUCKET.granted : BUCKET.purchased,\n amount: input.amount,\n capPerDay: granted ? (g.capPerDay ?? null) : null,\n capPerMonth: granted ? (g.capPerMonth ?? null) : null,\n expiresAt: granted ? g.expiresAt : null,\n onExhausted: granted ? g.onExhausted : ON_EXHAUSTED.fallbackPaid,\n source: input.source,\n createdAt: now,\n };\n await store.insertGrant(grant);\n await store.insertEntries([entry({ accountId: grant.accountId, grantId: grant.id, type: granted ? ENTRY_TYPE.grant : ENTRY_TYPE.purchase, amount: input.amount, reason: input.source, operation }, 0, now)]);\n await announce(tx, CREDIT_EVENT.granted, `granted:${grant.id}`, grantPayload(grant));\n return { grantId: grant.id, replayed: false };\n }\n\n const drawsOf = (lines: readonly Entry[]): Draw[] => lines.filter((l) => l.type === ENTRY_TYPE.debit && l.grantId).map((l) => ({ grantId: l.grantId!, amount: -l.amount }));\n\n /** Plans and writes the debits of one operation. The caller holds the account's lock and has checked the replay. */\n async function spend(tx: Query, store: CreditStore, args: { accountId: string; amount: number; reason: string; operation: string; holdId?: string; exceptHoldId?: string }): Promise<Draw[]> {\n const { timeZone } = await settings();\n const now = deps.clock.now();\n const held = await store.heldAmount(args.accountId, now, args.exceptHoldId);\n const states = await statesOf(store, args.accountId, now, timeZone);\n const plan = planDraws(states, held + args.amount, now);\n if (!plan.ok) throw new CreditsError(plan.code, REFUSAL_TEXT[plan.code] ?? plan.code);\n const draws = skipFirst(plan.draws, held);\n await store.insertEntries(draws.map((d, i) => entry({ accountId: args.accountId, grantId: d.grantId, type: ENTRY_TYPE.debit, amount: -d.amount, reason: args.reason, operation: args.operation, ...(args.holdId ? { holdId: args.holdId } : {}) }, i, now)));\n await announceExhausted(tx, states, draws);\n return draws;\n }\n\n async function expire(tx: Query, grantId: string): Promise<{ expired: number }> {\n const store = storeFor(tx);\n const found = await store.grantById(grantId);\n if (!found) throw new CreditsError(CREDITS_CODE.unknownGrant, `No grant ${grantId}`);\n await store.lock(found.accountId);\n const { timeZone } = await settings();\n const now = deps.clock.now();\n const state = (await statesOf(store, found.accountId, now, timeZone)).find((s) => s.grant.id === grantId)!;\n if (state.closed) return { expired: 0 };\n await store.insertEntries([entry({ accountId: found.accountId, grantId, type: ENTRY_TYPE.expiry, amount: -state.remaining, reason: \"expiry\", operation: `expire.${grantId}` }, 0, now)]);\n return { expired: state.remaining };\n }\n\n\n return {\n grant: (tx, input) => create(tx, \"grant\", input),\n purchase: (tx, input) => create(tx, \"purchase\", input),\n\n async consume(tx, input) {\n checkAmount(input.amount, \"a consumption\");\n checkKey(input.idempotencyKey);\n if (!input.reason || input.reason.length > MAX_REASON) throw new CreditsError(CREDITS_CODE.badGrant, `A reason is 1 to ${MAX_REASON} characters`);\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `consume.${input.idempotencyKey}`;\n const before = await replayOf(store, operation, input.accountId);\n if (before.length > 0) return { draws: drawsOf(before), replayed: true };\n return { draws: await spend(tx, store, { accountId: input.accountId, amount: input.amount, reason: input.reason, operation }), replayed: false };\n },\n\n async hold(tx, input) {\n checkAmount(input.amount, \"a hold\");\n checkKey(input.idempotencyKey);\n const s = await settings();\n const ttl = input.ttlSeconds ?? s.holdTtlSeconds;\n checkSettings({ ...s, holdTtlSeconds: ttl });\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `hold.${input.idempotencyKey}`;\n const known = await store.holdByOperation(operation);\n if (known) {\n if (known.accountId !== input.accountId) throw new CreditsError(CREDITS_CODE.keyReused, \"This idempotency key was used for another account\");\n return { holdId: known.id, expiresAt: known.expiresAt, replayed: true };\n }\n const now = deps.clock.now();\n const held = await store.heldAmount(input.accountId, now);\n const plan = planDraws(await statesOf(store, input.accountId, now, s.timeZone), held + input.amount, now);\n if (!plan.ok) throw new CreditsError(plan.code, REFUSAL_TEXT[plan.code] ?? plan.code);\n const hold = { id: newId(), accountId: input.accountId, amount: input.amount, expiresAt: addSeconds(now, ttl), status: HOLD_STATUS.active, settledAmount: null, createdAt: now, closedAt: null, operation };\n await store.insertHold(hold);\n return { holdId: hold.id, expiresAt: hold.expiresAt, replayed: false };\n },\n\n async settle(tx, holdId, options = {}) {\n const store = storeFor(tx);\n const found = await store.holdById(holdId);\n if (!found) throw new CreditsError(CREDITS_CODE.unknownHold, `No hold ${holdId}`);\n await store.lock(found.accountId);\n const hold = (await store.holdById(holdId))!;\n const operation = `settle.${holdId}`;\n if (hold.status === HOLD_STATUS.settled) return { draws: drawsOf(await store.entriesOf(operation)), replayed: true };\n if (hold.status === HOLD_STATUS.released) throw new CreditsError(CREDITS_CODE.holdClosed, \"This hold was released\");\n const amount = options.amount ?? hold.amount;\n if (!Number.isSafeInteger(amount) || amount < 0 || amount > hold.amount) throw new CreditsError(CREDITS_CODE.badAmount, `A hold of ${hold.amount} settles for 0 to ${hold.amount}, got ${amount}`);\n const now = deps.clock.now();\n const draws = amount === 0 ? [] : await spend(tx, store, { accountId: hold.accountId, amount, reason: options.reason ?? `hold:${holdId}`, operation, holdId, exceptHoldId: holdId });\n await store.closeHold(holdId, amount === 0 ? HOLD_STATUS.released : HOLD_STATUS.settled, amount, now);\n return { draws, replayed: false };\n },\n\n async release(tx, holdId) {\n const store = storeFor(tx);\n const found = await store.holdById(holdId);\n if (!found) throw new CreditsError(CREDITS_CODE.unknownHold, `No hold ${holdId}`);\n await store.lock(found.accountId);\n const hold = (await store.holdById(holdId))!;\n if (hold.status === HOLD_STATUS.released) return;\n if (hold.status === HOLD_STATUS.settled) throw new CreditsError(CREDITS_CODE.holdClosed, \"This hold was settled\");\n await store.closeHold(holdId, HOLD_STATUS.released, null, deps.clock.now());\n },\n\n async refund(tx, input) {\n const original = input.consumed !== undefined ? `consume.${checkKey(input.consumed)}` : input.settledHold !== undefined ? `settle.${input.settledHold}` : null;\n if (!original) throw new CreditsError(CREDITS_CODE.badKey, \"Name what to refund: a consumed key or a settled hold\");\n if (!input.reason || input.reason.length > MAX_REASON) throw new CreditsError(CREDITS_CODE.badGrant, `A reason is 1 to ${MAX_REASON} characters`);\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `refund.${original}`;\n const done = await replayOf(store, operation, input.accountId);\n if (done.length > 0) return { refunded: sum(done.map((l) => l.amount)), replayed: true };\n const spent = (await replayOf(store, original, input.accountId)).filter((l) => l.type === ENTRY_TYPE.debit);\n if (spent.length === 0) throw new CreditsError(CREDITS_CODE.nothingToRefund, \"Nothing was spent under this key for this account\");\n const now = deps.clock.now();\n await store.insertEntries(spent.map((l, i) => entry({ accountId: l.accountId, grantId: l.grantId, type: ENTRY_TYPE.refund, amount: -l.amount, reason: input.reason, operation, reverses: original }, i, now)));\n return { refunded: sum(spent.map((l) => -l.amount)), replayed: false };\n },\n\n expire,\n\n async expireDue(tx, options = {}) {\n const store = storeFor(tx);\n const due = await store.lapsedWithCredits(deps.clock.now(), options.limit ?? DEFAULT_SWEEP);\n let n = 0;\n for (const d of due) n += (await expire(tx, d.grantId)).expired > 0 ? 1 : 0;\n return n;\n },\n\n async adjust(tx, input) {\n checkKey(input.idempotencyKey);\n if (!Number.isSafeInteger(input.amount) || input.amount === 0) throw new CreditsError(CREDITS_CODE.badAmount, \"An adjustment is a non-zero integer\");\n if (!input.reason || input.reason.length > MAX_REASON) throw new CreditsError(CREDITS_CODE.badGrant, `A reason is 1 to ${MAX_REASON} characters`);\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `adjust.${input.idempotencyKey}`;\n if ((await replayOf(store, operation, input.accountId)).length > 0) return { replayed: true };\n const { timeZone } = await settings();\n const now = deps.clock.now();\n const state = (await statesOf(store, input.accountId, now, timeZone)).find((s) => s.grant.id === input.grantId);\n if (!state) throw new CreditsError(CREDITS_CODE.unknownGrant, `No grant ${input.grantId} for this account`);\n if (state.remaining + input.amount < 0) throw new CreditsError(CREDITS_CODE.badAmount, \"An adjustment never takes a pot below zero\");\n await store.insertEntries([entry({ accountId: input.accountId, grantId: input.grantId, type: ENTRY_TYPE.adjustment, amount: input.amount, reason: input.reason, operation }, 0, now)]);\n return { replayed: false };\n },\n\n async balance(query, accountId) {\n const store = storeFor(query);\n const { timeZone } = await settings();\n const now = deps.clock.now();\n const live = (await statesOf(store, accountId, now, timeZone)).filter((s) => !s.closed && (s.grant.expiresAt === null || s.grant.expiresAt > now) && s.remaining > 0);\n const of = (bucket: Grant[\"bucket\"]) => sum(live.filter((s) => s.grant.bucket === bucket).map((s) => s.remaining));\n const total = of(BUCKET.granted) + of(BUCKET.purchased);\n const held = await store.heldAmount(accountId, now);\n return {\n total,\n granted: of(BUCKET.granted),\n purchased: of(BUCKET.purchased),\n held,\n available: Math.max(0, total - held),\n grants: live.map((s) => ({ id: s.grant.id, bucket: s.grant.bucket, remaining: s.remaining, expiresAt: s.grant.expiresAt, source: s.grant.source, capPerDay: s.grant.capPerDay, capPerMonth: s.grant.capPerMonth, onExhausted: s.grant.onExhausted })),\n };\n },\n\n history: (query, accountId, options = {}) => storeFor(query).history(accountId, { limit: Math.min(options.limit ?? DEFAULT_HISTORY, MAX_HISTORY), ...(options.before ? { before: options.before } : {}) }),\n };\n}\n\nconst REFUSAL_TEXT: Record<string, string> = {\n [CREDITS_CODE.capDay]: \"The daily limit of free credits is reached\",\n [CREDITS_CODE.capMonth]: \"The monthly limit of free credits is reached\",\n [CREDITS_CODE.exhausted]: \"The free credits have run out\",\n [CREDITS_CODE.paywall]: \"Payment required: no credits left\",\n};\n","import { createHash } from \"node:crypto\";\n\nimport type { Query } from \"@krizaka/db\";\n\n/** The topic the ledger's events are appended to on the outbox. */\nexport const CREDITS_TOPIC = \"credits\";\n\n/** Event types: the names of `krizaka-billing-api`, so a TypeScript product and a Java service speak alike. */\nexport const CREDIT_EVENT = { granted: \"evt.credit.granted.v1\", exhausted: \"evt.credit.exhausted.v1\" } as const;\n\n/** The part of `@krizaka/outbox`'s `Outbox` this package needs: structural, so `credits` does not depend on `outbox`. */\nexport interface OutboxPort {\n append(tx: Query, message: { topic: string; type: string; payload: unknown; id?: string }): Promise<{ id: string; appended: boolean }>;\n}\n\n/** A stable UUID (version 5 shaped) from a seed: announcing the same fact twice is one outbox message. */\nexport function uuidOf(seed: string): string {\n const h = createHash(\"sha256\").update(`krizaka.credits:${seed}`).digest();\n h[6] = (h[6]! & 0x0f) | 0x50;\n h[8] = (h[8]! & 0x3f) | 0x80;\n const x = h.subarray(0, 16).toString(\"hex\");\n return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;\n}\n\nconst base = { type: \"object\", additionalProperties: false } as const;\n\n/** The JSON Schema of each event's payload (a consumer validates against it; the Java twin publishes the same names). */\nexport const CREDIT_EVENT_SCHEMAS = {\n [CREDIT_EVENT.granted]: {\n ...base,\n required: [\"accountId\", \"grantId\", \"bucket\", \"amount\", \"source\", \"onExhausted\"],\n properties: {\n accountId: { type: \"string\" },\n grantId: { type: \"string\" },\n bucket: { enum: [\"granted\", \"purchased\"] },\n amount: { type: \"integer\", minimum: 1 },\n expiresAt: { type: [\"string\", \"null\"], format: \"date-time\" },\n capPerDay: { type: [\"integer\", \"null\"], minimum: 1 },\n capPerMonth: { type: [\"integer\", \"null\"], minimum: 1 },\n onExhausted: { enum: [\"stop\", \"fallback_paid\"] },\n source: { type: \"string\" },\n },\n },\n [CREDIT_EVENT.exhausted]: {\n ...base,\n required: [\"accountId\", \"grantId\", \"bucket\", \"source\", \"onExhausted\"],\n properties: {\n accountId: { type: \"string\" },\n grantId: { type: \"string\" },\n bucket: { enum: [\"granted\", \"purchased\"] },\n onExhausted: { enum: [\"stop\", \"fallback_paid\"] },\n source: { type: \"string\" },\n },\n },\n} as const;\n","import { ident, type Query } from \"@krizaka/db\";\n\nimport { type Calendar, type CreditStore, type Entry, type Grant, type GrantState, type Hold } from \"./port\";\n\ninterface GrantRow {\n id: string;\n account_id: string;\n bucket: Grant[\"bucket\"];\n amount: string | number;\n cap_per_day: string | number | null;\n cap_per_month: string | number | null;\n expires_at: Date | string | null;\n on_exhausted: Grant[\"onExhausted\"];\n source: string;\n created_at: Date | string;\n}\ninterface StateRow extends GrantRow {\n remaining: string | number;\n used_today: string | number;\n used_month: string | number;\n closed: boolean;\n}\ninterface EntryRow {\n id: string;\n account_id: string;\n grant_id: string | null;\n type: Entry[\"type\"];\n amount: string | number;\n reason: string;\n operation: string;\n idempotency_key: string;\n reverses: string | null;\n hold_id: string | null;\n at: Date | string;\n}\ninterface HoldRow {\n id: string;\n account_id: string;\n amount: string | number;\n expires_at: Date | string;\n status: Hold[\"status\"];\n settled_amount: string | number | null;\n created_at: Date | string;\n closed_at: Date | string | null;\n operation: string;\n}\n\nconst num = (v: string | number): number => Number(v);\nconst numOrNull = (v: string | number | null): number | null => (v === null ? null : Number(v));\nconst date = (v: Date | string): Date => new Date(v);\nconst dateOrNull = (v: Date | string | null): Date | null => (v === null ? null : new Date(v));\n\nconst toGrant = (r: GrantRow): Grant => ({ id: r.id, accountId: r.account_id, bucket: r.bucket, amount: num(r.amount), capPerDay: numOrNull(r.cap_per_day), capPerMonth: numOrNull(r.cap_per_month), expiresAt: dateOrNull(r.expires_at), onExhausted: r.on_exhausted, source: r.source, createdAt: date(r.created_at) });\nconst toEntry = (r: EntryRow): Entry => ({ id: r.id, accountId: r.account_id, grantId: r.grant_id, type: r.type, amount: num(r.amount), reason: r.reason, operation: r.operation, idempotencyKey: r.idempotency_key, reverses: r.reverses, holdId: r.hold_id, at: date(r.at) });\nconst toHold = (r: HoldRow): Hold => ({ id: r.id, accountId: r.account_id, amount: num(r.amount), expiresAt: date(r.expires_at), status: r.status, settledAmount: numOrNull(r.settled_amount), createdAt: date(r.created_at), closedAt: dateOrNull(r.closed_at), operation: r.operation });\n\n/**\n * The PostgreSQL store of the ledger, in schema `credits` (see `creditsSchema`), over the `Query` it is given: the caller's transaction\n * for every writing call, so the lines commit or roll back with the product's work. `lock` is `pg_advisory_xact_lock` on the account:\n * two requests spending the last credit take turns, and the second sees the first's lines. Days and months are cut in the calendar's\n * IANA zone BY POSTGRES (`AT TIME ZONE`), so daylight-saving shifts are the database's own tz data. Times come from the caller.\n * @param schema the Postgres schema of the module, default `credits`\n */\nexport function dbCreditStore(query: Query, schema = \"credits\"): CreditStore {\n const s = ident(schema);\n const grant = `${s}.\"grant\"`;\n const entry = `${s}.entry`;\n const hold = `${s}.hold`;\n return {\n async lock(accountId) {\n await query(\"SELECT pg_advisory_xact_lock(hashtextextended('krizaka.credits:' || $1::text, 0))\", [accountId]);\n },\n async insertGrant(g) {\n await query(`INSERT INTO ${grant} (id, account_id, bucket, amount, cap_per_day, cap_per_month, expires_at, on_exhausted, source, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, [g.id, g.accountId, g.bucket, g.amount, g.capPerDay, g.capPerMonth, g.expiresAt, g.onExhausted, g.source, g.createdAt]);\n },\n async grantById(id) {\n const [row] = await query<GrantRow>(`SELECT * FROM ${grant} WHERE id = $1`, [id]);\n return row ? toGrant(row) : null;\n },\n async grantStates(accountId, calendar: Calendar): Promise<GrantState[]> {\n const rows = await query<StateRow>(\n `SELECT g.*,\n COALESCE(SUM(e.amount), 0) AS remaining,\n COALESCE(-SUM(e.amount) FILTER (WHERE e.type IN ('DEBIT', 'REFUND') AND (e.at AT TIME ZONE $2)::date = $3::date), 0) AS used_today,\n COALESCE(-SUM(e.amount) FILTER (WHERE e.type IN ('DEBIT', 'REFUND') AND to_char(e.at AT TIME ZONE $2, 'YYYY-MM') = $4), 0) AS used_month,\n COALESCE(bool_or(e.type = 'EXPIRY'), false) AS closed\n FROM ${grant} g LEFT JOIN ${entry} e ON e.grant_id = g.id\n WHERE g.account_id = $1\n GROUP BY g.id`,\n [accountId, calendar.timeZone, calendar.day, calendar.month],\n );\n return rows.map((r) => ({ grant: toGrant(r), remaining: num(r.remaining), usedToday: num(r.used_today), usedThisMonth: num(r.used_month), closed: r.closed }));\n },\n async insertEntries(entries) {\n for (const e of entries) {\n await query(`INSERT INTO ${entry} (id, account_id, grant_id, type, amount, reason, operation, idempotency_key, reverses, hold_id, at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [e.id, e.accountId, e.grantId, e.type, e.amount, e.reason, e.operation, e.idempotencyKey, e.reverses, e.holdId, e.at]);\n }\n },\n async entriesOf(operation) {\n return (await query<EntryRow>(`SELECT * FROM ${entry} WHERE operation = $1 ORDER BY seq`, [operation])).map(toEntry);\n },\n async history(accountId, { limit, before }) {\n return (await query<EntryRow>(`SELECT * FROM ${entry} WHERE account_id = $1 AND ($2::timestamptz IS NULL OR at < $2) ORDER BY seq DESC LIMIT $3`, [accountId, before ?? null, limit])).map(toEntry);\n },\n async insertHold(h) {\n await query(`INSERT INTO ${hold} (id, account_id, amount, expires_at, status, settled_amount, created_at, closed_at, operation) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [h.id, h.accountId, h.amount, h.expiresAt, h.status, h.settledAmount, h.createdAt, h.closedAt, h.operation]);\n },\n async holdById(id) {\n const [row] = await query<HoldRow>(`SELECT * FROM ${hold} WHERE id = $1`, [id]);\n return row ? toHold(row) : null;\n },\n async holdByOperation(operation) {\n const [row] = await query<HoldRow>(`SELECT * FROM ${hold} WHERE operation = $1`, [operation]);\n return row ? toHold(row) : null;\n },\n async heldAmount(accountId, now, exceptHoldId) {\n const [row] = await query<{ held: string | number }>(`SELECT COALESCE(SUM(amount), 0) AS held FROM ${hold} WHERE account_id = $1 AND status = 'active' AND expires_at > $2 AND ($3::uuid IS NULL OR id <> $3)`, [accountId, now, exceptHoldId ?? null]);\n return num(row?.held ?? 0);\n },\n async closeHold(id, status, settledAmount, at) {\n await query(`UPDATE ${hold} SET status = $2, settled_amount = $3, closed_at = $4 WHERE id = $1`, [id, status, settledAmount, at]);\n },\n async lapsedWithCredits(now, limit) {\n const rows = await query<{ id: string; account_id: string }>(\n `SELECT g.id, g.account_id FROM ${grant} g JOIN ${entry} e ON e.grant_id = g.id\n WHERE g.bucket = 'granted' AND g.expires_at <= $1\n GROUP BY g.id HAVING SUM(e.amount) > 0 AND NOT bool_or(e.type = 'EXPIRY')\n ORDER BY g.expires_at, g.id LIMIT $2`,\n [now, limit],\n );\n return rows.map((r) => ({ grantId: r.id, accountId: r.account_id }));\n },\n };\n}\n","import { localDay } from \"@krizaka/intl/date\";\nimport { isAfter } from \"@krizaka/time\";\n\nimport { BUCKET, type CreditStore, type Entry, ENTRY_TYPE, type Grant, type Hold, HOLD_STATUS } from \"./port\";\n\n/**\n * An in-memory ledger, for unit tests and demos: the same behaviour as the database's (the contract kit runs on both) except that\n * `lock` does nothing, because it has no transaction to hold it in. It is for one flow at a time; concurrency is proved on PostgreSQL.\n */\nexport function memoryCreditStore(): CreditStore & { entries(): readonly Readonly<Entry>[] } {\n const grants = new Map<string, Grant>();\n const entries: Entry[] = [];\n const holds = new Map<string, Hold>();\n return {\n entries: () => entries,\n async lock() {},\n async insertGrant(g) {\n grants.set(g.id, { ...g });\n },\n async grantById(id) {\n const g = grants.get(id);\n return g ? { ...g } : null;\n },\n async grantStates(accountId, calendar) {\n return [...grants.values()]\n .filter((g) => g.accountId === accountId)\n .map((g) => {\n const lines = entries.filter((e) => e.grantId === g.id);\n const used = (match: (e: Entry) => boolean) => -lines.filter((e) => (e.type === ENTRY_TYPE.debit || e.type === ENTRY_TYPE.refund) && match(e)).reduce((a, e) => a + e.amount, 0);\n return {\n grant: { ...g },\n remaining: lines.reduce((a, e) => a + e.amount, 0),\n usedToday: used((e) => localDay(e.at, calendar.timeZone) === calendar.day),\n usedThisMonth: used((e) => localDay(e.at, calendar.timeZone).slice(0, 7) === calendar.month),\n closed: lines.some((e) => e.type === ENTRY_TYPE.expiry),\n };\n });\n },\n async insertEntries(batch) {\n for (const e of batch) {\n if (entries.some((x) => x.idempotencyKey === e.idempotencyKey)) throw new Error(`duplicate idempotency key ${e.idempotencyKey}`);\n }\n entries.push(...batch.map((e) => ({ ...e })));\n },\n async entriesOf(operation) {\n return entries.filter((e) => e.operation === operation).map((e) => ({ ...e }));\n },\n async history(accountId, { limit, before }) {\n return entries\n .filter((e) => e.accountId === accountId && (!before || e.at < before))\n .reverse()\n .slice(0, limit)\n .map((e) => ({ ...e }));\n },\n async insertHold(h) {\n holds.set(h.id, { ...h });\n },\n async holdById(id) {\n const h = holds.get(id);\n return h ? { ...h } : null;\n },\n async holdByOperation(operation) {\n const h = [...holds.values()].find((x) => x.operation === operation);\n return h ? { ...h } : null;\n },\n async heldAmount(accountId, now, exceptHoldId) {\n return [...holds.values()].filter((h) => h.accountId === accountId && h.status === HOLD_STATUS.active && isAfter(h.expiresAt, now) && h.id !== exceptHoldId).reduce((a, h) => a + h.amount, 0);\n },\n async closeHold(id, status, settledAmount, at) {\n const h = holds.get(id);\n if (h) Object.assign(h, { status, settledAmount, closedAt: at });\n },\n async lapsedWithCredits(now, limit) {\n return [...grants.values()]\n .filter((g) => g.bucket === BUCKET.granted && g.expiresAt !== null && g.expiresAt <= now)\n .filter((g) => {\n const lines = entries.filter((e) => e.grantId === g.id);\n return lines.reduce((a, e) => a + e.amount, 0) > 0 && !lines.some((e) => e.type === ENTRY_TYPE.expiry);\n })\n .sort((a, b) => a.expiresAt!.valueOf() - b.expiresAt!.valueOf() || a.id.localeCompare(b.id))\n .slice(0, limit)\n .map((g) => ({ grantId: g.id, accountId: g.accountId }));\n },\n };\n}\n","import { defineModule } from \"@krizaka/db\";\n\n/**\n * The tables of « credits », in their own Postgres schema (ADR-001 §3), following `krizaka-billing`'s model and names: `grant` (a pot:\n * `granted` with an expiry and optional caps, or `purchased`), `entry` (the ledger: GRANT, PURCHASE, DEBIT, REFUND, EXPIRY, ADJUSTMENT;\n * APPEND-ONLY, a trigger refuses any UPDATE or DELETE) and `hold` (credits kept back for work in progress). Nothing stores a balance: what is\n * left of a pot is the sum of its lines. The one product table referenced is the owner (`owner`: the account table), declared by the product;\n * an account that has credits is anonymised, never deleted (it is an accounting record), exactly like an invoice.\n * Version 1 is frozen once released: a change is version 2.\n */\nexport const creditsSchema = defineModule({\n module: \"credits\",\n tables: [\"owner\"],\n migrations: [\n {\n version: 1,\n name: \"grants, ledger and holds\",\n sql: (c) => `\nCREATE TABLE ${c.schema}.\"grant\" (\n id uuid PRIMARY KEY,\n account_id uuid NOT NULL REFERENCES ${c.tables.owner}(id),\n bucket text NOT NULL CHECK (bucket IN ('granted', 'purchased')),\n amount bigint NOT NULL CHECK (amount > 0),\n cap_per_day bigint CHECK (cap_per_day IS NULL OR cap_per_day > 0),\n cap_per_month bigint CHECK (cap_per_month IS NULL OR cap_per_month > 0),\n expires_at timestamptz,\n on_exhausted text NOT NULL CHECK (on_exhausted IN ('stop', 'fallback_paid')),\n source text NOT NULL CHECK (char_length(source) BETWEEN 1 AND 200),\n created_at timestamptz NOT NULL,\n CHECK ((bucket = 'granted' AND expires_at IS NOT NULL)\n OR (bucket = 'purchased' AND expires_at IS NULL AND cap_per_day IS NULL AND cap_per_month IS NULL))\n);\nCREATE INDEX grant_account ON ${c.schema}.\"grant\" (account_id, created_at);\nCREATE INDEX grant_lapse ON ${c.schema}.\"grant\" (expires_at) WHERE bucket = 'granted';\nCREATE TABLE ${c.schema}.hold (\n id uuid PRIMARY KEY,\n account_id uuid NOT NULL REFERENCES ${c.tables.owner}(id),\n amount bigint NOT NULL CHECK (amount > 0),\n expires_at timestamptz NOT NULL,\n status text NOT NULL CHECK (status IN ('active', 'settled', 'released')),\n settled_amount bigint CHECK (settled_amount IS NULL OR settled_amount >= 0),\n created_at timestamptz NOT NULL,\n closed_at timestamptz,\n operation text NOT NULL UNIQUE\n);\nCREATE INDEX hold_active ON ${c.schema}.hold (account_id) WHERE status = 'active';\nCREATE TABLE ${c.schema}.entry (\n seq bigint GENERATED ALWAYS AS IDENTITY,\n id uuid PRIMARY KEY,\n account_id uuid NOT NULL,\n grant_id uuid REFERENCES ${c.schema}.\"grant\"(id),\n type text NOT NULL CHECK (type IN ('GRANT', 'PURCHASE', 'DEBIT', 'REFUND', 'EXPIRY', 'ADJUSTMENT')),\n amount bigint NOT NULL CHECK (amount <> 0 OR type = 'EXPIRY'),\n reason text NOT NULL CHECK (char_length(reason) BETWEEN 1 AND 300),\n operation text NOT NULL,\n idempotency_key text NOT NULL UNIQUE,\n reverses text,\n hold_id uuid,\n at timestamptz NOT NULL\n);\nCREATE INDEX entry_account ON ${c.schema}.entry (account_id, seq DESC);\nCREATE INDEX entry_grant ON ${c.schema}.entry (grant_id);\nCREATE INDEX entry_operation ON ${c.schema}.entry (operation);\nCREATE FUNCTION ${c.schema}.entry_is_append_only() RETURNS trigger LANGUAGE plpgsql AS $fn$\nBEGIN\n RAISE EXCEPTION 'credits.entry is append-only: % refused', TG_OP;\nEND;\n$fn$;\nCREATE TRIGGER entry_append_only BEFORE UPDATE OR DELETE ON ${c.schema}.entry FOR EACH ROW EXECUTE FUNCTION ${c.schema}.entry_is_append_only();`,\n },\n ],\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,kBAAkB;AAE3B,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;;;ACH3B,SAAS,kBAAkB;AAKpB,IAAM,gBAAgB;AAGtB,IAAM,eAAe,EAAE,SAAS,yBAAyB,WAAW,0BAA0B;AAQ9F,SAAS,OAAO,MAAsB;AAC3C,QAAM,IAAI,WAAW,QAAQ,EAAE,OAAO,mBAAmB,IAAI,EAAE,EAAE,OAAO;AACxE,IAAE,CAAC,IAAK,EAAE,CAAC,IAAK,KAAQ;AACxB,IAAE,CAAC,IAAK,EAAE,CAAC,IAAK,KAAQ;AACxB,QAAM,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,SAAS,KAAK;AAC1C,SAAO,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC;AACpG;AAEA,IAAM,OAAO,EAAE,MAAM,UAAU,sBAAsB,MAAM;AAGpD,IAAM,uBAAuB;AAAA,EAClC,CAAC,aAAa,OAAO,GAAG;AAAA,IACtB,GAAG;AAAA,IACH,UAAU,CAAC,aAAa,WAAW,UAAU,UAAU,UAAU,aAAa;AAAA,IAC9E,YAAY;AAAA,MACV,WAAW,EAAE,MAAM,SAAS;AAAA,MAC5B,SAAS,EAAE,MAAM,SAAS;AAAA,MAC1B,QAAQ,EAAE,MAAM,CAAC,WAAW,WAAW,EAAE;AAAA,MACzC,QAAQ,EAAE,MAAM,WAAW,SAAS,EAAE;AAAA,MACtC,WAAW,EAAE,MAAM,CAAC,UAAU,MAAM,GAAG,QAAQ,YAAY;AAAA,MAC3D,WAAW,EAAE,MAAM,CAAC,WAAW,MAAM,GAAG,SAAS,EAAE;AAAA,MACnD,aAAa,EAAE,MAAM,CAAC,WAAW,MAAM,GAAG,SAAS,EAAE;AAAA,MACrD,aAAa,EAAE,MAAM,CAAC,QAAQ,eAAe,EAAE;AAAA,MAC/C,QAAQ,EAAE,MAAM,SAAS;AAAA,IAC3B;AAAA,EACF;AAAA,EACA,CAAC,aAAa,SAAS,GAAG;AAAA,IACxB,GAAG;AAAA,IACH,UAAU,CAAC,aAAa,WAAW,UAAU,UAAU,aAAa;AAAA,IACpE,YAAY;AAAA,MACV,WAAW,EAAE,MAAM,SAAS;AAAA,MAC5B,SAAS,EAAE,MAAM,SAAS;AAAA,MAC1B,QAAQ,EAAE,MAAM,CAAC,WAAW,WAAW,EAAE;AAAA,MACzC,aAAa,EAAE,MAAM,CAAC,QAAQ,eAAe,EAAE;AAAA,MAC/C,QAAQ,EAAE,MAAM,SAAS;AAAA,IAC3B;AAAA,EACF;AACF;;;ACtDA,SAAS,aAAyB;AA+ClC,IAAM,MAAM,CAAC,MAA+B,OAAO,CAAC;AACpD,IAAM,YAAY,CAAC,MAA8C,MAAM,OAAO,OAAO,OAAO,CAAC;AAC7F,IAAM,OAAO,CAAC,MAA2B,IAAI,KAAK,CAAC;AACnD,IAAM,aAAa,CAAC,MAA0C,MAAM,OAAO,OAAO,IAAI,KAAK,CAAC;AAE5F,IAAM,UAAU,CAAC,OAAwB,EAAE,IAAI,EAAE,IAAI,WAAW,EAAE,YAAY,QAAQ,EAAE,QAAQ,QAAQ,IAAI,EAAE,MAAM,GAAG,WAAW,UAAU,EAAE,WAAW,GAAG,aAAa,UAAU,EAAE,aAAa,GAAG,WAAW,WAAW,EAAE,UAAU,GAAG,aAAa,EAAE,cAAc,QAAQ,EAAE,QAAQ,WAAW,KAAK,EAAE,UAAU,EAAE;AACvT,IAAM,UAAU,CAAC,OAAwB,EAAE,IAAI,EAAE,IAAI,WAAW,EAAE,YAAY,SAAS,EAAE,UAAU,MAAM,EAAE,MAAM,QAAQ,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,QAAQ,WAAW,EAAE,WAAW,gBAAgB,EAAE,iBAAiB,UAAU,EAAE,UAAU,QAAQ,EAAE,SAAS,IAAI,KAAK,EAAE,EAAE,EAAE;AAC7Q,IAAM,SAAS,CAAC,OAAsB,EAAE,IAAI,EAAE,IAAI,WAAW,EAAE,YAAY,QAAQ,IAAI,EAAE,MAAM,GAAG,WAAW,KAAK,EAAE,UAAU,GAAG,QAAQ,EAAE,QAAQ,eAAe,UAAU,EAAE,cAAc,GAAG,WAAW,KAAK,EAAE,UAAU,GAAG,UAAU,WAAW,EAAE,SAAS,GAAG,WAAW,EAAE,UAAU;AASjR,SAAS,cAAc,OAAc,SAAS,WAAwB;AAC3E,QAAM,IAAI,MAAM,MAAM;AACtB,QAAM,QAAQ,GAAG,CAAC;AAClB,QAAM,QAAQ,GAAG,CAAC;AAClB,QAAM,OAAO,GAAG,CAAC;AACjB,SAAO;AAAA,IACL,MAAM,KAAK,WAAW;AACpB,YAAM,MAAM,qFAAqF,CAAC,SAAS,CAAC;AAAA,IAC9G;AAAA,IACA,MAAM,YAAY,GAAG;AACnB,YAAM,MAAM,eAAe,KAAK,gKAAgK,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,CAAC;AAAA,IACxT;AAAA,IACA,MAAM,UAAU,IAAI;AAClB,YAAM,CAAC,GAAG,IAAI,MAAM,MAAgB,iBAAiB,KAAK,kBAAkB,CAAC,EAAE,CAAC;AAChF,aAAO,MAAM,QAAQ,GAAG,IAAI;AAAA,IAC9B;AAAA,IACA,MAAM,YAAY,WAAW,UAA2C;AACtE,YAAM,OAAO,MAAM;AAAA,QACjB;AAAA;AAAA;AAAA;AAAA;AAAA,kBAKU,KAAK,gBAAgB,KAAK;AAAA;AAAA;AAAA,QAGpC,CAAC,WAAW,SAAS,UAAU,SAAS,KAAK,SAAS,KAAK;AAAA,MAC7D;AACA,aAAO,KAAK,IAAI,CAAC,OAAO,EAAE,OAAO,QAAQ,CAAC,GAAG,WAAW,IAAI,EAAE,SAAS,GAAG,WAAW,IAAI,EAAE,UAAU,GAAG,eAAe,IAAI,EAAE,UAAU,GAAG,QAAQ,EAAE,OAAO,EAAE;AAAA,IAC/J;AAAA,IACA,MAAM,cAAc,SAAS;AAC3B,iBAAW,KAAK,SAAS;AACvB,cAAM,MAAM,eAAe,KAAK,8JAA8J,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,CAAC;AAAA,MACrT;AAAA,IACF;AAAA,IACA,MAAM,UAAU,WAAW;AACzB,cAAQ,MAAM,MAAgB,iBAAiB,KAAK,sCAAsC,CAAC,SAAS,CAAC,GAAG,IAAI,OAAO;AAAA,IACrH;AAAA,IACA,MAAM,QAAQ,WAAW,EAAE,OAAO,OAAO,GAAG;AAC1C,cAAQ,MAAM,MAAgB,iBAAiB,KAAK,8FAA8F,CAAC,WAAW,UAAU,MAAM,KAAK,CAAC,GAAG,IAAI,OAAO;AAAA,IACpM;AAAA,IACA,MAAM,WAAW,GAAG;AAClB,YAAM,MAAM,eAAe,IAAI,+IAA+I,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,CAAC;AAAA,IAC3R;AAAA,IACA,MAAM,SAAS,IAAI;AACjB,YAAM,CAAC,GAAG,IAAI,MAAM,MAAe,iBAAiB,IAAI,kBAAkB,CAAC,EAAE,CAAC;AAC9E,aAAO,MAAM,OAAO,GAAG,IAAI;AAAA,IAC7B;AAAA,IACA,MAAM,gBAAgB,WAAW;AAC/B,YAAM,CAAC,GAAG,IAAI,MAAM,MAAe,iBAAiB,IAAI,yBAAyB,CAAC,SAAS,CAAC;AAC5F,aAAO,MAAM,OAAO,GAAG,IAAI;AAAA,IAC7B;AAAA,IACA,MAAM,WAAW,WAAW,KAAK,cAAc;AAC7C,YAAM,CAAC,GAAG,IAAI,MAAM,MAAiC,gDAAgD,IAAI,uGAAuG,CAAC,WAAW,KAAK,gBAAgB,IAAI,CAAC;AACtP,aAAO,IAAI,KAAK,QAAQ,CAAC;AAAA,IAC3B;AAAA,IACA,MAAM,UAAU,IAAI,QAAQ,eAAe,IAAI;AAC7C,YAAM,MAAM,UAAU,IAAI,uEAAuE,CAAC,IAAI,QAAQ,eAAe,EAAE,CAAC;AAAA,IAClI;AAAA,IACA,MAAM,kBAAkB,KAAK,OAAO;AAClC,YAAM,OAAO,MAAM;AAAA,QACjB,kCAAkC,KAAK,WAAW,KAAK;AAAA;AAAA;AAAA;AAAA,QAIvD,CAAC,KAAK,KAAK;AAAA,MACb;AACA,aAAO,KAAK,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,WAAW,EAAE,WAAW,EAAE;AAAA,IACrE;AAAA,EACF;AACF;;;AFlBA,IAAM,aAAa;AAEnB,IAAM,kBAAkB;AAExB,IAAM,cAAc;AAEpB,IAAM,gBAAgB;AAEtB,IAAM,MAAM,CAAC,OAA0B,GAAG,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AAO5D,SAAS,cAAc,MAA4B;AACxD,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,WAAW,KAAK,aAAa,CAAC,UAAiB,cAAc,OAAO,MAAM;AAChF,QAAM,WAAW,YAAY,cAAc,OAAO,KAAK,aAAa,aAAa,MAAM,KAAK,SAAS,IAAI,KAAK,QAAQ;AACtH,QAAM,QAAQ,KAAK,SAAS;AAE5B,QAAM,aAAa,CAAC,KAAW,aAA+B;AAC5D,UAAM,MAAM,SAAS,KAAK,QAAQ;AAClC,WAAO,EAAE,UAAU,KAAK,OAAO,IAAI,MAAM,GAAG,CAAC,EAAE;AAAA,EACjD;AACA,QAAM,WAAW,OAAO,OAAoB,WAAmB,KAAW,aAA4C,MAAM,YAAY,WAAW,WAAW,KAAK,QAAQ,CAAC;AAE5K,QAAM,QAAQ,CAAC,MAAuI,GAAW,QAAqB;AAAA,IACpL,IAAI,MAAM;AAAA,IACV,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,GAAG;AAAA,IACH,gBAAgB,GAAG,KAAK,SAAS,IAAI,CAAC;AAAA,IACtC;AAAA,EACF;AAGA,QAAM,WAAW,OAAO,OAAoB,WAAmB,cAAwC;AACrG,UAAM,QAAQ,MAAM,MAAM,UAAU,SAAS;AAC7C,QAAI,MAAM,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS,EAAG,OAAM,IAAI,aAAa,aAAa,WAAW,mDAAmD;AACpJ,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,OAAO,IAAW,MAAc,MAAc,YAAqC;AAClG,UAAM,KAAK,QAAQ,OAAO,IAAI,EAAE,OAAO,eAAe,MAAM,SAAS,IAAI,OAAO,IAAI,EAAE,CAAC;AAAA,EACzF;AACA,QAAM,eAAe,CAAC,OAAc,EAAE,WAAW,EAAE,WAAW,SAAS,EAAE,IAAI,QAAQ,EAAE,QAAQ,QAAQ,EAAE,QAAQ,WAAW,EAAE,WAAW,YAAY,KAAK,MAAM,WAAW,EAAE,WAAW,aAAa,EAAE,aAAa,aAAa,EAAE,aAAa,QAAQ,EAAE,OAAO;AACjQ,QAAM,oBAAoB,CAAC,IAAW,QAA+B,UACnE,QAAQ;AAAA,IACN,MAAM,QAAQ,CAAC,MAAM;AACnB,YAAM,IAAI,OAAO,KAAK,CAAC,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO;AACrD,UAAI,CAAC,KAAK,EAAE,YAAY,EAAE,SAAS,EAAG,QAAO,CAAC;AAC9C,YAAM,IAAI,EAAE;AACZ,aAAO,CAAC,SAAS,IAAI,aAAa,WAAW,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,SAAS,EAAE,IAAI,QAAQ,EAAE,QAAQ,aAAa,EAAE,aAAa,QAAQ,EAAE,OAAO,CAAC,CAAC;AAAA,IAC9K,CAAC;AAAA,EACH;AAEF,iBAAe,OAAO,IAAW,MAA4B,OAAoF;AAC/I,UAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,kBAAc,MAAM,OAAO,GAAG;AAC9B,UAAM,UAAU,SAAS;AACzB,UAAM,IAAI;AACV,UAAM,QAAQ,SAAS,EAAE;AACzB,UAAM,MAAM,KAAK,MAAM,SAAS;AAChC,UAAM,YAAY,GAAG,IAAI,IAAI,MAAM,cAAc;AACjD,UAAM,SAAS,MAAM,SAAS,OAAO,WAAW,MAAM,SAAS;AAC/D,QAAI,OAAO,CAAC,GAAG,QAAS,QAAO,EAAE,SAAS,OAAO,CAAC,EAAE,SAAS,UAAU,KAAK;AAC5E,UAAM,QAAe;AAAA,MACnB,IAAI,MAAM;AAAA,MACV,WAAW,MAAM;AAAA,MACjB,QAAQ,UAAU,OAAO,UAAU,OAAO;AAAA,MAC1C,QAAQ,MAAM;AAAA,MACd,WAAW,UAAW,EAAE,aAAa,OAAQ;AAAA,MAC7C,aAAa,UAAW,EAAE,eAAe,OAAQ;AAAA,MACjD,WAAW,UAAU,EAAE,YAAY;AAAA,MACnC,aAAa,UAAU,EAAE,cAAc,aAAa;AAAA,MACpD,QAAQ,MAAM;AAAA,MACd,WAAW;AAAA,IACb;AACA,UAAM,MAAM,YAAY,KAAK;AAC7B,UAAM,MAAM,cAAc,CAAC,MAAM,EAAE,WAAW,MAAM,WAAW,SAAS,MAAM,IAAI,MAAM,UAAU,WAAW,QAAQ,WAAW,UAAU,QAAQ,MAAM,QAAQ,QAAQ,MAAM,QAAQ,UAAU,GAAG,GAAG,GAAG,CAAC,CAAC;AAC3M,UAAM,SAAS,IAAI,aAAa,SAAS,WAAW,MAAM,EAAE,IAAI,aAAa,KAAK,CAAC;AACnF,WAAO,EAAE,SAAS,MAAM,IAAI,UAAU,MAAM;AAAA,EAC9C;AAEA,QAAM,UAAU,CAAC,UAAoC,MAAM,OAAO,CAAC,MAAM,EAAE,SAAS,WAAW,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,SAAU,QAAQ,CAAC,EAAE,OAAO,EAAE;AAG1K,iBAAe,MAAM,IAAW,OAAoB,MAAyI;AAC3L,UAAM,EAAE,SAAS,IAAI,MAAM,SAAS;AACpC,UAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,UAAM,OAAO,MAAM,MAAM,WAAW,KAAK,WAAW,KAAK,KAAK,YAAY;AAC1E,UAAM,SAAS,MAAM,SAAS,OAAO,KAAK,WAAW,KAAK,QAAQ;AAClE,UAAM,OAAO,UAAU,QAAQ,OAAO,KAAK,QAAQ,GAAG;AACtD,QAAI,CAAC,KAAK,GAAI,OAAM,IAAI,aAAa,KAAK,MAAM,aAAa,KAAK,IAAI,KAAK,KAAK,IAAI;AACpF,UAAM,QAAQ,UAAU,KAAK,OAAO,IAAI;AACxC,UAAM,MAAM,cAAc,MAAM,IAAI,CAAC,GAAG,MAAM,MAAM,EAAE,WAAW,KAAK,WAAW,SAAS,EAAE,SAAS,MAAM,WAAW,OAAO,QAAQ,CAAC,EAAE,QAAQ,QAAQ,KAAK,QAAQ,WAAW,KAAK,WAAW,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC,EAAG,GAAG,GAAG,GAAG,CAAC,CAAC;AAC3P,UAAM,kBAAkB,IAAI,QAAQ,KAAK;AACzC,WAAO;AAAA,EACT;AAEA,iBAAe,OAAO,IAAW,SAA+C;AAC9E,UAAM,QAAQ,SAAS,EAAE;AACzB,UAAM,QAAQ,MAAM,MAAM,UAAU,OAAO;AAC3C,QAAI,CAAC,MAAO,OAAM,IAAI,aAAa,aAAa,cAAc,YAAY,OAAO,EAAE;AACnF,UAAM,MAAM,KAAK,MAAM,SAAS;AAChC,UAAM,EAAE,SAAS,IAAI,MAAM,SAAS;AACpC,UAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,UAAM,SAAS,MAAM,SAAS,OAAO,MAAM,WAAW,KAAK,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,MAAM,OAAO,OAAO;AACxG,QAAI,MAAM,OAAQ,QAAO,EAAE,SAAS,EAAE;AACtC,UAAM,MAAM,cAAc,CAAC,MAAM,EAAE,WAAW,MAAM,WAAW,SAAS,MAAM,WAAW,QAAQ,QAAQ,CAAC,MAAM,WAAW,QAAQ,UAAU,WAAW,UAAU,OAAO,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;AACvL,WAAO,EAAE,SAAS,MAAM,UAAU;AAAA,EACpC;AAGA,SAAO;AAAA,IACL,OAAO,CAAC,IAAI,UAAU,OAAO,IAAI,SAAS,KAAK;AAAA,IAC/C,UAAU,CAAC,IAAI,UAAU,OAAO,IAAI,YAAY,KAAK;AAAA,IAErD,MAAM,QAAQ,IAAI,OAAO;AACvB,kBAAY,MAAM,QAAQ,eAAe;AACzC,eAAS,MAAM,cAAc;AAC7B,UAAI,CAAC,MAAM,UAAU,MAAM,OAAO,SAAS,WAAY,OAAM,IAAI,aAAa,aAAa,UAAU,oBAAoB,UAAU,aAAa;AAChJ,YAAM,QAAQ,SAAS,EAAE;AACzB,YAAM,MAAM,KAAK,MAAM,SAAS;AAChC,YAAM,YAAY,WAAW,MAAM,cAAc;AACjD,YAAM,SAAS,MAAM,SAAS,OAAO,WAAW,MAAM,SAAS;AAC/D,UAAI,OAAO,SAAS,EAAG,QAAO,EAAE,OAAO,QAAQ,MAAM,GAAG,UAAU,KAAK;AACvE,aAAO,EAAE,OAAO,MAAM,MAAM,IAAI,OAAO,EAAE,WAAW,MAAM,WAAW,QAAQ,MAAM,QAAQ,QAAQ,MAAM,QAAQ,UAAU,CAAC,GAAG,UAAU,MAAM;AAAA,IACjJ;AAAA,IAEA,MAAM,KAAK,IAAI,OAAO;AACpB,kBAAY,MAAM,QAAQ,QAAQ;AAClC,eAAS,MAAM,cAAc;AAC7B,YAAM,IAAI,MAAM,SAAS;AACzB,YAAM,MAAM,MAAM,cAAc,EAAE;AAClC,oBAAc,EAAE,GAAG,GAAG,gBAAgB,IAAI,CAAC;AAC3C,YAAM,QAAQ,SAAS,EAAE;AACzB,YAAM,MAAM,KAAK,MAAM,SAAS;AAChC,YAAM,YAAY,QAAQ,MAAM,cAAc;AAC9C,YAAM,QAAQ,MAAM,MAAM,gBAAgB,SAAS;AACnD,UAAI,OAAO;AACT,YAAI,MAAM,cAAc,MAAM,UAAW,OAAM,IAAI,aAAa,aAAa,WAAW,mDAAmD;AAC3I,eAAO,EAAE,QAAQ,MAAM,IAAI,WAAW,MAAM,WAAW,UAAU,KAAK;AAAA,MACxE;AACA,YAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,YAAM,OAAO,MAAM,MAAM,WAAW,MAAM,WAAW,GAAG;AACxD,YAAM,OAAO,UAAU,MAAM,SAAS,OAAO,MAAM,WAAW,KAAK,EAAE,QAAQ,GAAG,OAAO,MAAM,QAAQ,GAAG;AACxG,UAAI,CAAC,KAAK,GAAI,OAAM,IAAI,aAAa,KAAK,MAAM,aAAa,KAAK,IAAI,KAAK,KAAK,IAAI;AACpF,YAAM,OAAO,EAAE,IAAI,MAAM,GAAG,WAAW,MAAM,WAAW,QAAQ,MAAM,QAAQ,WAAW,WAAW,KAAK,GAAG,GAAG,QAAQ,YAAY,QAAQ,eAAe,MAAM,WAAW,KAAK,UAAU,MAAM,UAAU;AAC1M,YAAM,MAAM,WAAW,IAAI;AAC3B,aAAO,EAAE,QAAQ,KAAK,IAAI,WAAW,KAAK,WAAW,UAAU,MAAM;AAAA,IACvE;AAAA,IAEA,MAAM,OAAO,IAAI,QAAQ,UAAU,CAAC,GAAG;AACrC,YAAM,QAAQ,SAAS,EAAE;AACzB,YAAM,QAAQ,MAAM,MAAM,SAAS,MAAM;AACzC,UAAI,CAAC,MAAO,OAAM,IAAI,aAAa,aAAa,aAAa,WAAW,MAAM,EAAE;AAChF,YAAM,MAAM,KAAK,MAAM,SAAS;AAChC,YAAM,OAAQ,MAAM,MAAM,SAAS,MAAM;AACzC,YAAM,YAAY,UAAU,MAAM;AAClC,UAAI,KAAK,WAAW,YAAY,QAAS,QAAO,EAAE,OAAO,QAAQ,MAAM,MAAM,UAAU,SAAS,CAAC,GAAG,UAAU,KAAK;AACnH,UAAI,KAAK,WAAW,YAAY,SAAU,OAAM,IAAI,aAAa,aAAa,YAAY,wBAAwB;AAClH,YAAM,SAAS,QAAQ,UAAU,KAAK;AACtC,UAAI,CAAC,OAAO,cAAc,MAAM,KAAK,SAAS,KAAK,SAAS,KAAK,OAAQ,OAAM,IAAI,aAAa,aAAa,WAAW,aAAa,KAAK,MAAM,qBAAqB,KAAK,MAAM,SAAS,MAAM,EAAE;AACjM,YAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,YAAM,QAAQ,WAAW,IAAI,CAAC,IAAI,MAAM,MAAM,IAAI,OAAO,EAAE,WAAW,KAAK,WAAW,QAAQ,QAAQ,QAAQ,UAAU,QAAQ,MAAM,IAAI,WAAW,QAAQ,cAAc,OAAO,CAAC;AACnL,YAAM,MAAM,UAAU,QAAQ,WAAW,IAAI,YAAY,WAAW,YAAY,SAAS,QAAQ,GAAG;AACpG,aAAO,EAAE,OAAO,UAAU,MAAM;AAAA,IAClC;AAAA,IAEA,MAAM,QAAQ,IAAI,QAAQ;AACxB,YAAM,QAAQ,SAAS,EAAE;AACzB,YAAM,QAAQ,MAAM,MAAM,SAAS,MAAM;AACzC,UAAI,CAAC,MAAO,OAAM,IAAI,aAAa,aAAa,aAAa,WAAW,MAAM,EAAE;AAChF,YAAM,MAAM,KAAK,MAAM,SAAS;AAChC,YAAM,OAAQ,MAAM,MAAM,SAAS,MAAM;AACzC,UAAI,KAAK,WAAW,YAAY,SAAU;AAC1C,UAAI,KAAK,WAAW,YAAY,QAAS,OAAM,IAAI,aAAa,aAAa,YAAY,uBAAuB;AAChH,YAAM,MAAM,UAAU,QAAQ,YAAY,UAAU,MAAM,KAAK,MAAM,IAAI,CAAC;AAAA,IAC5E;AAAA,IAEA,MAAM,OAAO,IAAI,OAAO;AACtB,YAAM,WAAW,MAAM,aAAa,SAAY,WAAW,SAAS,MAAM,QAAQ,CAAC,KAAK,MAAM,gBAAgB,SAAY,UAAU,MAAM,WAAW,KAAK;AAC1J,UAAI,CAAC,SAAU,OAAM,IAAI,aAAa,aAAa,QAAQ,uDAAuD;AAClH,UAAI,CAAC,MAAM,UAAU,MAAM,OAAO,SAAS,WAAY,OAAM,IAAI,aAAa,aAAa,UAAU,oBAAoB,UAAU,aAAa;AAChJ,YAAM,QAAQ,SAAS,EAAE;AACzB,YAAM,MAAM,KAAK,MAAM,SAAS;AAChC,YAAM,YAAY,UAAU,QAAQ;AACpC,YAAM,OAAO,MAAM,SAAS,OAAO,WAAW,MAAM,SAAS;AAC7D,UAAI,KAAK,SAAS,EAAG,QAAO,EAAE,UAAU,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,UAAU,KAAK;AACvF,YAAM,SAAS,MAAM,SAAS,OAAO,UAAU,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,WAAW,KAAK;AAC1G,UAAI,MAAM,WAAW,EAAG,OAAM,IAAI,aAAa,aAAa,iBAAiB,mDAAmD;AAChI,YAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,YAAM,MAAM,cAAc,MAAM,IAAI,CAAC,GAAG,MAAM,MAAM,EAAE,WAAW,EAAE,WAAW,SAAS,EAAE,SAAS,MAAM,WAAW,QAAQ,QAAQ,CAAC,EAAE,QAAQ,QAAQ,MAAM,QAAQ,WAAW,UAAU,SAAS,GAAG,GAAG,GAAG,CAAC,CAAC;AAC7M,aAAO,EAAE,UAAU,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,UAAU,MAAM;AAAA,IACvE;AAAA,IAEA;AAAA,IAEA,MAAM,UAAU,IAAI,UAAU,CAAC,GAAG;AAChC,YAAM,QAAQ,SAAS,EAAE;AACzB,YAAM,MAAM,MAAM,MAAM,kBAAkB,KAAK,MAAM,IAAI,GAAG,QAAQ,SAAS,aAAa;AAC1F,UAAI,IAAI;AACR,iBAAW,KAAK,IAAK,OAAM,MAAM,OAAO,IAAI,EAAE,OAAO,GAAG,UAAU,IAAI,IAAI;AAC1E,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,IAAI,OAAO;AACtB,eAAS,MAAM,cAAc;AAC7B,UAAI,CAAC,OAAO,cAAc,MAAM,MAAM,KAAK,MAAM,WAAW,EAAG,OAAM,IAAI,aAAa,aAAa,WAAW,qCAAqC;AACnJ,UAAI,CAAC,MAAM,UAAU,MAAM,OAAO,SAAS,WAAY,OAAM,IAAI,aAAa,aAAa,UAAU,oBAAoB,UAAU,aAAa;AAChJ,YAAM,QAAQ,SAAS,EAAE;AACzB,YAAM,MAAM,KAAK,MAAM,SAAS;AAChC,YAAM,YAAY,UAAU,MAAM,cAAc;AAChD,WAAK,MAAM,SAAS,OAAO,WAAW,MAAM,SAAS,GAAG,SAAS,EAAG,QAAO,EAAE,UAAU,KAAK;AAC5F,YAAM,EAAE,SAAS,IAAI,MAAM,SAAS;AACpC,YAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,YAAM,SAAS,MAAM,SAAS,OAAO,MAAM,WAAW,KAAK,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,MAAM,OAAO,MAAM,OAAO;AAC9G,UAAI,CAAC,MAAO,OAAM,IAAI,aAAa,aAAa,cAAc,YAAY,MAAM,OAAO,mBAAmB;AAC1G,UAAI,MAAM,YAAY,MAAM,SAAS,EAAG,OAAM,IAAI,aAAa,aAAa,WAAW,4CAA4C;AACnI,YAAM,MAAM,cAAc,CAAC,MAAM,EAAE,WAAW,MAAM,WAAW,SAAS,MAAM,SAAS,MAAM,WAAW,YAAY,QAAQ,MAAM,QAAQ,QAAQ,MAAM,QAAQ,UAAU,GAAG,GAAG,GAAG,CAAC,CAAC;AACrL,aAAO,EAAE,UAAU,MAAM;AAAA,IAC3B;AAAA,IAEA,MAAM,QAAQ,OAAO,WAAW;AAC9B,YAAM,QAAQ,SAAS,KAAK;AAC5B,YAAM,EAAE,SAAS,IAAI,MAAM,SAAS;AACpC,YAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,YAAM,QAAQ,MAAM,SAAS,OAAO,WAAW,KAAK,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,cAAc,QAAQ,EAAE,MAAM,YAAY,QAAQ,EAAE,YAAY,CAAC;AACpK,YAAM,KAAK,CAAC,WAA4B,IAAI,KAAK,OAAO,CAAC,MAAM,EAAE,MAAM,WAAW,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC;AACjH,YAAM,QAAQ,GAAG,OAAO,OAAO,IAAI,GAAG,OAAO,SAAS;AACtD,YAAM,OAAO,MAAM,MAAM,WAAW,WAAW,GAAG;AAClD,aAAO;AAAA,QACL;AAAA,QACA,SAAS,GAAG,OAAO,OAAO;AAAA,QAC1B,WAAW,GAAG,OAAO,SAAS;AAAA,QAC9B;AAAA,QACA,WAAW,KAAK,IAAI,GAAG,QAAQ,IAAI;AAAA,QACnC,QAAQ,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,QAAQ,EAAE,MAAM,QAAQ,WAAW,EAAE,WAAW,WAAW,EAAE,MAAM,WAAW,QAAQ,EAAE,MAAM,QAAQ,WAAW,EAAE,MAAM,WAAW,aAAa,EAAE,MAAM,aAAa,aAAa,EAAE,MAAM,YAAY,EAAE;AAAA,MACtP;AAAA,IACF;AAAA,IAEA,SAAS,CAAC,OAAO,WAAW,UAAU,CAAC,MAAM,SAAS,KAAK,EAAE,QAAQ,WAAW,EAAE,OAAO,KAAK,IAAI,QAAQ,SAAS,iBAAiB,WAAW,GAAG,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC,EAAG,CAAC;AAAA,EAC3M;AACF;AAEA,IAAM,eAAuC;AAAA,EAC3C,CAAC,aAAa,MAAM,GAAG;AAAA,EACvB,CAAC,aAAa,QAAQ,GAAG;AAAA,EACzB,CAAC,aAAa,SAAS,GAAG;AAAA,EAC1B,CAAC,aAAa,OAAO,GAAG;AAC1B;;;AG/WA,SAAS,YAAAA,iBAAgB;AACzB,SAAS,eAAe;AAQjB,SAAS,oBAA6E;AAC3F,QAAM,SAAS,oBAAI,IAAmB;AACtC,QAAM,UAAmB,CAAC;AAC1B,QAAM,QAAQ,oBAAI,IAAkB;AACpC,SAAO;AAAA,IACL,SAAS,MAAM;AAAA,IACf,MAAM,OAAO;AAAA,IAAC;AAAA,IACd,MAAM,YAAY,GAAG;AACnB,aAAO,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAAA,IAC3B;AAAA,IACA,MAAM,UAAU,IAAI;AAClB,YAAM,IAAI,OAAO,IAAI,EAAE;AACvB,aAAO,IAAI,EAAE,GAAG,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,MAAM,YAAY,WAAW,UAAU;AACrC,aAAO,CAAC,GAAG,OAAO,OAAO,CAAC,EACvB,OAAO,CAAC,MAAM,EAAE,cAAc,SAAS,EACvC,IAAI,CAAC,MAAM;AACV,cAAM,QAAQ,QAAQ,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE;AACtD,cAAM,OAAO,CAAC,UAAiC,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,SAAS,WAAW,SAAS,EAAE,SAAS,WAAW,WAAW,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC;AAC/K,eAAO;AAAA,UACL,OAAO,EAAE,GAAG,EAAE;AAAA,UACd,WAAW,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC;AAAA,UACjD,WAAW,KAAK,CAAC,MAAMC,UAAS,EAAE,IAAI,SAAS,QAAQ,MAAM,SAAS,GAAG;AAAA,UACzE,eAAe,KAAK,CAAC,MAAMA,UAAS,EAAE,IAAI,SAAS,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,SAAS,KAAK;AAAA,UAC3F,QAAQ,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,WAAW,MAAM;AAAA,QACxD;AAAA,MACF,CAAC;AAAA,IACL;AAAA,IACA,MAAM,cAAc,OAAO;AACzB,iBAAW,KAAK,OAAO;AACrB,YAAI,QAAQ,KAAK,CAAC,MAAM,EAAE,mBAAmB,EAAE,cAAc,EAAG,OAAM,IAAI,MAAM,6BAA6B,EAAE,cAAc,EAAE;AAAA,MACjI;AACA,cAAQ,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;AAAA,IAC9C;AAAA,IACA,MAAM,UAAU,WAAW;AACzB,aAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,cAAc,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;AAAA,IAC/E;AAAA,IACA,MAAM,QAAQ,WAAW,EAAE,OAAO,OAAO,GAAG;AAC1C,aAAO,QACJ,OAAO,CAAC,MAAM,EAAE,cAAc,cAAc,CAAC,UAAU,EAAE,KAAK,OAAO,EACrE,QAAQ,EACR,MAAM,GAAG,KAAK,EACd,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;AAAA,IAC1B;AAAA,IACA,MAAM,WAAW,GAAG;AAClB,YAAM,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAAA,IAC1B;AAAA,IACA,MAAM,SAAS,IAAI;AACjB,YAAM,IAAI,MAAM,IAAI,EAAE;AACtB,aAAO,IAAI,EAAE,GAAG,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,MAAM,gBAAgB,WAAW;AAC/B,YAAM,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS;AACnE,aAAO,IAAI,EAAE,GAAG,EAAE,IAAI;AAAA,IACxB;AAAA,IACA,MAAM,WAAW,WAAW,KAAK,cAAc;AAC7C,aAAO,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,cAAc,aAAa,EAAE,WAAW,YAAY,UAAU,QAAQ,EAAE,WAAW,GAAG,KAAK,EAAE,OAAO,YAAY,EAAE,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC;AAAA,IAC/L;AAAA,IACA,MAAM,UAAU,IAAI,QAAQ,eAAe,IAAI;AAC7C,YAAM,IAAI,MAAM,IAAI,EAAE;AACtB,UAAI,EAAG,QAAO,OAAO,GAAG,EAAE,QAAQ,eAAe,UAAU,GAAG,CAAC;AAAA,IACjE;AAAA,IACA,MAAM,kBAAkB,KAAK,OAAO;AAClC,aAAO,CAAC,GAAG,OAAO,OAAO,CAAC,EACvB,OAAO,CAAC,MAAM,EAAE,WAAW,OAAO,WAAW,EAAE,cAAc,QAAQ,EAAE,aAAa,GAAG,EACvF,OAAO,CAAC,MAAM;AACb,cAAM,QAAQ,QAAQ,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,EAAE;AACtD,eAAO,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,WAAW,MAAM;AAAA,MACvG,CAAC,EACA,KAAK,CAAC,GAAG,MAAM,EAAE,UAAW,QAAQ,IAAI,EAAE,UAAW,QAAQ,KAAK,EAAE,GAAG,cAAc,EAAE,EAAE,CAAC,EAC1F,MAAM,GAAG,KAAK,EACd,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,WAAW,EAAE,UAAU,EAAE;AAAA,IAC3D;AAAA,EACF;AACF;;;ACpFA,SAAS,oBAAoB;AAUtB,IAAM,gBAAgB,aAAa;AAAA,EACxC,QAAQ;AAAA,EACR,QAAQ,CAAC,OAAO;AAAA,EAChB,YAAY;AAAA,IACV;AAAA,MACE,SAAS;AAAA,MACT,MAAM;AAAA,MACN,KAAK,CAAC,MAAM;AAAA,eACH,EAAE,MAAM;AAAA;AAAA,2CAEoB,EAAE,OAAO,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gCAYzB,EAAE,MAAM;AAAA,8BACV,EAAE,MAAM;AAAA,eACvB,EAAE,MAAM;AAAA;AAAA,4CAEqB,EAAE,OAAO,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8BAS5B,EAAE,MAAM;AAAA,eACvB,EAAE,MAAM;AAAA;AAAA;AAAA;AAAA,oCAIa,EAAE,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gCAUZ,EAAE,MAAM;AAAA,8BACV,EAAE,MAAM;AAAA,kCACJ,EAAE,MAAM;AAAA,kBACxB,EAAE,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,8DAKoC,EAAE,MAAM,wCAAwC,EAAE,MAAM;AAAA,IAClH;AAAA,EACF;AACF,CAAC;","names":["localDay","localDay"]}
1
+ {"version":3,"sources":["../src/credits.ts","../src/events.ts","../src/persistence.ts","../src/memory.ts","../src/schema.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { localDay } from \"@krizaka/intl/date\";\nimport { addSeconds } from \"@krizaka/time\";\n\nimport { CREDIT_EVENT, CREDITS_TOPIC, type OutboxPort, uuidOf } from \"./events\";\nimport { dbCreditStore } from \"./persistence\";\nimport {\n BUCKET,\n type Calendar,\n type Clock,\n CREDITS_CODE,\n CreditsError,\n type CreditsSettings,\n type CreditStore,\n type Entry,\n ENTRY_TYPE,\n type Grant,\n type GrantState,\n HOLD_STATUS,\n ON_EXHAUSTED,\n type OnExhausted,\n type Query,\n} from \"./port\";\nimport { checkAmount, checkKey, checkNewGrant, checkSettings, type Draw, planDraws, skipFirst } from \"./rules\";\n\nexport interface CreditsDeps {\n clock: Clock;\n /** The settings, read at each call so the admin's change applies at once: an object, or a function over the settings service. */\n settings: CreditsSettings | (() => CreditsSettings | Promise<CreditsSettings>);\n /** The store over a query (a pool or a transaction). Default: the PostgreSQL store on schema `credits`. */\n storeFor?: (query: Query) => CreditStore;\n /** The Postgres schema, when the product renamed the module's (used by the default store). */\n schema?: string;\n /** Announces `evt.credit.granted.v1` and `evt.credit.exhausted.v1` in the caller's transaction (`createOutbox(...)` of `@krizaka/outbox`). */\n outbox?: OutboxPort;\n /** Fresh ids; default random UUIDs. */\n newId?: () => string;\n}\n\nexport interface GrantInput {\n accountId: string;\n /** Credits, a positive integer. */\n amount: number;\n /** When the pot lapses. Required, in the future. */\n expiresAt: Date;\n capPerDay?: number | null;\n capPerMonth?: number | null;\n onExhausted: OnExhausted;\n /** `coupon:<redemptionId>`, `plan:monthly`… */\n source: string;\n /** One key per grant: a replay answers the same grant and writes nothing. */\n idempotencyKey: string;\n}\n\nexport interface PurchaseInput {\n accountId: string;\n amount: number;\n /** `purchase:<paymentRef>`. */\n source: string;\n idempotencyKey: string;\n}\n\nexport interface ConsumeInput {\n accountId: string;\n amount: number;\n /** What it was spent on (`audit:<id>`): kept on the line. */\n reason: string;\n idempotencyKey: string;\n}\n\nexport interface Consumed {\n draws: Draw[];\n /** True when the key was seen before: the first answer is repeated and nothing was written. */\n replayed: boolean;\n}\n\nexport interface Balance {\n /** What can be spent now (not lapsed, not closed), holds not subtracted. */\n total: number;\n granted: number;\n purchased: number;\n /** Kept back by active holds. */\n held: number;\n /** `total - held`, never negative. */\n available: number;\n grants: {\n id: string;\n bucket: Grant[\"bucket\"];\n remaining: number;\n expiresAt: Date | null;\n source: string;\n capPerDay: number | null;\n capPerMonth: number | null;\n onExhausted: OnExhausted;\n }[];\n}\n\n/** The ledger as a product uses it. Every function takes the `Query` it works through: pass `tx` to join a unit of work. */\nexport interface Credits {\n /** Free usage that lapses, with optional caps. Idempotent by key. */\n grant(tx: Query, input: GrantInput): Promise<{ grantId: string; replayed: boolean }>;\n /** Paid usage: it does not lapse and has no cap. Idempotent by key. */\n purchase(tx: Query, input: PurchaseInput): Promise<{ grantId: string; replayed: boolean }>;\n /** Spends credits from the pots that lapse first, within their caps; refuses with `CREDITS_CAP_DAY`, `CREDITS_CAP_MONTH`, `CREDITS_EXHAUSTED` or `PAYWALL`. Nothing is partial. */\n consume(tx: Query, input: ConsumeInput): Promise<Consumed>;\n /** Keeps credits back for work that has not finished; the reserve counts against caps and balance. */\n hold(\n tx: Query,\n input: { accountId: string; amount: number; ttlSeconds?: number; idempotencyKey: string },\n ): Promise<{ holdId: string; expiresAt: Date; replayed: boolean }>;\n /** Spends what the work cost (at most the hold's amount; default all of it) and frees the rest. Once. */\n settle(tx: Query, holdId: string, options?: { amount?: number; reason?: string }): Promise<Consumed>;\n /** Frees a hold without spending. Idempotent. */\n release(tx: Query, holdId: string): Promise<void>;\n /** Gives back what an earlier consume or settle spent, to the same pots, once. */\n refund(\n tx: Query,\n input: { accountId: string; consumed?: string; settledHold?: string; reason: string },\n ): Promise<{ refunded: number; replayed: boolean }>;\n /** Closes a pot: what is left is written off by an EXPIRY line. Credits already spent are never taken back. Idempotent. */\n expire(tx: Query, grantId: string): Promise<{ expired: number }>;\n /** Closes the granted pots past their date that still hold credits (a job's sweep). */\n expireDue(tx: Query, options?: { limit?: number }): Promise<number>;\n /** A signed correction by an administrator, never taking a pot below zero. */\n adjust(\n tx: Query,\n input: { accountId: string; grantId: string; amount: number; reason: string; idempotencyKey: string },\n ): Promise<{ replayed: boolean }>;\n balance(query: Query, accountId: string): Promise<Balance>;\n history(query: Query, accountId: string, options?: { limit?: number; before?: Date }): Promise<Entry[]>;\n}\n\nconst MAX_REASON = 300;\n// @fixed: the page size of the history when the caller names none (a screen's worth)\nconst DEFAULT_HISTORY = 50;\n// @fixed: the most lines the history returns in one call\nconst MAX_HISTORY = 500;\n// @fixed: the sweep's default batch\nconst DEFAULT_SWEEP = 100;\n\nconst sum = (xs: readonly number[]) => xs.reduce((a, b) => a + b, 0);\n\n/**\n * The credits ledger of a product.\n * @param deps the clock, the settings, and optionally the store, the schema, the outbox and the id source\n * @returns the ledger's functions\n */\nexport function createCredits(deps: CreditsDeps): Credits {\n const schema = deps.schema ?? \"credits\";\n const storeFor = deps.storeFor ?? ((query: Query) => dbCreditStore(query, schema));\n const settings = async () =>\n checkSettings(typeof deps.settings === \"function\" ? await deps.settings() : deps.settings);\n const newId = deps.newId ?? randomUUID;\n\n const calendarOf = (now: Date, timeZone: string): Calendar => {\n const day = localDay(now, timeZone);\n return { timeZone, day, month: day.slice(0, 7) };\n };\n const statesOf = async (store: CreditStore, accountId: string, now: Date, timeZone: string): Promise<GrantState[]> =>\n store.grantStates(accountId, calendarOf(now, timeZone));\n\n const entry = (\n over: Pick<Entry, \"accountId\" | \"grantId\" | \"type\" | \"amount\" | \"reason\" | \"operation\"> &\n Partial<Pick<Entry, \"reverses\" | \"holdId\">>,\n n: number,\n at: Date,\n ): Entry => ({\n id: newId(),\n reverses: null,\n holdId: null,\n ...over,\n idempotencyKey: `${over.operation}:${n}`,\n at,\n });\n\n /** A replay finds the lines of its operation. A key already used by another account is a bug of the caller, refused. */\n const replayOf = async (store: CreditStore, operation: string, accountId: string): Promise<Entry[]> => {\n const lines = await store.entriesOf(operation);\n if (lines.some((l) => l.accountId !== accountId))\n throw new CreditsError(CREDITS_CODE.keyReused, \"This idempotency key was used for another account\");\n return lines;\n };\n\n const announce = async (tx: Query, type: string, seed: string, payload: Record<string, unknown>) => {\n await deps.outbox?.append(tx, { topic: CREDITS_TOPIC, type, payload, id: uuidOf(seed) });\n };\n const grantPayload = (g: Grant) => ({\n accountId: g.accountId,\n grantId: g.id,\n bucket: g.bucket,\n amount: g.amount,\n expiresAt: g.expiresAt?.toISOString() ?? null,\n capPerDay: g.capPerDay,\n capPerMonth: g.capPerMonth,\n onExhausted: g.onExhausted,\n source: g.source,\n });\n const announceExhausted = (tx: Query, states: readonly GrantState[], draws: readonly Draw[]) =>\n Promise.all(\n draws.flatMap((d) => {\n const s = states.find((x) => x.grant.id === d.grantId);\n if (!s || s.remaining - d.amount > 0) return [];\n const g = s.grant;\n return [\n announce(tx, CREDIT_EVENT.exhausted, `exhausted:${g.id}`, {\n accountId: g.accountId,\n grantId: g.id,\n bucket: g.bucket,\n onExhausted: g.onExhausted,\n source: g.source,\n }),\n ];\n }),\n );\n\n async function create(\n tx: Query,\n kind: \"grant\" | \"purchase\",\n input: GrantInput | PurchaseInput,\n ): Promise<{ grantId: string; replayed: boolean }> {\n const now = deps.clock.now();\n checkNewGrant(kind, input, now);\n const granted = kind === \"grant\";\n const g = input as GrantInput;\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `${kind}.${input.idempotencyKey}`;\n const before = await replayOf(store, operation, input.accountId);\n if (before[0]?.grantId) return { grantId: before[0].grantId, replayed: true };\n const grant: Grant = {\n id: newId(),\n accountId: input.accountId,\n bucket: granted ? BUCKET.granted : BUCKET.purchased,\n amount: input.amount,\n capPerDay: granted ? (g.capPerDay ?? null) : null,\n capPerMonth: granted ? (g.capPerMonth ?? null) : null,\n expiresAt: granted ? g.expiresAt : null,\n onExhausted: granted ? g.onExhausted : ON_EXHAUSTED.fallbackPaid,\n source: input.source,\n createdAt: now,\n };\n await store.insertGrant(grant);\n await store.insertEntries([\n entry(\n {\n accountId: grant.accountId,\n grantId: grant.id,\n type: granted ? ENTRY_TYPE.grant : ENTRY_TYPE.purchase,\n amount: input.amount,\n reason: input.source,\n operation,\n },\n 0,\n now,\n ),\n ]);\n await announce(tx, CREDIT_EVENT.granted, `granted:${grant.id}`, grantPayload(grant));\n return { grantId: grant.id, replayed: false };\n }\n\n const drawsOf = (lines: readonly Entry[]): Draw[] =>\n lines\n .filter((l) => l.type === ENTRY_TYPE.debit && l.grantId)\n .map((l) => ({ grantId: l.grantId!, amount: -l.amount }));\n\n /** Plans and writes the debits of one operation. The caller holds the account's lock and has checked the replay. */\n async function spend(\n tx: Query,\n store: CreditStore,\n args: {\n accountId: string;\n amount: number;\n reason: string;\n operation: string;\n holdId?: string;\n exceptHoldId?: string;\n },\n ): Promise<Draw[]> {\n const { timeZone } = await settings();\n const now = deps.clock.now();\n const held = await store.heldAmount(args.accountId, now, args.exceptHoldId);\n const states = await statesOf(store, args.accountId, now, timeZone);\n const plan = planDraws(states, held + args.amount, now);\n if (!plan.ok) throw new CreditsError(plan.code, REFUSAL_TEXT[plan.code] ?? plan.code);\n const draws = skipFirst(plan.draws, held);\n await store.insertEntries(\n draws.map((d, i) =>\n entry(\n {\n accountId: args.accountId,\n grantId: d.grantId,\n type: ENTRY_TYPE.debit,\n amount: -d.amount,\n reason: args.reason,\n operation: args.operation,\n ...(args.holdId ? { holdId: args.holdId } : {}),\n },\n i,\n now,\n ),\n ),\n );\n await announceExhausted(tx, states, draws);\n return draws;\n }\n\n async function expire(tx: Query, grantId: string): Promise<{ expired: number }> {\n const store = storeFor(tx);\n const found = await store.grantById(grantId);\n if (!found) throw new CreditsError(CREDITS_CODE.unknownGrant, `No grant ${grantId}`);\n await store.lock(found.accountId);\n const { timeZone } = await settings();\n const now = deps.clock.now();\n const state = (await statesOf(store, found.accountId, now, timeZone)).find((s) => s.grant.id === grantId)!;\n if (state.closed) return { expired: 0 };\n await store.insertEntries([\n entry(\n {\n accountId: found.accountId,\n grantId,\n type: ENTRY_TYPE.expiry,\n amount: -state.remaining,\n reason: \"expiry\",\n operation: `expire.${grantId}`,\n },\n 0,\n now,\n ),\n ]);\n return { expired: state.remaining };\n }\n\n return {\n grant: (tx, input) => create(tx, \"grant\", input),\n purchase: (tx, input) => create(tx, \"purchase\", input),\n\n async consume(tx, input) {\n checkAmount(input.amount, \"a consumption\");\n checkKey(input.idempotencyKey);\n if (!input.reason || input.reason.length > MAX_REASON)\n throw new CreditsError(CREDITS_CODE.badGrant, `A reason is 1 to ${MAX_REASON} characters`);\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `consume.${input.idempotencyKey}`;\n const before = await replayOf(store, operation, input.accountId);\n if (before.length > 0) return { draws: drawsOf(before), replayed: true };\n return {\n draws: await spend(tx, store, {\n accountId: input.accountId,\n amount: input.amount,\n reason: input.reason,\n operation,\n }),\n replayed: false,\n };\n },\n\n async hold(tx, input) {\n checkAmount(input.amount, \"a hold\");\n checkKey(input.idempotencyKey);\n const s = await settings();\n const ttl = input.ttlSeconds ?? s.holdTtlSeconds;\n checkSettings({ ...s, holdTtlSeconds: ttl });\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `hold.${input.idempotencyKey}`;\n const known = await store.holdByOperation(operation);\n if (known) {\n if (known.accountId !== input.accountId)\n throw new CreditsError(CREDITS_CODE.keyReused, \"This idempotency key was used for another account\");\n return { holdId: known.id, expiresAt: known.expiresAt, replayed: true };\n }\n const now = deps.clock.now();\n const held = await store.heldAmount(input.accountId, now);\n const plan = planDraws(await statesOf(store, input.accountId, now, s.timeZone), held + input.amount, now);\n if (!plan.ok) throw new CreditsError(plan.code, REFUSAL_TEXT[plan.code] ?? plan.code);\n const hold = {\n id: newId(),\n accountId: input.accountId,\n amount: input.amount,\n expiresAt: addSeconds(now, ttl),\n status: HOLD_STATUS.active,\n settledAmount: null,\n createdAt: now,\n closedAt: null,\n operation,\n };\n await store.insertHold(hold);\n return { holdId: hold.id, expiresAt: hold.expiresAt, replayed: false };\n },\n\n async settle(tx, holdId, options = {}) {\n const store = storeFor(tx);\n const found = await store.holdById(holdId);\n if (!found) throw new CreditsError(CREDITS_CODE.unknownHold, `No hold ${holdId}`);\n await store.lock(found.accountId);\n const hold = (await store.holdById(holdId))!;\n const operation = `settle.${holdId}`;\n if (hold.status === HOLD_STATUS.settled)\n return { draws: drawsOf(await store.entriesOf(operation)), replayed: true };\n if (hold.status === HOLD_STATUS.released)\n throw new CreditsError(CREDITS_CODE.holdClosed, \"This hold was released\");\n const amount = options.amount ?? hold.amount;\n if (!Number.isSafeInteger(amount) || amount < 0 || amount > hold.amount)\n throw new CreditsError(\n CREDITS_CODE.badAmount,\n `A hold of ${hold.amount} settles for 0 to ${hold.amount}, got ${amount}`,\n );\n const now = deps.clock.now();\n const draws =\n amount === 0\n ? []\n : await spend(tx, store, {\n accountId: hold.accountId,\n amount,\n reason: options.reason ?? `hold:${holdId}`,\n operation,\n holdId,\n exceptHoldId: holdId,\n });\n await store.closeHold(holdId, amount === 0 ? HOLD_STATUS.released : HOLD_STATUS.settled, amount, now);\n return { draws, replayed: false };\n },\n\n async release(tx, holdId) {\n const store = storeFor(tx);\n const found = await store.holdById(holdId);\n if (!found) throw new CreditsError(CREDITS_CODE.unknownHold, `No hold ${holdId}`);\n await store.lock(found.accountId);\n const hold = (await store.holdById(holdId))!;\n if (hold.status === HOLD_STATUS.released) return;\n if (hold.status === HOLD_STATUS.settled) throw new CreditsError(CREDITS_CODE.holdClosed, \"This hold was settled\");\n await store.closeHold(holdId, HOLD_STATUS.released, null, deps.clock.now());\n },\n\n async refund(tx, input) {\n const original =\n input.consumed !== undefined\n ? `consume.${checkKey(input.consumed)}`\n : input.settledHold !== undefined\n ? `settle.${input.settledHold}`\n : null;\n if (!original)\n throw new CreditsError(CREDITS_CODE.badKey, \"Name what to refund: a consumed key or a settled hold\");\n if (!input.reason || input.reason.length > MAX_REASON)\n throw new CreditsError(CREDITS_CODE.badGrant, `A reason is 1 to ${MAX_REASON} characters`);\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `refund.${original}`;\n const done = await replayOf(store, operation, input.accountId);\n if (done.length > 0) return { refunded: sum(done.map((l) => l.amount)), replayed: true };\n const spent = (await replayOf(store, original, input.accountId)).filter((l) => l.type === ENTRY_TYPE.debit);\n if (spent.length === 0)\n throw new CreditsError(CREDITS_CODE.nothingToRefund, \"Nothing was spent under this key for this account\");\n const now = deps.clock.now();\n await store.insertEntries(\n spent.map((l, i) =>\n entry(\n {\n accountId: l.accountId,\n grantId: l.grantId,\n type: ENTRY_TYPE.refund,\n amount: -l.amount,\n reason: input.reason,\n operation,\n reverses: original,\n },\n i,\n now,\n ),\n ),\n );\n return { refunded: sum(spent.map((l) => -l.amount)), replayed: false };\n },\n\n expire,\n\n async expireDue(tx, options = {}) {\n const store = storeFor(tx);\n const due = await store.lapsedWithCredits(deps.clock.now(), options.limit ?? DEFAULT_SWEEP);\n let n = 0;\n for (const d of due) n += (await expire(tx, d.grantId)).expired > 0 ? 1 : 0;\n return n;\n },\n\n async adjust(tx, input) {\n checkKey(input.idempotencyKey);\n if (!Number.isSafeInteger(input.amount) || input.amount === 0)\n throw new CreditsError(CREDITS_CODE.badAmount, \"An adjustment is a non-zero integer\");\n if (!input.reason || input.reason.length > MAX_REASON)\n throw new CreditsError(CREDITS_CODE.badGrant, `A reason is 1 to ${MAX_REASON} characters`);\n const store = storeFor(tx);\n await store.lock(input.accountId);\n const operation = `adjust.${input.idempotencyKey}`;\n if ((await replayOf(store, operation, input.accountId)).length > 0) return { replayed: true };\n const { timeZone } = await settings();\n const now = deps.clock.now();\n const state = (await statesOf(store, input.accountId, now, timeZone)).find((s) => s.grant.id === input.grantId);\n if (!state) throw new CreditsError(CREDITS_CODE.unknownGrant, `No grant ${input.grantId} for this account`);\n if (state.remaining + input.amount < 0)\n throw new CreditsError(CREDITS_CODE.badAmount, \"An adjustment never takes a pot below zero\");\n await store.insertEntries([\n entry(\n {\n accountId: input.accountId,\n grantId: input.grantId,\n type: ENTRY_TYPE.adjustment,\n amount: input.amount,\n reason: input.reason,\n operation,\n },\n 0,\n now,\n ),\n ]);\n return { replayed: false };\n },\n\n async balance(query, accountId) {\n const store = storeFor(query);\n const { timeZone } = await settings();\n const now = deps.clock.now();\n const live = (await statesOf(store, accountId, now, timeZone)).filter(\n (s) => !s.closed && (s.grant.expiresAt === null || s.grant.expiresAt > now) && s.remaining > 0,\n );\n const of = (bucket: Grant[\"bucket\"]) =>\n sum(live.filter((s) => s.grant.bucket === bucket).map((s) => s.remaining));\n const total = of(BUCKET.granted) + of(BUCKET.purchased);\n const held = await store.heldAmount(accountId, now);\n return {\n total,\n granted: of(BUCKET.granted),\n purchased: of(BUCKET.purchased),\n held,\n available: Math.max(0, total - held),\n grants: live.map((s) => ({\n id: s.grant.id,\n bucket: s.grant.bucket,\n remaining: s.remaining,\n expiresAt: s.grant.expiresAt,\n source: s.grant.source,\n capPerDay: s.grant.capPerDay,\n capPerMonth: s.grant.capPerMonth,\n onExhausted: s.grant.onExhausted,\n })),\n };\n },\n\n history: (query, accountId, options = {}) =>\n storeFor(query).history(accountId, {\n limit: Math.min(options.limit ?? DEFAULT_HISTORY, MAX_HISTORY),\n ...(options.before ? { before: options.before } : {}),\n }),\n };\n}\n\nconst REFUSAL_TEXT: Record<string, string> = {\n [CREDITS_CODE.capDay]: \"The daily limit of free credits is reached\",\n [CREDITS_CODE.capMonth]: \"The monthly limit of free credits is reached\",\n [CREDITS_CODE.exhausted]: \"The free credits have run out\",\n [CREDITS_CODE.paywall]: \"Payment required: no credits left\",\n};\n","import { createHash } from \"node:crypto\";\n\nimport type { Query } from \"@krizaka/db\";\n\n/** The topic the ledger's events are appended to on the outbox. */\nexport const CREDITS_TOPIC = \"credits\";\n\n/** Event types: the names of `krizaka-billing-api`, so a TypeScript product and a Java service speak alike. */\nexport const CREDIT_EVENT = { granted: \"evt.credit.granted.v1\", exhausted: \"evt.credit.exhausted.v1\" } as const;\n\n/** The part of `@krizaka/outbox`'s `Outbox` this package needs: structural, so `credits` does not depend on `outbox`. */\nexport interface OutboxPort {\n append(\n tx: Query,\n message: { topic: string; type: string; payload: unknown; id?: string },\n ): Promise<{ id: string; appended: boolean }>;\n}\n\n/** A stable UUID (version 5 shaped) from a seed: announcing the same fact twice is one outbox message. */\nexport function uuidOf(seed: string): string {\n const h = createHash(\"sha256\").update(`krizaka.credits:${seed}`).digest();\n h[6] = (h[6]! & 0x0f) | 0x50;\n h[8] = (h[8]! & 0x3f) | 0x80;\n const x = h.subarray(0, 16).toString(\"hex\");\n return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;\n}\n\nconst base = { type: \"object\", additionalProperties: false } as const;\n\n/** The JSON Schema of each event's payload (a consumer validates against it; the Java twin publishes the same names). */\nexport const CREDIT_EVENT_SCHEMAS = {\n [CREDIT_EVENT.granted]: {\n ...base,\n required: [\"accountId\", \"grantId\", \"bucket\", \"amount\", \"source\", \"onExhausted\"],\n properties: {\n accountId: { type: \"string\" },\n grantId: { type: \"string\" },\n bucket: { enum: [\"granted\", \"purchased\"] },\n amount: { type: \"integer\", minimum: 1 },\n expiresAt: { type: [\"string\", \"null\"], format: \"date-time\" },\n capPerDay: { type: [\"integer\", \"null\"], minimum: 1 },\n capPerMonth: { type: [\"integer\", \"null\"], minimum: 1 },\n onExhausted: { enum: [\"stop\", \"fallback_paid\"] },\n source: { type: \"string\" },\n },\n },\n [CREDIT_EVENT.exhausted]: {\n ...base,\n required: [\"accountId\", \"grantId\", \"bucket\", \"source\", \"onExhausted\"],\n properties: {\n accountId: { type: \"string\" },\n grantId: { type: \"string\" },\n bucket: { enum: [\"granted\", \"purchased\"] },\n onExhausted: { enum: [\"stop\", \"fallback_paid\"] },\n source: { type: \"string\" },\n },\n },\n} as const;\n","import { ident, type Query } from \"@krizaka/db\";\n\nimport { type Calendar, type CreditStore, type Entry, type Grant, type GrantState, type Hold } from \"./port\";\n\ninterface GrantRow {\n id: string;\n account_id: string;\n bucket: Grant[\"bucket\"];\n amount: string | number;\n cap_per_day: string | number | null;\n cap_per_month: string | number | null;\n expires_at: Date | string | null;\n on_exhausted: Grant[\"onExhausted\"];\n source: string;\n created_at: Date | string;\n}\ninterface StateRow extends GrantRow {\n remaining: string | number;\n used_today: string | number;\n used_month: string | number;\n closed: boolean;\n}\ninterface EntryRow {\n id: string;\n account_id: string;\n grant_id: string | null;\n type: Entry[\"type\"];\n amount: string | number;\n reason: string;\n operation: string;\n idempotency_key: string;\n reverses: string | null;\n hold_id: string | null;\n at: Date | string;\n}\ninterface HoldRow {\n id: string;\n account_id: string;\n amount: string | number;\n expires_at: Date | string;\n status: Hold[\"status\"];\n settled_amount: string | number | null;\n created_at: Date | string;\n closed_at: Date | string | null;\n operation: string;\n}\n\nconst num = (v: string | number): number => Number(v);\nconst numOrNull = (v: string | number | null): number | null => (v === null ? null : Number(v));\nconst date = (v: Date | string): Date => new Date(v);\nconst dateOrNull = (v: Date | string | null): Date | null => (v === null ? null : new Date(v));\n\nconst toGrant = (r: GrantRow): Grant => ({\n id: r.id,\n accountId: r.account_id,\n bucket: r.bucket,\n amount: num(r.amount),\n capPerDay: numOrNull(r.cap_per_day),\n capPerMonth: numOrNull(r.cap_per_month),\n expiresAt: dateOrNull(r.expires_at),\n onExhausted: r.on_exhausted,\n source: r.source,\n createdAt: date(r.created_at),\n});\nconst toEntry = (r: EntryRow): Entry => ({\n id: r.id,\n accountId: r.account_id,\n grantId: r.grant_id,\n type: r.type,\n amount: num(r.amount),\n reason: r.reason,\n operation: r.operation,\n idempotencyKey: r.idempotency_key,\n reverses: r.reverses,\n holdId: r.hold_id,\n at: date(r.at),\n});\nconst toHold = (r: HoldRow): Hold => ({\n id: r.id,\n accountId: r.account_id,\n amount: num(r.amount),\n expiresAt: date(r.expires_at),\n status: r.status,\n settledAmount: numOrNull(r.settled_amount),\n createdAt: date(r.created_at),\n closedAt: dateOrNull(r.closed_at),\n operation: r.operation,\n});\n\n/**\n * The PostgreSQL store of the ledger, in schema `credits` (see `creditsSchema`), over the `Query` it is given: the caller's transaction\n * for every writing call, so the lines commit or roll back with the product's work. `lock` is `pg_advisory_xact_lock` on the account:\n * two requests spending the last credit take turns, and the second sees the first's lines. Days and months are cut in the calendar's\n * IANA zone BY POSTGRES (`AT TIME ZONE`), so daylight-saving shifts are the database's own tz data. Times come from the caller.\n * @param schema the Postgres schema of the module, default `credits`\n */\nexport function dbCreditStore(query: Query, schema = \"credits\"): CreditStore {\n const s = ident(schema);\n const grant = `${s}.\"grant\"`;\n const entry = `${s}.entry`;\n const hold = `${s}.hold`;\n return {\n async lock(accountId) {\n await query(\"SELECT pg_advisory_xact_lock(hashtextextended('krizaka.credits:' || $1::text, 0))\", [accountId]);\n },\n async insertGrant(g) {\n await query(\n `INSERT INTO ${grant} (id, account_id, bucket, amount, cap_per_day, cap_per_month, expires_at, on_exhausted, source, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,\n [\n g.id,\n g.accountId,\n g.bucket,\n g.amount,\n g.capPerDay,\n g.capPerMonth,\n g.expiresAt,\n g.onExhausted,\n g.source,\n g.createdAt,\n ],\n );\n },\n async grantById(id) {\n const [row] = await query<GrantRow>(`SELECT * FROM ${grant} WHERE id = $1`, [id]);\n return row ? toGrant(row) : null;\n },\n async grantStates(accountId, calendar: Calendar): Promise<GrantState[]> {\n const rows = await query<StateRow>(\n `SELECT g.*,\n COALESCE(SUM(e.amount), 0) AS remaining,\n COALESCE(-SUM(e.amount) FILTER (WHERE e.type IN ('DEBIT', 'REFUND') AND (e.at AT TIME ZONE $2)::date = $3::date), 0) AS used_today,\n COALESCE(-SUM(e.amount) FILTER (WHERE e.type IN ('DEBIT', 'REFUND') AND to_char(e.at AT TIME ZONE $2, 'YYYY-MM') = $4), 0) AS used_month,\n COALESCE(bool_or(e.type = 'EXPIRY'), false) AS closed\n FROM ${grant} g LEFT JOIN ${entry} e ON e.grant_id = g.id\n WHERE g.account_id = $1\n GROUP BY g.id`,\n [accountId, calendar.timeZone, calendar.day, calendar.month],\n );\n return rows.map((r) => ({\n grant: toGrant(r),\n remaining: num(r.remaining),\n usedToday: num(r.used_today),\n usedThisMonth: num(r.used_month),\n closed: r.closed,\n }));\n },\n async insertEntries(entries) {\n for (const e of entries) {\n await query(\n `INSERT INTO ${entry} (id, account_id, grant_id, type, amount, reason, operation, idempotency_key, reverses, hold_id, at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,\n [\n e.id,\n e.accountId,\n e.grantId,\n e.type,\n e.amount,\n e.reason,\n e.operation,\n e.idempotencyKey,\n e.reverses,\n e.holdId,\n e.at,\n ],\n );\n }\n },\n async entriesOf(operation) {\n return (await query<EntryRow>(`SELECT * FROM ${entry} WHERE operation = $1 ORDER BY seq`, [operation])).map(\n toEntry,\n );\n },\n async history(accountId, { limit, before }) {\n return (\n await query<EntryRow>(\n `SELECT * FROM ${entry} WHERE account_id = $1 AND ($2::timestamptz IS NULL OR at < $2) ORDER BY seq DESC LIMIT $3`,\n [accountId, before ?? null, limit],\n )\n ).map(toEntry);\n },\n async insertHold(h) {\n await query(\n `INSERT INTO ${hold} (id, account_id, amount, expires_at, status, settled_amount, created_at, closed_at, operation) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,\n [h.id, h.accountId, h.amount, h.expiresAt, h.status, h.settledAmount, h.createdAt, h.closedAt, h.operation],\n );\n },\n async holdById(id) {\n const [row] = await query<HoldRow>(`SELECT * FROM ${hold} WHERE id = $1`, [id]);\n return row ? toHold(row) : null;\n },\n async holdByOperation(operation) {\n const [row] = await query<HoldRow>(`SELECT * FROM ${hold} WHERE operation = $1`, [operation]);\n return row ? toHold(row) : null;\n },\n async heldAmount(accountId, now, exceptHoldId) {\n const [row] = await query<{ held: string | number }>(\n `SELECT COALESCE(SUM(amount), 0) AS held FROM ${hold} WHERE account_id = $1 AND status = 'active' AND expires_at > $2 AND ($3::uuid IS NULL OR id <> $3)`,\n [accountId, now, exceptHoldId ?? null],\n );\n return num(row?.held ?? 0);\n },\n async closeHold(id, status, settledAmount, at) {\n await query(`UPDATE ${hold} SET status = $2, settled_amount = $3, closed_at = $4 WHERE id = $1`, [\n id,\n status,\n settledAmount,\n at,\n ]);\n },\n async lapsedWithCredits(now, limit) {\n const rows = await query<{ id: string; account_id: string }>(\n `SELECT g.id, g.account_id FROM ${grant} g JOIN ${entry} e ON e.grant_id = g.id\n WHERE g.bucket = 'granted' AND g.expires_at <= $1\n GROUP BY g.id HAVING SUM(e.amount) > 0 AND NOT bool_or(e.type = 'EXPIRY')\n ORDER BY g.expires_at, g.id LIMIT $2`,\n [now, limit],\n );\n return rows.map((r) => ({ grantId: r.id, accountId: r.account_id }));\n },\n };\n}\n","import { localDay } from \"@krizaka/intl/date\";\nimport { isAfter } from \"@krizaka/time\";\n\nimport { BUCKET, type CreditStore, type Entry, ENTRY_TYPE, type Grant, type Hold, HOLD_STATUS } from \"./port\";\n\n/**\n * An in-memory ledger, for unit tests and demos: the same behaviour as the database's (the contract kit runs on both) except that\n * `lock` does nothing, because it has no transaction to hold it in. It is for one flow at a time; concurrency is proved on PostgreSQL.\n */\nexport function memoryCreditStore(): CreditStore & { entries(): readonly Readonly<Entry>[] } {\n const grants = new Map<string, Grant>();\n const entries: Entry[] = [];\n const holds = new Map<string, Hold>();\n return {\n entries: () => entries,\n async lock() {},\n async insertGrant(g) {\n grants.set(g.id, { ...g });\n },\n async grantById(id) {\n const g = grants.get(id);\n return g ? { ...g } : null;\n },\n async grantStates(accountId, calendar) {\n return [...grants.values()]\n .filter((g) => g.accountId === accountId)\n .map((g) => {\n const lines = entries.filter((e) => e.grantId === g.id);\n const used = (match: (e: Entry) => boolean) =>\n -lines\n .filter((e) => (e.type === ENTRY_TYPE.debit || e.type === ENTRY_TYPE.refund) && match(e))\n .reduce((a, e) => a + e.amount, 0);\n return {\n grant: { ...g },\n remaining: lines.reduce((a, e) => a + e.amount, 0),\n usedToday: used((e) => localDay(e.at, calendar.timeZone) === calendar.day),\n usedThisMonth: used((e) => localDay(e.at, calendar.timeZone).slice(0, 7) === calendar.month),\n closed: lines.some((e) => e.type === ENTRY_TYPE.expiry),\n };\n });\n },\n async insertEntries(batch) {\n for (const e of batch) {\n if (entries.some((x) => x.idempotencyKey === e.idempotencyKey))\n throw new Error(`duplicate idempotency key ${e.idempotencyKey}`);\n }\n entries.push(...batch.map((e) => ({ ...e })));\n },\n async entriesOf(operation) {\n return entries.filter((e) => e.operation === operation).map((e) => ({ ...e }));\n },\n async history(accountId, { limit, before }) {\n return entries\n .filter((e) => e.accountId === accountId && (!before || e.at < before))\n .reverse()\n .slice(0, limit)\n .map((e) => ({ ...e }));\n },\n async insertHold(h) {\n holds.set(h.id, { ...h });\n },\n async holdById(id) {\n const h = holds.get(id);\n return h ? { ...h } : null;\n },\n async holdByOperation(operation) {\n const h = [...holds.values()].find((x) => x.operation === operation);\n return h ? { ...h } : null;\n },\n async heldAmount(accountId, now, exceptHoldId) {\n return [...holds.values()]\n .filter(\n (h) =>\n h.accountId === accountId &&\n h.status === HOLD_STATUS.active &&\n isAfter(h.expiresAt, now) &&\n h.id !== exceptHoldId,\n )\n .reduce((a, h) => a + h.amount, 0);\n },\n async closeHold(id, status, settledAmount, at) {\n const h = holds.get(id);\n if (h) Object.assign(h, { status, settledAmount, closedAt: at });\n },\n async lapsedWithCredits(now, limit) {\n return [...grants.values()]\n .filter((g) => g.bucket === BUCKET.granted && g.expiresAt !== null && g.expiresAt <= now)\n .filter((g) => {\n const lines = entries.filter((e) => e.grantId === g.id);\n return lines.reduce((a, e) => a + e.amount, 0) > 0 && !lines.some((e) => e.type === ENTRY_TYPE.expiry);\n })\n .sort((a, b) => a.expiresAt!.valueOf() - b.expiresAt!.valueOf() || a.id.localeCompare(b.id))\n .slice(0, limit)\n .map((g) => ({ grantId: g.id, accountId: g.accountId }));\n },\n };\n}\n","import { defineModule } from \"@krizaka/db\";\n\n/**\n * The tables of « credits », in their own Postgres schema (ADR-001 §3), following `krizaka-billing`'s model and names: `grant` (a pot:\n * `granted` with an expiry and optional caps, or `purchased`), `entry` (the ledger: GRANT, PURCHASE, DEBIT, REFUND, EXPIRY, ADJUSTMENT;\n * APPEND-ONLY, a trigger refuses any UPDATE or DELETE) and `hold` (credits kept back for work in progress). Nothing stores a balance: what is\n * left of a pot is the sum of its lines. The one product table referenced is the owner (`owner`: the account table), declared by the product;\n * an account that has credits is anonymised, never deleted (it is an accounting record), exactly like an invoice.\n * Version 1 is frozen once released: a change is version 2.\n */\nexport const creditsSchema = defineModule({\n module: \"credits\",\n tables: [\"owner\"],\n migrations: [\n {\n version: 1,\n name: \"grants, ledger and holds\",\n sql: (c) => `\nCREATE TABLE ${c.schema}.\"grant\" (\n id uuid PRIMARY KEY,\n account_id uuid NOT NULL REFERENCES ${c.tables.owner}(id),\n bucket text NOT NULL CHECK (bucket IN ('granted', 'purchased')),\n amount bigint NOT NULL CHECK (amount > 0),\n cap_per_day bigint CHECK (cap_per_day IS NULL OR cap_per_day > 0),\n cap_per_month bigint CHECK (cap_per_month IS NULL OR cap_per_month > 0),\n expires_at timestamptz,\n on_exhausted text NOT NULL CHECK (on_exhausted IN ('stop', 'fallback_paid')),\n source text NOT NULL CHECK (char_length(source) BETWEEN 1 AND 200),\n created_at timestamptz NOT NULL,\n CHECK ((bucket = 'granted' AND expires_at IS NOT NULL)\n OR (bucket = 'purchased' AND expires_at IS NULL AND cap_per_day IS NULL AND cap_per_month IS NULL))\n);\nCREATE INDEX grant_account ON ${c.schema}.\"grant\" (account_id, created_at);\nCREATE INDEX grant_lapse ON ${c.schema}.\"grant\" (expires_at) WHERE bucket = 'granted';\nCREATE TABLE ${c.schema}.hold (\n id uuid PRIMARY KEY,\n account_id uuid NOT NULL REFERENCES ${c.tables.owner}(id),\n amount bigint NOT NULL CHECK (amount > 0),\n expires_at timestamptz NOT NULL,\n status text NOT NULL CHECK (status IN ('active', 'settled', 'released')),\n settled_amount bigint CHECK (settled_amount IS NULL OR settled_amount >= 0),\n created_at timestamptz NOT NULL,\n closed_at timestamptz,\n operation text NOT NULL UNIQUE\n);\nCREATE INDEX hold_active ON ${c.schema}.hold (account_id) WHERE status = 'active';\nCREATE TABLE ${c.schema}.entry (\n seq bigint GENERATED ALWAYS AS IDENTITY,\n id uuid PRIMARY KEY,\n account_id uuid NOT NULL,\n grant_id uuid REFERENCES ${c.schema}.\"grant\"(id),\n type text NOT NULL CHECK (type IN ('GRANT', 'PURCHASE', 'DEBIT', 'REFUND', 'EXPIRY', 'ADJUSTMENT')),\n amount bigint NOT NULL CHECK (amount <> 0 OR type = 'EXPIRY'),\n reason text NOT NULL CHECK (char_length(reason) BETWEEN 1 AND 300),\n operation text NOT NULL,\n idempotency_key text NOT NULL UNIQUE,\n reverses text,\n hold_id uuid,\n at timestamptz NOT NULL\n);\nCREATE INDEX entry_account ON ${c.schema}.entry (account_id, seq DESC);\nCREATE INDEX entry_grant ON ${c.schema}.entry (grant_id);\nCREATE INDEX entry_operation ON ${c.schema}.entry (operation);\nCREATE FUNCTION ${c.schema}.entry_is_append_only() RETURNS trigger LANGUAGE plpgsql AS $fn$\nBEGIN\n RAISE EXCEPTION 'credits.entry is append-only: % refused', TG_OP;\nEND;\n$fn$;\nCREATE TRIGGER entry_append_only BEFORE UPDATE OR DELETE ON ${c.schema}.entry FOR EACH ROW EXECUTE FUNCTION ${c.schema}.entry_is_append_only();`,\n },\n ],\n});\n"],"mappings":"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","names":["localDay","localDay"]}
package/dist/settings.js CHANGED
@@ -1,20 +1,27 @@
1
1
  import {
2
2
  CREDITS_DEFAULTS,
3
3
  HOLD_TTL_BOUNDS
4
- } from "./chunk-34Y2HXOB.js";
5
- import "./chunk-WDHQ7QHY.js";
4
+ } from "./chunk-FPQSMNV2.js";
5
+ import "./chunk-NKDQNDSD.js";
6
6
 
7
7
  // src/settings.ts
8
8
  import { choice, integer } from "@krizaka/settings";
9
9
  var ZONES = Intl.supportedValuesOf("timeZone");
10
10
  var creditsSettings = {
11
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" })
12
+ "credits.holdTtlSeconds": integer("credits", CREDITS_DEFAULTS.holdTtlSeconds, {
13
+ min: HOLD_TTL_BOUNDS[0],
14
+ max: HOLD_TTL_BOUNDS[1],
15
+ unit: "seconds"
16
+ })
13
17
  };
14
18
  async function readCreditsSettings(read) {
15
19
  const zone = await read("credits.timeZone");
16
20
  const ttl = await read("credits.holdTtlSeconds");
17
- return { timeZone: typeof zone === "string" ? zone : CREDITS_DEFAULTS.timeZone, holdTtlSeconds: typeof ttl === "number" ? ttl : CREDITS_DEFAULTS.holdTtlSeconds };
21
+ return {
22
+ timeZone: typeof zone === "string" ? zone : CREDITS_DEFAULTS.timeZone,
23
+ holdTtlSeconds: typeof ttl === "number" ? ttl : CREDITS_DEFAULTS.holdTtlSeconds
24
+ };
18
25
  }
19
26
  export {
20
27
  creditsSettings,
@@ -1 +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":[]}
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, {\n min: HOLD_TTL_BOUNDS[0],\n max: HOLD_TTL_BOUNDS[1],\n unit: \"seconds\",\n }),\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(\n read: (key: keyof typeof creditsSettings) => string | number | undefined | Promise<string | number | undefined>,\n): Promise<CreditsSettings> {\n const zone = await read(\"credits.timeZone\");\n const ttl = await read(\"credits.holdTtlSeconds\");\n return {\n timeZone: typeof zone === \"string\" ? zone : CREDITS_DEFAULTS.timeZone,\n holdTtlSeconds: typeof ttl === \"number\" ? ttl : CREDITS_DEFAULTS.holdTtlSeconds,\n };\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;AAAA,IAC5E,KAAK,gBAAgB,CAAC;AAAA,IACtB,KAAK,gBAAgB,CAAC;AAAA,IACtB,MAAM;AAAA,EACR,CAAC;AACH;AAMA,eAAsB,oBACpB,MAC0B;AAC1B,QAAM,OAAO,MAAM,KAAK,kBAAkB;AAC1C,QAAM,MAAM,MAAM,KAAK,wBAAwB;AAC/C,SAAO;AAAA,IACL,UAAU,OAAO,SAAS,WAAW,OAAO,iBAAiB;AAAA,IAC7D,gBAAgB,OAAO,QAAQ,WAAW,MAAM,iBAAiB;AAAA,EACnE;AACF;","names":[]}
package/dist/testkit.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ENTRY_TYPE,
3
3
  ON_EXHAUSTED
4
- } from "./chunk-WDHQ7QHY.js";
4
+ } from "./chunk-NKDQNDSD.js";
5
5
 
6
6
  // src/testkit.ts
7
7
  import { randomUUID } from "crypto";
@@ -18,7 +18,15 @@ async function refusal(work) {
18
18
  throw new Error("expected a refusal, got an answer");
19
19
  }
20
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 });
21
+ const grant = (h, accountId, over = {}) => h.credits.grant(h.tx, {
22
+ accountId,
23
+ amount: 10,
24
+ expiresAt: plus(T0, 30 * DAY),
25
+ onExhausted: ON_EXHAUSTED.stop,
26
+ source: "coupon:test",
27
+ idempotencyKey: randomUUID(),
28
+ ...over
29
+ });
22
30
  const buy = (h, accountId, amount) => h.credits.purchase(h.tx, { accountId, amount, source: "purchase:pi_1", idempotencyKey: randomUUID() });
23
31
  const consume = (h, accountId, amount, idempotencyKey = randomUUID()) => h.credits.consume(h.tx, { accountId, amount, reason: "audit:1", idempotencyKey });
24
32
  it(`${name}: derives the balance from the ledger: a grant and a purchase, the history newest first`, async () => {
@@ -27,9 +35,18 @@ function creditsLedgerContract(name, make) {
27
35
  expect(await h.credits.balance(h.tx, a)).toMatchObject({ total: 0, available: 0, grants: [] });
28
36
  await grant(h, a, { amount: 5 });
29
37
  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 });
38
+ expect(await h.credits.balance(h.tx, a)).toMatchObject({
39
+ total: 12,
40
+ granted: 5,
41
+ purchased: 7,
42
+ held: 0,
43
+ available: 12
44
+ });
31
45
  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]]);
46
+ expect(lines.map((l) => [l.type, l.amount])).toEqual([
47
+ [ENTRY_TYPE.purchase, 7],
48
+ [ENTRY_TYPE.grant, 5]
49
+ ]);
33
50
  });
34
51
  it(`${name}: a grant or a purchase with a key seen before changes nothing`, async () => {
35
52
  const h = await make();
@@ -38,7 +55,9 @@ function creditsLedgerContract(name, make) {
38
55
  const again = await grant(h, a, { idempotencyKey: "g-1", amount: 4 });
39
56
  expect(again).toEqual({ grantId: first.grantId, replayed: true });
40
57
  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);
58
+ expect(
59
+ (await h.credits.purchase(h.tx, { accountId: a, amount: 3, source: "purchase:x", idempotencyKey: "p-1" })).grantId
60
+ ).toBe(p.grantId);
42
61
  expect((await h.credits.balance(h.tx, a)).total).toBe(7);
43
62
  expect(await h.credits.history(h.tx, a)).toHaveLength(2);
44
63
  });
@@ -53,13 +72,23 @@ function creditsLedgerContract(name, make) {
53
72
  it(`${name}: spends from the pot that lapses first, then the next; purchased credits last`, async () => {
54
73
  const h = await make();
55
74
  const a = await h.newAccount();
56
- const late = await grant(h, a, { amount: 5, expiresAt: plus(T0, 20 * DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });
75
+ const late = await grant(h, a, {
76
+ amount: 5,
77
+ expiresAt: plus(T0, 20 * DAY),
78
+ onExhausted: ON_EXHAUSTED.fallbackPaid
79
+ });
57
80
  const soon = await grant(h, a, { amount: 3, expiresAt: plus(T0, 5 * DAY), onExhausted: ON_EXHAUSTED.fallbackPaid });
58
81
  const paid = await buy(h, a, 10);
59
82
  const out = await consume(h, a, 4);
60
- expect(out.draws).toEqual([{ grantId: soon.grantId, amount: 3 }, { grantId: late.grantId, amount: 1 }]);
83
+ expect(out.draws).toEqual([
84
+ { grantId: soon.grantId, amount: 3 },
85
+ { grantId: late.grantId, amount: 1 }
86
+ ]);
61
87
  const rest = await consume(h, a, 4 + 4 + 1);
62
- expect(rest.draws).toEqual([{ grantId: late.grantId, amount: 4 }, { grantId: paid.grantId, amount: 5 }]);
88
+ expect(rest.draws).toEqual([
89
+ { grantId: late.grantId, amount: 4 },
90
+ { grantId: paid.grantId, amount: 5 }
91
+ ]);
63
92
  });
64
93
  it(`${name}: a consume with a key seen before answers the first answer and spends nothing more`, async () => {
65
94
  const h = await make();
@@ -131,7 +160,10 @@ function creditsLedgerContract(name, make) {
131
160
  const capped = await grant(h, a, { amount: 10, capPerDay: 2, expiresAt: plus(T0, 5 * DAY) });
132
161
  const open = await grant(h, a, { amount: 10, expiresAt: plus(T0, 9 * DAY) });
133
162
  const out = await consume(h, a, 5);
134
- expect(out.draws).toEqual([{ grantId: capped.grantId, amount: 2 }, { grantId: open.grantId, amount: 3 }]);
163
+ expect(out.draws).toEqual([
164
+ { grantId: capped.grantId, amount: 2 },
165
+ { grantId: open.grantId, amount: 3 }
166
+ ]);
135
167
  });
136
168
  it(`${name}: a pot is not spendable after its date, and expire() writes off what is left once`, async () => {
137
169
  const h = await make();
@@ -143,7 +175,11 @@ function creditsLedgerContract(name, make) {
143
175
  expect(await refusal(consume(h, a, 1))).toBe("PAYWALL");
144
176
  expect(await h.credits.expire(h.tx, g.grantId)).toEqual({ expired: 4 });
145
177
  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]]);
178
+ expect((await h.credits.history(h.tx, a)).map((l) => [l.type, l.amount])).toEqual([
179
+ [ENTRY_TYPE.expiry, -4],
180
+ [ENTRY_TYPE.debit, -2],
181
+ [ENTRY_TYPE.grant, 6]
182
+ ]);
147
183
  });
148
184
  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
185
  const h = await make();
@@ -187,7 +223,9 @@ function creditsLedgerContract(name, make) {
187
223
  const a = await h.newAccount();
188
224
  await grant(h, a, { amount: 10, capPerDay: 3 });
189
225
  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");
226
+ expect(await refusal(h.credits.hold(h.tx, { accountId: a, amount: 1, idempotencyKey: "h-2" }))).toBe(
227
+ "CREDITS_CAP_DAY"
228
+ );
191
229
  await h.credits.release(h.tx, hold.holdId);
192
230
  await h.credits.release(h.tx, hold.holdId);
193
231
  expect(await refusal(h.credits.settle(h.tx, hold.holdId))).toBe("CREDITS_HOLD_CLOSED");
@@ -222,14 +260,24 @@ function creditsLedgerContract(name, make) {
222
260
  await buy(h, a, 5);
223
261
  const spent = await consume(h, a, 7, "job-7");
224
262
  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 });
263
+ expect(await h.credits.refund(h.tx, { accountId: a, consumed: "job-7", reason: "the run failed" })).toEqual({
264
+ refunded: 7,
265
+ replayed: false
266
+ });
267
+ expect(await h.credits.refund(h.tx, { accountId: a, consumed: "job-7", reason: "the run failed" })).toEqual({
268
+ refunded: 7,
269
+ replayed: true
270
+ });
227
271
  const b = await h.credits.balance(h.tx, a);
228
272
  expect(b.total).toBe(10);
229
273
  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");
274
+ expect(await refusal(h.credits.refund(h.tx, { accountId: a, consumed: "nope", reason: "x" }))).toBe(
275
+ "CREDITS_NOTHING_TO_REFUND"
276
+ );
231
277
  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");
278
+ expect(await refusal(h.credits.refund(h.tx, { accountId: other, consumed: "job-7", reason: "x" }))).toBe(
279
+ "CREDITS_KEY_REUSED"
280
+ );
233
281
  });
234
282
  it(`${name}: a refund of a settled hold, and a refund counts back against the cap`, async () => {
235
283
  const h = await make();
@@ -245,13 +293,55 @@ function creditsLedgerContract(name, make) {
245
293
  const h = await make();
246
294
  const a = await h.newAccount();
247
295
  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 });
296
+ expect(
297
+ await h.credits.adjust(h.tx, {
298
+ accountId: a,
299
+ grantId: g.grantId,
300
+ amount: 2,
301
+ reason: "goodwill",
302
+ idempotencyKey: "adj-1"
303
+ })
304
+ ).toEqual({ replayed: false });
305
+ expect(
306
+ await h.credits.adjust(h.tx, {
307
+ accountId: a,
308
+ grantId: g.grantId,
309
+ amount: 2,
310
+ reason: "goodwill",
311
+ idempotencyKey: "adj-1"
312
+ })
313
+ ).toEqual({ replayed: true });
250
314
  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" });
315
+ expect(
316
+ await refusal(
317
+ h.credits.adjust(h.tx, {
318
+ accountId: a,
319
+ grantId: g.grantId,
320
+ amount: -6,
321
+ reason: "oops",
322
+ idempotencyKey: "adj-2"
323
+ })
324
+ )
325
+ ).toBe("CREDITS_BAD_AMOUNT");
326
+ await h.credits.adjust(h.tx, {
327
+ accountId: a,
328
+ grantId: g.grantId,
329
+ amount: -5,
330
+ reason: "revoked",
331
+ idempotencyKey: "adj-3"
332
+ });
253
333
  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");
334
+ expect(
335
+ await refusal(
336
+ h.credits.adjust(h.tx, {
337
+ accountId: a,
338
+ grantId: randomUUID(),
339
+ amount: 1,
340
+ reason: "x",
341
+ idempotencyKey: "adj-4"
342
+ })
343
+ )
344
+ ).toBe("CREDITS_UNKNOWN_GRANT");
255
345
  });
256
346
  it(`${name}: announces a grant, and a pot running out, once each`, async () => {
257
347
  const h = await make();
@@ -263,7 +353,12 @@ function creditsLedgerContract(name, make) {
263
353
  const types = h.announced().map((m) => m.type);
264
354
  expect(types.filter((t) => t === "evt.credit.granted.v1")).toHaveLength(1);
265
355
  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" });
356
+ expect(h.announced().find((m) => m.type === "evt.credit.exhausted.v1")?.payload).toMatchObject({
357
+ accountId: a,
358
+ bucket: "granted",
359
+ onExhausted: "stop",
360
+ source: "coupon:test"
361
+ });
267
362
  });
268
363
  it(`${name}: history pages back from a date`, async () => {
269
364
  const h = await make();