@krizaka/coupons 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/kernel.ts","../src/events.ts","../src/persistence.ts","../src/admin.ts","../src/flows.ts","../src/coupons.ts","../src/memory.ts","../src/schema.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { COUPONS_TOPIC, uuidOf } from \"./events\";\nimport { dbCouponStore } from \"./persistence\";\nimport {\n type Actor,\n type AuditPort,\n type Clock,\n type CouponSettings,\n type CouponStore,\n type CreditsPort,\n type OutboxPort,\n type Query,\n type RateLimitPort,\n} from \"./port\";\nimport { checkSettings } from \"./rules\";\n\nexport interface CouponsDeps {\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: CouponSettings | (() => CouponSettings | Promise<CouponSettings>);\n /** The trail (`createAudit(...)` of `@krizaka/audit`): every admin change and every redemption is recorded in the caller's transaction. */\n audit: AuditPort;\n /** The limiter (`createLimiter(...)` of `@krizaka/rate-limit`): each attempt with a code counts one hit on `COUPON_ATTEMPT_ACTION`. */\n rateLimit: RateLimitPort;\n /** The credits ledger (`createCredits(...)` of `@krizaka/credits`): needed for credit coupons. */\n credits?: CreditsPort;\n /** The outbox (`createOutbox(...)` of `@krizaka/outbox`): the events are appended in the caller's transaction. */\n outbox?: OutboxPort;\n /** The store over a query (a pool or a transaction). Default: the PostgreSQL store on schema `coupons`. */\n storeFor?: (query: Query) => CouponStore;\n /** The Postgres schema, when the product renamed the module's (used by the default store). */\n schema?: string;\n /** Fresh ids; default random UUIDs. */\n newId?: () => string;\n}\n\n/** The system as an actor (a webhook, a sweeper). */\nexport const SYSTEM: Actor = { id: null, label: \"system\" };\n\n/** What the flows and the admin share: the clock, the settings, the store, the trail and the outbox. */\nexport interface Kernel {\n deps: CouponsDeps;\n now(): Date;\n settings(): Promise<CouponSettings>;\n store(query: Query): CouponStore;\n newId(): string;\n record(tx: Query, actor: Actor, action: string, target: { type: string; id: string }, detail?: Record<string, unknown>): Promise<void>;\n announce(tx: Query, type: string, seed: string, payload: Record<string, unknown>): Promise<void>;\n}\n\nexport function makeKernel(deps: CouponsDeps): Kernel {\n const storeFor = deps.storeFor ?? ((query: Query) => dbCouponStore(query, deps.schema ?? \"coupons\"));\n return {\n deps,\n now: () => deps.clock.now(),\n settings: async () => checkSettings(typeof deps.settings === \"function\" ? await deps.settings() : deps.settings),\n store: storeFor,\n newId: deps.newId ?? randomUUID,\n record: (tx, actor, action, target, detail = {}) => deps.audit.record(tx, { actorId: actor.id, actorLabel: actor.label, action, target, detail }),\n async announce(tx, type, seed, payload) {\n await deps.outbox?.append(tx, { topic: COUPONS_TOPIC, type, payload, id: uuidOf(seed) });\n },\n };\n}\n","import { createHash } from \"node:crypto\";\n\n/** The topic the coupons' events are appended to on the outbox. */\nexport const COUPONS_TOPIC = \"coupons\";\n\nexport const COUPON_EVENT = {\n redeemed: \"evt.coupon.redeemed.v1\",\n released: \"evt.coupon.released.v1\",\n refunded: \"evt.coupon.refunded.v1\",\n exhausted: \"evt.coupon.exhausted.v1\",\n} as const;\n\n/** A stable UUID (version 5 shaped) from a seed: the same fact announced twice is one outbox message. */\nexport function uuidOf(seed: string): string {\n const h = createHash(\"sha256\").update(`krizaka.coupons:${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 nullableString = { type: [\"string\", \"null\"] } as const;\nconst nullableInt = { type: [\"integer\", \"null\"] } as const;\n\n/** The JSON Schema of each event's payload. `evt.coupon.redeemed.v1` is the hook of the later commissions module: it carries the partner and the amounts. */\nexport const COUPON_EVENT_SCHEMAS = {\n [COUPON_EVENT.redeemed]: {\n type: \"object\",\n additionalProperties: false,\n required: [\"redemptionId\", \"couponId\", \"kind\", \"purpose\", \"partnerId\", \"amountPaidMinor\", \"amountDiscountMinor\", \"currency\", \"credits\", \"overLimit\"],\n properties: {\n redemptionId: { type: \"string\" },\n couponId: { type: \"string\" },\n kind: { enum: [\"discount\", \"credit\"] },\n purpose: { enum: [\"partner\", \"promo\", \"demo\", \"support\"] },\n partnerId: nullableString,\n amountPaidMinor: nullableInt,\n amountDiscountMinor: nullableInt,\n currency: nullableString,\n credits: nullableInt,\n overLimit: { type: \"boolean\" },\n },\n },\n [COUPON_EVENT.released]: { type: \"object\", additionalProperties: false, required: [\"redemptionId\", \"couponId\"], properties: { redemptionId: { type: \"string\" }, couponId: { type: \"string\" } } },\n [COUPON_EVENT.refunded]: {\n type: \"object\",\n additionalProperties: false,\n required: [\"redemptionId\", \"couponId\", \"partnerId\", \"amountRefundedMinor\", \"currency\", \"useRestored\"],\n properties: { redemptionId: { type: \"string\" }, couponId: { type: \"string\" }, partnerId: nullableString, amountRefundedMinor: { type: \"integer\", minimum: 0 }, currency: nullableString, useRestored: { type: \"boolean\" } },\n },\n [COUPON_EVENT.exhausted]: { type: \"object\", additionalProperties: false, required: [\"couponId\", \"partnerId\"], properties: { couponId: { type: \"string\" }, partnerId: nullableString } },\n} as const;\n","import { ident } from \"@krizaka/db\";\n\nimport {\n type Coupon,\n COUPON_ERROR,\n CouponError,\n type CouponPatch,\n type CouponStore,\n type Partner,\n type PartnerRow,\n type Query,\n type Redemption,\n type RedemptionPatch,\n} from \"./port\";\n\ntype Row = Record<string, unknown>;\nconst n = (v: unknown): number => Number(v);\nconst nOrNull = (v: unknown): number | null => (v === null || v === undefined ? null : Number(v));\nconst d = (v: unknown): Date => new Date(v as string | Date);\nconst dOrNull = (v: unknown): Date | null => (v === null || v === undefined ? null : new Date(v as string | Date));\n\nconst toCoupon = (r: Row): Coupon => ({\n id: r.id as string, code: r.code as string, codeKey: r.code_key as string, kind: r.kind as Coupon[\"kind\"], purpose: r.purpose as Coupon[\"purpose\"], partnerId: (r.partner_id as string | null) ?? null,\n status: r.status as Coupon[\"status\"], startsAt: d(r.starts_at), endsAt: dOrNull(r.ends_at), maxRedemptions: nOrNull(r.max_redemptions), maxPerAccount: n(r.max_per_account),\n newCustomersOnly: r.new_customers_only as boolean, plans: r.plans as string[], percentOffBp: nOrNull(r.percent_off_bp), amountOffMinor: nOrNull(r.amount_off_minor), currency: (r.currency as string | null) ?? null,\n duration: (r.duration as Coupon[\"duration\"]) ?? null, durationMonths: nOrNull(r.duration_months), minimumMinor: nOrNull(r.minimum_minor), credits: nOrNull(r.credits), capPerDay: nOrNull(r.cap_per_day),\n capPerMonth: nOrNull(r.cap_per_month), creditsValidDays: nOrNull(r.credits_valid_days), onExhausted: (r.on_exhausted as Coupon[\"onExhausted\"]) ?? null, termsVersion: n(r.terms_version),\n createdAt: d(r.created_at), createdBy: (r.created_by as string | null) ?? null, updatedAt: d(r.updated_at),\n});\n\nconst toPartner = (r: Row): Partner => ({\n id: r.id as string, displayName: (r.display_name as string | null) ?? null, contactEmail: (r.contact_email as string | null) ?? null, status: r.status as Partner[\"status\"],\n createdAt: d(r.created_at), createdBy: (r.created_by as string | null) ?? null, erasedAt: dOrNull(r.erased_at),\n});\n\nconst toRedemption = (r: Row): Redemption => ({\n id: r.id as string, couponId: r.coupon_id as string, accountId: (r.account_id as string | null) ?? null, partnerId: (r.partner_id as string | null) ?? null, status: r.status as Redemption[\"status\"],\n idempotencyKey: r.idempotency_key as string, reservedUntil: dOrNull(r.reserved_until), checkoutRef: (r.checkout_ref as string | null) ?? null, paymentRef: (r.payment_ref as string | null) ?? null,\n amountPaidMinor: nOrNull(r.amount_paid_minor), amountDiscountMinor: nOrNull(r.amount_discount_minor), amountRefundedMinor: n(r.amount_refunded_minor), useRestored: r.use_restored as boolean,\n currency: (r.currency as string | null) ?? null, creditGrantId: (r.credit_grant_id as string | null) ?? null, overLimit: r.over_limit as boolean, createdAt: d(r.created_at), confirmedAt: dOrNull(r.confirmed_at), endedAt: dOrNull(r.ended_at),\n});\n\n/** Field name to column, for the columns a patch may set. Nothing else ever reaches SQL text. */\nconst COUPON_COLUMNS: Record<keyof CouponPatch, string> = {\n code: \"code\", purpose: \"purpose\", partnerId: \"partner_id\", status: \"status\", startsAt: \"starts_at\", endsAt: \"ends_at\", maxRedemptions: \"max_redemptions\", maxPerAccount: \"max_per_account\", newCustomersOnly: \"new_customers_only\",\n plans: \"plans\", percentOffBp: \"percent_off_bp\", amountOffMinor: \"amount_off_minor\", currency: \"currency\", duration: \"duration\", durationMonths: \"duration_months\", minimumMinor: \"minimum_minor\", credits: \"credits\",\n capPerDay: \"cap_per_day\", capPerMonth: \"cap_per_month\", creditsValidDays: \"credits_valid_days\", onExhausted: \"on_exhausted\", termsVersion: \"terms_version\",\n};\nconst REDEMPTION_COLUMNS: Record<keyof RedemptionPatch, string> = {\n status: \"status\", reservedUntil: \"reserved_until\", checkoutRef: \"checkout_ref\", paymentRef: \"payment_ref\", amountPaidMinor: \"amount_paid_minor\", amountDiscountMinor: \"amount_discount_minor\",\n amountRefundedMinor: \"amount_refunded_minor\", useRestored: \"use_restored\", currency: \"currency\", creditGrantId: \"credit_grant_id\", overLimit: \"over_limit\", confirmedAt: \"confirmed_at\", endedAt: \"ended_at\",\n};\nconst PARTNER_COLUMNS = { displayName: \"display_name\", contactEmail: \"contact_email\", status: \"status\", erasedAt: \"erased_at\" } as const;\n\n/** `col = $n, …` and the values, for the keys of `patch` that are set (a key with `undefined` is left alone). */\nfunction setClause(patch: Record<string, unknown>, columns: Record<string, string>, first: number): { sql: string; values: unknown[] } {\n const keys = Object.keys(patch).filter((key) => patch[key] !== undefined && key in columns);\n return { sql: keys.map((key, i) => `${columns[key]} = $${first + i}`).join(\", \"), values: keys.map((key) => patch[key]) };\n}\n\nconst COUNTS = `((status IN ('confirmed', 'revoked') AND NOT use_restored) OR (status = 'reserved' AND reserved_until > $NOW))`;\n\n/**\n * The PostgreSQL store of coupons, in schema `coupons` (see `couponsSchema`), over the `Query` it is given: the caller's\n * transaction, so the work commits or rolls back with the product's. `couponByKey(…, true)` is `SELECT … FOR UPDATE`: the last use of a\n * code is taken by one request at a time. The guarded transition of `updateRedemption` is one `UPDATE … WHERE status = ANY(…)`.\n * Times come from the caller (the injected clock), never from the database's `now()`.\n * @param schema the Postgres schema of the module, default `coupons`\n */\nexport function dbCouponStore(query: Query, schema = \"coupons\"): CouponStore {\n const s = ident(schema);\n const coupon = `${s}.coupon`;\n const partner = `${s}.partner`;\n const redemption = `${s}.redemption`;\n return {\n async insertCoupon(c) {\n try {\n await query(\n `INSERT INTO ${coupon} (id, code, code_key, kind, purpose, partner_id, status, starts_at, ends_at, max_redemptions, max_per_account, new_customers_only, plans, percent_off_bp, amount_off_minor, currency, duration, duration_months, minimum_minor, credits, cap_per_day, cap_per_month, credits_valid_days, on_exhausted, terms_version, created_at, created_by, updated_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)`,\n [c.id, c.code, c.codeKey, c.kind, c.purpose, c.partnerId, c.status, c.startsAt, c.endsAt, c.maxRedemptions, c.maxPerAccount, c.newCustomersOnly, c.plans, c.percentOffBp, c.amountOffMinor, c.currency, c.duration, c.durationMonths, c.minimumMinor, c.credits, c.capPerDay, c.capPerMonth, c.creditsValidDays, c.onExhausted, c.termsVersion, c.createdAt, c.createdBy, c.updatedAt],\n );\n } catch (error) {\n if ((error as { code?: string }).code === \"23505\") throw new CouponError(COUPON_ERROR.codeTaken, \"This code is already used\");\n throw error;\n }\n },\n async couponById(id, forUpdate = false) {\n const [row] = await query<Row>(`SELECT * FROM ${coupon} WHERE id = $1${forUpdate ? \" FOR UPDATE\" : \"\"}`, [id]);\n return row ? toCoupon(row) : null;\n },\n async couponByKey(codeKey, forUpdate = false) {\n const [row] = await query<Row>(`SELECT * FROM ${coupon} WHERE code_key = $1${forUpdate ? \" FOR UPDATE\" : \"\"}`, [codeKey]);\n return row ? toCoupon(row) : null;\n },\n async updateCoupon(id, patch, at) {\n const set = setClause(patch, COUPON_COLUMNS, 3);\n const [row] = await query<Row>(`UPDATE ${coupon} SET ${set.sql ? `${set.sql}, ` : \"\"}updated_at = $2 WHERE id = $1 RETURNING *`, [id, at, ...set.values]);\n return row ? toCoupon(row) : null;\n },\n async listCoupons(f) {\n const rows = await query<Row>(\n `SELECT * FROM ${coupon} WHERE ($1::text IS NULL OR status = $1) AND ($2::text IS NULL OR kind = $2) AND ($3::text IS NULL OR purpose = $3) AND ($4::uuid IS NULL OR partner_id = $4)\n ORDER BY created_at DESC, id LIMIT $5 OFFSET $6`,\n [f.status ?? null, f.kind ?? null, f.purpose ?? null, f.partnerId ?? null, f.limit, f.offset],\n );\n return rows.map(toCoupon);\n },\n async hasRedemptions(couponId) {\n return (await query(`SELECT 1 FROM ${redemption} WHERE coupon_id = $1 LIMIT 1`, [couponId])).length > 0;\n },\n async insertPartner(p) {\n await query(`INSERT INTO ${partner} (id, display_name, contact_email, status, created_at, created_by, erased_at) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [p.id, p.displayName, p.contactEmail, p.status, p.createdAt, p.createdBy, p.erasedAt]);\n },\n async partnerById(id) {\n const [row] = await query<Row>(`SELECT * FROM ${partner} WHERE id = $1`, [id]);\n return row ? toPartner(row) : null;\n },\n async updatePartner(id, patch) {\n const set = setClause(patch, PARTNER_COLUMNS, 2);\n if (!set.sql) return (await this.partnerById(id)) ?? null;\n const [row] = await query<Row>(`UPDATE ${partner} SET ${set.sql} WHERE id = $1 RETURNING *`, [id, ...set.values]);\n return row ? toPartner(row) : null;\n },\n async listPartners(o) {\n const rows = await query<Row>(`SELECT * FROM ${partner} WHERE ($1 OR status = 'active') ORDER BY created_at DESC, id LIMIT $2 OFFSET $3`, [o.includeArchived, o.limit, o.offset]);\n return rows.map(toPartner);\n },\n async insertRedemption(r) {\n await query(\n `INSERT INTO ${redemption} (id, coupon_id, account_id, partner_id, status, idempotency_key, reserved_until, checkout_ref, payment_ref, amount_paid_minor, amount_discount_minor, amount_refunded_minor, use_restored, currency, credit_grant_id, over_limit, created_at, confirmed_at, ended_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,\n [r.id, r.couponId, r.accountId, r.partnerId, r.status, r.idempotencyKey, r.reservedUntil, r.checkoutRef, r.paymentRef, r.amountPaidMinor, r.amountDiscountMinor, r.amountRefundedMinor, r.useRestored, r.currency, r.creditGrantId, r.overLimit, r.createdAt, r.confirmedAt, r.endedAt],\n );\n },\n async redemptionById(id) {\n const [row] = await query<Row>(`SELECT * FROM ${redemption} WHERE id = $1`, [id]);\n return row ? toRedemption(row) : null;\n },\n async redemptionByKey(key) {\n const [row] = await query<Row>(`SELECT * FROM ${redemption} WHERE idempotency_key = $1`, [key]);\n return row ? toRedemption(row) : null;\n },\n async redemptionByCheckoutRef(ref) {\n const [row] = await query<Row>(`SELECT * FROM ${redemption} WHERE checkout_ref = $1`, [ref]);\n return row ? toRedemption(row) : null;\n },\n async redemptionByPaymentRef(ref) {\n const [row] = await query<Row>(`SELECT * FROM ${redemption} WHERE payment_ref = $1`, [ref]);\n return row ? toRedemption(row) : null;\n },\n async updateRedemption(id, from, patch) {\n const set = setClause(patch, REDEMPTION_COLUMNS, 3);\n const [row] = await query<Row>(`UPDATE ${redemption} SET ${set.sql} WHERE id = $1 AND ($2::text[] IS NULL OR status = ANY($2::text[])) RETURNING *`, [id, from ? [...from] : null, ...set.values]);\n return row ? toRedemption(row) : null;\n },\n async usage(couponId, accountId, now) {\n const counted = COUNTS.replaceAll(\"$NOW\", () => \"$3\");\n const [row] = await query<{ total: number; account: number }>(\n `SELECT count(*) FILTER (WHERE ${counted})::int AS total, count(*) FILTER (WHERE ${counted} AND account_id = $2)::int AS account FROM ${redemption} WHERE coupon_id = $1`,\n [couponId, accountId, now],\n );\n return { total: row?.total ?? 0, account: row?.account ?? 0 };\n },\n async listRedemptions(f) {\n const rows = await query<Row>(\n `SELECT r.* FROM ${redemption} r\n WHERE ($1::uuid IS NULL OR r.coupon_id = $1) AND ($2::uuid IS NULL OR r.partner_id = $2) AND ($3::text IS NULL OR r.status = $3)\n AND ($4::timestamptz IS NULL OR r.created_at >= $4) AND ($5::timestamptz IS NULL OR r.created_at < $5)\n ORDER BY r.created_at DESC, r.id LIMIT $6 OFFSET $7`,\n [f.couponId ?? null, f.partnerId ?? null, f.status ?? null, f.from ?? null, f.to ?? null, f.limit, f.offset],\n );\n return rows.map(toRedemption);\n },\n async releaseExpired(now, limit) {\n const rows = await query<Row>(\n `UPDATE ${redemption} SET status = 'released', reserved_until = NULL, ended_at = $1\n WHERE id IN (SELECT id FROM ${redemption} WHERE status = 'reserved' AND reserved_until <= $1 ORDER BY reserved_until, id LIMIT $2 FOR UPDATE SKIP LOCKED)\n RETURNING *`,\n [now, limit],\n );\n return rows.map(toRedemption);\n },\n async partnerReport(partnerId, period, now): Promise<PartnerRow[]> {\n const rows = await query<Row>(\n `SELECT c.id AS coupon_id, c.code, r.currency,\n count(*)::int AS redemptions,\n COALESCE(sum(r.amount_discount_minor), 0) AS discount,\n COALESCE(sum(COALESCE(r.amount_paid_minor, 0) - r.amount_refunded_minor), 0) AS revenue,\n COALESCE(sum(CASE WHEN c.kind = 'credit' THEN c.credits ELSE 0 END), 0) AS credits_granted\n FROM ${redemption} r JOIN ${coupon} c ON c.id = r.coupon_id\n WHERE r.partner_id = $1\n AND ((r.status IN ('confirmed', 'revoked') AND NOT r.use_restored) OR (r.status = 'reserved' AND r.reserved_until > $2))\n AND ($3::timestamptz IS NULL OR COALESCE(r.confirmed_at, r.created_at) >= $3) AND ($4::timestamptz IS NULL OR COALESCE(r.confirmed_at, r.created_at) < $4)\n GROUP BY c.id, c.code, r.currency ORDER BY c.code, r.currency NULLS FIRST`,\n [partnerId, now, period.from ?? null, period.to ?? null],\n );\n return rows.map((r) => ({ couponId: r.coupon_id as string, code: r.code as string, currency: (r.currency as string | null) ?? null, redemptions: n(r.redemptions), discountMinor: n(r.discount), revenueMinor: n(r.revenue), creditsGranted: n(r.credits_granted) }));\n },\n async activeAmountCoupons() {\n return (await query<Row>(`SELECT * FROM ${coupon} WHERE status = 'active' AND amount_off_minor IS NOT NULL ORDER BY created_at, id`)).map(toCoupon);\n },\n };\n}\n","import { type Kernel, SYSTEM } from \"./kernel\";\nimport {\n type Actor,\n type Coupon,\n COUPON_ERROR,\n COUPON_STATUS,\n CouponError,\n type CouponFilter,\n type CouponPatch,\n type Partner,\n type PartnerRow,\n type Query,\n type Redemption,\n type RedemptionFilter,\n} from \"./port\";\nimport { checkCoupon, generateCode, type NewCoupon, normalizeCode } from \"./rules\";\n\nconst bad = (message: string) => new CouponError(COUPON_ERROR.badInput, message);\n// @fixed: a page of the admin's lists\nconst PAGE = 50;\n// @fixed: the most rows one admin list returns\nconst MAX_PAGE = 200;\n// @fixed: tries at a generated code that is free (the alphabet has 32^8 or more codes)\nconst CODE_TRIES = 5;\n\n/** The fields that are a coupon's TERMS: locked once anyone has used the coupon (the provider's object is made from them, per version). */\nconst TERMS = [\"percentOffBp\", \"amountOffMinor\", \"currency\", \"duration\", \"durationMonths\", \"credits\", \"capPerDay\", \"capPerMonth\", \"creditsValidDays\", \"onExhausted\"] as const;\n\nconst page = (limit: number | undefined, offset: number | undefined) => ({ limit: Math.min(Math.max(limit ?? PAGE, 1), MAX_PAGE), offset: Math.max(offset ?? 0, 0) });\n\nexport interface PartnerInput {\n displayName: string;\n contactEmail?: string | null;\n}\n\n/** The admin's side (ADR-002 §6): coupons and partners, every change audited in the caller's transaction. */\nexport function makeAdmin(k: Kernel) {\n const must = async <T>(found: Promise<T | null>, code: typeof COUPON_ERROR.notFound | typeof COUPON_ERROR.partnerNotFound, what: string): Promise<T> => {\n const value = await found;\n if (!value) throw new CouponError(code, what);\n return value;\n };\n const partnerOk = async (tx: Query, partnerId: string | null | undefined) => {\n if (partnerId) await must(k.store(tx).partnerById(partnerId), COUPON_ERROR.partnerNotFound, `No partner ${partnerId}`);\n };\n\n async function generateFreeCode(query: Query): Promise<string> {\n const length = (await k.settings()).generatedCodeLength;\n for (let i = 0; i < CODE_TRIES; i += 1) {\n const code = generateCode(length);\n if (!(await k.store(query).couponByKey(code))) return code;\n }\n throw bad(\"Could not find a free code, try again\");\n }\n\n async function create(tx: Query, actor: Actor, input: NewCoupon): Promise<Coupon> {\n const settings = await k.settings();\n checkCoupon(input, settings);\n const codeKey = normalizeCode(input.code)!;\n const store = k.store(tx);\n if (await store.couponByKey(codeKey)) throw new CouponError(COUPON_ERROR.codeTaken, \"This code is already used\");\n await partnerOk(tx, input.partnerId);\n const now = k.now();\n const coupon: Coupon = {\n id: k.newId(), code: input.code.trim(), codeKey, kind: input.kind, purpose: input.purpose, partnerId: input.partnerId ?? null, status: COUPON_STATUS.active,\n startsAt: input.startsAt, endsAt: input.endsAt ?? null, maxRedemptions: input.maxRedemptions ?? null, maxPerAccount: input.maxPerAccount ?? 1, newCustomersOnly: input.newCustomersOnly ?? false, plans: input.plans ?? [],\n percentOffBp: input.percentOffBp ?? null, amountOffMinor: input.amountOffMinor ?? null, currency: input.currency ?? null, duration: input.duration ?? null, durationMonths: input.durationMonths ?? null, minimumMinor: input.minimumMinor ?? null,\n credits: input.credits ?? null, capPerDay: input.capPerDay ?? null, capPerMonth: input.capPerMonth ?? null, creditsValidDays: input.creditsValidDays ?? null, onExhausted: input.onExhausted ?? null,\n termsVersion: 1, createdAt: now, createdBy: actor.id, updatedAt: now,\n };\n await store.insertCoupon(coupon);\n await k.record(tx, actor, \"coupon.created\", { type: \"coupon\", id: coupon.id }, { code: coupon.code, kind: coupon.kind, purpose: coupon.purpose, partnerId: coupon.partnerId });\n return coupon;\n }\n\n async function update(tx: Query, actor: Actor, id: string, patch: CouponPatch): Promise<Coupon> {\n const store = k.store(tx);\n const before = await must(store.couponById(id, true), COUPON_ERROR.notFound, `No coupon ${id}`);\n const changed = (Object.keys(patch) as (keyof CouponPatch)[]).filter((key) => patch[key] !== undefined && JSON.stringify(patch[key]) !== JSON.stringify(before[key]));\n if (changed.length === 0) return before;\n if (changed.some((key) => (TERMS as readonly string[]).includes(key)) && (await store.hasRedemptions(id))) throw new CouponError(COUPON_ERROR.termsLocked, \"The terms of a coupon that has been used are locked: make a new coupon\");\n const next = { ...before, ...patch };\n checkCoupon(next, await k.settings());\n await partnerOk(tx, patch.partnerId);\n const after = (await store.updateCoupon(id, patch, k.now()))!;\n await k.record(tx, actor, \"coupon.updated\", { type: \"coupon\", id }, { changed, before: Object.fromEntries(changed.map((key) => [key, before[key]])), after: Object.fromEntries(changed.map((key) => [key, after[key]])) });\n return after;\n }\n\n async function setStatus(tx: Query, actor: Actor, id: string, status: Coupon[\"status\"], action: string): Promise<Coupon> {\n const store = k.store(tx);\n const before = await must(store.couponById(id, true), COUPON_ERROR.notFound, `No coupon ${id}`);\n if (before.status === status) return before;\n if (before.status === COUPON_STATUS.archived) throw bad(\"An archived coupon stays archived\");\n const after = (await store.updateCoupon(id, { status }, k.now()))!;\n await k.record(tx, actor, action, { type: \"coupon\", id }, { from: before.status, to: status });\n return after;\n }\n\n /** Pauses the live fixed-amount coupons that a price drop would make too large (the bound `coupons.maxPercentOff`); answers their ids. */\n async function checkPrices(tx: Query, prices: readonly { plan: string; amountMinor: number; currency: string }[]): Promise<string[]> {\n const { maxPercentOff } = await k.settings();\n const paused: string[] = [];\n for (const c of await k.store(tx).activeAmountCoupons()) {\n const applicable = prices.filter((p) => p.currency === c.currency && (c.plans.length === 0 || c.plans.includes(p.plan)));\n if (!applicable.some((p) => c.amountOffMinor! * 100 > p.amountMinor * maxPercentOff)) continue;\n await k.store(tx).updateCoupon(c.id, { status: COUPON_STATUS.paused }, k.now());\n await k.record(tx, SYSTEM, \"coupon.paused_by_price\", { type: \"coupon\", id: c.id }, { maxPercentOff });\n paused.push(c.id);\n }\n return paused;\n }\n\n async function createPartner(tx: Query, actor: Actor, input: PartnerInput): Promise<Partner> {\n const name = input.displayName.trim();\n const email = input.contactEmail?.trim().toLowerCase() || null;\n if (name.length < 1 || name.length > 80) throw bad(\"A partner has a display name of 1 to 80 characters\");\n if (email !== null && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) throw bad(\"A contact e-mail looks like a@b.c\");\n const partner: Partner = { id: k.newId(), displayName: name, contactEmail: email, status: \"active\", createdAt: k.now(), createdBy: actor.id, erasedAt: null };\n await k.store(tx).insertPartner(partner);\n await k.record(tx, actor, \"partner.created\", { type: \"partner\", id: partner.id }, {});\n return partner;\n }\n\n async function setPartnerStatus(tx: Query, actor: Actor, id: string, status: Partner[\"status\"]): Promise<Partner> {\n await must(k.store(tx).partnerById(id), COUPON_ERROR.partnerNotFound, `No partner ${id}`);\n const after = (await k.store(tx).updatePartner(id, { status }))!;\n await k.record(tx, actor, `partner.${status === \"active\" ? \"restored\" : \"archived\"}`, { type: \"partner\", id }, {});\n return after;\n }\n\n /** Loi 25: blanks the name and the e-mail and keeps the aggregates (the redemptions are accounting records). */\n async function erasePartner(tx: Query, actor: Actor, id: string): Promise<Partner> {\n const partner = await must(k.store(tx).partnerById(id), COUPON_ERROR.partnerNotFound, `No partner ${id}`);\n if (partner.erasedAt) return partner;\n const after = (await k.store(tx).updatePartner(id, { displayName: null, contactEmail: null, status: \"archived\", erasedAt: k.now() }))!;\n await k.record(tx, actor, \"partner.erased\", { type: \"partner\", id }, {});\n return after;\n }\n\n /** Loi 25: the partner's record and its aggregates. */\n async function exportPartner(query: Query, id: string): Promise<{ partner: Partner; rows: PartnerRow[] }> {\n const partner = await must(k.store(query).partnerById(id), COUPON_ERROR.partnerNotFound, `No partner ${id}`);\n return { partner, rows: await k.store(query).partnerReport(id, {}, k.now()) };\n }\n\n /** The partner view, per coupon, in a period. Admin only; every read is audited (so it takes the transaction). */\n async function partnerReport(tx: Query, actor: Actor, id: string, period: { from?: Date; to?: Date } = {}): Promise<PartnerRow[]> {\n await must(k.store(tx).partnerById(id), COUPON_ERROR.partnerNotFound, `No partner ${id}`);\n await k.record(tx, actor, \"partner.report_read\", { type: \"partner\", id }, {});\n return k.store(tx).partnerReport(id, period, k.now());\n }\n\n return {\n generateCode: generateFreeCode,\n create,\n update,\n pause: (tx: Query, actor: Actor, id: string) => setStatus(tx, actor, id, COUPON_STATUS.paused, \"coupon.paused\"),\n resume: (tx: Query, actor: Actor, id: string) => setStatus(tx, actor, id, COUPON_STATUS.active, \"coupon.resumed\"),\n archive: (tx: Query, actor: Actor, id: string) => setStatus(tx, actor, id, COUPON_STATUS.archived, \"coupon.archived\"),\n get: (query: Query, id: string) => k.store(query).couponById(id),\n list: (query: Query, filter: Partial<CouponFilter> = {}) => k.store(query).listCoupons({ ...filter, ...page(filter.limit, filter.offset) }),\n redemptions: (query: Query, filter: Partial<RedemptionFilter> = {}): Promise<Redemption[]> => k.store(query).listRedemptions({ ...filter, ...page(filter.limit, filter.offset) }),\n checkPrices,\n partners: {\n create: createPartner,\n archive: (tx: Query, actor: Actor, id: string) => setPartnerStatus(tx, actor, id, \"archived\"),\n restore: (tx: Query, actor: Actor, id: string) => setPartnerStatus(tx, actor, id, \"active\"),\n erase: erasePartner,\n get: (query: Query, id: string) => k.store(query).partnerById(id),\n list: (query: Query, options: { includeArchived?: boolean; limit?: number; offset?: number } = {}) => k.store(query).listPartners({ includeArchived: options.includeArchived ?? false, ...page(options.limit, options.offset) }),\n export: exportPartner,\n report: partnerReport,\n },\n };\n}\n\nexport type CouponAdmin = ReturnType<typeof makeAdmin>;\n","import { addHours, addMinutes } from \"@krizaka/time\";\n\nimport { COUPON_EVENT } from \"./events\";\nimport { type Kernel, SYSTEM } from \"./kernel\";\nimport {\n type Actor,\n type Coupon,\n COUPON_CODE,\n COUPON_ERROR,\n CouponError,\n type CouponRefusal,\n type DiscountTerms,\n KIND,\n type Query,\n type Redemption,\n REDEMPTION_STATUS,\n} from \"./port\";\nimport { type CheckoutContext, countsAsUse, COUPON_ATTEMPT_ACTION, type CreditEffect, type DiscountEffect, effectOf, normalizeCode, refusalOf, termsOf } from \"./rules\";\n\n/** The coupon as a checkout shows it. Never the limits or the partner. */\nexport interface CouponView {\n id: string;\n code: string;\n kind: Coupon[\"kind\"];\n purpose: Coupon[\"purpose\"];\n endsAt: Date | null;\n}\n\nexport type Validation = { ok: true; coupon: CouponView; effect: DiscountEffect | CreditEffect } | { ok: false; code: CouponRefusal; retryAfterSeconds?: number };\n\nexport interface ApplyInput extends Omit<CheckoutContext, \"mode\"> {\n code: string;\n /** One key per checkout attempt: a replay returns the same reservation. */\n idempotencyKey: string;\n}\n\nexport interface Applied {\n redemptionId: string;\n /** The immutable terms for the payments port's `checkout({ discount })`. */\n discount: DiscountTerms;\n effect: DiscountEffect;\n reservedUntil: Date;\n replayed: boolean;\n}\n\nexport interface RedeemInput {\n /** From the checkout's metadata (`redemption_id`); found first. */\n redemptionId?: string | null;\n /** The checkout session's reference; found when the metadata is missing. */\n checkoutRef?: string | null;\n /** The payment (a payment intent), or the invoice for a subscription; null for a free checkout. */\n paymentRef: string | null;\n amountPaidMinor: number | null;\n amountDiscountMinor: number;\n currency: string | null;\n}\n\nexport interface CreditRedemption {\n redemptionId: string;\n grantId: string;\n credits: number;\n expiresAt: Date;\n replayed: boolean;\n}\n\nconst view = (c: Coupon): CouponView => ({ id: c.id, code: c.code, kind: c.kind, purpose: c.purpose, endsAt: c.endsAt });\nconst REFUSALS: readonly string[] = Object.values(COUPON_CODE);\nconst isRefusal = (code: string): code is CouponRefusal => REFUSALS.includes(code);\nconst refuse = (code: CouponRefusal, retryAfterSeconds?: number) => new CouponError(code, code, retryAfterSeconds);\n// @fixed: a session's reservation outlives its session by five minutes, so a payment at the very end is still found (ADR-002 §4.1)\nconst ATTACH_GRACE_MINUTES = 5;\n// @fixed: the sweeper's batch\nconst SWEEP_BATCH = 500;\n\n/** The flows of a coupon's life at checkout and at redemption (ADR-002 §4). */\nexport function makeFlows(k: Kernel) {\n /** Counts one attempt with a code; refuses past the limit, before any lookup. */\n async function attempt(ctx: Pick<CheckoutContext, \"accountId\" | \"ip\">): Promise<void> {\n const r = await k.deps.rateLimit.check(COUPON_ATTEMPT_ACTION, { ip: ctx.ip, subject: ctx.accountId });\n if (!r.ok) throw refuse(COUPON_CODE.rateLimited, r.retryAfterSeconds);\n }\n\n /** The coupon behind a typed code, or the refusal. */\n async function lookup(tx: Query, code: string, forUpdate: boolean): Promise<Coupon> {\n const key = normalizeCode(code);\n const coupon = key ? await k.store(tx).couponByKey(key, forUpdate) : null;\n if (!coupon) throw refuse(COUPON_CODE.unknown);\n return coupon;\n }\n\n async function check(tx: Query, coupon: Coupon, ctx: CheckoutContext): Promise<void> {\n const settings = await k.settings();\n const now = k.now();\n const usage = await k.store(tx).usage(coupon.id, ctx.accountId, now);\n const refusal = refusalOf(coupon, ctx, usage, now, settings);\n if (refusal) throw refuse(refusal);\n }\n\n const mustBeEnabled = async () => {\n if (!(await k.settings()).enabled) throw refuse(COUPON_CODE.disabled);\n };\n\n const needsAmount = (ctx: CheckoutContext) => {\n if (ctx.mode === \"checkout\" && (ctx.plan === undefined || ctx.amountMinor === undefined || ctx.currency === undefined)) {\n throw new CouponError(COUPON_ERROR.badInput, \"A checkout names its plan, amount and currency\");\n }\n };\n\n async function validate(query: Query, code: string, ctx: CheckoutContext): Promise<Validation> {\n try {\n await mustBeEnabled();\n needsAmount(ctx);\n await attempt(ctx);\n const coupon = await lookup(query, code, false);\n await check(query, coupon, ctx);\n return { ok: true, coupon: view(coupon), effect: effectOf(coupon, ctx.amountMinor ?? 0) };\n } catch (error) {\n if (error instanceof CouponError && isRefusal(error.code)) return { ok: false, code: error.code, ...(error.retryAfterSeconds === undefined ? {} : { retryAfterSeconds: error.retryAfterSeconds }) };\n throw error;\n }\n }\n\n async function apply(tx: Query, input: ApplyInput): Promise<Applied> {\n const ctx: CheckoutContext = { ...input, mode: \"checkout\" };\n await mustBeEnabled();\n needsAmount(ctx);\n await attempt(ctx);\n const coupon = await lookup(tx, input.code, true);\n const store = k.store(tx);\n const known = await store.redemptionByKey(input.idempotencyKey);\n if (known) {\n if (known.couponId !== coupon.id || known.accountId !== input.accountId) throw new CouponError(COUPON_ERROR.keyReused, \"This idempotency key was used for another coupon or account\");\n return { redemptionId: known.id, discount: termsOf(coupon), effect: effectOf(coupon, input.amountMinor!) as DiscountEffect, reservedUntil: known.reservedUntil ?? known.createdAt, replayed: true };\n }\n await check(tx, coupon, ctx);\n const now = k.now();\n const reservedUntil = addMinutes(now, (await k.settings()).reservationMinutes);\n const redemption: Redemption = {\n id: k.newId(), couponId: coupon.id, accountId: input.accountId, partnerId: coupon.partnerId, status: REDEMPTION_STATUS.reserved, idempotencyKey: input.idempotencyKey,\n reservedUntil, checkoutRef: null, paymentRef: null, amountPaidMinor: null, amountDiscountMinor: null, amountRefundedMinor: 0, useRestored: false, currency: input.currency ?? null,\n creditGrantId: null, overLimit: false, createdAt: now, confirmedAt: null, endedAt: null,\n };\n await store.insertRedemption(redemption);\n return { redemptionId: redemption.id, discount: termsOf(coupon), effect: effectOf(coupon, input.amountMinor!) as DiscountEffect, reservedUntil, replayed: false };\n }\n\n async function attachCheckout(tx: Query, redemptionId: string, checkoutRef: string, sessionExpiresAt?: Date | null): Promise<void> {\n const store = k.store(tx);\n const r = await store.redemptionById(redemptionId);\n if (!r) throw new CouponError(COUPON_ERROR.redemptionNotFound, `No redemption ${redemptionId}`);\n await store.updateRedemption(redemptionId, null, { checkoutRef });\n if (r.status !== REDEMPTION_STATUS.reserved || !sessionExpiresAt) return;\n const wanted = addMinutes(sessionExpiresAt, ATTACH_GRACE_MINUTES);\n if (!r.reservedUntil || wanted > r.reservedUntil) await store.updateRedemption(redemptionId, [REDEMPTION_STATUS.reserved], { reservedUntil: wanted });\n }\n\n /** The reservation a payment event is about: by the metadata's id first, then by the session. */\n async function findFor(tx: Query, ref: { redemptionId?: string | null; checkoutRef?: string | null }): Promise<Redemption | null> {\n const store = k.store(tx);\n return (ref.redemptionId ? await store.redemptionById(ref.redemptionId) : null) ?? (ref.checkoutRef ? await store.redemptionByCheckoutRef(ref.checkoutRef) : null);\n }\n\n async function redeem(tx: Query, input: RedeemInput): Promise<\"confirmed\" | \"already\" | \"unknown\"> {\n const store = k.store(tx);\n const found = await findFor(tx, input);\n if (!found) return \"unknown\";\n const coupon = (await store.couponById(found.couponId, true))!;\n const r = (await store.redemptionById(found.id))!;\n const withRef = input.paymentRef ? await store.redemptionByPaymentRef(input.paymentRef) : null;\n if (withRef || (r.status !== REDEMPTION_STATUS.reserved && r.status !== REDEMPTION_STATUS.released)) return \"already\";\n const now = k.now();\n const others = (await store.usage(coupon.id, null, now)).total - (countsAsUse(r, now) ? 1 : 0);\n const late = r.status === REDEMPTION_STATUS.released;\n const overLimit = late || (coupon.maxRedemptions !== null && others + 1 > coupon.maxRedemptions);\n const done = await store.updateRedemption(r.id, [REDEMPTION_STATUS.reserved, REDEMPTION_STATUS.released], {\n status: REDEMPTION_STATUS.confirmed, paymentRef: input.paymentRef, amountPaidMinor: input.amountPaidMinor, amountDiscountMinor: input.amountDiscountMinor, currency: input.currency ?? r.currency,\n reservedUntil: null, confirmedAt: now, endedAt: null, overLimit,\n });\n if (!done) return \"already\";\n await confirmed(tx, coupon, done, others + 1);\n return \"confirmed\";\n }\n\n /** The audit line and the events of a confirmed use. */\n async function confirmed(tx: Query, coupon: Coupon, r: Redemption, usesNow: number): Promise<void> {\n await k.record(tx, SYSTEM, \"coupon.redeemed\", { type: \"coupon\", id: coupon.id }, { redemptionId: r.id, kind: coupon.kind, overLimit: r.overLimit, amountPaidMinor: r.amountPaidMinor, amountDiscountMinor: r.amountDiscountMinor });\n await k.announce(tx, COUPON_EVENT.redeemed, `redeemed:${r.id}`, {\n redemptionId: r.id, couponId: coupon.id, kind: coupon.kind, purpose: coupon.purpose, partnerId: r.partnerId, amountPaidMinor: r.amountPaidMinor, amountDiscountMinor: r.amountDiscountMinor, currency: r.currency, credits: coupon.kind === KIND.credit ? coupon.credits : null, overLimit: r.overLimit,\n });\n if (coupon.maxRedemptions !== null && usesNow === coupon.maxRedemptions) await k.announce(tx, COUPON_EVENT.exhausted, `exhausted:${coupon.id}`, { couponId: coupon.id, partnerId: coupon.partnerId });\n }\n\n async function release(tx: Query, ref: { redemptionId?: string; checkoutRef?: string }): Promise<boolean> {\n const found = await findFor(tx, ref);\n if (!found) return false;\n const done = await k.store(tx).updateRedemption(found.id, [REDEMPTION_STATUS.reserved], { status: REDEMPTION_STATUS.released, reservedUntil: null, endedAt: k.now() });\n if (!done) return false;\n await k.announce(tx, COUPON_EVENT.released, `released:${done.id}`, { redemptionId: done.id, couponId: done.couponId });\n return true;\n }\n\n async function releaseExpired(tx: Query): Promise<number> {\n const released = await k.store(tx).releaseExpired(k.now(), SWEEP_BATCH);\n for (const r of released) await k.announce(tx, COUPON_EVENT.released, `released:${r.id}`, { redemptionId: r.id, couponId: r.couponId });\n return released.length;\n }\n\n async function refund(tx: Query, input: { paymentRef?: string | null; invoiceRef?: string | null; amountRefunded: number }): Promise<\"refunded\" | \"already\" | \"unknown\"> {\n const store = k.store(tx);\n const r = (input.paymentRef ? await store.redemptionByPaymentRef(input.paymentRef) : null) ?? (input.invoiceRef ? await store.redemptionByPaymentRef(input.invoiceRef) : null);\n if (!r) return \"unknown\";\n const total = Math.max(r.amountRefundedMinor, input.amountRefunded);\n if (total === r.amountRefundedMinor) return \"already\";\n const full = r.amountPaidMinor !== null && r.amountPaidMinor > 0 && total >= r.amountPaidMinor;\n const restore = full && (await k.settings()).refundRestoresUse && !r.useRestored;\n const done = (await store.updateRedemption(r.id, null, { amountRefundedMinor: total, ...(restore ? { useRestored: true } : {}) }))!;\n await k.record(tx, SYSTEM, \"coupon.refunded\", { type: \"coupon\", id: r.couponId }, { redemptionId: r.id, amountRefundedMinor: total, useRestored: done.useRestored });\n await k.announce(tx, COUPON_EVENT.refunded, `refunded:${r.id}:${total}`, { redemptionId: r.id, couponId: r.couponId, partnerId: r.partnerId, amountRefundedMinor: total, currency: r.currency, useRestored: done.useRestored });\n return \"refunded\";\n }\n\n async function redeemCredit(tx: Query, input: { code: string; accountId: string; idempotencyKey: string; ip?: string | null; hasPaidBefore?: boolean }): Promise<CreditRedemption> {\n const credits = k.deps.credits;\n if (!credits) throw new CouponError(COUPON_ERROR.badSettings, \"Credit coupons need the credits ledger in the module's dependencies\");\n await mustBeEnabled();\n await attempt(input);\n const coupon = await lookup(tx, input.code, true);\n const store = k.store(tx);\n const known = await store.redemptionByKey(input.idempotencyKey);\n if (known) return replayOf(known, coupon, input.accountId);\n await check(tx, coupon, { mode: \"redeem\", accountId: input.accountId, ...(input.hasPaidBefore === undefined ? {} : { hasPaidBefore: input.hasPaidBefore }) });\n const now = k.now();\n const expiresAt = addHours(now, coupon.creditsValidDays! * 24);\n const redemption: Redemption = {\n id: k.newId(), couponId: coupon.id, accountId: input.accountId, partnerId: coupon.partnerId, status: REDEMPTION_STATUS.confirmed, idempotencyKey: input.idempotencyKey,\n reservedUntil: null, checkoutRef: null, paymentRef: null, amountPaidMinor: null, amountDiscountMinor: null, amountRefundedMinor: 0, useRestored: false, currency: null,\n creditGrantId: null, overLimit: false, createdAt: now, confirmedAt: now, endedAt: null,\n };\n await store.insertRedemption(redemption);\n const { grantId } = await credits.grant(tx, { accountId: input.accountId, amount: coupon.credits!, capPerDay: coupon.capPerDay, capPerMonth: coupon.capPerMonth, expiresAt, onExhausted: coupon.onExhausted!, source: `coupon:${redemption.id}`, idempotencyKey: `coupon-${redemption.id}` });\n const done = (await store.updateRedemption(redemption.id, null, { creditGrantId: grantId }))!;\n await confirmed(tx, coupon, done, (await store.usage(coupon.id, null, now)).total);\n return { redemptionId: done.id, grantId, credits: coupon.credits!, expiresAt, replayed: false };\n }\n\n function replayOf(known: Redemption, coupon: Coupon, accountId: string): CreditRedemption {\n if (known.couponId !== coupon.id || known.accountId !== accountId || !known.creditGrantId) throw new CouponError(COUPON_ERROR.keyReused, \"This idempotency key was used for another coupon or account\");\n return { redemptionId: known.id, grantId: known.creditGrantId, credits: coupon.credits!, expiresAt: addHours(known.createdAt, coupon.creditsValidDays! * 24), replayed: true };\n }\n\n async function revoke(tx: Query, actor: Actor, redemptionId: string): Promise<void> {\n const store = k.store(tx);\n const r = await store.redemptionById(redemptionId);\n if (!r) throw new CouponError(COUPON_ERROR.redemptionNotFound, `No redemption ${redemptionId}`);\n if (r.status === REDEMPTION_STATUS.revoked) return;\n if (r.status !== REDEMPTION_STATUS.confirmed || !r.creditGrantId || !k.deps.credits) throw new CouponError(COUPON_ERROR.notRevocable, \"Only a confirmed credit redemption can be revoked\");\n await store.couponById(r.couponId, true);\n const done = await store.updateRedemption(r.id, [REDEMPTION_STATUS.confirmed], { status: REDEMPTION_STATUS.revoked, endedAt: k.now() });\n if (!done) return;\n const { expired } = await k.deps.credits.expire(tx, r.creditGrantId);\n await k.record(tx, actor, \"coupon.revoked\", { type: \"coupon\", id: r.couponId }, { redemptionId: r.id, creditsWrittenOff: expired });\n }\n\n return { validate, apply, attachCheckout, redeem, release, releaseExpired, refund, redeemCredit, revoke };\n}\n","import { type CouponAdmin, makeAdmin } from \"./admin\";\nimport { type Applied, type ApplyInput, type CreditRedemption, makeFlows, type RedeemInput, type Validation } from \"./flows\";\nimport { type CouponsDeps, makeKernel } from \"./kernel\";\nimport type { Actor, Query } from \"./port\";\nimport type { CheckoutContext } from \"./rules\";\n\n/** The coupons of a product (ADR-002 §5.1). Every function takes the `Query` it works through: pass `tx` to join a unit of work. */\nexport interface Coupons {\n /** Read-only: what a code would give, or why not. Counts one attempt on the limiter, before any lookup. Never reserves. */\n validate(query: Query, code: string, context: CheckoutContext): Promise<Validation>;\n /** Reserves a use of a discount code for a checkout (a short transaction: no network inside), idempotent by key. */\n apply(tx: Query, input: ApplyInput): Promise<Applied>;\n /** Stores the provider's session on the reservation and makes it outlive the session. */\n attachCheckout(tx: Query, redemptionId: string, checkoutRef: string, sessionExpiresAt?: Date | null): Promise<void>;\n /** On the payment event, in its transaction: confirms the use once. A payment after the release is confirmed anyway (`overLimit`). */\n redeem(tx: Query, input: RedeemInput): Promise<\"confirmed\" | \"already\" | \"unknown\">;\n /** Frees an unpaid reservation (a failed session, an expired checkout). `false` when there was none to free. */\n release(tx: Query, ref: { redemptionId?: string; checkoutRef?: string }): Promise<boolean>;\n /** The sweeper: frees the reservations past their deadline. */\n releaseExpired(tx: Query): Promise<number>;\n /** A refund (cumulative amount). Gives the use back only on a full refund with `coupons.refundRestoresUse`. */\n refund(tx: Query, input: { paymentRef?: string | null; invoiceRef?: string | null; amountRefunded: number }): Promise<\"refunded\" | \"already\" | \"unknown\">;\n /** Redeems a credit code: the same rules, then a capped grant in the credits ledger, in the same transaction. Idempotent by key. */\n redeemCredit(tx: Query, input: { code: string; accountId: string; idempotencyKey: string; ip?: string | null; hasPaidBefore?: boolean }): Promise<CreditRedemption>;\n /** The admin revokes a credit redemption: what is left of its grant is written off; credits already spent are never taken back. */\n revoke(tx: Query, actor: Actor, redemptionId: string): Promise<void>;\n admin: CouponAdmin;\n}\n\n/**\n * The coupons of a product.\n * @param deps the clock, the settings, the trail, the limiter, and optionally the credits ledger, the outbox, the store\n */\nexport function createCoupons(deps: CouponsDeps): Coupons {\n const kernel = makeKernel(deps);\n return { ...makeFlows(kernel), admin: makeAdmin(kernel) };\n}\n","import { type Coupon, COUPON_STATUS, type CouponStore, KIND, type Partner, type PartnerRow, type Redemption, REDEMPTION_STATUS } from \"./port\";\nimport { countsAsUse } from \"./rules\";\n\nconst PARTNER_ACTIVE: Partner[\"status\"] = \"active\";\nconst clone = <T extends object>(v: T): T => ({ ...v });\n\n/**\n * An in-memory store, for unit tests and demos: the same behaviour as the database's (the contract kit runs on both) except that\n * `forUpdate` locks nothing, because it has no transaction to hold a lock in. Concurrency is proved on PostgreSQL.\n */\nexport function memoryCouponStore(): CouponStore {\n const coupons = new Map<string, Coupon>();\n const partners = new Map<string, Partner>();\n const redemptions = new Map<string, Redemption>();\n const unique = (field: \"idempotencyKey\" | \"checkoutRef\" | \"paymentRef\", value: string | null, except: string) => {\n if (value !== null && [...redemptions.values()].some((r) => r.id !== except && r[field] === value)) throw new Error(`duplicate ${field}`);\n };\n const page = <T>(rows: T[], limit: number, offset: number) => rows.slice(offset, offset + limit);\n const newestFirst = <T extends { createdAt: Date; id: string }>(rows: T[]) => rows.sort((a, b) => b.createdAt.valueOf() - a.createdAt.valueOf() || a.id.localeCompare(b.id));\n return {\n async insertCoupon(c) {\n if ([...coupons.values()].some((x) => x.codeKey === c.codeKey)) throw Object.assign(new Error(\"duplicate code_key\"), { code: \"23505\" });\n coupons.set(c.id, clone(c));\n },\n async couponById(id) {\n const c = coupons.get(id);\n return c ? clone(c) : null;\n },\n async couponByKey(codeKey) {\n const c = [...coupons.values()].find((x) => x.codeKey === codeKey);\n return c ? clone(c) : null;\n },\n async updateCoupon(id, patch, at) {\n const c = coupons.get(id);\n if (!c) return null;\n for (const [key, value] of Object.entries(patch)) if (value !== undefined) (c as unknown as Record<string, unknown>)[key] = value;\n c.updatedAt = at;\n return clone(c);\n },\n async listCoupons(f) {\n const rows = [...coupons.values()].filter((c) => (!f.status || c.status === f.status) && (!f.kind || c.kind === f.kind) && (!f.purpose || c.purpose === f.purpose) && (!f.partnerId || c.partnerId === f.partnerId));\n return page(newestFirst(rows), f.limit, f.offset).map(clone);\n },\n async hasRedemptions(couponId) {\n return [...redemptions.values()].some((r) => r.couponId === couponId);\n },\n async insertPartner(p) {\n partners.set(p.id, clone(p));\n },\n async partnerById(id) {\n const p = partners.get(id);\n return p ? clone(p) : null;\n },\n async updatePartner(id, patch) {\n const p = partners.get(id);\n if (!p) return null;\n for (const [key, value] of Object.entries(patch)) if (value !== undefined) (p as unknown as Record<string, unknown>)[key] = value;\n return clone(p);\n },\n async listPartners(o) {\n return page(newestFirst([...partners.values()].filter((p) => o.includeArchived || p.status === PARTNER_ACTIVE)), o.limit, o.offset).map(clone);\n },\n async insertRedemption(r) {\n unique(\"idempotencyKey\", r.idempotencyKey, r.id);\n unique(\"checkoutRef\", r.checkoutRef, r.id);\n unique(\"paymentRef\", r.paymentRef, r.id);\n redemptions.set(r.id, clone(r));\n },\n async redemptionById(id) {\n const r = redemptions.get(id);\n return r ? clone(r) : null;\n },\n async redemptionByKey(key) {\n const r = [...redemptions.values()].find((x) => x.idempotencyKey === key);\n return r ? clone(r) : null;\n },\n async redemptionByCheckoutRef(ref) {\n const r = [...redemptions.values()].find((x) => x.checkoutRef === ref);\n return r ? clone(r) : null;\n },\n async redemptionByPaymentRef(ref) {\n const r = [...redemptions.values()].find((x) => x.paymentRef === ref);\n return r ? clone(r) : null;\n },\n async updateRedemption(id, from, patch) {\n const r = redemptions.get(id);\n if (!r || (from && !from.includes(r.status))) return null;\n unique(\"checkoutRef\", patch.checkoutRef ?? null, id);\n unique(\"paymentRef\", patch.paymentRef ?? null, id);\n for (const [key, value] of Object.entries(patch)) if (value !== undefined) (r as unknown as Record<string, unknown>)[key] = value;\n return clone(r);\n },\n async usage(couponId, accountId, now) {\n const live = [...redemptions.values()].filter((r) => r.couponId === couponId && countsAsUse(r, now));\n return { total: live.length, account: accountId === null ? 0 : live.filter((r) => r.accountId === accountId).length };\n },\n async listRedemptions(f) {\n const rows = [...redemptions.values()].filter((r) => (!f.couponId || r.couponId === f.couponId) && (!f.partnerId || r.partnerId === f.partnerId) && (!f.status || r.status === f.status) && (!f.from || r.createdAt >= f.from) && (!f.to || r.createdAt < f.to));\n return page(newestFirst(rows), f.limit, f.offset).map(clone);\n },\n async releaseExpired(now, limit) {\n const due = [...redemptions.values()].filter((r) => r.status === REDEMPTION_STATUS.reserved && r.reservedUntil !== null && r.reservedUntil <= now).sort((a, b) => a.reservedUntil!.valueOf() - b.reservedUntil!.valueOf() || a.id.localeCompare(b.id)).slice(0, limit);\n for (const r of due) Object.assign(r, { status: REDEMPTION_STATUS.released, reservedUntil: null, endedAt: now });\n return due.map(clone);\n },\n async partnerReport(partnerId, period, now): Promise<PartnerRow[]> {\n const rows = new Map<string, PartnerRow>();\n for (const r of redemptions.values()) {\n const at = r.confirmedAt ?? r.createdAt;\n if (r.partnerId !== partnerId || !countsAsUse(r, now) || (period.from && at < period.from) || (period.to && at >= period.to)) continue;\n const c = coupons.get(r.couponId)!;\n const key = `${c.id}|${r.currency ?? \"\"}`;\n const row = rows.get(key) ?? { couponId: c.id, code: c.code, currency: r.currency, redemptions: 0, discountMinor: 0, revenueMinor: 0, creditsGranted: 0 };\n row.redemptions += 1;\n row.discountMinor += r.amountDiscountMinor ?? 0;\n row.revenueMinor += (r.amountPaidMinor ?? 0) - r.amountRefundedMinor;\n row.creditsGranted += c.kind === KIND.credit ? (c.credits ?? 0) : 0;\n rows.set(key, row);\n }\n return [...rows.values()].sort((a, b) => a.code.localeCompare(b.code) || (a.currency ?? \"\").localeCompare(b.currency ?? \"\"));\n },\n async activeAmountCoupons() {\n return [...coupons.values()].filter((c) => c.status === COUPON_STATUS.active && c.amountOffMinor !== null).sort((a, b) => a.createdAt.valueOf() - b.createdAt.valueOf() || a.id.localeCompare(b.id)).map(clone);\n },\n };\n}\n","import { defineModule } from \"@krizaka/db\";\n\n/**\n * The tables of « coupons » (ADR-002), in their own Postgres schema: `partner` (a person who sells for us: a name and an optional\n * e-mail, erasable), `coupon` (the code, its window, its limits and its terms: a discount or capped credits) and `redemption` (a use:\n * reserved, confirmed, released or revoked, with the amounts and the provider's references). The one product table referenced is the\n * owner (`owner`, the account table): a deleted account leaves its redemptions (`ON DELETE SET NULL`), because uses and revenue stay.\n * Version 1 is frozen once released: a change is version 2.\n */\nexport const couponsSchema = defineModule({\n module: \"coupons\",\n tables: [\"owner\"],\n migrations: [\n {\n version: 1,\n name: \"partners, coupons and redemptions\",\n sql: (c) => `\nCREATE TABLE ${c.schema}.partner (\n id uuid PRIMARY KEY,\n display_name text CHECK (display_name IS NULL OR char_length(display_name) BETWEEN 1 AND 80),\n contact_email text CHECK (contact_email IS NULL OR contact_email = lower(contact_email)),\n status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')),\n created_at timestamptz NOT NULL,\n created_by uuid REFERENCES ${c.tables.owner}(id) ON DELETE SET NULL,\n erased_at timestamptz,\n CHECK (erased_at IS NOT NULL OR display_name IS NOT NULL),\n CHECK (erased_at IS NULL OR (display_name IS NULL AND contact_email IS NULL))\n);\nCREATE TABLE ${c.schema}.coupon (\n id uuid PRIMARY KEY,\n code text NOT NULL CHECK (char_length(code) BETWEEN 4 AND 60),\n code_key text NOT NULL UNIQUE CHECK (code_key = upper(code_key) AND char_length(code_key) BETWEEN 4 AND 40),\n kind text NOT NULL CHECK (kind IN ('discount', 'credit')),\n purpose text NOT NULL CHECK (purpose IN ('partner', 'promo', 'demo', 'support')),\n partner_id uuid REFERENCES ${c.schema}.partner(id) ON DELETE RESTRICT,\n status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'paused', 'archived')),\n starts_at timestamptz NOT NULL,\n ends_at timestamptz,\n max_redemptions integer CHECK (max_redemptions > 0),\n max_per_account integer NOT NULL DEFAULT 1 CHECK (max_per_account > 0),\n new_customers_only boolean NOT NULL DEFAULT false,\n plans text[] NOT NULL DEFAULT '{}',\n percent_off_bp integer CHECK (percent_off_bp BETWEEN 1 AND 9999),\n amount_off_minor integer CHECK (amount_off_minor > 0),\n currency text CHECK (currency ~ '^[a-z]{3}$'),\n duration text CHECK (duration IN ('once', 'repeating', 'forever')),\n duration_months integer CHECK (duration_months BETWEEN 1 AND 36),\n minimum_minor integer CHECK (minimum_minor >= 0),\n credits integer CHECK (credits > 0),\n cap_per_day integer CHECK (cap_per_day > 0),\n cap_per_month integer CHECK (cap_per_month > 0),\n credits_valid_days integer CHECK (credits_valid_days > 0),\n on_exhausted text CHECK (on_exhausted IN ('stop', 'fallback_paid')),\n terms_version integer NOT NULL DEFAULT 1,\n created_at timestamptz NOT NULL,\n created_by uuid REFERENCES ${c.tables.owner}(id) ON DELETE SET NULL,\n updated_at timestamptz NOT NULL,\n CHECK (ends_at IS NULL OR ends_at > starts_at),\n CHECK ((purpose = 'partner') = (partner_id IS NOT NULL)),\n CHECK (kind <> 'discount' OR (credits IS NULL AND num_nonnulls(percent_off_bp, amount_off_minor) = 1\n AND (amount_off_minor IS NULL OR currency IS NOT NULL) AND duration IS NOT NULL\n AND ((duration = 'repeating') = (duration_months IS NOT NULL))\n AND (amount_off_minor IS NULL OR duration <> 'forever'))),\n CHECK (kind <> 'credit' OR (credits IS NOT NULL AND on_exhausted IS NOT NULL\n AND credits_valid_days IS NOT NULL\n AND num_nulls(percent_off_bp, amount_off_minor, duration) = 3))\n);\nCREATE INDEX coupon_partner ON ${c.schema}.coupon (partner_id) WHERE partner_id IS NOT NULL;\nCREATE TABLE ${c.schema}.redemption (\n id uuid PRIMARY KEY,\n coupon_id uuid NOT NULL REFERENCES ${c.schema}.coupon(id) ON DELETE RESTRICT,\n account_id uuid REFERENCES ${c.tables.owner}(id) ON DELETE SET NULL,\n partner_id uuid REFERENCES ${c.schema}.partner(id) ON DELETE SET NULL,\n status text NOT NULL CHECK (status IN ('reserved', 'confirmed', 'released', 'revoked')),\n idempotency_key text NOT NULL UNIQUE,\n reserved_until timestamptz,\n checkout_ref text UNIQUE,\n payment_ref text UNIQUE,\n amount_paid_minor bigint,\n amount_discount_minor bigint,\n amount_refunded_minor bigint NOT NULL DEFAULT 0 CHECK (amount_refunded_minor >= 0),\n use_restored boolean NOT NULL DEFAULT false,\n currency text,\n credit_grant_id uuid,\n over_limit boolean NOT NULL DEFAULT false,\n created_at timestamptz NOT NULL,\n confirmed_at timestamptz,\n ended_at timestamptz\n);\nCREATE INDEX redemption_uses ON ${c.schema}.redemption (coupon_id, account_id) WHERE status <> 'released' AND NOT use_restored;\nCREATE INDEX redemption_reserved ON ${c.schema}.redemption (reserved_until) WHERE status = 'reserved';\nCREATE INDEX redemption_partner ON ${c.schema}.redemption (partner_id, confirmed_at) WHERE partner_id IS NOT NULL;`,\n },\n ],\n});\n"],"mappings":"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","names":["page"]}
@@ -0,0 +1,228 @@
1
+ // src/port.ts
2
+ var KIND = { discount: "discount", credit: "credit" };
3
+ var PURPOSE = { partner: "partner", promo: "promo", demo: "demo", support: "support" };
4
+ var COUPON_STATUS = { active: "active", paused: "paused", archived: "archived" };
5
+ var REDEMPTION_STATUS = { reserved: "reserved", confirmed: "confirmed", released: "released", revoked: "revoked" };
6
+ var DURATION = { once: "once", repeating: "repeating", forever: "forever" };
7
+ var ON_EXHAUSTED = { stop: "stop", fallbackPaid: "fallback_paid" };
8
+ var COUPON_CODE = {
9
+ unknown: "COUPON_UNKNOWN",
10
+ notStarted: "COUPON_NOT_STARTED",
11
+ expired: "COUPON_EXPIRED",
12
+ paused: "COUPON_PAUSED",
13
+ exhausted: "COUPON_EXHAUSTED",
14
+ accountLimit: "COUPON_ACCOUNT_LIMIT",
15
+ newCustomersOnly: "COUPON_NEW_CUSTOMERS_ONLY",
16
+ notForPlan: "COUPON_NOT_FOR_PLAN",
17
+ minimum: "COUPON_MINIMUM",
18
+ currency: "COUPON_CURRENCY",
19
+ tooLarge: "COUPON_TOO_LARGE",
20
+ kind: "COUPON_KIND",
21
+ provider: "COUPON_PROVIDER",
22
+ rateLimited: "COUPON_RATE_LIMITED",
23
+ disabled: "COUPON_DISABLED"
24
+ };
25
+ var COUPON_ERROR = {
26
+ badInput: "COUPON_BAD_INPUT",
27
+ codeTaken: "COUPON_CODE_TAKEN",
28
+ termsLocked: "COUPON_TERMS_LOCKED",
29
+ notFound: "COUPON_NOT_FOUND",
30
+ redemptionNotFound: "COUPON_REDEMPTION_NOT_FOUND",
31
+ partnerNotFound: "COUPON_PARTNER_NOT_FOUND",
32
+ keyReused: "COUPON_KEY_REUSED",
33
+ notRevocable: "COUPON_NOT_REVOCABLE",
34
+ badSettings: "COUPON_BAD_SETTINGS"
35
+ };
36
+ var COUPON_STATUS_CODE = {
37
+ COUPON_UNKNOWN: 404,
38
+ COUPON_NOT_STARTED: 422,
39
+ COUPON_EXPIRED: 422,
40
+ COUPON_PAUSED: 422,
41
+ COUPON_EXHAUSTED: 422,
42
+ COUPON_ACCOUNT_LIMIT: 422,
43
+ COUPON_NEW_CUSTOMERS_ONLY: 422,
44
+ COUPON_NOT_FOR_PLAN: 422,
45
+ COUPON_MINIMUM: 422,
46
+ COUPON_CURRENCY: 422,
47
+ COUPON_TOO_LARGE: 422,
48
+ COUPON_KIND: 422,
49
+ COUPON_PROVIDER: 422,
50
+ COUPON_RATE_LIMITED: 429,
51
+ COUPON_DISABLED: 404,
52
+ COUPON_BAD_INPUT: 400,
53
+ COUPON_CODE_TAKEN: 409,
54
+ COUPON_TERMS_LOCKED: 409,
55
+ COUPON_NOT_FOUND: 404,
56
+ COUPON_REDEMPTION_NOT_FOUND: 404,
57
+ COUPON_PARTNER_NOT_FOUND: 404,
58
+ COUPON_KEY_REUSED: 409,
59
+ COUPON_NOT_REVOCABLE: 409,
60
+ COUPON_BAD_SETTINGS: 500
61
+ };
62
+ var CouponError = class extends Error {
63
+ constructor(code, message = code, retryAfterSeconds) {
64
+ super(message);
65
+ this.code = code;
66
+ this.retryAfterSeconds = retryAfterSeconds;
67
+ this.name = "CouponError";
68
+ this.status = COUPON_STATUS_CODE[code];
69
+ }
70
+ code;
71
+ retryAfterSeconds;
72
+ status;
73
+ };
74
+
75
+ // src/rules.ts
76
+ import { randomInt } from "crypto";
77
+ import { isAfter } from "@krizaka/time";
78
+ var COUPON_DEFAULTS = {
79
+ enabled: false,
80
+ reservationMinutes: 60,
81
+ attemptsPerAccountPerHour: 10,
82
+ attemptsPerIpPerHour: 30,
83
+ minCodeLength: 8,
84
+ generatedCodeLength: 10,
85
+ maxPercentOff: 50,
86
+ refundRestoresUse: false
87
+ };
88
+ var COUPON_ATTEMPT_ACTION = "couponAttempt";
89
+ var COUPON_BOUNDS = {
90
+ reservationMinutes: [40, 1440],
91
+ attemptsPerAccountPerHour: [1, 100],
92
+ attemptsPerIpPerHour: [1, 500],
93
+ minCodeLength: [4, 40],
94
+ generatedCodeLength: [8, 20],
95
+ maxPercentOff: [1, 99]
96
+ };
97
+ function checkSettings(s) {
98
+ for (const [key, [min, max]] of Object.entries(COUPON_BOUNDS)) {
99
+ const value = s[key];
100
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new CouponError(COUPON_ERROR.badSettings, `${key} is ${min} to ${max}, got ${value}`);
101
+ }
102
+ return s;
103
+ }
104
+ var bad = (message) => new CouponError(COUPON_ERROR.badInput, message);
105
+ var MAX_CODE = 40;
106
+ var MIN_CODE = 4;
107
+ function normalizeCode(input) {
108
+ const key = input.normalize("NFKC").replace(/[\s\-_]+/g, "").toUpperCase();
109
+ return /^[A-Z0-9]+$/.test(key) && key.length >= 1 && key.length <= MAX_CODE ? key : null;
110
+ }
111
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
112
+ function generateCode(length, pick = (max) => randomInt(max)) {
113
+ return Array.from({ length }, () => ALPHABET[pick(ALPHABET.length)]).join("");
114
+ }
115
+ function countsAsUse(r, now) {
116
+ if (r.status === REDEMPTION_STATUS.confirmed || r.status === REDEMPTION_STATUS.revoked) return !r.useRestored;
117
+ return r.status === REDEMPTION_STATUS.reserved && r.reservedUntil !== null && isAfter(r.reservedUntil, now);
118
+ }
119
+ function effectOf(coupon, amountMinor) {
120
+ if (coupon.kind === KIND.credit) {
121
+ return { kind: KIND.credit, credits: coupon.credits, capPerDay: coupon.capPerDay, capPerMonth: coupon.capPerMonth, validDays: coupon.creditsValidDays, onExhausted: coupon.onExhausted };
122
+ }
123
+ const raw = coupon.percentOffBp !== null ? Math.round(amountMinor * coupon.percentOffBp / 1e4) : coupon.amountOffMinor;
124
+ const discountMinor = Math.min(raw, amountMinor);
125
+ return { kind: KIND.discount, percentOffBp: coupon.percentOffBp, amountOffMinor: coupon.amountOffMinor, currency: coupon.currency, duration: coupon.duration, durationMonths: coupon.durationMonths, discountMinor, payableMinor: amountMinor - discountMinor };
126
+ }
127
+ function termsOf(coupon) {
128
+ return {
129
+ couponId: coupon.id,
130
+ termsVersion: coupon.termsVersion,
131
+ ...coupon.percentOffBp !== null ? { percentOffBp: coupon.percentOffBp } : { amountOffMinor: coupon.amountOffMinor, currency: coupon.currency },
132
+ duration: coupon.duration,
133
+ ...coupon.durationMonths !== null ? { durationMonths: coupon.durationMonths } : {}
134
+ };
135
+ }
136
+ function commonRefusal(coupon, ctx, usage, now) {
137
+ if (isAfter(coupon.startsAt, now)) return COUPON_CODE.notStarted;
138
+ if (coupon.endsAt !== null && !isAfter(coupon.endsAt, now)) return COUPON_CODE.expired;
139
+ if (coupon.status !== COUPON_STATUS.active) return COUPON_CODE.paused;
140
+ if (coupon.maxRedemptions !== null && usage.total >= coupon.maxRedemptions) return COUPON_CODE.exhausted;
141
+ if (usage.account >= coupon.maxPerAccount) return COUPON_CODE.accountLimit;
142
+ if (coupon.newCustomersOnly && ctx.hasPaidBefore) return COUPON_CODE.newCustomersOnly;
143
+ return null;
144
+ }
145
+ function discountRefusal(coupon, ctx, settings) {
146
+ const amount = ctx.amountMinor ?? 0;
147
+ if (ctx.providerSupportsDiscounts === false) return COUPON_CODE.provider;
148
+ if (coupon.plans.length > 0 && !coupon.plans.includes(ctx.plan ?? "")) return COUPON_CODE.notForPlan;
149
+ if (coupon.minimumMinor !== null && amount < coupon.minimumMinor) return COUPON_CODE.minimum;
150
+ if (coupon.currency !== null && coupon.currency !== ctx.currency) return COUPON_CODE.currency;
151
+ const tooLarge = coupon.percentOffBp !== null ? coupon.percentOffBp > settings.maxPercentOff * 100 : coupon.amountOffMinor * 100 > amount * settings.maxPercentOff;
152
+ return tooLarge ? COUPON_CODE.tooLarge : null;
153
+ }
154
+ function refusalOf(coupon, ctx, usage, now, settings) {
155
+ const discount = coupon.kind === KIND.discount;
156
+ if (discount !== (ctx.mode === "checkout")) return COUPON_CODE.kind;
157
+ return commonRefusal(coupon, ctx, usage, now) ?? (discount ? discountRefusal(coupon, ctx, settings) : null);
158
+ }
159
+ var pos = (n, what, max = Number.MAX_SAFE_INTEGER) => {
160
+ if (n === null || n === void 0) return;
161
+ if (!Number.isSafeInteger(n) || n < 1 || n > max) throw bad(`${what} is a whole number from 1 to ${max}, got ${n}`);
162
+ };
163
+ function checkCoupon(c, settings) {
164
+ const code = c.code.trim();
165
+ const key = normalizeCode(code);
166
+ if (!key || key.length < Math.max(MIN_CODE, settings.minCodeLength)) throw bad(`A code is letters and digits, ${Math.max(MIN_CODE, settings.minCodeLength)} to ${MAX_CODE} characters`);
167
+ if (c.kind !== KIND.discount && c.kind !== KIND.credit) throw bad("A coupon is a discount or a credit");
168
+ if (!Object.values(PURPOSE).includes(c.purpose)) throw bad("A purpose is partner, promo, demo or support");
169
+ if (c.purpose === PURPOSE.partner !== Boolean(c.partnerId)) throw bad("A partner coupon names its partner, and only a partner coupon does");
170
+ if (!(c.startsAt instanceof Date) || Number.isNaN(c.startsAt.valueOf())) throw bad("A coupon starts on a date");
171
+ if (c.endsAt && !isAfter(c.endsAt, c.startsAt)) throw bad("A coupon ends after it starts");
172
+ pos(c.maxRedemptions, "maxRedemptions");
173
+ pos(c.maxPerAccount, "maxPerAccount");
174
+ if (c.kind === KIND.discount) return checkDiscount(c, settings);
175
+ return checkCredit(c);
176
+ }
177
+ function checkDiscount(c, settings) {
178
+ if (c.credits != null || c.capPerDay != null || c.capPerMonth != null || c.creditsValidDays != null || c.onExhausted != null) throw bad("A discount carries no credit terms");
179
+ const percent = c.percentOffBp != null;
180
+ const amount = c.amountOffMinor != null;
181
+ if (percent === amount) throw bad("A discount is a percentage or a fixed amount, never both");
182
+ if (percent) pos(c.percentOffBp, "percentOffBp", Math.min(9999, settings.maxPercentOff * 100));
183
+ if (amount) {
184
+ pos(c.amountOffMinor, "amountOffMinor");
185
+ if (!/^[a-z]{3}$/.test(c.currency ?? "")) throw bad("A fixed amount has a lowercase currency");
186
+ }
187
+ if (!c.duration || !Object.values(DURATION).includes(c.duration)) throw bad("A discount has a duration: once, repeating or forever");
188
+ if (c.duration === DURATION.repeating !== (c.durationMonths != null)) throw bad("Only a repeating discount names its months");
189
+ pos(c.durationMonths, "durationMonths", 36);
190
+ if (amount && c.duration === DURATION.forever) throw bad("A fixed amount never runs forever");
191
+ if (c.minimumMinor != null && (!Number.isSafeInteger(c.minimumMinor) || c.minimumMinor < 0)) throw bad("A minimum is a non-negative whole number");
192
+ if (c.currency != null && !/^[a-z]{3}$/.test(c.currency)) throw bad("A currency is three lowercase letters");
193
+ }
194
+ function checkCredit(c) {
195
+ if (c.percentOffBp != null || c.amountOffMinor != null || c.duration != null || c.durationMonths != null || c.minimumMinor != null) throw bad("A credit coupon carries no discount terms");
196
+ pos(c.credits, "credits");
197
+ if (c.credits == null) throw bad("A credit coupon grants credits: a free offer is always a capped credit coupon");
198
+ pos(c.capPerDay, "capPerDay");
199
+ pos(c.capPerMonth, "capPerMonth");
200
+ if (c.creditsValidDays == null) throw bad("Every grant expires: set creditsValidDays");
201
+ pos(c.creditsValidDays, "creditsValidDays", 3650);
202
+ if (c.onExhausted !== ON_EXHAUSTED.stop && c.onExhausted !== ON_EXHAUSTED.fallbackPaid) throw bad("onExhausted is stop or fallback_paid");
203
+ }
204
+
205
+ export {
206
+ KIND,
207
+ PURPOSE,
208
+ COUPON_STATUS,
209
+ REDEMPTION_STATUS,
210
+ DURATION,
211
+ ON_EXHAUSTED,
212
+ COUPON_CODE,
213
+ COUPON_ERROR,
214
+ COUPON_STATUS_CODE,
215
+ CouponError,
216
+ COUPON_DEFAULTS,
217
+ COUPON_ATTEMPT_ACTION,
218
+ COUPON_BOUNDS,
219
+ checkSettings,
220
+ normalizeCode,
221
+ generateCode,
222
+ countsAsUse,
223
+ effectOf,
224
+ termsOf,
225
+ refusalOf,
226
+ checkCoupon
227
+ };
228
+ //# sourceMappingURL=chunk-JMQO6PTK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/port.ts","../src/rules.ts"],"sourcesContent":["/**\n * The ports of « coupons » (ADR-002): discount and credit coupons that the admin manages, with the product's database as the source\n * of truth. Generic: no product import, no vendor, no database client (a `Query` comes in), no wall clock (a `Clock` comes in), no\n * network. The provider is `@krizaka/payments`' port (this module hands it immutable DiscountTerms); credits are\n * `@krizaka/credits`' ledger; the audit trail, the outbox and the limiter are structural ports, so this package depends on none of them.\n */\nimport type { Clock, Query } from \"@krizaka/db\";\n\nexport type { Clock, Query };\n\nexport const KIND = { discount: \"discount\", credit: \"credit\" } as const;\nexport type Kind = (typeof KIND)[keyof typeof KIND];\n\n/** Why the coupon exists: a person who sells for us, a public promotion, a demo, a support gesture. */\nexport const PURPOSE = { partner: \"partner\", promo: \"promo\", demo: \"demo\", support: \"support\" } as const;\nexport type Purpose = (typeof PURPOSE)[keyof typeof PURPOSE];\n\n/** Stored states. « Not started », « expired » and « exhausted » are COMPUTED from the dates and the uses, so no cron is needed. */\nexport const COUPON_STATUS = { active: \"active\", paused: \"paused\", archived: \"archived\" } as const;\nexport type CouponStatus = (typeof COUPON_STATUS)[keyof typeof COUPON_STATUS];\n\nexport const REDEMPTION_STATUS = { reserved: \"reserved\", confirmed: \"confirmed\", released: \"released\", revoked: \"revoked\" } as const;\nexport type RedemptionStatus = (typeof REDEMPTION_STATUS)[keyof typeof REDEMPTION_STATUS];\n\nexport const DURATION = { once: \"once\", repeating: \"repeating\", forever: \"forever\" } as const;\nexport type Duration = (typeof DURATION)[keyof typeof DURATION];\n\nexport const ON_EXHAUSTED = { stop: \"stop\", fallbackPaid: \"fallback_paid\" } as const;\nexport type OnExhausted = (typeof ON_EXHAUSTED)[keyof typeof ON_EXHAUSTED];\n\n/** The terms a provider adapter needs to apply a discount: vendor-free, IMMUTABLE per `(couponId, termsVersion)` (`@krizaka/payments`' `DiscountTerms`). */\nexport interface DiscountTerms {\n couponId: string;\n termsVersion: number;\n percentOffBp?: number;\n amountOffMinor?: number;\n currency?: string;\n duration: Duration;\n durationMonths?: number;\n}\n\nexport interface Coupon {\n id: string;\n /** As the admin typed it. */\n code: string;\n /** Normalised (upper case, no spaces or hyphens): the lookup key, unique in the product. */\n codeKey: string;\n kind: Kind;\n purpose: Purpose;\n partnerId: string | null;\n status: CouponStatus;\n startsAt: Date;\n endsAt: Date | null;\n maxRedemptions: number | null;\n maxPerAccount: number;\n newCustomersOnly: boolean;\n /** Plan kinds of the product; empty = all. */\n plans: string[];\n percentOffBp: number | null;\n amountOffMinor: number | null;\n currency: string | null;\n duration: Duration | null;\n durationMonths: number | null;\n minimumMinor: number | null;\n credits: number | null;\n capPerDay: number | null;\n capPerMonth: number | null;\n creditsValidDays: number | null;\n onExhausted: OnExhausted | null;\n termsVersion: number;\n createdAt: Date;\n createdBy: string | null;\n updatedAt: Date;\n}\n\nexport interface Partner {\n id: string;\n /** Null once erased (Loi 25). */\n displayName: string | null;\n contactEmail: string | null;\n status: \"active\" | \"archived\";\n createdAt: Date;\n createdBy: string | null;\n erasedAt: Date | null;\n}\n\nexport interface Redemption {\n id: string;\n couponId: string;\n /** Null once the account was deleted: the use still counts and the revenue stays. */\n accountId: string | null;\n /** Copied at redemption: attribution never moves. */\n partnerId: string | null;\n status: RedemptionStatus;\n idempotencyKey: string;\n reservedUntil: Date | null;\n checkoutRef: string | null;\n paymentRef: string | null;\n amountPaidMinor: number | null;\n amountDiscountMinor: number | null;\n /** CUMULATIVE, partial or full. */\n amountRefundedMinor: number;\n /** A full refund gave the use back (the setting). */\n useRestored: boolean;\n currency: string | null;\n creditGrantId: string | null;\n /** A late payment honoured past a limit. */\n overLimit: boolean;\n createdAt: Date;\n confirmedAt: Date | null;\n endedAt: Date | null;\n}\n\n/** What a guarded update may change. */\nexport type RedemptionPatch = Partial<Pick<Redemption, \"status\" | \"reservedUntil\" | \"checkoutRef\" | \"paymentRef\" | \"amountPaidMinor\" | \"amountDiscountMinor\" | \"amountRefundedMinor\" | \"useRestored\" | \"currency\" | \"creditGrantId\" | \"overLimit\" | \"confirmedAt\" | \"endedAt\">>;\n\n/** What a coupon's terms and rules may be changed to (the admin's update). */\nexport type CouponPatch = Partial<Omit<Coupon, \"id\" | \"codeKey\" | \"createdAt\" | \"createdBy\" | \"updatedAt\" | \"kind\">>;\n\nexport interface Usage {\n /** Uses that count overall. */\n total: number;\n /** Uses that count for the account. */\n account: number;\n}\n\nexport interface CouponFilter {\n status?: CouponStatus;\n kind?: Kind;\n purpose?: Purpose;\n partnerId?: string;\n limit: number;\n offset: number;\n}\n\nexport interface RedemptionFilter {\n couponId?: string;\n partnerId?: string;\n status?: RedemptionStatus;\n from?: Date;\n to?: Date;\n limit: number;\n offset: number;\n}\n\n/** One row of the partner view: a coupon's uses in a period, in one currency. Never a customer's identity. */\nexport interface PartnerRow {\n couponId: string;\n code: string;\n currency: string | null;\n redemptions: number;\n discountMinor: number;\n /** Paid minus refunded. */\n revenueMinor: number;\n creditsGranted: number;\n}\n\n/** Where coupons live. Every adapter runs `couponsContract` (`@krizaka/coupons/testkit`). Times come from the caller. */\nexport interface CouponStore {\n insertCoupon(coupon: Coupon): Promise<void>;\n /** `forUpdate` locks the row until the transaction ends: the last use is taken by one request at a time. */\n couponById(id: string, forUpdate?: boolean): Promise<Coupon | null>;\n couponByKey(codeKey: string, forUpdate?: boolean): Promise<Coupon | null>;\n updateCoupon(id: string, patch: CouponPatch, at: Date): Promise<Coupon | null>;\n listCoupons(filter: CouponFilter): Promise<Coupon[]>;\n /** Whether any redemption (even released) exists: the terms are locked once there is one. */\n hasRedemptions(couponId: string): Promise<boolean>;\n insertPartner(partner: Partner): Promise<void>;\n partnerById(id: string): Promise<Partner | null>;\n updatePartner(id: string, patch: Partial<Pick<Partner, \"displayName\" | \"contactEmail\" | \"status\" | \"erasedAt\">>): Promise<Partner | null>;\n listPartners(options: { includeArchived: boolean; limit: number; offset: number }): Promise<Partner[]>;\n insertRedemption(redemption: Redemption): Promise<void>;\n redemptionById(id: string): Promise<Redemption | null>;\n redemptionByKey(idempotencyKey: string): Promise<Redemption | null>;\n redemptionByCheckoutRef(checkoutRef: string): Promise<Redemption | null>;\n redemptionByPaymentRef(paymentRef: string): Promise<Redemption | null>;\n /** The guarded transition: applies the patch only if the redemption is in one of `from`. Answers the new row, or null when the guard failed. */\n updateRedemption(id: string, from: readonly RedemptionStatus[] | null, patch: RedemptionPatch): Promise<Redemption | null>;\n /** The uses that count (see `countsAsUse`), overall and for the account, at `now`. */\n usage(couponId: string, accountId: string | null, now: Date): Promise<Usage>;\n listRedemptions(filter: RedemptionFilter): Promise<Redemption[]>;\n /** Releases the reservations past `reserved_until` and answers them. */\n releaseExpired(now: Date, limit: number): Promise<Redemption[]>;\n partnerReport(partnerId: string, period: { from?: Date; to?: Date }, now: Date): Promise<PartnerRow[]>;\n /** Active coupons with a fixed amount (for `checkPrices`). */\n activeAmountCoupons(): Promise<Coupon[]>;\n}\n\n/** The knobs of the module, read from the admin's settings (`@krizaka/coupons/settings`). */\nexport interface CouponSettings {\n /** Shows the field and accepts codes. */\n enabled: boolean;\n /** How long a checkout holds a use (40 to 1440 minutes: the provider's session window). */\n reservationMinutes: number;\n attemptsPerAccountPerHour: number;\n attemptsPerIpPerHour: number;\n minCodeLength: number;\n generatedCodeLength: number;\n /** The highest percent a discount may have (1 to 99). */\n maxPercentOff: number;\n /** A full refund gives the use back. */\n refundRestoresUse: boolean;\n}\n\n/** Who acts (for the audit trail). */\nexport interface Actor {\n id: string | null;\n label: string;\n}\n\n/** The trail (`@krizaka/audit`'s `Audit`), structurally. */\nexport interface AuditPort {\n record(query: Query, entry: { actorId: string | null; actorLabel: string; action: string; target?: { type: string; id: string } | null; detail?: Record<string, unknown> }): Promise<void>;\n}\n\n/** The outbox (`@krizaka/outbox`'s `Outbox`), structurally. */\nexport interface OutboxPort {\n append(tx: Query, message: { topic: string; type: string; payload: unknown; id?: string }): Promise<{ id: string; appended: boolean }>;\n}\n\n/** The limiter (`@krizaka/rate-limit`'s `Limiter`), structurally. */\nexport interface RateLimitPort {\n check(action: string, keys: { ip?: string | null | undefined; subject?: string | null | undefined }): Promise<{ ok: true } | { ok: false; retryAfterSeconds: number }>;\n}\n\n/** The credits ledger (`@krizaka/credits`' `Credits`), structurally. */\nexport interface CreditsPort {\n grant(tx: Query, input: { accountId: string; amount: number; expiresAt: Date; capPerDay?: number | null; capPerMonth?: number | null; onExhausted: OnExhausted; source: string; idempotencyKey: string }): Promise<{ grantId: string; replayed: boolean }>;\n expire(tx: Query, grantId: string): Promise<{ expired: number }>;\n}\n\n/** The refusals of validation and redemption (typed, `as const`). */\nexport const COUPON_CODE = {\n unknown: \"COUPON_UNKNOWN\",\n notStarted: \"COUPON_NOT_STARTED\",\n expired: \"COUPON_EXPIRED\",\n paused: \"COUPON_PAUSED\",\n exhausted: \"COUPON_EXHAUSTED\",\n accountLimit: \"COUPON_ACCOUNT_LIMIT\",\n newCustomersOnly: \"COUPON_NEW_CUSTOMERS_ONLY\",\n notForPlan: \"COUPON_NOT_FOR_PLAN\",\n minimum: \"COUPON_MINIMUM\",\n currency: \"COUPON_CURRENCY\",\n tooLarge: \"COUPON_TOO_LARGE\",\n kind: \"COUPON_KIND\",\n provider: \"COUPON_PROVIDER\",\n rateLimited: \"COUPON_RATE_LIMITED\",\n disabled: \"COUPON_DISABLED\",\n} as const;\nexport type CouponRefusal = (typeof COUPON_CODE)[keyof typeof COUPON_CODE];\n\n/** The errors of the admin and of the flows (not refusals of a customer's code). */\nexport const COUPON_ERROR = {\n badInput: \"COUPON_BAD_INPUT\",\n codeTaken: \"COUPON_CODE_TAKEN\",\n termsLocked: \"COUPON_TERMS_LOCKED\",\n notFound: \"COUPON_NOT_FOUND\",\n redemptionNotFound: \"COUPON_REDEMPTION_NOT_FOUND\",\n partnerNotFound: \"COUPON_PARTNER_NOT_FOUND\",\n keyReused: \"COUPON_KEY_REUSED\",\n notRevocable: \"COUPON_NOT_REVOCABLE\",\n badSettings: \"COUPON_BAD_SETTINGS\",\n} as const;\nexport type CouponErrorCode = (typeof COUPON_ERROR)[keyof typeof COUPON_ERROR];\n\n/** The HTTP status a route may answer for each refusal or error. */\nexport const COUPON_STATUS_CODE: Record<CouponRefusal | CouponErrorCode, number> = {\n COUPON_UNKNOWN: 404,\n COUPON_NOT_STARTED: 422,\n COUPON_EXPIRED: 422,\n COUPON_PAUSED: 422,\n COUPON_EXHAUSTED: 422,\n COUPON_ACCOUNT_LIMIT: 422,\n COUPON_NEW_CUSTOMERS_ONLY: 422,\n COUPON_NOT_FOR_PLAN: 422,\n COUPON_MINIMUM: 422,\n COUPON_CURRENCY: 422,\n COUPON_TOO_LARGE: 422,\n COUPON_KIND: 422,\n COUPON_PROVIDER: 422,\n COUPON_RATE_LIMITED: 429,\n COUPON_DISABLED: 404,\n COUPON_BAD_INPUT: 400,\n COUPON_CODE_TAKEN: 409,\n COUPON_TERMS_LOCKED: 409,\n COUPON_NOT_FOUND: 404,\n COUPON_REDEMPTION_NOT_FOUND: 404,\n COUPON_PARTNER_NOT_FOUND: 404,\n COUPON_KEY_REUSED: 409,\n COUPON_NOT_REVOCABLE: 409,\n COUPON_BAD_SETTINGS: 500,\n};\n\n/** A refusal thrown by a flow (`apply`, `redeemCredit`): a stable `code`, the HTTP `status`, and `retryAfterSeconds` for a rate limit. */\nexport class CouponError extends Error {\n readonly status: number;\n constructor(\n readonly code: CouponRefusal | CouponErrorCode,\n message: string = code,\n readonly retryAfterSeconds?: number,\n ) {\n super(message);\n this.name = \"CouponError\";\n this.status = COUPON_STATUS_CODE[code];\n }\n}\n","import { randomInt } from \"node:crypto\";\n\nimport { isAfter } from \"@krizaka/time\";\n\nimport {\n type Coupon,\n COUPON_CODE,\n COUPON_ERROR,\n COUPON_STATUS,\n CouponError,\n type CouponRefusal,\n type CouponSettings,\n type DiscountTerms,\n DURATION,\n KIND,\n ON_EXHAUSTED,\n PURPOSE,\n type Redemption,\n REDEMPTION_STATUS,\n type Usage,\n} from \"./port\";\n\n/** Defaults and bounds of the settings (the admin changes them without a redeploy). `maxPercentOff`'s default is Oussama's call (krizaka-ts#61). */\nexport const COUPON_DEFAULTS: CouponSettings = {\n enabled: false,\n reservationMinutes: 60,\n attemptsPerAccountPerHour: 10,\n attemptsPerIpPerHour: 30,\n minCodeLength: 8,\n generatedCodeLength: 10,\n maxPercentOff: 50,\n refundRestoresUse: false,\n};\n\n/** The action name of the limiter for attempts with a code: each `validate`, `apply` and `redeemCredit` counts one hit. */\nexport const COUPON_ATTEMPT_ACTION = \"couponAttempt\";\n\n/** [min, max] of each numeric setting. */\nexport const COUPON_BOUNDS = {\n reservationMinutes: [40, 1440],\n attemptsPerAccountPerHour: [1, 100],\n attemptsPerIpPerHour: [1, 500],\n minCodeLength: [4, 40],\n generatedCodeLength: [8, 20],\n maxPercentOff: [1, 99],\n} as const satisfies Record<string, readonly [number, number]>;\n\n/** Checks the settings the service was given. @throws CouponError `COUPON_BAD_SETTINGS` */\nexport function checkSettings(s: CouponSettings): CouponSettings {\n for (const [key, [min, max]] of Object.entries(COUPON_BOUNDS)) {\n const value = s[key as keyof typeof COUPON_BOUNDS];\n if (!Number.isSafeInteger(value) || value < min || value > max) throw new CouponError(COUPON_ERROR.badSettings, `${key} is ${min} to ${max}, got ${value}`);\n }\n return s;\n}\n\nconst bad = (message: string) => new CouponError(COUPON_ERROR.badInput, message);\n\n// @fixed: the longest normalised code the table accepts (its CHECK)\nconst MAX_CODE = 40;\n// @fixed: the shortest normalised code the table accepts (its CHECK); the admin's `coupons.minCodeLength` raises it\nconst MIN_CODE = 4;\n\n/**\n * The lookup key of a code: upper case, without spaces, hyphens or underscores, so « summer-2026 », « SUMMER 2026 » and « Summer2026 »\n * are one code. A code is then letters and digits only.\n * @returns the key, or null when what is left is empty, too long or not letters and digits\n */\nexport function normalizeCode(input: string): string | null {\n const key = input.normalize(\"NFKC\").replace(/[\\s\\-_]+/g, \"\").toUpperCase();\n return /^[A-Z0-9]+$/.test(key) && key.length >= 1 && key.length <= MAX_CODE ? key : null;\n}\n\n// Crockford's base32 without I, L, O and U: nothing to confuse when read out loud or copied by hand.\nconst ALPHABET = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\n/** A random code of `length` characters from an alphabet without I, L, O, U (about 5 bits each). */\nexport function generateCode(length: number, pick: (max: number) => number = (max) => randomInt(max)): string {\n return Array.from({ length }, () => ALPHABET[pick(ALPHABET.length)]).join(\"\");\n}\n\n/** Whether a redemption consumes a use at `now`: confirmed or revoked and not restored, or reserved and not yet past its deadline. */\nexport function countsAsUse(r: Pick<Redemption, \"status\" | \"useRestored\" | \"reservedUntil\">, now: Date): boolean {\n if (r.status === REDEMPTION_STATUS.confirmed || r.status === REDEMPTION_STATUS.revoked) return !r.useRestored;\n return r.status === REDEMPTION_STATUS.reserved && r.reservedUntil !== null && isAfter(r.reservedUntil, now);\n}\n\n/** What the person typed and where they are: a checkout (discount codes) or a place that redeems credits (credit codes). */\nexport interface CheckoutContext {\n mode: \"checkout\" | \"redeem\";\n accountId: string;\n /** The plan kind being bought. Checkout only. */\n plan?: string;\n /** The price before the discount, in minor units. Checkout only. */\n amountMinor?: number;\n currency?: string;\n /** Whether the account has paid before (from `payments`). */\n hasPaidBefore?: boolean;\n /** False when the product's provider cannot apply discounts (CCBill, Segpay). Default true. */\n providerSupportsDiscounts?: boolean;\n ip?: string | null;\n}\n\nexport interface DiscountEffect {\n kind: typeof KIND.discount;\n percentOffBp: number | null;\n amountOffMinor: number | null;\n currency: string | null;\n duration: NonNullable<Coupon[\"duration\"]>;\n durationMonths: number | null;\n /** What the first payment loses, in minor units. */\n discountMinor: number;\n payableMinor: number;\n}\n\nexport interface CreditEffect {\n kind: typeof KIND.credit;\n credits: number;\n capPerDay: number | null;\n capPerMonth: number | null;\n validDays: number;\n onExhausted: NonNullable<Coupon[\"onExhausted\"]>;\n}\n\n/** What a code gives, in numbers. Informational for a discount: the provider computes the real amount and its taxes. */\nexport function effectOf(coupon: Coupon, amountMinor: number): DiscountEffect | CreditEffect {\n if (coupon.kind === KIND.credit) {\n return { kind: KIND.credit, credits: coupon.credits!, capPerDay: coupon.capPerDay, capPerMonth: coupon.capPerMonth, validDays: coupon.creditsValidDays!, onExhausted: coupon.onExhausted! };\n }\n const raw = coupon.percentOffBp !== null ? Math.round((amountMinor * coupon.percentOffBp) / 10_000) : coupon.amountOffMinor!;\n const discountMinor = Math.min(raw, amountMinor);\n return { kind: KIND.discount, percentOffBp: coupon.percentOffBp, amountOffMinor: coupon.amountOffMinor, currency: coupon.currency, duration: coupon.duration!, durationMonths: coupon.durationMonths, discountMinor, payableMinor: amountMinor - discountMinor };\n}\n\n/** The immutable terms a provider adapter turns into its object. */\nexport function termsOf(coupon: Coupon): DiscountTerms {\n return {\n couponId: coupon.id,\n termsVersion: coupon.termsVersion,\n ...(coupon.percentOffBp !== null ? { percentOffBp: coupon.percentOffBp } : { amountOffMinor: coupon.amountOffMinor!, currency: coupon.currency! }),\n duration: coupon.duration!,\n ...(coupon.durationMonths !== null ? { durationMonths: coupon.durationMonths } : {}),\n };\n}\n\n/** The window, the status and the limits: what any coupon is checked against. */\nfunction commonRefusal(coupon: Coupon, ctx: CheckoutContext, usage: Usage, now: Date): CouponRefusal | null {\n if (isAfter(coupon.startsAt, now)) return COUPON_CODE.notStarted;\n if (coupon.endsAt !== null && !isAfter(coupon.endsAt, now)) return COUPON_CODE.expired;\n if (coupon.status !== COUPON_STATUS.active) return COUPON_CODE.paused;\n if (coupon.maxRedemptions !== null && usage.total >= coupon.maxRedemptions) return COUPON_CODE.exhausted;\n if (usage.account >= coupon.maxPerAccount) return COUPON_CODE.accountLimit;\n if (coupon.newCustomersOnly && ctx.hasPaidBefore) return COUPON_CODE.newCustomersOnly;\n return null;\n}\n\n/** What only a discount is checked against: the provider, the plan, the minimum, the currency and the bound on its size. */\nfunction discountRefusal(coupon: Coupon, ctx: CheckoutContext, settings: CouponSettings): CouponRefusal | null {\n const amount = ctx.amountMinor ?? 0;\n if (ctx.providerSupportsDiscounts === false) return COUPON_CODE.provider;\n if (coupon.plans.length > 0 && !coupon.plans.includes(ctx.plan ?? \"\")) return COUPON_CODE.notForPlan;\n if (coupon.minimumMinor !== null && amount < coupon.minimumMinor) return COUPON_CODE.minimum;\n if (coupon.currency !== null && coupon.currency !== ctx.currency) return COUPON_CODE.currency;\n const tooLarge = coupon.percentOffBp !== null ? coupon.percentOffBp > settings.maxPercentOff * 100 : coupon.amountOffMinor! * 100 > amount * settings.maxPercentOff;\n return tooLarge ? COUPON_CODE.tooLarge : null;\n}\n\n/** The first rule a code breaks for a context at `now`, or none. The order is the order of the refusals' table. */\nexport function refusalOf(coupon: Coupon, ctx: CheckoutContext, usage: Usage, now: Date, settings: CouponSettings): CouponRefusal | null {\n const discount = coupon.kind === KIND.discount;\n if (discount !== (ctx.mode === \"checkout\")) return COUPON_CODE.kind;\n return commonRefusal(coupon, ctx, usage, now) ?? (discount ? discountRefusal(coupon, ctx, settings) : null);\n}\n\n/** What the admin gives to create a coupon (the kind decides which terms are required). */\nexport interface NewCoupon {\n code: string;\n kind: Coupon[\"kind\"];\n purpose: Coupon[\"purpose\"];\n partnerId?: string | null;\n startsAt: Date;\n endsAt?: Date | null;\n maxRedemptions?: number | null;\n maxPerAccount?: number;\n newCustomersOnly?: boolean;\n plans?: string[];\n percentOffBp?: number | null;\n amountOffMinor?: number | null;\n currency?: string | null;\n duration?: Coupon[\"duration\"];\n durationMonths?: number | null;\n minimumMinor?: number | null;\n credits?: number | null;\n capPerDay?: number | null;\n capPerMonth?: number | null;\n creditsValidDays?: number | null;\n onExhausted?: Coupon[\"onExhausted\"];\n}\n\nconst pos = (n: number | null | undefined, what: string, max = Number.MAX_SAFE_INTEGER) => {\n if (n === null || n === undefined) return;\n if (!Number.isSafeInteger(n) || n < 1 || n > max) throw bad(`${what} is a whole number from 1 to ${max}, got ${n}`);\n};\n\n/**\n * Checks a coupon against the rules of the table and of the product's settings, so a bad one never reaches the database.\n * A free offer is always a capped credit coupon: a discount is never 100 %, a fixed amount never runs forever.\n * @throws CouponError `COUPON_BAD_INPUT`\n */\nexport function checkCoupon(c: Coupon | (NewCoupon & { codeKey?: string }), settings: CouponSettings): void {\n const code = c.code.trim();\n const key = normalizeCode(code);\n if (!key || key.length < Math.max(MIN_CODE, settings.minCodeLength)) throw bad(`A code is letters and digits, ${Math.max(MIN_CODE, settings.minCodeLength)} to ${MAX_CODE} characters`);\n if (c.kind !== KIND.discount && c.kind !== KIND.credit) throw bad(\"A coupon is a discount or a credit\");\n if (!Object.values(PURPOSE).includes(c.purpose)) throw bad(\"A purpose is partner, promo, demo or support\");\n if ((c.purpose === PURPOSE.partner) !== Boolean(c.partnerId)) throw bad(\"A partner coupon names its partner, and only a partner coupon does\");\n if (!(c.startsAt instanceof Date) || Number.isNaN(c.startsAt.valueOf())) throw bad(\"A coupon starts on a date\");\n if (c.endsAt && !isAfter(c.endsAt, c.startsAt)) throw bad(\"A coupon ends after it starts\");\n pos(c.maxRedemptions, \"maxRedemptions\");\n pos(c.maxPerAccount, \"maxPerAccount\");\n if (c.kind === KIND.discount) return checkDiscount(c, settings);\n return checkCredit(c);\n}\n\nfunction checkDiscount(c: NewCoupon, settings: CouponSettings): void {\n if (c.credits != null || c.capPerDay != null || c.capPerMonth != null || c.creditsValidDays != null || c.onExhausted != null) throw bad(\"A discount carries no credit terms\");\n const percent = c.percentOffBp != null;\n const amount = c.amountOffMinor != null;\n if (percent === amount) throw bad(\"A discount is a percentage or a fixed amount, never both\");\n if (percent) pos(c.percentOffBp, \"percentOffBp\", Math.min(9999, settings.maxPercentOff * 100));\n if (amount) {\n pos(c.amountOffMinor, \"amountOffMinor\");\n if (!/^[a-z]{3}$/.test(c.currency ?? \"\")) throw bad(\"A fixed amount has a lowercase currency\");\n }\n if (!c.duration || !Object.values(DURATION).includes(c.duration)) throw bad(\"A discount has a duration: once, repeating or forever\");\n if ((c.duration === DURATION.repeating) !== (c.durationMonths != null)) throw bad(\"Only a repeating discount names its months\");\n pos(c.durationMonths, \"durationMonths\", 36);\n if (amount && c.duration === DURATION.forever) throw bad(\"A fixed amount never runs forever\");\n if (c.minimumMinor != null && (!Number.isSafeInteger(c.minimumMinor) || c.minimumMinor < 0)) throw bad(\"A minimum is a non-negative whole number\");\n if (c.currency != null && !/^[a-z]{3}$/.test(c.currency)) throw bad(\"A currency is three lowercase letters\");\n}\n\nfunction checkCredit(c: NewCoupon): void {\n if (c.percentOffBp != null || c.amountOffMinor != null || c.duration != null || c.durationMonths != null || c.minimumMinor != null) throw bad(\"A credit coupon carries no discount terms\");\n pos(c.credits, \"credits\");\n if (c.credits == null) throw bad(\"A credit coupon grants credits: a free offer is always a capped credit coupon\");\n pos(c.capPerDay, \"capPerDay\");\n pos(c.capPerMonth, \"capPerMonth\");\n if (c.creditsValidDays == null) throw bad(\"Every grant expires: set creditsValidDays\");\n pos(c.creditsValidDays, \"creditsValidDays\", 3650);\n if (c.onExhausted !== ON_EXHAUSTED.stop && c.onExhausted !== ON_EXHAUSTED.fallbackPaid) throw bad(\"onExhausted is stop or fallback_paid\");\n}\n"],"mappings":";AAUO,IAAM,OAAO,EAAE,UAAU,YAAY,QAAQ,SAAS;AAItD,IAAM,UAAU,EAAE,SAAS,WAAW,OAAO,SAAS,MAAM,QAAQ,SAAS,UAAU;AAIvF,IAAM,gBAAgB,EAAE,QAAQ,UAAU,QAAQ,UAAU,UAAU,WAAW;AAGjF,IAAM,oBAAoB,EAAE,UAAU,YAAY,WAAW,aAAa,UAAU,YAAY,SAAS,UAAU;AAGnH,IAAM,WAAW,EAAE,MAAM,QAAQ,WAAW,aAAa,SAAS,UAAU;AAG5E,IAAM,eAAe,EAAE,MAAM,QAAQ,cAAc,gBAAgB;AA6MnE,IAAM,cAAc;AAAA,EACzB,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AACZ;AAIO,IAAM,eAAe;AAAA,EAC1B,UAAU;AAAA,EACV,WAAW;AAAA,EACX,aAAa;AAAA,EACb,UAAU;AAAA,EACV,oBAAoB;AAAA,EACpB,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,cAAc;AAAA,EACd,aAAa;AACf;AAIO,IAAM,qBAAsE;AAAA,EACjF,gBAAgB;AAAA,EAChB,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,sBAAsB;AAAA,EACtB,2BAA2B;AAAA,EAC3B,qBAAqB;AAAA,EACrB,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,qBAAqB;AAAA,EACrB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,6BAA6B;AAAA,EAC7B,0BAA0B;AAAA,EAC1B,mBAAmB;AAAA,EACnB,sBAAsB;AAAA,EACtB,qBAAqB;AACvB;AAGO,IAAM,cAAN,cAA0B,MAAM;AAAA,EAErC,YACW,MACT,UAAkB,MACT,mBACT;AACA,UAAM,OAAO;AAJJ;AAEA;AAGT,SAAK,OAAO;AACZ,SAAK,SAAS,mBAAmB,IAAI;AAAA,EACvC;AAAA,EAPW;AAAA,EAEA;AAAA,EAJF;AAUX;;;ACjTA,SAAS,iBAAiB;AAE1B,SAAS,eAAe;AAqBjB,IAAM,kBAAkC;AAAA,EAC7C,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,2BAA2B;AAAA,EAC3B,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,mBAAmB;AACrB;AAGO,IAAM,wBAAwB;AAG9B,IAAM,gBAAgB;AAAA,EAC3B,oBAAoB,CAAC,IAAI,IAAI;AAAA,EAC7B,2BAA2B,CAAC,GAAG,GAAG;AAAA,EAClC,sBAAsB,CAAC,GAAG,GAAG;AAAA,EAC7B,eAAe,CAAC,GAAG,EAAE;AAAA,EACrB,qBAAqB,CAAC,GAAG,EAAE;AAAA,EAC3B,eAAe,CAAC,GAAG,EAAE;AACvB;AAGO,SAAS,cAAc,GAAmC;AAC/D,aAAW,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,KAAK,OAAO,QAAQ,aAAa,GAAG;AAC7D,UAAM,QAAQ,EAAE,GAAiC;AACjD,QAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,OAAO,QAAQ,IAAK,OAAM,IAAI,YAAY,aAAa,aAAa,GAAG,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,KAAK,EAAE;AAAA,EAC5J;AACA,SAAO;AACT;AAEA,IAAM,MAAM,CAAC,YAAoB,IAAI,YAAY,aAAa,UAAU,OAAO;AAG/E,IAAM,WAAW;AAEjB,IAAM,WAAW;AAOV,SAAS,cAAc,OAA8B;AAC1D,QAAM,MAAM,MAAM,UAAU,MAAM,EAAE,QAAQ,aAAa,EAAE,EAAE,YAAY;AACzE,SAAO,cAAc,KAAK,GAAG,KAAK,IAAI,UAAU,KAAK,IAAI,UAAU,WAAW,MAAM;AACtF;AAGA,IAAM,WAAW;AAGV,SAAS,aAAa,QAAgB,OAAgC,CAAC,QAAQ,UAAU,GAAG,GAAW;AAC5G,SAAO,MAAM,KAAK,EAAE,OAAO,GAAG,MAAM,SAAS,KAAK,SAAS,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE;AAC9E;AAGO,SAAS,YAAY,GAAiE,KAAoB;AAC/G,MAAI,EAAE,WAAW,kBAAkB,aAAa,EAAE,WAAW,kBAAkB,QAAS,QAAO,CAAC,EAAE;AAClG,SAAO,EAAE,WAAW,kBAAkB,YAAY,EAAE,kBAAkB,QAAQ,QAAQ,EAAE,eAAe,GAAG;AAC5G;AAwCO,SAAS,SAAS,QAAgB,aAAoD;AAC3F,MAAI,OAAO,SAAS,KAAK,QAAQ;AAC/B,WAAO,EAAE,MAAM,KAAK,QAAQ,SAAS,OAAO,SAAU,WAAW,OAAO,WAAW,aAAa,OAAO,aAAa,WAAW,OAAO,kBAAmB,aAAa,OAAO,YAAa;AAAA,EAC5L;AACA,QAAM,MAAM,OAAO,iBAAiB,OAAO,KAAK,MAAO,cAAc,OAAO,eAAgB,GAAM,IAAI,OAAO;AAC7G,QAAM,gBAAgB,KAAK,IAAI,KAAK,WAAW;AAC/C,SAAO,EAAE,MAAM,KAAK,UAAU,cAAc,OAAO,cAAc,gBAAgB,OAAO,gBAAgB,UAAU,OAAO,UAAU,UAAU,OAAO,UAAW,gBAAgB,OAAO,gBAAgB,eAAe,cAAc,cAAc,cAAc;AACjQ;AAGO,SAAS,QAAQ,QAA+B;AACrD,SAAO;AAAA,IACL,UAAU,OAAO;AAAA,IACjB,cAAc,OAAO;AAAA,IACrB,GAAI,OAAO,iBAAiB,OAAO,EAAE,cAAc,OAAO,aAAa,IAAI,EAAE,gBAAgB,OAAO,gBAAiB,UAAU,OAAO,SAAU;AAAA,IAChJ,UAAU,OAAO;AAAA,IACjB,GAAI,OAAO,mBAAmB,OAAO,EAAE,gBAAgB,OAAO,eAAe,IAAI,CAAC;AAAA,EACpF;AACF;AAGA,SAAS,cAAc,QAAgB,KAAsB,OAAc,KAAiC;AAC1G,MAAI,QAAQ,OAAO,UAAU,GAAG,EAAG,QAAO,YAAY;AACtD,MAAI,OAAO,WAAW,QAAQ,CAAC,QAAQ,OAAO,QAAQ,GAAG,EAAG,QAAO,YAAY;AAC/E,MAAI,OAAO,WAAW,cAAc,OAAQ,QAAO,YAAY;AAC/D,MAAI,OAAO,mBAAmB,QAAQ,MAAM,SAAS,OAAO,eAAgB,QAAO,YAAY;AAC/F,MAAI,MAAM,WAAW,OAAO,cAAe,QAAO,YAAY;AAC9D,MAAI,OAAO,oBAAoB,IAAI,cAAe,QAAO,YAAY;AACrE,SAAO;AACT;AAGA,SAAS,gBAAgB,QAAgB,KAAsB,UAAgD;AAC7G,QAAM,SAAS,IAAI,eAAe;AAClC,MAAI,IAAI,8BAA8B,MAAO,QAAO,YAAY;AAChE,MAAI,OAAO,MAAM,SAAS,KAAK,CAAC,OAAO,MAAM,SAAS,IAAI,QAAQ,EAAE,EAAG,QAAO,YAAY;AAC1F,MAAI,OAAO,iBAAiB,QAAQ,SAAS,OAAO,aAAc,QAAO,YAAY;AACrF,MAAI,OAAO,aAAa,QAAQ,OAAO,aAAa,IAAI,SAAU,QAAO,YAAY;AACrF,QAAM,WAAW,OAAO,iBAAiB,OAAO,OAAO,eAAe,SAAS,gBAAgB,MAAM,OAAO,iBAAkB,MAAM,SAAS,SAAS;AACtJ,SAAO,WAAW,YAAY,WAAW;AAC3C;AAGO,SAAS,UAAU,QAAgB,KAAsB,OAAc,KAAW,UAAgD;AACvI,QAAM,WAAW,OAAO,SAAS,KAAK;AACtC,MAAI,cAAc,IAAI,SAAS,YAAa,QAAO,YAAY;AAC/D,SAAO,cAAc,QAAQ,KAAK,OAAO,GAAG,MAAM,WAAW,gBAAgB,QAAQ,KAAK,QAAQ,IAAI;AACxG;AA2BA,IAAM,MAAM,CAAC,GAA8B,MAAc,MAAM,OAAO,qBAAqB;AACzF,MAAI,MAAM,QAAQ,MAAM,OAAW;AACnC,MAAI,CAAC,OAAO,cAAc,CAAC,KAAK,IAAI,KAAK,IAAI,IAAK,OAAM,IAAI,GAAG,IAAI,gCAAgC,GAAG,SAAS,CAAC,EAAE;AACpH;AAOO,SAAS,YAAY,GAAgD,UAAgC;AAC1G,QAAM,OAAO,EAAE,KAAK,KAAK;AACzB,QAAM,MAAM,cAAc,IAAI;AAC9B,MAAI,CAAC,OAAO,IAAI,SAAS,KAAK,IAAI,UAAU,SAAS,aAAa,EAAG,OAAM,IAAI,iCAAiC,KAAK,IAAI,UAAU,SAAS,aAAa,CAAC,OAAO,QAAQ,aAAa;AACtL,MAAI,EAAE,SAAS,KAAK,YAAY,EAAE,SAAS,KAAK,OAAQ,OAAM,IAAI,oCAAoC;AACtG,MAAI,CAAC,OAAO,OAAO,OAAO,EAAE,SAAS,EAAE,OAAO,EAAG,OAAM,IAAI,8CAA8C;AACzG,MAAK,EAAE,YAAY,QAAQ,YAAa,QAAQ,EAAE,SAAS,EAAG,OAAM,IAAI,oEAAoE;AAC5I,MAAI,EAAE,EAAE,oBAAoB,SAAS,OAAO,MAAM,EAAE,SAAS,QAAQ,CAAC,EAAG,OAAM,IAAI,2BAA2B;AAC9G,MAAI,EAAE,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAG,OAAM,IAAI,+BAA+B;AACzF,MAAI,EAAE,gBAAgB,gBAAgB;AACtC,MAAI,EAAE,eAAe,eAAe;AACpC,MAAI,EAAE,SAAS,KAAK,SAAU,QAAO,cAAc,GAAG,QAAQ;AAC9D,SAAO,YAAY,CAAC;AACtB;AAEA,SAAS,cAAc,GAAc,UAAgC;AACnE,MAAI,EAAE,WAAW,QAAQ,EAAE,aAAa,QAAQ,EAAE,eAAe,QAAQ,EAAE,oBAAoB,QAAQ,EAAE,eAAe,KAAM,OAAM,IAAI,oCAAoC;AAC5K,QAAM,UAAU,EAAE,gBAAgB;AAClC,QAAM,SAAS,EAAE,kBAAkB;AACnC,MAAI,YAAY,OAAQ,OAAM,IAAI,0DAA0D;AAC5F,MAAI,QAAS,KAAI,EAAE,cAAc,gBAAgB,KAAK,IAAI,MAAM,SAAS,gBAAgB,GAAG,CAAC;AAC7F,MAAI,QAAQ;AACV,QAAI,EAAE,gBAAgB,gBAAgB;AACtC,QAAI,CAAC,aAAa,KAAK,EAAE,YAAY,EAAE,EAAG,OAAM,IAAI,yCAAyC;AAAA,EAC/F;AACA,MAAI,CAAC,EAAE,YAAY,CAAC,OAAO,OAAO,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAG,OAAM,IAAI,uDAAuD;AACnI,MAAK,EAAE,aAAa,SAAS,eAAgB,EAAE,kBAAkB,MAAO,OAAM,IAAI,4CAA4C;AAC9H,MAAI,EAAE,gBAAgB,kBAAkB,EAAE;AAC1C,MAAI,UAAU,EAAE,aAAa,SAAS,QAAS,OAAM,IAAI,mCAAmC;AAC5F,MAAI,EAAE,gBAAgB,SAAS,CAAC,OAAO,cAAc,EAAE,YAAY,KAAK,EAAE,eAAe,GAAI,OAAM,IAAI,0CAA0C;AACjJ,MAAI,EAAE,YAAY,QAAQ,CAAC,aAAa,KAAK,EAAE,QAAQ,EAAG,OAAM,IAAI,uCAAuC;AAC7G;AAEA,SAAS,YAAY,GAAoB;AACvC,MAAI,EAAE,gBAAgB,QAAQ,EAAE,kBAAkB,QAAQ,EAAE,YAAY,QAAQ,EAAE,kBAAkB,QAAQ,EAAE,gBAAgB,KAAM,OAAM,IAAI,2CAA2C;AACzL,MAAI,EAAE,SAAS,SAAS;AACxB,MAAI,EAAE,WAAW,KAAM,OAAM,IAAI,+EAA+E;AAChH,MAAI,EAAE,WAAW,WAAW;AAC5B,MAAI,EAAE,aAAa,aAAa;AAChC,MAAI,EAAE,oBAAoB,KAAM,OAAM,IAAI,2CAA2C;AACrF,MAAI,EAAE,kBAAkB,oBAAoB,IAAI;AAChD,MAAI,EAAE,gBAAgB,aAAa,QAAQ,EAAE,gBAAgB,aAAa,aAAc,OAAM,IAAI,sCAAsC;AAC1I;","names":[]}