@nehorai/credits 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/index.cjs +1 -0
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +139 -23
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +113 -22
- package/dist/core/index.d.ts +113 -22
- package/dist/core/index.js +128 -21
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +246 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +235 -59
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +183 -41
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +6 -5
- package/dist/repository/index.d.ts +6 -5
- package/dist/repository/index.js +183 -41
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.cjs +1 -0
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.d.cts +2 -2
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/service/index.cjs +25 -16
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +2 -2
- package/dist/service/index.d.ts +2 -2
- package/dist/service/index.js +25 -16
- package/dist/service/index.js.map +1 -1
- package/dist/{types-MaViK5tD.d.cts → types-C3sxDvNb.d.cts} +29 -6
- package/dist/{types-MaViK5tD.d.ts → types-C3sxDvNb.d.ts} +29 -6
- package/dist/{types-3c-IAwZI.d.ts → types-CY-oHIMA.d.ts} +19 -3
- package/dist/{types-XSjNXO5l.d.cts → types-DOjguhEX.d.cts} +19 -3
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/errors.ts","../src/core/amount.ts","../src/core/types.ts","../src/core/outcomes.ts","../src/core/reservation-integrity.ts","../src/core/payment-ref.ts","../src/core/idempotency.ts","../src/core/error-classify.ts","../src/core/deferred.ts","../src/config/index.ts","../src/config/provider.ts","../src/repository/types.ts","../src/repository/flow.ts","../src/core/operations.ts","../src/config/costs.ts","../src/repository/v2-types.ts","../src/repository/utils.ts","../src/repository/memory/snapshot.ts","../src/repository/memory/mutex.ts","../src/repository/memory/store.ts","../src/repository/memory/transition-journal.ts","../src/repository/memory/v2.ts","../src/repository/memory/index.ts","../src/auth/api-key.ts","../src/service/credits-service.ts","../src/adapters/generic.ts","../src/notifications/index.ts","../src/sdk/types.ts","../src/sdk/errors.ts","../src/sdk/client.ts","../src/sdk/admin-client.ts","../src/utils/error-utils.ts","../src/utils/index.ts"],"sourcesContent":["import { sumAmounts } from \"./amount.js\";\r\n/**\r\n * Credit system error classes - framework agnostic\r\n *\r\n * Provides typed error handling for credit operations.\r\n * These errors can be used across different environments.\r\n */\r\n\r\n/**\r\n * Error codes for credit operations\r\n */\r\nexport const CreditErrorCode = {\r\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\r\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\r\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\r\n RESERVATION_ALREADY_PROCESSED: \"RESERVATION_ALREADY_PROCESSED\",\r\n USER_NOT_FOUND: \"USER_NOT_FOUND\",\r\n INVALID_OPERATION_TYPE: \"INVALID_OPERATION_TYPE\",\r\n CONFIGURATION_ERROR: \"CONFIGURATION_ERROR\",\r\n DATABASE_ERROR: \"DATABASE_ERROR\",\r\n /** Caller reused an idempotency key with a different immutable payload. */\r\n IDEMPOTENCY_CONFLICT: \"IDEMPOTENCY_CONFLICT\",\r\n /**\r\n * Retryable infrastructure failure — serialization/deadlock, lock timeout,\r\n * connection loss, pool exhaustion. Distinct from `DATABASE_ERROR`, which is\r\n * a deterministic failure that will fail again identically on retry.\r\n */\r\n TRANSIENT_ERROR: \"TRANSIENT_ERROR\",\r\n /** The configured repository adapter does not implement the requested V2 method. */\r\n UNSUPPORTED_OPERATION: \"UNSUPPORTED_OPERATION\",\r\n /** Amount was not a finite positive number. */\r\n INVALID_AMOUNT: \"INVALID_AMOUNT\",\r\n /** Idempotency key was supplied but empty or whitespace-only. */\r\n INVALID_IDEMPOTENCY_KEY: \"INVALID_IDEMPOTENCY_KEY\",\r\n /**\r\n * A persisted reservation holds a status this library does not define, so no\r\n * transition can reason about it. Quarantined rather than cast; nothing moved.\r\n */\r\n CORRUPT_RESERVATION_STATUS: \"CORRUPT_RESERVATION_STATUS\",\r\n /**\r\n * A persisted reservation does not record that its hold was placed\r\n * atomically, so the credits it claims are not known to be reserved. Moving\r\n * it would spend another hold's coverage; nothing moved.\r\n */\r\n UNBACKED_RESERVATION: \"UNBACKED_RESERVATION\",\r\n} as const;\r\n\r\nexport type CreditErrorCodeType = (typeof CreditErrorCode)[keyof typeof CreditErrorCode];\r\n\r\n/**\r\n * Custom error class for credit operations\r\n *\r\n * Provides structured error information including:\r\n * - Error code for programmatic handling\r\n * - Human-readable message\r\n * - Optional details for debugging\r\n */\r\nexport class CreditError extends Error {\r\n public readonly code: CreditErrorCodeType;\r\n public readonly details?: Record<string, unknown>;\r\n\r\n constructor(\r\n message: string,\r\n code: CreditErrorCodeType,\r\n details?: Record<string, unknown>\r\n ) {\r\n super(message);\r\n this.name = \"CreditError\";\r\n this.code = code;\r\n this.details = details;\r\n\r\n // Maintains proper stack trace for where error was thrown\r\n if (Error.captureStackTrace) {\r\n Error.captureStackTrace(this, CreditError);\r\n }\r\n }\r\n\r\n /**\r\n * Convert to JSON-serializable object\r\n */\r\n toJSON(): { name: string; code: string; message: string; details?: Record<string, unknown> } {\r\n return {\r\n name: this.name,\r\n code: this.code,\r\n message: this.message,\r\n ...(this.details && { details: this.details }),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Type guard to check if an error is a CreditError\r\n */\r\nexport function isCreditError(error: unknown): error is CreditError {\r\n return error instanceof CreditError;\r\n}\r\n\r\n/**\r\n * Check if an error represents insufficient credits\r\n *\r\n * Works with both CreditError and regular Error instances.\r\n * Useful for handling errors from different sources.\r\n */\r\nexport function isInsufficientCreditsError(error: unknown): boolean {\r\n if (isCreditError(error)) {\r\n return error.code === CreditErrorCode.INSUFFICIENT_CREDITS;\r\n }\r\n\r\n if (error instanceof Error) {\r\n return error.message.toLowerCase().includes(\"insufficient credits\");\r\n }\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Create an insufficient credits error\r\n */\r\nexport function createInsufficientCreditsError(\r\n required: number,\r\n available: number\r\n): CreditError {\r\n return new CreditError(\r\n `Insufficient credits: available ${available}, required ${required}`,\r\n CreditErrorCode.INSUFFICIENT_CREDITS,\r\n { required, available, shortfall: sumAmounts(required, -available) }\r\n );\r\n}\r\n\r\n/**\r\n * Create a reservation not found error\r\n */\r\nexport function createReservationNotFoundError(reservationId: string): CreditError {\r\n return new CreditError(\r\n `Reservation ${reservationId} not found`,\r\n CreditErrorCode.RESERVATION_NOT_FOUND,\r\n { reservationId }\r\n );\r\n}\r\n\r\n/**\r\n * Create a reservation expired error\r\n */\r\nexport function createReservationExpiredError(reservationId: string): CreditError {\r\n return new CreditError(\r\n `Reservation ${reservationId} has expired`,\r\n CreditErrorCode.RESERVATION_EXPIRED,\r\n { reservationId }\r\n );\r\n}\r\n\r\n/**\r\n * Create a reservation already processed error\r\n */\r\nexport function createReservationAlreadyProcessedError(\r\n reservationId: string,\r\n status: string\r\n): CreditError {\r\n return new CreditError(\r\n `Reservation ${reservationId} has already been ${status}`,\r\n CreditErrorCode.RESERVATION_ALREADY_PROCESSED,\r\n { reservationId, status }\r\n );\r\n}\r\n\r\n/**\r\n * Create a user not found error\r\n */\r\nexport function createUserNotFoundError(userId: string): CreditError {\r\n return new CreditError(\r\n `User ${userId} not found`,\r\n CreditErrorCode.USER_NOT_FOUND,\r\n { userId }\r\n );\r\n}\r\n\r\n/**\r\n * Create an invalid operation type error\r\n */\r\nexport function createInvalidOperationTypeError(\r\n operationType: string,\r\n validTypes: string[]\r\n): CreditError {\r\n return new CreditError(\r\n `Invalid operation type: ${operationType}. Valid types: ${validTypes.join(\", \")}`,\r\n CreditErrorCode.INVALID_OPERATION_TYPE,\r\n { operationType, validTypes }\r\n );\r\n}\r\n\r\n/**\r\n * Create an idempotency conflict error\r\n *\r\n * Raised when a caller reuses an idempotency key with a different immutable\r\n * payload (different amount or operation type). This is a caller bug, never a\r\n * retryable condition — the details carry both payloads so it can be diffed.\r\n */\r\nexport function createIdempotencyConflictError(\r\n idempotencyKey: string,\r\n details?: Record<string, unknown>\r\n): CreditError {\r\n return new CreditError(\r\n `Idempotency key ${idempotencyKey} was reused with a different payload`,\r\n CreditErrorCode.IDEMPOTENCY_CONFLICT,\r\n { idempotencyKey, ...details }\r\n );\r\n}\r\n\r\n/**\r\n * Create a transient (retryable) error\r\n */\r\nexport function createTransientError(\r\n message: string,\r\n details?: Record<string, unknown>\r\n): CreditError {\r\n return new CreditError(message, CreditErrorCode.TRANSIENT_ERROR, details);\r\n}\r\n\r\n/**\r\n * Create an error for a V2 method the configured adapter does not implement\r\n */\r\nexport function createUnsupportedOperationError(\r\n operation: string,\r\n hint?: string\r\n): CreditError {\r\n return new CreditError(\r\n hint\r\n ? `Repository does not support ${operation}. ${hint}`\r\n : `Repository does not support ${operation}`,\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n { operation, ...(hint ? { hint } : {}) }\r\n );\r\n}\r\n\r\n/**\r\n * Create an invalid amount error\r\n */\r\nexport function createInvalidAmountError(amount: unknown): CreditError {\r\n return new CreditError(\r\n `Credit amount must be a finite positive number (got ${String(amount)})`,\r\n CreditErrorCode.INVALID_AMOUNT,\r\n { amount }\r\n );\r\n}\r\n\r\n/**\r\n * Check if an error is an idempotency conflict\r\n */\r\nexport function isIdempotencyConflictError(error: unknown): boolean {\r\n return isCreditError(error) && error.code === CreditErrorCode.IDEMPOTENCY_CONFLICT;\r\n}\r\n\r\n/**\r\n * Check if an error is safe to retry\r\n *\r\n * Only `TRANSIENT_ERROR` qualifies. Use {@link classifyDatabaseError} first to\r\n * turn a raw driver error into a classified `CreditError`.\r\n */\r\nexport function isTransientError(error: unknown): boolean {\r\n return isCreditError(error) && error.code === CreditErrorCode.TRANSIENT_ERROR;\r\n}\r\n","/**\n * Validation and exact comparison for credit amounts.\n *\n * Every amount ends up in a `numeric(12, 2)` column, so the ledger can only\n * represent values on the cent grid, with at most ten integer digits. A value\n * outside that grid is not \"close enough\" — PostgreSQL silently rounds it on\n * write, and the row that comes back no longer equals what the caller asked\n * for. Rejecting it before any row is touched is the only way the ledger and\n * the caller can agree on what happened.\n *\n * These live in core rather than in an adapter so every repository — shipped\n * or third-party — validates identically.\n */\n\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\nimport type { PortableReservation } from \"./types.js\";\n\n/** `numeric(12, 2)`: 12 significant digits, 2 of them after the point. */\nexport const CREDIT_AMOUNT_SCALE = 2;\n/** Largest representable amount: ten integer digits plus two decimals. */\nexport const CREDIT_AMOUNT_MAX = 9_999_999_999.99;\nconst MAX_CENTS = 999_999_999_999;\n\n/**\n * Convert an amount to whole cents, or `null` if it is not exactly on the grid.\n *\n * `Math.round(value * 100) === value * 100` is the tempting check and it is\n * wrong: `1.005 * 100` is `100.49999999999999` in binary float, so a genuinely\n * invalid value can pass and a valid one can fail. Rounding first and then\n * confirming the value round-trips (`value === cents / 100`) is exact for\n * everything in range, because a cent count below 2^53 divided by 100 is the\n * nearest double to that decimal, which is what the literal parsed to.\n */\nexport function toCents(value: number): number | null {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\n const cents = Math.round(value * 100);\n if (!Number.isSafeInteger(cents)) return null;\n return value === cents / 100 ? cents : null;\n}\n\n/**\n * Add amounts on the cent grid, exactly.\n *\n * Deriving a total with `+` is not safe in a ledger that validates against the\n * grid: `0.10 + 0.20` is `0.30000000000000004` and `0.30 - 0.10` is\n * `0.19999999999999998`, neither of which is a value `numeric(12, 2)` can hold.\n * Two perfectly legal columns would therefore produce a total the validator\n * rejects with `INVALID_AMOUNT`, blaming the caller for a rounding artefact.\n * Summing whole cents is exact for every value in range, and `cents / 100` is\n * the nearest double to that decimal — the same one the literal would parse to.\n *\n * Subtract by negating: `sumAmounts(total, -amount)`. Negation is exact.\n *\n * This is arithmetic, not validation. If any input is itself off the grid the\n * float sum is returned unchanged — deliberately, so a corrupt value is never\n * rounded into legitimacy here — but that is not a guarantee the result will be\n * rejected downstream: two off-grid inputs can cancel, and `sumAmounts(0.005,\n * -0.005)` is a perfectly representable `0`. Inputs read from storage must be\n * validated as inputs, with `assertValidStoredAmount` or\n * `assertValidStoredAmountRaw`, before they are summed. Checking only the\n * derived result is not enough and never was.\n */\nexport function sumAmounts(...values: number[]): number {\n let cents = 0;\n for (const value of values) {\n const valueCents = toCents(value);\n if (valueCents === null) return floatSum(values);\n cents += valueCents;\n }\n return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);\n}\n\nfunction floatSum(values: number[]): number {\n let total = 0;\n for (const value of values) total += value;\n return total;\n}\n\n/** Amounts that may be *moved*: strictly positive and on the cent grid. */\nexport function isValidCreditAmount(value: unknown): value is number {\n if (typeof value !== \"number\") return false;\n const cents = toCents(value);\n return cents !== null && cents > 0 && cents <= MAX_CENTS;\n}\n\n/**\n * Reject an unusable amount before any row is written.\n *\n * @param context - merged into the error details, e.g. `{ userId }`.\n */\nexport function assertValidCreditAmount(\n value: unknown,\n context?: Record<string, unknown>\n): asserts value is number {\n if (isValidCreditAmount(value)) return;\n throw new CreditError(\n creditAmountReason(value),\n CreditErrorCode.INVALID_AMOUNT,\n { amount: value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }\n );\n}\n\n/** Say precisely which rule the value broke, so the caller can fix it. */\nfunction creditAmountReason(value: unknown): string {\n if (typeof value !== \"number\") {\n return `Credit amount must be a number (got ${typeof value})`;\n }\n if (!Number.isFinite(value)) {\n return `Credit amount must be finite (got ${String(value)})`;\n }\n if (value <= 0) {\n return `Credit amount must be greater than zero (got ${value})`;\n }\n if (value > CREDIT_AMOUNT_MAX) {\n return `Credit amount ${value} exceeds the maximum of ${CREDIT_AMOUNT_MAX}`;\n }\n return `Credit amount ${value} is not exact to ${CREDIT_AMOUNT_SCALE} decimal places`;\n}\n\n/**\n * Amounts the ledger can *record*: on the cent grid and in range, but free to\n * be zero or negative.\n *\n * Not every numeric column holds something spendable. A release journal entry\n * records `amount: 0` because no credits changed hands, and `balanceAfter` goes\n * negative for an account that was corrected or allowed to overdraw. Forcing\n * those through {@link isValidCreditAmount} would reject legitimate ledger\n * rows; skipping validation entirely would let `Infinity` or a third decimal\n * reach the column and be silently rounded or rejected by the driver.\n */\nexport function isRepresentableAmount(value: unknown): value is number {\n if (typeof value !== \"number\") return false;\n const cents = toCents(value);\n return cents !== null && Math.abs(cents) <= MAX_CENTS;\n}\n\n/**\n * Reject a ledger value the `numeric(12, 2)` column cannot hold exactly.\n *\n * @param field - the column being written, so the error names the culprit.\n */\nexport function assertRepresentableAmount(\n value: unknown,\n field: string,\n context?: Record<string, unknown>\n): asserts value is number {\n if (isRepresentableAmount(value)) return;\n throw new CreditError(\n `${field} must be a finite number exact to ${CREDIT_AMOUNT_SCALE} decimal places ` +\n `within ±${CREDIT_AMOUNT_MAX} (got ${String(value)})`,\n CreditErrorCode.INVALID_AMOUNT,\n { field, value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }\n );\n}\n\n/**\n * Check a whole set of optional ledger fields at once.\n *\n * The balance writers each take a handful of independently optional numbers,\n * and validating them one `if` at a time is where one quietly gets missed.\n * Undefined entries are skipped — they mean \"leave this column alone\".\n */\nexport function assertRepresentableFields(\n fields: Record<string, unknown>,\n context?: Record<string, unknown>\n): void {\n for (const [field, value] of Object.entries(fields)) {\n if (value === undefined) continue;\n assertRepresentableAmount(value, field, context);\n }\n}\n\n/**\n * The lowest `balance` that keeps every outstanding hold committable.\n *\n * Commit's funding guard is `balance + bonusCredits >= amount`, and every live\n * hold is counted in `reserved`, so any write that lowers `balance` must keep\n * `balance + bonusCredits >= reserved` — otherwise a hold that was fully backed\n * when it was placed throws `INSUFFICIENT_CREDITS` at commit time and is\n * stranded. Monthly resets, tier downgrades and explicit tier writes all lower\n * `balance` to a configured target; this is the floor they may not cut through.\n *\n * The floor is `reserved - bonusCredits`, never below zero: bonus credits also\n * fund commits, so they count toward the backing.\n */\nexport function backedBalanceFloor(reserved: number, bonusCredits: number): number {\n const floor = Math.max(0, sumAmounts(reserved, -bonusCredits));\n // A floor beyond the representable range means the row itself is corrupt —\n // e.g. a large negative `bonusCredits` pushing `reserved - bonusCredits`\n // past the `numeric(12,2)` cap. Refuse loudly: writing it would succeed in\n // memory and be rejected by the SQL column, splitting the adapters.\n assertRepresentableAmount(floor, \"backedBalanceFloor\", { reserved, bonusCredits });\n return floor;\n}\n\n/**\n * The balance left by a cap: lowered to `cap` at most, never raised, and never\n * cut through the credits backing outstanding holds (see\n * {@link backedBalanceFloor}). A balance already under `cap` is left alone,\n * which is the point of computing this from the stored row inside a transaction\n * rather than writing a value the caller read earlier.\n */\nexport function cappedBalance(\n balance: number,\n cap: number,\n reserved: number,\n bonusCredits: number\n): number {\n return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));\n}\n\n/** A balance a lowering write lands on, and how much of it only backs holds. */\nexport interface PlannedBalance {\n balance: number;\n expiredHoldBacking: number;\n}\n\n/** The part of a balance not kept only for holds. A missing field reads as 0. */\nexport function spendableBalance(balance: number, expiredHoldBacking = 0): number {\n return sumAmounts(balance, -Math.min(expiredHoldBacking, balance));\n}\n\n/**\n * An absolute balance write: `target`, floored at what backs outstanding holds.\n * Whatever the floor kept above `target` is not the account's to spend.\n */\nexport function planBalanceTarget(\n target: number,\n reserved: number,\n bonusCredits: number\n): PlannedBalance {\n const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));\n return { balance, expiredHoldBacking: sumAmounts(balance, -target) };\n}\n\n/**\n * A cap on the spendable part of a balance. Like {@link cappedBalance} it never\n * raises the balance and never cuts through what backs holds, and it records the\n * part kept above the cap so a later release cannot hand it back. Credits already\n * marked stay marked, so applying a cap twice changes nothing.\n */\nexport function planBalanceCap(\n balance: number,\n cap: number,\n reserved: number,\n bonusCredits: number,\n expiredHoldBacking = 0\n): PlannedBalance {\n const spendable = spendableBalance(balance, expiredHoldBacking);\n const target = Math.min(spendable, cap);\n // Marked credits belong to the holds they back. A cap adds to them when the floor\n // needs it and never takes them away, even if later bonus credits lowered the floor.\n const kept = Math.max(\n sumAmounts(balance, -spendable),\n sumAmounts(backedBalanceFloor(reserved, bonusCredits), -target)\n );\n const next = Math.min(balance, sumAmounts(target, kept));\n return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };\n}\n\n/**\n * What a closing hold takes out of the credits kept only for holds: its own share\n * and nothing of another hold's. `holdBacking` comes from\n * {@link attributeExpiredBacking}.\n */\nexport function settleExpiredHoldBacking(\n balance: number,\n expiredHoldBacking: number,\n holdBacking: number\n): { used: number; remaining: number } {\n const held = Math.min(expiredHoldBacking, balance);\n const used = Math.min(held, holdBacking);\n return { used, remaining: sumAmounts(held, -used) };\n}\n\n/** The balances a commit lands on, and whether the account could pay it. */\nexport interface PlannedHoldCommit {\n covered: boolean;\n balance: number;\n bonusCredits: number;\n expiredHoldBacking: number;\n}\n\n/**\n * Pay a hold: first its own share of the marked credits, then spendable balance,\n * then bonus. Marked credits backing other holds are never touched, so what this\n * commit costs does not depend on which holds closed before it.\n */\nexport function planHoldCommit(\n balance: number,\n bonusCredits: number,\n expiredHoldBacking: number,\n holdBacking: number,\n amount: number\n): PlannedHoldCommit {\n const backing = settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking);\n const rest = sumAmounts(amount, -backing.used);\n const fromSpendable = Math.min(spendableBalance(balance, expiredHoldBacking), rest);\n const fromBonus = sumAmounts(rest, -fromSpendable);\n return {\n covered: fromBonus <= bonusCredits,\n balance: sumAmounts(balance, -backing.used, -fromSpendable),\n bonusCredits: sumAmounts(bonusCredits, -fromBonus),\n expiredHoldBacking: backing.remaining,\n };\n}\n\n/** The fields of an open hold that decide its share of the marked credits. */\nexport type ExpiredBackingHold = Pick<\n PortableReservation,\n \"id\" | \"amount\" | \"expiredBacking\" | \"holdPlacedAt\"\n>;\n\n/**\n * Split `total` marked credits across open holds, returning each hold's share.\n *\n * Shares a hold already has are kept first, oldest hold first, so lowering `total`\n * trims the newest and raising it never moves credits off a hold. Only what is left\n * goes to holds with room, again oldest first, which makes the split a function of\n * the holds and not of the order they later close in. A hold never carries more than\n * its amount. A row without `holdPlacedAt` is a record, not a hold, and gets nothing.\n *\n * The account's `expiredHoldBacking` is written as the sum of these shares, in the\n * same write, so what `total` could not attribute is not marked. Such credits stay out\n * of reach anyway, because the `reserved` that kept them still counts against them.\n */\nexport function attributeExpiredBacking(\n holds: readonly ExpiredBackingHold[],\n total: number\n): Map<string, number> {\n const placed = holds\n .filter((hold) => hold.holdPlacedAt)\n .sort((a, b) =>\n a.holdPlacedAt === b.holdPlacedAt\n ? a.id < b.id ? -1 : 1\n : (a.holdPlacedAt as string) < (b.holdPlacedAt as string) ? -1 : 1\n );\n const shares = new Map<string, number>();\n let left = total;\n for (const hold of placed) {\n const kept = Math.min(hold.expiredBacking ?? 0, hold.amount, left);\n shares.set(hold.id, kept);\n left = sumAmounts(left, -kept);\n }\n for (const hold of placed) {\n const share = shares.get(hold.id) as number;\n const added = Math.min(sumAmounts(hold.amount, -share), left);\n shares.set(hold.id, sumAmounts(share, added));\n left = sumAmounts(left, -added);\n }\n return shares;\n}\n\n/**\n * The field to write for a new backing value: nothing when it was and stays 0, so\n * an account that never had any keeps a document without the field.\n */\nexport function expiredHoldBackingPatch(\n next: number,\n stored = 0\n): { expiredHoldBacking?: number } {\n return next > 0 || stored > 0 ? { expiredHoldBacking: next } : {};\n}\n\n/**\n * Validate a balance target supplied by the application: a cap or the result of\n * a monthly-reset decision. It must be storable and cannot be negative.\n */\nexport function assertValidBalanceTarget(\n value: unknown,\n field: string,\n context?: Record<string, unknown>\n): asserts value is number {\n assertRepresentableAmount(value, field, context);\n if (value < 0) {\n throw new CreditError(\n `${field} cannot be negative (got ${value})`,\n CreditErrorCode.INVALID_AMOUNT,\n { field, value, ...context }\n );\n }\n}\n\n/**\n * Validate how a tier write changes the balance. `balance` sets a value the\n * caller computed and `balanceCap` lowers the stored value, so naming both has\n * no single meaning and is refused.\n */\nexport function assertValidTierBalanceInput(\n input: { balance?: number; balanceCap?: number },\n context?: Record<string, unknown>\n): void {\n if (input.balanceCap === undefined) return;\n if (input.balance !== undefined) {\n throw new CreditError(\n \"A tier update cannot set both balance and balanceCap\",\n CreditErrorCode.INVALID_AMOUNT,\n { balance: input.balance, balanceCap: input.balanceCap, ...context }\n );\n }\n assertValidBalanceTarget(input.balanceCap, \"balanceCap\", context);\n}\n\n/**\n * Validate a limit derived from tier configuration.\n *\n * `Infinity` is this codebase's sentinel for an unlimited tier, so it is\n * accepted here; {@link storedMonthlyLimit} decides what actually reaches the\n * column. Every other value ends up in `numeric(12, 2)` and has to fit, so a\n * tier configured with `monthlyCredits: 1.005` is caught here rather than\n * silently rounded to `1.01` by PostgreSQL.\n */\nexport function assertRepresentableTierAmount(\n value: unknown,\n field: string,\n context?: Record<string, unknown>\n): void {\n if (value === Number.POSITIVE_INFINITY) return;\n assertRepresentableAmount(value, field, context);\n}\n\n/**\n * Validate a stored amount in the representation storage actually returned.\n *\n * Checking the mapped float is not enough, because the mapping is lossy in both\n * directions. A `numeric` column with no scale constraint can hold\n * `9999999999.9900001`; `Number()` rounds that to `9999999999.99`, which is a\n * perfectly valid amount, so a validator looking at the mapped value sees\n * nothing wrong and the transition proceeds — while PostgreSQL goes on doing\n * exact arithmetic with the digits JavaScript threw away. `NaN` and values past\n * double range are worse: the mapper turns them into `0`, which misreports what\n * the row holds.\n *\n * So this runs on the driver's own output — a string, for `numeric` — and uses\n * exact integer arithmetic. It returns the amount as a float only after\n * establishing that the float is a faithful representation of it.\n *\n * @returns the stored amount, exactly representable as a JS number.\n */\n/**\n * How an unlimited tier's monthly limit is stored.\n *\n * `getConfigMonthlyLimit` returns `Infinity` for an unlimited tier, and no\n * numeric column can hold that. Both adapters therefore store the ceiling of\n * the representable range instead: any real usage compares below it, so\n * \"unlimited\" keeps behaving like unlimited, and a reader that knows nothing\n * about the sentinel sees a very large allowance rather than a wrong one.\n *\n * This exists so the two adapters cannot drift. The SQL adapter previously\n * stored `0` here, which reads as *no* allowance — the exact opposite — while\n * the in-memory adapter kept `Infinity`.\n */\nexport function storedMonthlyLimit(limit: number): number {\n return Number.isFinite(limit) ? limit : CREDIT_AMOUNT_MAX;\n}\n\nexport function assertValidStoredAmountRaw(\n raw: unknown,\n context: Record<string, unknown>\n): number {\n const cents = numericToCents(raw);\n if (cents !== null && cents > 0n && cents <= BigInt(MAX_CENTS)) {\n return Number(cents) / 100;\n }\n throw new CreditError(\n `Stored credit amount ${String(raw)} is not a valid amount, so this ` +\n \"transition was refused before any state changed. The row must be \" +\n \"repaired before it can be committed, released or expired.\",\n CreditErrorCode.INVALID_AMOUNT,\n {\n amount: typeof raw === \"string\" ? raw : String(raw),\n reason: \"corrupt_stored_amount\",\n max: CREDIT_AMOUNT_MAX,\n scale: CREDIT_AMOUNT_SCALE,\n ...context,\n }\n );\n}\n\n/**\n * Reject a *persisted* amount that the ledger cannot honour.\n *\n * A reservation row is written once and read back on every transition, and\n * nothing stops a direct SQL write, a botched migration, or an older version of\n * this library from leaving an amount the invariants cannot reason about. A\n * negative one is the dangerous case: `reserved >= amount` is trivially true\n * for it, and `balance - (-10)` *adds* credits, so a transition that trusts the\n * stored value mints money. Every transition therefore re-validates what it\n * locked before it changes anything.\n *\n * Deliberately fails release and expire as well as commit. Returning a hold of\n * an unknown size is not obviously safer than spending one — it hands back\n * coverage that may never have been taken — so a corrupt row stops moving\n * entirely and waits for an operator, rather than being partially honoured.\n *\n * This is the float-typed form, used by the in-memory adapter where the stored\n * value *is* a JS number and no conversion has happened. A repository whose\n * storage returns some other representation must use\n * {@link assertValidStoredAmountRaw} instead, or the conversion will hide the\n * corruption from the check.\n */\nexport function assertValidStoredAmount(\n value: unknown,\n context: Record<string, unknown>\n): asserts value is number {\n if (isValidCreditAmount(value)) return;\n throw new CreditError(\n `Stored credit amount ${String(value)} is not a valid amount, so this ` +\n \"transition was refused before any state changed. The row must be \" +\n \"repaired before it can be committed, released or expired.\",\n CreditErrorCode.INVALID_AMOUNT,\n { amount: value, reason: \"corrupt_stored_amount\", max: CREDIT_AMOUNT_MAX, ...context }\n );\n}\n\n/**\n * Parse a value read back from a `numeric` column into exact cents.\n *\n * Drivers return `numeric` as a string precisely so no precision is lost in\n * transit; comparing the parsed floats would throw that away again. Returns\n * `null` for anything unparseable so callers can treat it as \"not equal\"\n * rather than crashing on malformed ledger data.\n */\nexport function numericToCents(value: unknown): bigint | null {\n if (typeof value === \"number\") {\n const cents = toCents(value);\n return cents === null ? null : BigInt(cents);\n }\n if (typeof value !== \"string\") return null;\n\n const match = /^([+-]?)(\\d+)(?:\\.(\\d*))?$/.exec(value.trim());\n if (!match) return null;\n\n const [, sign, whole, fraction = \"\"] = match;\n // More than two decimals is only acceptable when the extra digits are zeros;\n // anything else never came from a numeric(12,2) column.\n if (/[1-9]/.test(fraction.slice(CREDIT_AMOUNT_SCALE))) return null;\n\n const cents =\n BigInt(whole) * 100n + BigInt(fraction.slice(0, CREDIT_AMOUNT_SCALE).padEnd(CREDIT_AMOUNT_SCALE, \"0\"));\n return sign === \"-\" ? -cents : cents;\n}\n\n/**\n * Exact equality for two amounts that may be numbers or `numeric` strings.\n *\n * Unparseable input is *not* equal to anything, including itself — a\n * comparison against corrupt data must never come back \"matches\".\n */\nexport function sameAmount(a: unknown, b: unknown): boolean {\n const left = numericToCents(a);\n const right = numericToCents(b);\n return left !== null && right !== null && left === right;\n}\n","import { sumAmounts } from \"./amount.js\";\r\n/**\r\n * Portable credits types - framework and database agnostic\r\n *\r\n * These types use standard JavaScript types (string, Date) instead of\r\n * Firebase-specific types (Timestamp). This makes the credits system\r\n * portable to other environments without Firebase dependencies.\r\n *\r\n * Note: Firestore Timestamp is handled by the credits-firestore adapter.\r\n */\r\n\r\n// ==================== Utility Functions ====================\r\n\r\n/**\r\n * Calculate available credits from balance, bonus, and reserved amounts.\r\n *\r\n * Available = balance + bonusCredits - reserved\r\n *\r\n * @param balance - Regular monthly credits\r\n * @param bonusCredits - Purchased/admin credits\r\n * @param reserved - Credits locked for in-flight operations\r\n * @returns Available credits for new operations\r\n */\r\nexport function calculateAvailableCredits(\r\n balance: number,\r\n bonusCredits: number,\r\n reserved: number\r\n): number {\r\n return sumAmounts(balance, bonusCredits, -reserved);\r\n}\r\n\r\n/**\r\n * Convert various timestamp formats to ISO string.\r\n *\r\n * Handles:\r\n * - Date objects\r\n * - ISO string (returned as-is)\r\n * - Timestamp-like objects with toDate() method (Firebase)\r\n * - undefined/null (returns current time)\r\n *\r\n * @param value - Value to convert\r\n * @returns ISO 8601 string\r\n */\r\nexport function toPortableTimestamp(value: unknown): string {\r\n if (!value) {\r\n return new Date().toISOString();\r\n }\r\n\r\n if (typeof value === \"string\") {\r\n return value;\r\n }\r\n\r\n if (value instanceof Date) {\r\n return value.toISOString();\r\n }\r\n\r\n if (\r\n typeof value === \"object\" &&\r\n value !== null &&\r\n \"toDate\" in value &&\r\n typeof (value as { toDate: () => Date }).toDate === \"function\"\r\n ) {\r\n return (value as { toDate: () => Date }).toDate().toISOString();\r\n }\r\n\r\n return new Date().toISOString();\r\n}\r\n\r\n/**\r\n * Convert various timestamp formats to Date object.\r\n *\r\n * Handles:\r\n * - Date objects (returned as-is)\r\n * - ISO string\r\n * - Timestamp-like objects with toDate() method (Firebase)\r\n * - Invalid values (returns current date)\r\n *\r\n * @param value - Value to convert\r\n * @returns Date object\r\n */\r\nexport function toDate(value: unknown): Date {\r\n if (value instanceof Date) {\r\n return value;\r\n }\r\n\r\n if (typeof value === \"string\") {\r\n return new Date(value);\r\n }\r\n\r\n if (\r\n typeof value === \"object\" &&\r\n value !== null &&\r\n \"toDate\" in value &&\r\n typeof (value as { toDate: () => Date }).toDate === \"function\"\r\n ) {\r\n return (value as { toDate: () => Date }).toDate();\r\n }\r\n\r\n return new Date();\r\n}\r\n\r\n// ==================== Subscription Types ====================\r\n\r\n/**\r\n * Built-in tier ids (autocomplete) — apps may add more via config.\r\n */\r\nexport type BuiltinTier = \"free\" | \"basic\" | \"premium\" | \"unlimited\";\r\n\r\n/**\r\n * Subscription tiers for the credits system.\r\n *\r\n * Any built-in OR any config-defined tier id. The `(string & {})` keeps builtin\r\n * autocomplete while accepting arbitrary configured tiers.\r\n */\r\nexport type SubscriptionTier = BuiltinTier | (string & {});\r\n\r\n// ==================== Operation Types ====================\r\n\r\n/**\r\n * Operation types for cost tracking (provider-agnostic)\r\n *\r\n * Dynamic string type - any operation configured in the system is valid.\r\n */\r\nexport type CreditOperationType = string;\r\n\r\n/**\r\n * AI providers\r\n */\r\nexport type AIProviderType = \"gemini\";\r\n\r\n/**\r\n * Resource type for usage logging\r\n */\r\nexport type ResourceType = string;\r\n\r\n// ==================== User Credits ====================\r\n\r\n/**\r\n * Portable user credits balance\r\n *\r\n * Uses ISO string timestamps instead of Firebase Timestamp.\r\n * This type is safe for JSON serialization and works across frameworks.\r\n */\r\nexport interface PortableUserCredits {\r\n userId: string;\r\n /** Current available balance (monthly credits, reset each month) */\r\n balance: number;\r\n /** Bonus credits from purchases/admin (never reset, persist until used) */\r\n bonusCredits: number;\r\n /** Credits currently reserved for in-flight operations */\r\n reserved: number;\r\n /** User's subscription tier */\r\n tier: SubscriptionTier;\r\n /** Monthly credit limit based on tier (0 = unlimited) */\r\n monthlyLimit: number;\r\n /** Credits used this month (resets monthly) */\r\n monthlyUsed: number;\r\n /** When monthly credits reset (start of next month) - ISO 8601 */\r\n monthlyResetAt: string;\r\n /** Subscription expiry (null/undefined for free tier) - ISO 8601 or null */\r\n subscriptionExpiresAt?: string | null;\r\n /**\r\n * The part of `balance` a balance-lowering write kept only to back outstanding\r\n * holds. A commit consumes it; a release or expiry removes it from `balance`.\r\n * Absent means 0. See `planBalanceCap`.\r\n */\r\n expiredHoldBacking?: number;\r\n /**\r\n * Set when the account was created with a signup-grant claim: the amount\r\n * granted, 0 when the claim was already taken. Absent on any other account.\r\n */\r\n signupGrant?: number;\r\n /** Creation timestamp - ISO 8601 */\r\n createdAt: string;\r\n /** Last update timestamp - ISO 8601 */\r\n updatedAt: string;\r\n}\r\n\r\n// ==================== Reservation Types ====================\r\n\r\n/**\r\n * Credit reservation status for two-phase commit\r\n */\r\nexport type ReservationStatus = \"reserved\" | \"committed\" | \"released\" | \"expired\";\r\n\r\n/**\r\n * Portable credit reservation\r\n *\r\n * Used for two-phase commit to prevent double-spending.\r\n */\r\nexport interface PortableReservation {\r\n id: string;\r\n userId: string;\r\n /** Amount of credits reserved */\r\n amount: number;\r\n /** Operation type for tracking */\r\n operationType: CreditOperationType;\r\n /** Current status of reservation */\r\n status: ReservationStatus;\r\n /** When reservation was created - ISO 8601 */\r\n createdAt: string;\r\n /** When reservation expires (for cleanup) - ISO 8601 */\r\n expiresAt: string;\r\n /** When reservation was committed/released - ISO 8601 or undefined */\r\n completedAt?: string;\r\n /**\r\n * Caller-supplied idempotency key (V2). Absent on legacy reservations.\r\n * Unique per user; a replay with the same key returns this same reservation.\r\n */\r\n idempotencyKey?: string;\r\n /**\r\n * When the atomic reserve placed this hold - ISO 8601, or absent.\r\n *\r\n * Written by `reserveCreditsV2` inside the same transaction that increases\r\n * `reserved`, so its presence is proof that the hold exists. `createReservation`\r\n * never sets it, and the V2 transitions refuse to move a row without it.\r\n * See `assertHoldPlaced` in `core/reservation-integrity.ts`.\r\n */\r\n holdPlacedAt?: string;\r\n /**\r\n * The part of this hold backed by credits that expired while it was open, and so\r\n * leave the balance when it closes, however it closes. Absent means 0, as on every\r\n * hold placed after the expiry. Open holds' shares sum to the account's\r\n * `expiredHoldBacking`.\r\n */\r\n expiredBacking?: number;\r\n}\r\n\r\n// ==================== Result Types ====================\r\n\r\n/**\r\n * Result of a credit check operation\r\n */\r\nexport interface CreditCheckResult {\r\n /** Whether user has sufficient credits */\r\n hasCredits: boolean;\r\n /** Current balance (balance + bonusCredits) */\r\n balance: number;\r\n /** Required credits for operation */\r\n required: number;\r\n /** Shortfall if insufficient */\r\n shortfall: number;\r\n}\r\n\r\n/**\r\n * Result of a one-shot atomic credit deduction (see `CreditsService.deductCredits`).\r\n *\r\n * Discriminated on `success` so callers get exhaustive narrowing:\r\n * - `success: true` -> `newBalance` is populated, `available`/`shortfall` are not present.\r\n * - `success: false` -> `available`/`shortfall` describe the shortfall, no mutation occurred.\r\n */\r\nexport type DeductCreditsResult =\r\n | {\r\n success: true;\r\n /** Combined balance (balance + bonusCredits) after the deduction */\r\n newBalance: number;\r\n }\r\n | {\r\n success: false;\r\n reason: \"insufficient\";\r\n /** Combined balance (balance + bonusCredits - reserved) available before the attempt */\r\n available: number;\r\n /** Credits that were requested */\r\n required: number;\r\n /** How many more credits would have been needed */\r\n shortfall: number;\r\n };\r\n\r\n/**\r\n * Result of monthly reset operation\r\n */\r\nexport interface MonthlyResetResult {\r\n /** Whether the reset was performed */\r\n wasReset: boolean;\r\n /** The user credits after the operation */\r\n credits: PortableUserCredits;\r\n /**\r\n * `true` when the repository wrote the reset's journal entry inside the same\r\n * atomic step as the balance write. The service then skips its own journal\r\n * call — a separate call sits outside the reset's atomicity, and if it fails\r\n * after the CAS was consumed, the line is lost for good (no retry ever sees\r\n * the reset again). Absent/`false` means the repository did not journal and\r\n * the service falls back to the legacy non-atomic write.\r\n */\r\n journaled?: boolean;\r\n}\r\n\r\n/**\r\n * Result of subscription expiry check\r\n */\r\nexport interface SubscriptionExpiryResult {\r\n /** Whether the subscription was downgraded */\r\n wasDowngraded: boolean;\r\n /** Whether user is in grace period */\r\n inGracePeriod: boolean;\r\n /** Days remaining in grace period (0 if not in grace) */\r\n graceDaysRemaining: number;\r\n /** The user credits after the operation */\r\n credits: PortableUserCredits;\r\n /**\r\n * `true` when the repository wrote the downgrade's journal entry inside the\r\n * same atomic step as the tier write. The service then skips its own journal\r\n * call — a separate call sits outside the downgrade's atomicity, and if it\r\n * fails after the tier write committed, the account is downgraded with no\r\n * audit line and no retry ever fires (the row is no longer eligible).\r\n * Absent/`false` means the repository did not journal and the service falls\r\n * back to the legacy non-atomic write.\r\n */\r\n journaled?: boolean;\r\n}\r\n\r\n// ==================== Transaction Types ====================\r\n\r\n/**\r\n * Credit transaction type\r\n */\r\nexport type TransactionType = \"purchase\" | \"subscription\" | \"bonus\" | \"refund\" | \"adjustment\";\r\n\r\n/**\r\n * Portable credit transaction\r\n */\r\nexport interface PortableTransaction {\r\n id: string;\r\n userId: string;\r\n type: TransactionType;\r\n /** Amount of credits (positive for additions, negative for deductions) */\r\n amount: number;\r\n description: string;\r\n /** Reference to payment provider */\r\n paymentRef?: string;\r\n previousBalance: number;\r\n newBalance: number;\r\n createdAt: string;\r\n}\r\n\r\n// ==================== Journal Types ====================\r\n\r\n/**\r\n * Source of a credit journal entry\r\n */\r\nexport type CreditSource =\r\n | \"operation_commit\"\r\n | \"operation_release\"\r\n | \"purchase\"\r\n | \"subscription_grant\"\r\n | \"subscription_upgrade\"\r\n | \"subscription_downgrade\"\r\n | \"monthly_reset\"\r\n | \"bonus\"\r\n | \"refund\"\r\n | \"admin_adjustment\"\r\n | \"expiry\"\r\n | \"reservation_expired\";\r\n\r\n/**\r\n * Type of reference for a journal entry\r\n */\r\nexport type JournalReferenceType =\r\n | \"reservation\"\r\n | \"transaction\"\r\n | \"adjustment\"\r\n | \"reset\"\r\n | \"subscription\";\r\n\r\n/**\r\n * Portable credit journal entry for audit trail\r\n */\r\nexport interface PortableJournalEntry {\r\n id: string;\r\n userId: string;\r\n /** Type of entry - debit decreases balance, credit increases balance */\r\n entryType: \"debit\" | \"credit\";\r\n /** Amount of credits involved */\r\n amount: number;\r\n /** Balance after this entry */\r\n balanceAfter: number;\r\n /** Source of the credit change */\r\n source: CreditSource;\r\n /** Reference ID (e.g., reservationId, transactionId) */\r\n referenceId: string;\r\n /** Type of reference */\r\n referenceType: JournalReferenceType;\r\n /** Human-readable description */\r\n description: string;\r\n /** Additional metadata */\r\n metadata?: Record<string, unknown>;\r\n /**\r\n * Deterministic key that makes this entry unique per user (V2). Absent on\r\n * legacy entries; present entries cannot be written twice.\r\n */\r\n idempotencyKey?: string;\r\n /** When this entry was created - ISO 8601 */\r\n createdAt: string;\r\n}\r\n\r\n// ==================== Usage Types ====================\r\n\r\n/**\r\n * Portable usage log entry\r\n */\r\nexport interface PortableUsageLog {\r\n id: string;\r\n userId: string;\r\n operationType: CreditOperationType;\r\n provider: AIProviderType;\r\n creditsUsed: number;\r\n success: boolean;\r\n errorMessage?: string;\r\n resourceId?: string;\r\n resourceType?: ResourceType;\r\n requestId?: string;\r\n metadata?: Record<string, unknown>;\r\n createdAt: string;\r\n}\r\n\r\n/**\r\n * User-friendly usage history entry\r\n */\r\nexport interface UsageHistoryEntry {\r\n id: string;\r\n /** Type of entry for display */\r\n type: \"usage\" | \"purchase\" | \"bonus\" | \"reset\" | \"refund\" | \"adjustment\";\r\n /** Positive = earned, Negative = spent */\r\n creditsChange: number;\r\n /** Balance after this entry */\r\n balanceAfter: number;\r\n /** Human-readable description */\r\n description: string;\r\n /** When this occurred - ISO 8601 */\r\n createdAt: string;\r\n}\r\n\r\n/**\r\n * Paginated usage history response\r\n */\r\nexport interface UsageHistoryResponse {\r\n entries: UsageHistoryEntry[];\r\n pagination: {\r\n total: number;\r\n limit: number;\r\n offset: number;\r\n hasMore: boolean;\r\n };\r\n}\r\n\r\n// ==================== Tier Configuration ====================\r\n\r\n/**\r\n * Tier configuration for credit limits and pricing\r\n */\r\nexport interface TierConfig {\r\n tier: SubscriptionTier;\r\n monthlyCredits: number;\r\n priceUsd: number;\r\n features: string[];\r\n /** Free/default tier marker — no subscription expiry, balance untouched on tier change. Defaults to priceUsd === 0. */\r\n isFree?: boolean;\r\n /** Unlimited tier marker. Defaults to monthlyCredits === 0. */\r\n unlimited?: boolean;\r\n /** Tier assigned to brand-new users. Defaults to the tier flagged isFree. */\r\n isDefault?: boolean;\r\n}\r\n\r\n// ==================== Options Types ====================\r\n\r\n/**\r\n * Options for withCredits HOF\r\n */\r\nexport interface WithCreditsOptions {\r\n /** Operation type for cost lookup */\r\n operationType: CreditOperationType;\r\n /** AI provider (defaults to \"gemini\") */\r\n provider?: AIProviderType;\r\n /**\r\n * Custom cost override (use with caution). Must be a movable amount: only a\r\n * cost of 0 in `operationCosts` makes an operation free, so a computed\r\n * override that lands on 0 is refused rather than run for nothing.\r\n */\r\n customCost?: number;\r\n /** Resource ID for usage logging */\r\n resourceId?: string;\r\n /** Resource type for usage logging */\r\n resourceType?: ResourceType;\r\n}\r\n\r\n// ==================== Constants ====================\r\n","/**\r\n * Typed outcomes for the V2 reservation boundary.\r\n *\r\n * The legacy `*Atomic` repository methods signal \"you lost the race\" by\r\n * throwing (or, worse, by silently returning). V2 makes every terminal\r\n * transition an explicit, serialisable value so callers can tell a winner\r\n * from a duplicate delivery without string-matching an error message.\r\n *\r\n * Every outcome is discriminated on `outcome` (never `status`, which is the\r\n * reservation's own lifecycle field and would collide).\r\n */\r\n\r\nimport type {\r\n PortableReservation,\r\n PortableTransaction,\r\n ReservationStatus,\r\n} from \"./types.js\";\r\n\r\n/** Statuses a reservation can no longer transition out of. */\r\nexport type TerminalReservationStatus = Exclude<ReservationStatus, \"reserved\">;\r\n\r\n/**\r\n * Result of `reserveCreditsV2`.\r\n *\r\n * - `created` — this caller minted the hold.\r\n * - `replayed` — same `(userId, idempotencyKey)` with an identical immutable\r\n * payload; the original reservation is returned and no new hold was placed.\r\n * - `insufficient` — the guarded hold could not be placed; nothing was written.\r\n * - `idempotency_conflict` — the key was reused with a different\r\n * amount/operation. Nothing was written; the existing reservation is\r\n * returned so the caller can diff it.\r\n */\r\nexport type ReserveOutcome =\r\n | { outcome: \"created\"; reservation: PortableReservation }\r\n | { outcome: \"replayed\"; reservation: PortableReservation }\r\n | {\r\n outcome: \"insufficient\";\r\n available: number;\r\n required: number;\r\n shortfall: number;\r\n }\r\n | {\r\n outcome: \"idempotency_conflict\";\r\n idempotencyKey: string;\r\n existing: PortableReservation;\r\n };\r\n\r\n/**\r\n * Result of `commitReservationV2`.\r\n *\r\n * Exactly one concurrent caller ever observes `committed`; that call — and\r\n * only that call — moved the balance and wrote the journal entry.\r\n */\r\nexport type CommitOutcome =\r\n | {\r\n outcome: \"committed\";\r\n reservation: PortableReservation;\r\n amount: number;\r\n /** `balance + bonusCredits` after the deduction, read from the same UPDATE. */\r\n balanceAfter: number;\r\n journalEntryId: string;\r\n }\r\n | {\r\n outcome: \"already_terminal\";\r\n reservation: PortableReservation;\r\n terminalStatus: TerminalReservationStatus;\r\n }\r\n | { outcome: \"not_found\"; reservationId: string };\r\n\r\n/** Result of `releaseReservationV2`. Mirrors {@link CommitOutcome}. */\r\nexport type ReleaseOutcome =\r\n | {\r\n outcome: \"released\";\r\n reservation: PortableReservation;\r\n amount: number;\r\n journalEntryId: string;\r\n }\r\n | {\r\n outcome: \"already_terminal\";\r\n reservation: PortableReservation;\r\n terminalStatus: TerminalReservationStatus;\r\n }\r\n | { outcome: \"not_found\"; reservationId: string };\r\n\r\n/**\r\n * Result of `expireReservationV2`.\r\n *\r\n * `not_due` means the reservation is still `reserved` but its `expiresAt` is\r\n * in the future relative to the sweep's `asOf` — a no-op, not a failure.\r\n */\r\nexport type ExpireOutcome =\r\n | {\r\n outcome: \"expired\";\r\n reservation: PortableReservation;\r\n amount: number;\r\n journalEntryId: string;\r\n }\r\n | { outcome: \"not_due\"; reservation: PortableReservation }\r\n | {\r\n outcome: \"already_terminal\";\r\n reservation: PortableReservation;\r\n terminalStatus: TerminalReservationStatus;\r\n }\r\n | { outcome: \"not_found\"; reservationId: string };\r\n\r\n/**\r\n * Result of crediting an account against a `paymentRef`.\r\n *\r\n * - `created` — this delivery credited the account.\r\n * - `replayed` — the reference was already used for the *same* credit event;\r\n * nothing was written and the original transaction is returned.\r\n * - `conflict` — the reference was already used for a *different* event\r\n * (another user, another amount, another source). Nothing was written;\r\n * `mismatch` names the first field that disagreed.\r\n *\r\n * A call with no `paymentRef` is always `created`: without a reference there is\r\n * nothing to deduplicate on, and every delivery is a distinct credit.\r\n *\r\n * **`created` is only a single-delivery guarantee on an adapter that implements\r\n * `addCreditsV2`.** Routed through a legacy adapter, `created` means \"the call\r\n * was made and did not throw\" — there is no global unique index behind it, so a\r\n * re-delivered reference credits again. `supportsIdempotentCredit` is the probe\r\n * that tells the two apart, and it is also why `transaction` is optional here:\r\n * the legacy method returns nothing, and inventing a transaction to fill the\r\n * field would be a fabricated audit record.\r\n */\r\nexport type AddCreditsOutcome =\r\n | {\r\n outcome: \"created\";\r\n /** The reference this credit was recorded under, if there was one. */\r\n paymentRef?: string;\r\n /** Absent only on the legacy fallback path; see above. */\r\n transaction?: PortableTransaction;\r\n /** Absent only on the legacy fallback path; see above. */\r\n journalEntryId?: string;\r\n }\r\n | {\r\n outcome: \"replayed\";\r\n paymentRef: string;\r\n transaction: PortableTransaction;\r\n }\r\n | {\r\n outcome: \"conflict\";\r\n paymentRef: string;\r\n existing: PortableTransaction;\r\n /** The first field of the canonical payload that disagreed. */\r\n mismatch: string;\r\n };\r\n\r\n/** True when this delivery actually moved credits. */\r\nexport function isCreditedOutcome(\r\n outcome: AddCreditsOutcome\r\n): outcome is Extract<AddCreditsOutcome, { outcome: \"created\" }> {\r\n return outcome.outcome === \"created\";\r\n}\r\n\r\n/** True when this caller performed the state transition. */\r\nexport function isWinningOutcome(\r\n outcome: CommitOutcome | ReleaseOutcome | ExpireOutcome\r\n): boolean {\r\n return (\r\n outcome.outcome === \"committed\" ||\r\n outcome.outcome === \"released\" ||\r\n outcome.outcome === \"expired\"\r\n );\r\n}\r\n\r\n/** Narrow a reserve outcome to the two cases that yield a usable reservation. */\r\nexport function isReservedOutcome(\r\n outcome: ReserveOutcome\r\n): outcome is Extract<ReserveOutcome, { reservation: PortableReservation }> {\r\n return outcome.outcome === \"created\" || outcome.outcome === \"replayed\";\r\n}\r\n","/**\r\n * Trust checks for a *persisted* reservation row.\r\n *\r\n * A reservation is written once and read back on every transition, and nothing\r\n * between those two moments belongs to this library: a direct `UPDATE`, a\r\n * half-finished migration, a legacy adapter or a future version can all leave a\r\n * row this code never wrote. Two of the fields it reads back are load-bearing.\r\n *\r\n * **`status`** decides whether a transition runs at all. It used to be cast —\r\n * `reservation.status as TerminalReservationStatus` — so a row holding\r\n * `'gremlin'` was reported as `already_terminal` with `terminalStatus:\r\n * 'gremlin'`: a *success* outcome, telling the caller the reservation had been\r\n * resolved, over a row nothing in this library can resolve. The cast is a lie\r\n * the type system cannot catch, so the value is checked against the closed set\r\n * instead and anything outside it quarantines the row.\r\n *\r\n * **`holdPlacedAt`** decides whether the hold behind the row exists. See\r\n * {@link assertHoldPlaced}.\r\n *\r\n * Both refusals happen before any state changes, and neither one repairs\r\n * anything: a corrupt row stops moving and waits for an operator. Silently\r\n * reconciling it would need an audit trail and a recovery procedure, and\r\n * guessing is how a bad row becomes a wrong balance.\r\n */\r\n\r\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\r\nimport type { TerminalReservationStatus } from \"./outcomes.js\";\r\nimport type { PortableReservation, ReservationStatus } from \"./types.js\";\r\n\r\n/** Every status this library knows how to reason about. */\r\nexport const RESERVATION_STATUSES: readonly ReservationStatus[] = [\r\n \"reserved\",\r\n \"committed\",\r\n \"released\",\r\n \"expired\",\r\n];\r\n\r\n/** The statuses a reservation can no longer transition out of. */\r\nexport const TERMINAL_RESERVATION_STATUSES: readonly TerminalReservationStatus[] = [\r\n \"committed\",\r\n \"released\",\r\n \"expired\",\r\n];\r\n\r\nexport function isReservationStatus(value: unknown): value is ReservationStatus {\r\n return RESERVATION_STATUSES.includes(value as ReservationStatus);\r\n}\r\n\r\nexport function isTerminalReservationStatus(\r\n value: unknown\r\n): value is TerminalReservationStatus {\r\n return TERMINAL_RESERVATION_STATUSES.includes(value as TerminalReservationStatus);\r\n}\r\n\r\n/** The subset of a reservation these checks read, so callers can pass a row. */\r\nexport interface ReservationIntegrityView {\r\n id: string;\r\n userId: string;\r\n status: unknown;\r\n /** Set by the atomic reserve, in the same transaction as the hold. */\r\n holdPlacedAt?: string | null;\r\n idempotencyKey?: string | null;\r\n}\r\n\r\n/**\r\n * Refuse a transition over a status this library does not define.\r\n *\r\n * Runs ahead of every early exit, including the `already_terminal` one, for the\r\n * reason in the module header: `already_terminal` is a success outcome, and a\r\n * success outcome over an unknown status is the failure mode being prevented.\r\n */\r\nexport function assertKnownReservationStatus(\r\n reservation: ReservationIntegrityView,\r\n transition: string\r\n): asserts reservation is ReservationIntegrityView & { status: ReservationStatus } {\r\n if (isReservationStatus(reservation.status)) return;\r\n throw new CreditError(\r\n `Reservation ${reservation.id} holds the status ${JSON.stringify(reservation.status)}, ` +\r\n `which is not one of ${RESERVATION_STATUSES.join(\", \")}. The ${transition} was ` +\r\n \"refused before any state changed; the row must be repaired by an operator.\",\r\n CreditErrorCode.CORRUPT_RESERVATION_STATUS,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n transition,\r\n status: typeof reservation.status === \"string\" ? reservation.status : String(reservation.status),\r\n allowed: [...RESERVATION_STATUSES],\r\n reason: \"corrupt_reservation_status\",\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Narrow a non-`reserved` status for the `already_terminal` outcomes.\r\n *\r\n * Validates rather than casts. `assertKnownReservationStatus` has usually run\r\n * already; this repeats the check because the cast it replaces was the defect,\r\n * and a second closed-set test costs nothing next to a database round trip.\r\n */\r\nexport function terminalStatusOf(\r\n reservation: ReservationIntegrityView,\r\n transition: string\r\n): TerminalReservationStatus {\r\n assertKnownReservationStatus(reservation, transition);\r\n if (isTerminalReservationStatus(reservation.status)) return reservation.status;\r\n throw new CreditError(\r\n `Reservation ${reservation.id} is ${reservation.status}, which is not a terminal ` +\r\n `status, but the ${transition} tried to report it as one.`,\r\n CreditErrorCode.CORRUPT_RESERVATION_STATUS,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n transition,\r\n status: reservation.status,\r\n reason: \"not_terminal\",\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Refuse a transition over a hold that was never atomically placed.\r\n *\r\n * `holdPlacedAt` is the single invariant behind the V2 boundary: **a row is\r\n * only a reservation if the same atomic operation that wrote it also increased\r\n * `reserved` by its amount.** The atomic reserve sets the field inside that one\r\n * transaction, so the fact and the hold commit together or not at all.\r\n *\r\n * Without it, `createReservation` — a plain row writer that does not touch\r\n * `reserved` — could mint a keyed row, and a later `reserveCreditsV2` with that\r\n * key would adopt it as a `replayed` hold that no credits back. The caller then\r\n * commits it, and the commit passes its `reserved >= amount` guard by consuming\r\n * coverage that belongs to a *different*, real hold: two holds, one payment.\r\n *\r\n * So this is checked before adopting a replay and before every transition, and\r\n * `createReservation` refuses a key outright — the two halves of the same rule.\r\n */\r\nexport function assertHoldPlaced(\r\n reservation: ReservationIntegrityView,\r\n transition: string\r\n): void {\r\n if (reservation.holdPlacedAt) return;\r\n throw new CreditError(\r\n `Reservation ${reservation.id} does not record that its hold was placed atomically ` +\r\n \"(holdPlacedAt is absent), so the credits it claims to hold may never have been \" +\r\n `reserved. The ${transition} was refused before any state changed: honouring it ` +\r\n \"would spend coverage belonging to another reservation. Rows written by \" +\r\n \"createReservation are not V2 reservations; place holds with reserveCredits.\",\r\n CreditErrorCode.UNBACKED_RESERVATION,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n transition,\r\n idempotencyKey: reservation.idempotencyKey ?? undefined,\r\n reason: \"hold_not_placed\",\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Refuse an idempotency key on the direct, non-atomic reservation writer.\r\n *\r\n * The other half of {@link assertHoldPlaced}. `createReservation` inserts a row\r\n * and does not touch `reserved`, so a key registered here would name a hold\r\n * that does not exist. Rejecting the key at the boundary means the unbacked\r\n * keyed row cannot be created in the first place, rather than only being\r\n * refused later by every transition that tries to move it.\r\n *\r\n * The amount check that runs alongside this is about a value the ledger cannot\r\n * honour; this is about a *claim* the writer cannot honour.\r\n */\r\nexport function assertUnkeyedDirectReservation(input: {\r\n userId: string;\r\n idempotencyKey?: string;\r\n}): void {\r\n if (input.idempotencyKey === undefined || input.idempotencyKey === null) return;\r\n throw new CreditError(\r\n \"createReservation cannot accept an idempotencyKey: it writes the reservation \" +\r\n \"row without placing the hold, so the key would name credits that were never \" +\r\n \"reserved, and a later reserveCredits with the same key would adopt it as a \" +\r\n \"replay. Call reserveCredits (or reserveCreditsV2) with the key instead — it \" +\r\n \"claims the key and places the hold in one transaction.\",\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n {\r\n userId: input.userId,\r\n idempotencyKey: input.idempotencyKey,\r\n operation: \"createReservation\",\r\n reason: \"keyed_direct_reservation\",\r\n }\r\n );\r\n}\r\n\r\n/** True when the row records an atomically placed hold. */\r\nexport function hasPlacedHold(reservation: Pick<PortableReservation, \"holdPlacedAt\">): boolean {\r\n return Boolean(reservation.holdPlacedAt);\r\n}\r\n\r\n/**\r\n * Refuse a direct status write that would bypass the V2 state machine.\r\n *\r\n * `updateReservationStatus` assigns a status and nothing else: it does not\r\n * check the current status, move the balance, adjust `reserved`, or journal.\r\n * On a row whose hold was atomically placed, that is not an update — it is a\r\n * bypass. Writing `committed` onto a live hold strands `reserved` forever (the\r\n * real commit then reports `already_terminal` and never debits), and writing\r\n * `reserved` onto a terminal row re-arms a settled reservation so it can be\r\n * settled again.\r\n *\r\n * Two refusals, mirroring the createReservation/reserveCredits split:\r\n *\r\n * - A row bearing `holdPlacedAt` belongs to the V2 transitions. Every status it\r\n * will ever hold is assigned by commit/release/expire, atomically with the\r\n * ledger movement that status implies.\r\n * - No row, V2 or legacy, may be *reopened* — status can never be assigned back\r\n * to `reserved`, because nothing here re-places the hold that status claims.\r\n *\r\n * Rows written by `createReservation` (no hold, no key) remain freely\r\n * annotatable with terminal statuses: their statuses never carried ledger\r\n * meaning, and the V2 transitions refuse them anyway.\r\n */\r\nexport function assertDirectStatusWriteAllowed(\r\n reservation: ReservationIntegrityView,\r\n status: ReservationStatus\r\n): void {\r\n if (status === \"reserved\") {\r\n throw new CreditError(\r\n `Reservation ${reservation.id} cannot have its status set back to \"reserved\": ` +\r\n \"updateReservationStatus does not place a hold, so the reopened row would claim \" +\r\n \"credits nothing reserved. Create a new reservation with reserveCredits instead.\",\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n status,\r\n reason: \"reopen_reservation\",\r\n }\r\n );\r\n }\r\n if (!reservation.holdPlacedAt) return;\r\n throw new CreditError(\r\n `Reservation ${reservation.id} is backed by an atomically placed hold, so its ` +\r\n \"status may only be assigned by the transition that settles it. Writing \" +\r\n `\"${status}\" directly would change the status without moving the credits it ` +\r\n \"stands for, stranding the hold. Use commitReservation / releaseReservation / \" +\r\n \"expireReservation (or their V2 forms) instead.\",\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n status,\r\n reason: \"direct_status_write_on_backed_hold\",\r\n }\r\n );\r\n}\r\n","/**\r\n * `paymentRef` as a global, semantic idempotency boundary.\r\n *\r\n * A payment reference is the identity of a *credit event* — one webhook, one\r\n * invoice, one charge. It is deliberately not scoped to a user: a processor's\r\n * reference is unique across the processor's whole account, and the SQL adapter\r\n * has always enforced it with a unique index over `payment_ref` alone. Two\r\n * problems followed from treating it as anything less.\r\n *\r\n * **It was scoped per user in memory and globally in SQL.** The in-memory\r\n * adapter searched only the crediting user's own transactions, so replaying a\r\n * reference against a *different* user credited both accounts, while the SQL\r\n * adapter silently no-opped. Same call, two ledgers, opposite results — and the\r\n * memory one is the direction that invents money.\r\n *\r\n * **It was compared on presence, never on payload.** A reference that arrived a\r\n * second time carrying a different amount was accepted as a replay of the\r\n * first, so a corrected or spoofed webhook credited the original amount and\r\n * reported success. Presence is only sound when the payload is known to match;\r\n * otherwise the honest answer is a conflict.\r\n *\r\n * So a reference now resolves to exactly one of three outcomes — `created`,\r\n * `replayed`, `conflict` — in both adapters, and `conflict` mutates nothing.\r\n */\r\n\r\nimport { sameAmount } from \"./amount.js\";\r\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\r\nimport type {\r\n CreditSource,\r\n JournalReferenceType,\r\n PortableTransaction,\r\n} from \"./types.js\";\r\n\r\n/**\r\n * The fields that define the credit event a reference names.\r\n *\r\n * `description` is excluded on purpose: it is human-facing copy that a retry\r\n * may legitimately regenerate (\"Purchase 100 credits\" versus \"Purchase of 100\r\n * credits\") without the event being different. Everything that decides *how\r\n * much lands where* is in.\r\n */\r\nexport interface PaymentEventPayload {\r\n userId: string;\r\n amount: number;\r\n /** The transaction row's `type` — the operation the reference names. */\r\n type: PortableTransaction[\"type\"];\r\n /** The journal entry's `source` — where the credits came from. */\r\n source: CreditSource;\r\n /** The journal entry's `referenceType`. */\r\n referenceType: JournalReferenceType;\r\n}\r\n\r\n/**\r\n * The stored side of the comparison, as either adapter can produce it.\r\n *\r\n * `amount` is `unknown` because SQL returns `numeric` as a string, and\r\n * comparing the mapped `Number` is exactly the rounding hole that let\r\n * `9999999999.9900001` match a request for `9999999999.99`.\r\n */\r\nexport interface StoredPaymentEvent {\r\n userId: string;\r\n amount: unknown;\r\n type: string;\r\n /** Absent when no journal entry could be located for the stored transaction. */\r\n source?: string;\r\n referenceType?: string;\r\n}\r\n\r\n/**\r\n * Normalise a payment reference to \"a reference\" or \"no reference\".\r\n *\r\n * Empty and whitespace-only strings are *not* references: they cannot identify\r\n * a payment, and treating them as one produced the divergence where SQL skipped\r\n * the duplicate check (falsy) but still stored the value, so the replay hit the\r\n * unique index and threw while memory quietly credited twice.\r\n *\r\n * Unlike an idempotency key, a blank reference is normalised rather than\r\n * rejected. A key is the caller's explicit request for exactly-once and a blank\r\n * one means they got it wrong; `paymentRef` is an optional annotation on a\r\n * credit, and an absent one is a normal, supported call.\r\n */\r\nexport function normalizePaymentRef(ref?: string | null): string | undefined {\r\n if (typeof ref !== \"string\") return undefined;\r\n const trimmed = ref.trim();\r\n return trimmed.length > 0 ? trimmed : undefined;\r\n}\r\n\r\n/**\r\n * Name the first field on which a stored credit event disagrees, or `null`.\r\n *\r\n * `null` means the delivery is a genuine replay and must change nothing.\r\n * Anything else is a conflict: the same reference was used for a different\r\n * event, and crediting on it would either double-credit or credit the wrong\r\n * amount to the wrong account.\r\n *\r\n * `source` and `referenceType` are compared only when the stored side has them.\r\n * They live on the journal entry, and a transaction written before this library\r\n * journalled its purchases has none to compare against — treating that absence\r\n * as a mismatch would turn every pre-existing reference into a permanent\r\n * conflict, which is a refusal to credit rather than a protection against one.\r\n */\r\nexport function describePaymentMismatch(\r\n stored: StoredPaymentEvent,\r\n expected: PaymentEventPayload\r\n): string | null {\r\n if (stored.userId !== expected.userId) return \"userId\";\r\n if (!sameAmount(stored.amount, expected.amount)) return \"amount\";\r\n if (stored.type !== expected.type) return \"type\";\r\n if (stored.source !== undefined && stored.source !== expected.source) return \"source\";\r\n if (stored.referenceType !== undefined && stored.referenceType !== expected.referenceType) {\r\n return \"referenceType\";\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * Refuse a payment reference on the direct, record-only transaction writer.\r\n *\r\n * `createTransaction` writes a ledger *record*: no balance moves and no journal\r\n * entry is created. But a `paymentRef` on that record still occupies the global\r\n * unique boundary, and `addCreditsV2` treats the row that holds a reference as\r\n * the authoritative credit event — a later delivery of the real payment with\r\n * the same reference then matches the record on user/amount/type, reports\r\n * `replayed`, and credits nothing, forever. The record would have consumed the\r\n * payment.\r\n *\r\n * Same shape as `assertUnkeyedDirectReservation`: the claim is refused at the\r\n * boundary, so the row that could impersonate a credit event cannot be written\r\n * in the first place. Credits that carry a reference go through\r\n * `addCreditsAtomic` / `addCreditsV2`, which claim the reference and move the\r\n * balance in one atomic step.\r\n */\r\nexport function assertUnreferencedDirectTransaction(input: {\r\n userId: string;\r\n paymentRef?: string;\r\n}): void {\r\n if (normalizePaymentRef(input.paymentRef) === undefined) return;\r\n throw new CreditError(\r\n \"createTransaction cannot accept a paymentRef: it records a transaction \" +\r\n \"without crediting the account, and the reference would then be treated \" +\r\n \"as an already-delivered payment — a later addCredits call with the same \" +\r\n \"reference reports `replayed` and credits nothing. Record unreferenced \" +\r\n \"transactions here, or deliver referenced payments through addCredits \" +\r\n \"(addCreditsAtomic / addCreditsV2), which claims the reference and moves \" +\r\n \"the balance atomically.\",\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n {\r\n userId: input.userId,\r\n paymentRef: input.paymentRef,\r\n operation: \"createTransaction\",\r\n reason: \"referenced_direct_transaction\",\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * The conflict, as an error, for the legacy `addCreditsAtomic` signature.\r\n *\r\n * That method returns `void`, so it has nowhere to put a `conflict` outcome.\r\n * Swallowing one would be the original bug in a new place — the SQL adapter\r\n * used to no-op on a cross-user reference reuse, which reads to the caller as\r\n * \"credited\" — so the legacy path throws instead. Callers that want to *handle*\r\n * a conflict rather than catch it should use `addCreditsV2`.\r\n */\r\nexport function createPaymentRefConflictError(\r\n paymentRef: string,\r\n details: { userId: string; mismatch: string; existingUserId?: string }\r\n): CreditError {\r\n return new CreditError(\r\n `Payment reference ${paymentRef} has already been used for a different credit ` +\r\n `event (${details.mismatch} differs). Nothing was credited. A payment reference ` +\r\n \"identifies one credit event globally; reusing it for another user, amount or \" +\r\n \"source is refused rather than silently ignored.\",\r\n CreditErrorCode.IDEMPOTENCY_CONFLICT,\r\n { paymentRef, reason: \"payment_ref_conflict\", ...details }\r\n );\r\n}\r\n","/**\r\n * Validation for caller-supplied idempotency keys.\r\n *\r\n * The keys are enforced by a *partial* unique index (`WHERE idempotency_key IS\r\n * NOT NULL`), so \"no key\" and \"a key\" are genuinely different states and the\r\n * boundary between them has to be unambiguous. An empty string sat in between:\r\n * the SQL adapter stored it — occupying a row in the unique index — while every\r\n * `if (key)` check read it as absent, so the key was written but never\r\n * deduplicated anything, and the in-memory adapter ignored it outright. Two\r\n * adapters, two behaviours, neither of them the one the caller asked for.\r\n *\r\n * Rejecting is preferred over trimming. Normalising `\" job-1 \"` to `\"job-1\"`\r\n * would mean two callers who sent different strings silently share a hold, and\r\n * a caller whose key is accidentally whitespace would rather hear about it than\r\n * have it quietly repaired.\r\n */\r\n\r\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\r\n\r\n/** A usable key: a string with at least one non-whitespace character. */\r\nexport function isValidIdempotencyKey(value: unknown): value is string {\r\n return typeof value === \"string\" && value.trim().length > 0;\r\n}\r\n\r\n/**\r\n * Reject an unusable idempotency key before anything is written.\r\n *\r\n * `undefined` is allowed and means \"no key\" — that is the legacy, at-least-once\r\n * behaviour, and it is a valid choice. Only a key that was *supplied* and\r\n * cannot work is an error.\r\n */\r\nexport function assertValidIdempotencyKey(\r\n value: unknown,\r\n context?: Record<string, unknown>\r\n): asserts value is string | undefined {\r\n if (value === undefined || value === null) return;\r\n if (isValidIdempotencyKey(value)) return;\r\n throw new CreditError(\r\n typeof value === \"string\"\r\n ? \"An idempotency key was supplied but is empty or whitespace-only. It \" +\r\n \"cannot deduplicate anything, so it was rejected rather than stored \" +\r\n \"as a key that never matches. Omit the key for at-least-once \" +\r\n \"behaviour, or send a meaningful one.\"\r\n : `Idempotency key must be a string (got ${typeof value})`,\r\n CreditErrorCode.INVALID_IDEMPOTENCY_KEY,\r\n { idempotencyKey: value, ...context }\r\n );\r\n}\r\n\r\n/**\r\n * Namespace the V2 reservation transitions own. Public writes may not use it.\r\n *\r\n * A transition that finds its deterministic key already present treats an\r\n * exactly-matching row as its own retry and reuses it. That is correct against\r\n * a row the transition itself wrote — and a hole if any caller can write one.\r\n * A caller who pre-seeds `reservation:<id>:commit` with a plausible-looking\r\n * entry would have the commit adopt it: the reservation flips to committed and\r\n * the balance moves, while the only journal row is the caller's, never written\r\n * by the transition and not necessarily describing what it did.\r\n *\r\n * Reserving the prefix removes the premise. Nothing outside the transitions can\r\n * create a key in it, so an existing key is either the transition's own retry\r\n * or corruption — and corruption is refused.\r\n */\r\nexport const RESERVED_JOURNAL_KEY_PREFIX = \"reservation:\";\r\n\r\n/** True when a key belongs to the namespace the V2 transitions own. */\r\nexport function isReservedJournalKey(key: unknown): boolean {\r\n return typeof key === \"string\" && key.startsWith(RESERVED_JOURNAL_KEY_PREFIX);\r\n}\r\n\r\n/**\r\n * Refuse a public journal write that claims a key in the reserved namespace.\r\n *\r\n * Called from the adapters' public `createJournalEntry`, never from the\r\n * transitions themselves — they are the ones the namespace belongs to.\r\n */\r\nexport function assertPublicJournalKey(key: unknown, userId: string): void {\r\n if (!isReservedJournalKey(key)) return;\r\n throw new CreditError(\r\n `Idempotency keys starting with \"${RESERVED_JOURNAL_KEY_PREFIX}\" are reserved for ` +\r\n 'the V2 reservation transitions and cannot be written directly. Use a key of ' +\r\n 'your own, or commit/release/expire the reservation through the V2 methods.',\r\n CreditErrorCode.INVALID_IDEMPOTENCY_KEY,\r\n { userId, idempotencyKey: key, reason: \"reserved_namespace\" }\r\n );\r\n}\r\n","/**\r\n * Driver-error classification.\r\n *\r\n * Consuming apps need to react differently to \"the user is out of credits\"\r\n * (show a paywall), \"the database blinked\" (retry), and \"our SQL is wrong\"\r\n * (page someone). Drivers signal that distinction through a SQLSTATE on the\r\n * thrown object, so this maps SQLSTATE classes onto {@link CreditErrorCode}\r\n * without taking a dependency on any particular driver.\r\n */\r\n\r\nimport {\r\n CreditError,\r\n CreditErrorCode,\r\n isCreditError,\r\n} from \"./errors.js\";\r\n\r\n/**\r\n * PostgreSQL SQLSTATEs whose failure is a property of the moment, not of the\r\n * statement — the identical call may well succeed on retry.\r\n *\r\n * - `40001` serialization_failure, `40P01` deadlock_detected, `40000` transaction_rollback\r\n * - `55P03` lock_not_available, `57014` query_canceled (statement timeout)\r\n * - `53300` too_many_connections, `53200` out_of_memory, `53400` configuration_limit_exceeded\r\n * - `08xxx` connection exception class\r\n */\r\nconst TRANSIENT_SQLSTATES = new Set([\r\n \"40000\",\r\n \"40001\",\r\n \"40P01\",\r\n \"55P03\",\r\n \"57014\",\r\n \"57P01\",\r\n \"57P02\",\r\n \"57P03\",\r\n \"53200\",\r\n \"53300\",\r\n \"53400\",\r\n]);\r\n\r\n/** Node driver-level codes that mean \"the socket/pool failed\", not \"the query is wrong\". */\r\nconst TRANSIENT_DRIVER_CODES = new Set([\r\n \"ECONNRESET\",\r\n \"ECONNREFUSED\",\r\n \"EPIPE\",\r\n \"ETIMEDOUT\",\r\n \"ENOTFOUND\",\r\n \"EHOSTUNREACH\",\r\n]);\r\n\r\n/**\r\n * `22003 numeric_value_out_of_range` — the arithmetic overflowed the column.\r\n *\r\n * Validating the operands cannot prevent this: `9999999999.99 + 0.01` is two\r\n * perfectly valid amounts whose *sum* the `numeric(12, 2)` column cannot hold.\r\n * It is a deterministic domain failure, not a broken query, so it is reported\r\n * as one instead of as a generic `DATABASE_ERROR`.\r\n */\r\nconst NUMERIC_OVERFLOW = \"22003\";\r\n\r\n/**\r\n * Read a SQLSTATE-ish `code` off an unknown thrown value.\r\n *\r\n * The chain matters. Wrapping drivers — and Drizzle itself, in places — rethrow\r\n * with the original error on `cause` and no `code` of their own, so looking only\r\n * at the outermost object reports \"no SQLSTATE\" for an error that plainly has\r\n * one. The walk is bounded and tracks what it has seen, since a `cause` graph\r\n * can be circular.\r\n */\r\nexport function getSqlState(error: unknown): string | undefined {\r\n const seen = new Set<unknown>();\r\n let current: unknown = error;\r\n for (let depth = 0; depth < 8; depth += 1) {\r\n if (typeof current !== \"object\" || current === null || seen.has(current)) return undefined;\r\n seen.add(current);\r\n const code = (current as { code?: unknown }).code;\r\n if (typeof code === \"string\") return code;\r\n current = (current as { cause?: unknown }).cause;\r\n }\r\n return undefined;\r\n}\r\n\r\n/**\r\n * True when the SQLSTATE is a unique-violation (`23505`).\r\n *\r\n * Callers that race on a partial unique index use this to tell \"someone else\r\n * inserted the same idempotency key\" from a genuine schema error.\r\n */\r\nexport function isUniqueViolation(error: unknown): boolean {\r\n return getSqlState(error) === \"23505\";\r\n}\r\n\r\n/** True when the error is worth retrying. */\r\nexport function isTransientDatabaseError(error: unknown): boolean {\r\n const code = getSqlState(error);\r\n if (!code) return false;\r\n if (TRANSIENT_SQLSTATES.has(code)) return true;\r\n if (TRANSIENT_DRIVER_CODES.has(code)) return true;\r\n // Whole connection-exception class (08000, 08003, 08006, 08001, 08004, 08007, 08P01).\r\n return code.startsWith(\"08\");\r\n}\r\n\r\n/**\r\n * Wrap an unknown thrown value in a classified {@link CreditError}.\r\n *\r\n * Already-classified `CreditError`s pass through untouched so a domain outcome\r\n * (insufficient credits, idempotency conflict) is never downgraded to\r\n * `DATABASE_ERROR` by a generic catch further up the stack.\r\n */\r\nexport function classifyDatabaseError(\r\n error: unknown,\r\n context?: Record<string, unknown>\r\n): CreditError {\r\n if (isCreditError(error)) return error;\r\n\r\n const sqlState = getSqlState(error);\r\n const message = error instanceof Error ? error.message : String(error);\r\n const details = { ...context, ...(sqlState ? { sqlState } : {}) };\r\n\r\n if (isTransientDatabaseError(error)) {\r\n return new CreditError(message, CreditErrorCode.TRANSIENT_ERROR, details);\r\n }\r\n\r\n if (sqlState === NUMERIC_OVERFLOW) {\r\n return new CreditError(message, CreditErrorCode.INVALID_AMOUNT, {\r\n ...details,\r\n reason: \"amount_out_of_range\",\r\n });\r\n }\r\n\r\n return new CreditError(message, CreditErrorCode.DATABASE_ERROR, details);\r\n}\r\n","/**\r\n * Deferred execution abstraction - framework agnostic\r\n *\r\n * Provides a way to schedule work to run after the current operation completes.\r\n * Different frameworks can provide their own implementations:\r\n * - Next.js: uses `after()` from `next/server`\r\n * - Generic: uses `setImmediate()` or `setTimeout()`\r\n * - Testing: can use synchronous execution\r\n */\r\n\r\n/**\r\n * Interface for deferred execution\r\n *\r\n * Implementations should schedule the function to run asynchronously\r\n * without blocking the current operation.\r\n */\r\nexport interface DeferredExecutor {\r\n /**\r\n * Schedule a function to run asynchronously\r\n *\r\n * The function should be executed after the current operation completes.\r\n * Errors should be caught and logged, not propagated.\r\n *\r\n * @param fn - Async function to execute\r\n */\r\n defer(fn: () => Promise<void>): void;\r\n}\r\n\r\n/**\r\n * Generic deferred executor using setImmediate/setTimeout\r\n *\r\n * Works in Node.js and browser environments.\r\n * Suitable for non-framework environments or testing.\r\n */\r\nexport const genericDeferred: DeferredExecutor = {\r\n defer(fn: () => Promise<void>): void {\r\n // Use setImmediate in Node.js, setTimeout as fallback\r\n const scheduler = typeof setImmediate !== \"undefined\" ? setImmediate : setTimeout;\r\n scheduler(() => {\r\n fn().catch((error) => {\r\n console.error(\"[Credits] Deferred task error:\", error);\r\n });\r\n });\r\n },\r\n};\r\n\r\n/**\r\n * Synchronous deferred executor\r\n *\r\n * Executes the function immediately (fire-and-forget).\r\n * Useful for testing or when deferred execution isn't needed.\r\n */\r\nexport const synchronousDeferred: DeferredExecutor = {\r\n defer(fn: () => Promise<void>): void {\r\n fn().catch((error) => {\r\n console.error(\"[Credits] Deferred task error:\", error);\r\n });\r\n },\r\n};\r\n\r\n/**\r\n * Create a custom deferred executor\r\n *\r\n * Allows frameworks to provide their own scheduling mechanism.\r\n *\r\n * @example\r\n * ```typescript\r\n * // Next.js adapter\r\n * import { after } from 'next/server';\r\n *\r\n * const nextJsDeferred = createDeferredExecutor((fn) => after(fn));\r\n * ```\r\n *\r\n * @param schedule - Function that schedules async work\r\n * @returns DeferredExecutor instance\r\n */\r\nexport function createDeferredExecutor(\r\n schedule: (fn: () => Promise<void>) => void\r\n): DeferredExecutor {\r\n return {\r\n defer: schedule,\r\n };\r\n}\r\n\r\n/**\r\n * No-op deferred executor\r\n *\r\n * Discards deferred work. Useful when you want to disable\r\n * background tasks (e.g., in certain test scenarios).\r\n */\r\nexport const noopDeferred: DeferredExecutor = {\r\n defer(_fn: () => Promise<void>): void {\r\n // Intentionally do nothing\r\n },\r\n};\r\n","import { z } from \"zod\";\r\nimport type { SubscriptionTier, TierConfig } from \"../core/types.js\";\r\nimport { getConfigProvider } from \"./provider.js\";\r\n\r\n/**\r\n * Schema for tier configuration\r\n */\r\nconst tierConfigSchema = z.object({\r\n tier: z.string().min(1),\r\n monthlyCredits: z.number().min(0),\r\n priceUsd: z.number().min(0),\r\n features: z.array(z.string()),\r\n isFree: z.boolean().optional(),\r\n unlimited: z.boolean().optional(),\r\n isDefault: z.boolean().optional(),\r\n});\r\n\r\n/**\r\n * Schema for credit system configuration\r\n */\r\nconst creditSystemConfigSchema = z.object({\r\n /**\r\n * Operation costs in credits\r\n * Keys are operation type names (any non-empty string)\r\n * Values are non-negative numbers representing credit cost. 0 marks a free\r\n * operation: `withCredits` runs it without touching the ledger. Amounts that\r\n * move credits stay strictly positive, so `reserveCredits` refuses a 0.\r\n */\r\n operationCosts: z.record(\r\n z.string().min(1),\r\n z.number().nonnegative()\r\n ),\r\n\r\n /**\r\n * Display labels for operation types (English canonical labels).\r\n * Falls back to auto-generating from snake_case keys if not provided.\r\n */\r\n operationLabels: z.record(\r\n z.string().min(1),\r\n z.string().min(1)\r\n ).optional().default({}),\r\n\r\n /**\r\n * Tier configurations\r\n */\r\n tierConfigs: z.record(\r\n z.string().min(1),\r\n tierConfigSchema\r\n ),\r\n\r\n /**\r\n * Reservation expiry time in milliseconds\r\n */\r\n reservationExpiryMs: z.number().positive().default(5 * 60 * 1000),\r\n\r\n /**\r\n * Default credits for new users (free tier). 0 means no signup grant.\r\n */\r\n defaultFreeCredits: z.number().min(0).default(25),\r\n\r\n /**\r\n * Grace period in days after subscription expires before downgrade\r\n */\r\n subscriptionGracePeriodDays: z.number().min(0).default(3),\r\n\r\n /**\r\n * Low balance notification threshold\r\n */\r\n lowBalanceThreshold: z.number().min(0).default(10),\r\n\r\n /**\r\n * Cooldown in hours between low balance notifications\r\n */\r\n lowBalanceNotificationCooldownHours: z.number().positive().default(24),\r\n\r\n /**\r\n * Feature flags for optional features\r\n */\r\n features: z.object({\r\n journalEntries: z.boolean().default(true),\r\n notifications: z.boolean().default(true),\r\n subscriptionExpiry: z.boolean().default(true),\r\n usageHistory: z.boolean().default(true),\r\n }).default({\r\n journalEntries: true,\r\n notifications: true,\r\n subscriptionExpiry: true,\r\n usageHistory: true,\r\n }),\r\n});\r\n\r\n/**\r\n * Credit system configuration type\r\n */\r\nexport type CreditSystemConfig = z.infer<typeof creditSystemConfigSchema>;\r\n\r\n/**\r\n * Default configuration\r\n */\r\nconst DEFAULT_CONFIG: CreditSystemConfig = {\r\n operationCosts: {\r\n story_generation: 5,\r\n conversation: 2,\r\n image_generation: 10,\r\n template_generation: 10,\r\n },\r\n operationLabels: {\r\n story_generation: \"Story generation\",\r\n conversation: \"Conversation\",\r\n image_generation: \"Image generation\",\r\n template_generation: \"Template generation\",\r\n },\r\n tierConfigs: {\r\n free: {\r\n tier: \"free\",\r\n monthlyCredits: 25,\r\n priceUsd: 0,\r\n isFree: true,\r\n isDefault: true,\r\n features: [\r\n \"25 credits per month\",\r\n \"Basic story generation\",\r\n \"Standard image quality\",\r\n ],\r\n },\r\n basic: {\r\n tier: \"basic\",\r\n monthlyCredits: 200,\r\n priceUsd: 9.99,\r\n features: [\r\n \"200 credits per month\",\r\n \"All story features\",\r\n \"High quality images\",\r\n \"Priority support\",\r\n ],\r\n },\r\n premium: {\r\n tier: \"premium\",\r\n monthlyCredits: 500,\r\n priceUsd: 19.99,\r\n features: [\r\n \"500 credits per month\",\r\n \"All features\",\r\n \"Highest quality images\",\r\n \"Early access to new features\",\r\n \"Priority support\",\r\n ],\r\n },\r\n unlimited: {\r\n tier: \"unlimited\",\r\n monthlyCredits: 0, // 0 = unlimited\r\n unlimited: true,\r\n priceUsd: 49.99,\r\n features: [\r\n \"Unlimited credits\",\r\n \"All features\",\r\n \"Highest quality images\",\r\n \"Early access to new features\",\r\n \"Dedicated support\",\r\n ],\r\n },\r\n },\r\n reservationExpiryMs: 5 * 60 * 1000, // 5 minutes\r\n defaultFreeCredits: 25,\r\n subscriptionGracePeriodDays: 3,\r\n lowBalanceThreshold: 10,\r\n lowBalanceNotificationCooldownHours: 24,\r\n features: {\r\n journalEntries: true,\r\n notifications: true,\r\n subscriptionExpiry: true,\r\n usageHistory: true,\r\n },\r\n};\r\n\r\n/**\r\n * Current configuration (can be overridden)\r\n */\r\nlet currentConfig: CreditSystemConfig = DEFAULT_CONFIG;\r\n\r\n/**\r\n * Load configuration from environment variables\r\n * Can be extended to load from other sources (file, remote config, etc.)\r\n *\r\n * Operation costs are loaded from CREDITS_OPERATION_COSTS as JSON:\r\n * CREDITS_OPERATION_COSTS={\"story_generation\":5,\"image_generation\":10}\r\n */\r\nexport function loadConfigFromEnv(): Partial<CreditSystemConfig> {\r\n const envConfig: Partial<CreditSystemConfig> = {};\r\n\r\n // Load operation costs from env as JSON\r\n const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;\r\n if (operationCostsEnv) {\r\n try {\r\n const parsed = JSON.parse(operationCostsEnv);\r\n if (typeof parsed === \"object\" && parsed !== null) {\r\n envConfig.operationCosts = {\r\n ...DEFAULT_CONFIG.operationCosts,\r\n ...parsed,\r\n };\r\n }\r\n } catch {\r\n console.warn(\"[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON\");\r\n }\r\n }\r\n\r\n // Load other settings from env\r\n const reservationExpiry = process.env.CREDITS_RESERVATION_EXPIRY_MS;\r\n if (reservationExpiry) {\r\n envConfig.reservationExpiryMs = parseInt(reservationExpiry, 10);\r\n }\r\n\r\n const defaultCredits = process.env.CREDITS_DEFAULT_FREE;\r\n if (defaultCredits) {\r\n envConfig.defaultFreeCredits = parseInt(defaultCredits, 10);\r\n }\r\n\r\n const gracePeriod = process.env.CREDITS_GRACE_PERIOD_DAYS;\r\n if (gracePeriod) {\r\n envConfig.subscriptionGracePeriodDays = parseInt(gracePeriod, 10);\r\n }\r\n\r\n const lowBalanceThreshold = process.env.CREDITS_LOW_BALANCE_THRESHOLD;\r\n if (lowBalanceThreshold) {\r\n envConfig.lowBalanceThreshold = parseInt(lowBalanceThreshold, 10);\r\n }\r\n\r\n // Load feature flags\r\n const featuresEnv = process.env.CREDITS_FEATURES;\r\n if (featuresEnv) {\r\n try {\r\n const features = JSON.parse(featuresEnv);\r\n envConfig.features = {\r\n ...DEFAULT_CONFIG.features,\r\n ...features,\r\n };\r\n } catch {\r\n console.warn(\"[Credits Config] Failed to parse CREDITS_FEATURES env var\");\r\n }\r\n }\r\n\r\n return envConfig;\r\n}\r\n\r\n/**\r\n * Initialize configuration\r\n * Merges default config with environment overrides\r\n */\r\nexport function initializeConfig(overrides?: Partial<CreditSystemConfig>): CreditSystemConfig {\r\n const envConfig = loadConfigFromEnv();\r\n\r\n const mergedConfig = {\r\n ...DEFAULT_CONFIG,\r\n ...envConfig,\r\n ...overrides,\r\n operationCosts: {\r\n ...DEFAULT_CONFIG.operationCosts,\r\n ...envConfig.operationCosts,\r\n ...overrides?.operationCosts,\r\n },\r\n operationLabels: {\r\n ...DEFAULT_CONFIG.operationLabels,\r\n ...envConfig.operationLabels,\r\n ...overrides?.operationLabels,\r\n },\r\n tierConfigs: {\r\n ...DEFAULT_CONFIG.tierConfigs,\r\n ...envConfig.tierConfigs,\r\n ...overrides?.tierConfigs,\r\n },\r\n features: {\r\n ...DEFAULT_CONFIG.features,\r\n ...envConfig.features,\r\n ...overrides?.features,\r\n },\r\n };\r\n\r\n // Validate configuration\r\n const result = creditSystemConfigSchema.safeParse(mergedConfig);\r\n if (!result.success) {\r\n console.error(\"[Credits Config] Invalid configuration:\", result.error.issues);\r\n throw new Error(\"Invalid credit system configuration\");\r\n }\r\n\r\n currentConfig = result.data;\r\n return currentConfig;\r\n}\r\n\r\n/**\r\n * Get the current configuration.\r\n * If an external provider is registered, delegates to it.\r\n * Otherwise falls back to the local currentConfig.\r\n */\r\nexport function getConfig(): CreditSystemConfig {\r\n const provider = getConfigProvider();\r\n if (provider) {\r\n return provider.getConfig();\r\n }\r\n return currentConfig;\r\n}\r\n\r\n/**\r\n * Get all valid operation types from configuration\r\n * Returns the keys of the operationCosts object\r\n */\r\nexport function getValidOperationTypes(): string[] {\r\n return Object.keys(getConfig().operationCosts);\r\n}\r\n\r\n/**\r\n * Check if an operation type is valid (configured in the system)\r\n * @param type - The operation type to check\r\n * @returns true if the operation type is configured\r\n */\r\nexport function isValidOperationType(type: string): boolean {\r\n return type in getConfig().operationCosts;\r\n}\r\n\r\n/**\r\n * Get operation cost from configuration\r\n * @throws Error if the operation type is not configured\r\n */\r\nexport function getConfigOperationCost(operationType: string): number {\r\n const cost = getConfig().operationCosts[operationType];\r\n if (cost === undefined) {\r\n throw new Error(\r\n `Unknown operation type: ${operationType}. Valid types: ${getValidOperationTypes().join(\", \")}`\r\n );\r\n }\r\n return cost;\r\n}\r\n\r\n/**\r\n * Get all valid tier ids from configuration\r\n * Returns the keys of the tierConfigs object\r\n */\r\nexport function getValidTiers(): string[] {\r\n return Object.keys(getConfig().tierConfigs);\r\n}\r\n\r\n/**\r\n * Check if a tier id is valid (configured in the system)\r\n */\r\nexport function isValidTier(tier: string): boolean {\r\n return tier in getConfig().tierConfigs;\r\n}\r\n\r\n/**\r\n * Check if a tier is the free/default tier.\r\n * Falls back to priceUsd === 0 when the isFree flag is not set.\r\n */\r\nexport function isFreeTier(tier: SubscriptionTier): boolean {\r\n const c = getConfig().tierConfigs[tier];\r\n if (!c) return false;\r\n return c.isFree ?? c.priceUsd === 0;\r\n}\r\n\r\n/**\r\n * Check if a tier is unlimited.\r\n * Falls back to monthlyCredits === 0 when the unlimited flag is not set.\r\n */\r\nexport function isUnlimitedTier(tier: SubscriptionTier): boolean {\r\n const c = getConfig().tierConfigs[tier];\r\n if (!c) return false;\r\n return c.unlimited ?? c.monthlyCredits === 0;\r\n}\r\n\r\n/**\r\n * Get the tier assigned to brand-new users.\r\n * Prefers the tier flagged isDefault, then the first isFree tier, then \"free\".\r\n */\r\nexport function getDefaultTier(): SubscriptionTier {\r\n const entries = Object.entries(getConfig().tierConfigs);\r\n const def =\r\n entries.find(([, c]) => c.isDefault)?.[0] ??\r\n entries.find(([, c]) => c.isFree ?? c.priceUsd === 0)?.[0];\r\n return def ?? \"free\";\r\n}\r\n\r\n/**\r\n * Magic balance assigned to unlimited tiers on upgrade.\r\n * Kept at 999999 for back-compat with stored/displayed balances.\r\n */\r\nexport const UNLIMITED_BALANCE_SENTINEL = 999999;\r\n\r\n/**\r\n * Get the magic balance assigned to unlimited tiers on upgrade.\r\n */\r\nexport function getUnlimitedSentinelBalance(): number {\r\n return UNLIMITED_BALANCE_SENTINEL;\r\n}\r\n\r\n/**\r\n * What a monthly reset must leave in `balance`, as a storable instruction.\r\n *\r\n * This is the one place the unlimited contract is defined, because it is the\r\n * one thing the two adapters kept disagreeing about. The canonical persisted\r\n * representation of an unlimited tier is:\r\n *\r\n * - `monthlyLimit` = `storedMonthlyLimit(Infinity)`, i.e. the top of the\r\n * representable range. Never `0`, which reads as *no* allowance.\r\n * - `balance` = at least {@link UNLIMITED_BALANCE_SENTINEL}, which is what\r\n * `updateTier` writes on upgrade.\r\n *\r\n * The reset used to leave an unlimited balance strictly alone (\"`Infinity` is\r\n * not storable, so change nothing\"), which is correct for a healthy account and\r\n * a trap for a degraded one: an unlimited user whose balance had been driven to\r\n * `0` — by a spend, or by the older code that stored `0` as the limit — stayed\r\n * at `0` through every subsequent reset and could never buy anything again. So\r\n * the instruction is `atLeast`, not \"leave alone\": it restores a degraded\r\n * balance to the sentinel and leaves a topped-up one (a purchase on top of an\r\n * unlimited plan) untouched. It is expressed as a floor rather than as a number\r\n * so the SQL adapter can apply it as `greatest(balance, n)` inside its guarded\r\n * UPDATE instead of writing a literal read before the lock.\r\n */\r\nexport type MonthlyResetBalance =\r\n /** Store exactly this value. */\r\n | { kind: \"absolute\"; value: number }\r\n /** Raise the stored balance to this value; leave anything higher alone. */\r\n | { kind: \"atLeast\"; value: number };\r\n\r\nexport function monthlyResetBalance(tier: SubscriptionTier): MonthlyResetBalance {\r\n if (isUnlimitedTier(tier)) {\r\n return { kind: \"atLeast\", value: UNLIMITED_BALANCE_SENTINEL };\r\n }\r\n return { kind: \"absolute\", value: getConfigMonthlyLimit(tier) };\r\n}\r\n\r\n/**\r\n * Get tier configuration from configuration.\r\n * Falls back to the default tier (with a warning) if the tier is unknown.\r\n */\r\nexport function getConfigTierConfig(tier: SubscriptionTier): TierConfig {\r\n const cfg = getConfig().tierConfigs[tier];\r\n if (cfg) return cfg;\r\n const fallback = getDefaultTier();\r\n console.warn(\r\n `[Credits Config] Unknown tier \"${tier}\". Falling back to \"${fallback}\". Valid: ${getValidTiers().join(\", \")}`\r\n );\r\n return getConfig().tierConfigs[fallback]!;\r\n}\r\n\r\n/**\r\n * Get monthly limit for a tier from configuration\r\n * Returns Infinity for unlimited tier\r\n */\r\nexport function getConfigMonthlyLimit(tier: SubscriptionTier): number {\r\n return isUnlimitedTier(tier) ? Infinity : getConfigTierConfig(tier).monthlyCredits;\r\n}\r\n\r\n/**\r\n * Runtime zod validator for tier ids. Validates against the LIVE config keys\r\n * at parse time, so apps that add tiers via config get correct validation.\r\n */\r\nexport const tierSchema = z\r\n .string()\r\n .refine(isValidTier, { message: \"Unknown subscription tier\" }) as unknown as z.ZodType<SubscriptionTier>;\r\n\r\n/**\r\n * Parse/validate a tier id against the live config keys.\r\n * @throws ZodError if the tier is not configured\r\n */\r\nexport function parseTier(value: unknown): SubscriptionTier {\r\n return tierSchema.parse(value);\r\n}\r\n\r\n/**\r\n * Check if a feature is enabled\r\n */\r\nexport function isFeatureEnabled(feature: keyof CreditSystemConfig[\"features\"]): boolean {\r\n return getConfig().features[feature];\r\n}\r\n\r\n/**\r\n * Convert a snake_case string to Title Case.\r\n * E.g., \"story_generation\" -> \"Story Generation\"\r\n */\r\nfunction snakeCaseToTitleCase(str: string): string {\r\n return str\r\n .replace(/_/g, \" \")\r\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\r\n}\r\n\r\n/**\r\n * Get the display label for an operation type.\r\n * Falls back to converting snake_case to Title Case if no label is configured.\r\n */\r\nexport function getOperationLabel(operationType: string): string {\r\n const labels = getConfig().operationLabels;\r\n if (labels && operationType in labels) {\r\n return labels[operationType];\r\n }\r\n return snakeCaseToTitleCase(operationType);\r\n}\r\n\r\n/**\r\n * Get all operation labels as a record.\r\n * For any operation in operationCosts that lacks a label, generates one from the key.\r\n */\r\nexport function getOperationLabels(): Record<string, string> {\r\n const config = getConfig();\r\n const result: Record<string, string> = {};\r\n for (const key of Object.keys(config.operationCosts)) {\r\n result[key] = config.operationLabels?.[key]\r\n ?? snakeCaseToTitleCase(key);\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * Reset configuration to defaults (for testing)\r\n */\r\nexport function resetConfig(): void {\r\n currentConfig = DEFAULT_CONFIG;\r\n}\r\n\r\n// Initialize configuration on module load\r\ninitializeConfig();\r\n","import type { CreditSystemConfig } from \"./index.js\";\r\n\r\n/** Interface for external config providers (e.g. Firestore-backed) */\r\nexport interface ICreditConfigProvider {\r\n getConfig(): CreditSystemConfig;\r\n}\r\n\r\nlet configProvider: ICreditConfigProvider | null = null;\r\n\r\n/** Register an external config provider. Called once at app startup. */\r\nexport function registerConfigProvider(provider: ICreditConfigProvider): void {\r\n configProvider = provider;\r\n}\r\n\r\n/** Get the registered provider, or null if none registered. */\r\nexport function getConfigProvider(): ICreditConfigProvider | null {\r\n return configProvider;\r\n}\r\n\r\n/** Clear registered provider (for testing). */\r\nexport function clearConfigProvider(): void {\r\n configProvider = null;\r\n}\r\n","import type {\r\n ICreditRepositoryCreditsV2,\r\n ICreditRepositoryV2,\r\n} from \"./v2-types.js\";\r\nimport type {\r\n PortableUserCredits,\r\n PortableReservation,\r\n PortableTransaction,\r\n PortableUsageLog,\r\n PortableJournalEntry,\r\n CreditOperationType,\r\n SubscriptionTier,\r\n ReservationStatus,\r\n AIProviderType,\r\n MonthlyResetResult,\r\n SubscriptionExpiryResult,\r\n CreditSource,\r\n JournalReferenceType,\r\n} from \"../core/types.js\";\r\n\r\n/**\r\n * Input for creating a credit reservation\r\n */\r\nexport interface CreateReservationInput {\r\n userId: string;\r\n amount: number;\r\n operationType: CreditOperationType;\r\n expiresAt: Date;\r\n /**\r\n * @deprecated Rejected — this writer does not place the hold.\r\n *\r\n * `createReservation` inserts a row and leaves `reserved` alone, so a keyed\r\n * row written here names a hold that does not exist, and `reserveCreditsV2`\r\n * would adopt it as a `replayed` reservation whose commit then spends another\r\n * hold's coverage. Both adapters throw `UNSUPPORTED_OPERATION` when this is\r\n * set. Pass the key to `reserveCredits` / `reserveCreditsV2` instead, which\r\n * claims the key and places the hold in one transaction.\r\n *\r\n * The field is kept rather than deleted so the refusal reaches a JavaScript\r\n * caller as a typed error instead of a silently ignored property.\r\n */\r\n idempotencyKey?: string;\r\n}\r\n\r\n/**\r\n * Input for creating a credit transaction\r\n */\r\nexport interface CreateTransactionInput {\r\n userId: string;\r\n type: PortableTransaction[\"type\"];\r\n amount: number;\r\n description: string;\r\n paymentRef?: string;\r\n previousBalance: number;\r\n newBalance: number;\r\n}\r\n\r\n/**\r\n * Input for logging usage\r\n */\r\nexport interface CreateUsageLogInput {\r\n userId: string;\r\n operationType: CreditOperationType;\r\n provider: AIProviderType;\r\n creditsUsed: number;\r\n success: boolean;\r\n errorMessage?: string;\r\n resourceId?: string;\r\n resourceType?: string;\r\n requestId?: string;\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Query options for usage logs\r\n */\r\nexport interface UsageLogQuery {\r\n userId?: string;\r\n operationType?: CreditOperationType;\r\n success?: boolean;\r\n limit?: number;\r\n offset?: number;\r\n startDate?: Date;\r\n endDate?: Date;\r\n}\r\n\r\n/**\r\n * Input for creating a journal entry\r\n */\r\nexport interface CreateJournalEntryInput {\r\n userId: string;\r\n entryType: \"debit\" | \"credit\";\r\n amount: number;\r\n balanceAfter: number;\r\n source: CreditSource;\r\n referenceId: string;\r\n referenceType: JournalReferenceType;\r\n description: string;\r\n metadata?: Record<string, unknown>;\r\n /** Deterministic key making this entry unique per user (V2, optional). */\r\n idempotencyKey?: string;\r\n}\r\n\r\n/**\r\n * Query options for journal entries\r\n */\r\nexport interface JournalEntryQuery {\r\n userId: string;\r\n source?: CreditSource;\r\n referenceType?: JournalReferenceType;\r\n limit?: number;\r\n offset?: number;\r\n startDate?: Date;\r\n endDate?: Date;\r\n}\r\n\r\n/**\r\n * Partial update for user credits balance\r\n */\r\nexport interface CreditBalanceUpdate {\r\n balance?: number;\r\n bonusCredits?: number;\r\n reserved?: number;\r\n tier?: SubscriptionTier;\r\n monthlyLimit?: number;\r\n monthlyUsed?: number;\r\n monthlyResetAt?: Date | string;\r\n subscriptionExpiresAt?: Date | string | null;\r\n /** Increment balance by this amount (alternative to absolute value) */\r\n balanceIncrement?: number;\r\n /** Increment bonusCredits by this amount (alternative to absolute value) */\r\n bonusCreditsIncrement?: number;\r\n /** Increment reserved by this amount (alternative to absolute value) */\r\n reservedIncrement?: number;\r\n /** Increment monthlyUsed by this amount */\r\n monthlyUsedIncrement?: number;\r\n}\r\n\r\n/**\r\n * Input for tier update\r\n */\r\nexport interface TierUpdateInput {\r\n tier: SubscriptionTier;\r\n monthlyLimit: number;\r\n balance?: number;\r\n /**\r\n * Atomically lowers balance to at most this, only when the account's current tier is not free.\r\n * Ignored for an account already free. Cannot be combined with `balance`.\r\n * Like every balance reduction, it stops at the credits backing outstanding holds.\r\n */\r\n balanceCap?: number;\r\n monthlyUsed?: number;\r\n subscriptionExpiresAt?: Date | string | null;\r\n}\r\n\r\n/**\r\n * Free-tier policy an application hands to a repository, for the decisions that\r\n * have to be made inside the write, from the account's current state.\r\n */\r\nexport interface CreditPolicyOptions {\r\n /**\r\n * Decides a due monthly reset, inside the reset transaction, from the account's current state.\r\n * null: no reset and no write (wasReset false, monthlyResetAt unchanged).\r\n * Takes precedence over getMonthlyLimit. monthlyLimit is written only when returned.\r\n * A throw aborts the transaction.\r\n *\r\n * Must be synchronous and free of side effects: a contended transaction runs it again.\r\n * The balance is validated, and floored at the credits backing outstanding holds.\r\n */\r\n resolveMonthlyReset?: (credits: { tier: SubscriptionTier; balance: number }) =>\r\n { balance: number; monthlyLimit?: number } | null;\r\n /**\r\n * Balance cap and monthlyLimit on a subscription-expiry downgrade.\r\n * Default: DEFAULT_FREE_CREDITS in the Firestore adapter, the default tier's\r\n * configured limit in the in-memory adapter.\r\n */\r\n getDowngradeBalanceCap?: () => number;\r\n}\r\n\r\n/**\r\n * Optional journal customization for `addCreditsAtomic`.\r\n *\r\n * By default a credit add records a journal entry with `source: \"purchase\"`,\r\n * `referenceType: \"transaction\"`, and `{ paymentRef }` metadata. These options\r\n * let callers override that — e.g. tag a signup grant as `source: \"bonus\"`, or\r\n * carry revenue-attribution fields (gross amount, credits granted) so a single\r\n * journal can serve as the app's revenue ledger.\r\n */\r\nexport interface AddCreditsAtomicOptions {\r\n /** Journal entry source (defaults to `\"purchase\"`). */\r\n source?: CreditSource;\r\n /** Journal reference type (defaults to `\"transaction\"`). */\r\n referenceType?: JournalReferenceType;\r\n /**\r\n * Extra metadata merged onto the journal entry. When `paymentRef` is present\r\n * it is merged in too (explicit keys here win on collision).\r\n */\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Repository interface for credits database operations\r\n *\r\n * Implementations can use any database (Firestore, PostgreSQL, etc.)\r\n * All methods should handle their own error handling and transactions\r\n */\r\nexport interface ICreditRepository\r\n extends Partial<ICreditRepositoryV2>,\r\n Partial<ICreditRepositoryCreditsV2> {\r\n // ==================== User Credits ====================\r\n\r\n /**\r\n * Get user credits balance\r\n * @param userId - User ID\r\n * @returns User credits or null if not found\r\n */\r\n getUserCredits(userId: string): Promise<PortableUserCredits | null>;\r\n\r\n /**\r\n * Initialize credits for a new user\r\n * @param userId - User ID\r\n * @param tier - Initial subscription tier\r\n * @param initialBalance - Initial credit balance\r\n * @returns Initialized user credits\r\n */\r\n initializeUserCredits(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n initialBalance: number\r\n ): Promise<PortableUserCredits>;\r\n\r\n /**\r\n * Update user credits balance\r\n * @param userId - User ID\r\n * @param updates - Partial updates to apply\r\n */\r\n updateUserCredits(userId: string, updates: CreditBalanceUpdate): Promise<void>;\r\n\r\n /**\r\n * Update user subscription tier\r\n * @param userId - User ID\r\n * @param input - Tier update data\r\n */\r\n updateUserTier(userId: string, input: TierUpdateInput): Promise<void>;\r\n\r\n // ==================== Reservations ====================\r\n\r\n /**\r\n * Create a reservation *record* — not a hold.\r\n *\r\n * Writes a row and leaves `reserved` untouched, so the row is not backed by\r\n * credits and no settlement path will move money for it: the commit /\r\n * release / expire transitions (legacy and V2 alike) refuse it with\r\n * `UNBACKED_RESERVATION`, and it refuses an `idempotencyKey` so it can never\r\n * be adopted as a V2 replay. Use it for imported or annotation-only records.\r\n *\r\n * To place a hold that can actually be committed, call `reserveCreditsAtomic`\r\n * or `reserveCreditsV2` — they raise `reserved` and write the row in one\r\n * atomic step.\r\n *\r\n * @param input - Reservation data\r\n * @returns Created reservation record\r\n */\r\n createReservation(input: CreateReservationInput): Promise<PortableReservation>;\r\n\r\n /**\r\n * Get a reservation by ID\r\n * @param userId - User ID\r\n * @param reservationId - Reservation ID\r\n * @returns Reservation or null\r\n */\r\n getReservation(userId: string, reservationId: string): Promise<PortableReservation | null>;\r\n\r\n /**\r\n * Annotate a reservation *record* with a status.\r\n *\r\n * Assigns a status and nothing else — no balance movement, no `reserved`\r\n * adjustment, no journal entry. It therefore refuses two writes that would\r\n * make the status lie about the ledger: any write to a row whose hold was\r\n * atomically placed (those statuses belong to commit/release/expire), and\r\n * any write back to `reserved` (nothing here re-places a hold). Rows written\r\n * by `createReservation` accept terminal statuses freely.\r\n *\r\n * @param userId - User ID\r\n * @param reservationId - Reservation ID\r\n * @param status - New status\r\n * @param completedAt - Completion timestamp\r\n */\r\n updateReservationStatus(\r\n userId: string,\r\n reservationId: string,\r\n status: ReservationStatus,\r\n completedAt?: Date\r\n ): Promise<void>;\r\n\r\n // ==================== Atomic Operations ====================\r\n\r\n /**\r\n * Reserve credits atomically (creates reservation + updates balance in transaction)\r\n * @param userId - User ID\r\n * @param amount - Credits to reserve\r\n * @param operationType - Operation type for tracking\r\n * @param expiresAt - Reservation expiry time\r\n * @returns Created reservation\r\n * @throws Error if insufficient credits\r\n */\r\n reserveCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n operationType: CreditOperationType,\r\n expiresAt: Date\r\n ): Promise<PortableReservation>;\r\n\r\n /**\r\n * Commit a reservation atomically (deducts credits + marks reservation committed)\r\n * @param userId - User ID\r\n * @param reservationId - Reservation ID\r\n * @throws Error if reservation not found or not in reserved state\r\n */\r\n commitReservationAtomic(userId: string, reservationId: string): Promise<void>;\r\n\r\n /**\r\n * Release a reservation atomically (releases reserved credits + marks reservation released)\r\n * @param userId - User ID\r\n * @param reservationId - Reservation ID\r\n */\r\n releaseReservationAtomic(userId: string, reservationId: string): Promise<void>;\r\n\r\n /**\r\n * Add credits atomically (creates transaction + updates balance)\r\n * @param userId - User ID\r\n * @param amount - Credits to add\r\n * @param description - Transaction description\r\n * @param paymentRef - Optional payment reference\r\n * @param options - Optional journal source / reference type / extra metadata\r\n * for the credit journal entry (e.g. revenue attribution fields). Lets\r\n * callers record a purchase's gross amount, a bonus/refund source, etc.,\r\n * instead of the default `source: \"purchase\"` + `{ paymentRef }` metadata.\r\n */\r\n addCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n description: string,\r\n paymentRef?: string,\r\n options?: AddCreditsAtomicOptions\r\n ): Promise<void>;\r\n\r\n /**\r\n * Deduct credits from a user's balance atomically.\r\n *\r\n * Splits the deduction across `balance` and `bonusCredits`, draining\r\n * `balance` (monthly, resets each cycle) first so persistent bonus\r\n * credits survive longer. Runs in a single atomic transaction so\r\n * concurrent deducts cannot drive either field negative.\r\n *\r\n * Callers are responsible for writing any audit record (journal /\r\n * transaction) — this method only moves credits.\r\n *\r\n * @param userId - User ID\r\n * @param amount - Credits to deduct (positive)\r\n * @returns Combined totals (balance + bonusCredits) before and after.\r\n * @throws If user has no credits doc or insufficient credits\r\n */\r\n deductCreditsAtomic(\r\n userId: string,\r\n amount: number\r\n ): Promise<{ previousBalance: number; newBalance: number }>;\r\n\r\n // ==================== Transactions ====================\r\n\r\n /**\r\n * Create a credit transaction *record* — no balance moves.\r\n *\r\n * Refuses a `paymentRef`: the reference is the global identity of a credit\r\n * event, and a record that carried one without crediting anyone would make a\r\n * later `addCredits` with the same reference report `replayed` and credit\r\n * nothing. Deliver referenced payments through `addCreditsAtomic` /\r\n * `addCreditsV2`, which claim the reference and move the balance atomically.\r\n *\r\n * @param input - Transaction data\r\n * @returns Created transaction\r\n */\r\n createTransaction(input: CreateTransactionInput): Promise<PortableTransaction>;\r\n\r\n /**\r\n * Get user's transaction history\r\n * @param userId - User ID\r\n * @param limit - Max results\r\n * @param offset - Skip results\r\n * @returns List of transactions\r\n */\r\n getTransactions(\r\n userId: string,\r\n limit?: number,\r\n offset?: number\r\n ): Promise<PortableTransaction[]>;\r\n\r\n // ==================== Usage Logs ====================\r\n\r\n /**\r\n * Log a usage event\r\n * @param input - Usage log data\r\n * @returns Created usage log\r\n */\r\n logUsage(input: CreateUsageLogInput): Promise<PortableUsageLog>;\r\n\r\n /**\r\n * Query usage logs\r\n * @param query - Query parameters\r\n * @returns List of usage logs\r\n */\r\n getUsageLogs(query: UsageLogQuery): Promise<PortableUsageLog[]>;\r\n\r\n /**\r\n * Get usage log count (for pagination)\r\n * @param query - Query parameters (without limit/offset)\r\n * @returns Count of matching logs\r\n */\r\n getUsageLogsCount(query: Omit<UsageLogQuery, \"limit\" | \"offset\">): Promise<number>;\r\n\r\n // ==================== Cleanup Operations ====================\r\n\r\n /**\r\n * Find and expire reservations past their expiration time\r\n * Used by cron job to clean up stale reservations\r\n * @param batchSize - Maximum number of reservations to process per batch (default: 100)\r\n * @param maxIterations - Maximum number of pagination iterations to prevent infinite loops (default: 100)\r\n * @returns Cleanup results with counts and errors\r\n */\r\n findAndExpireReservations(batchSize?: number, maxIterations?: number): Promise<{\r\n expiredCount: number;\r\n creditsReleased: number;\r\n errors: string[];\r\n }>;\r\n\r\n // ==================== Atomic Monthly Reset ====================\r\n\r\n /**\r\n * Atomically perform monthly reset if needed\r\n * Uses optimistic locking to prevent race conditions\r\n *\r\n * @param userId - User ID\r\n * @param tier - The tier the caller last read. Not trusted: it may be stale by the\r\n * time the write runs, so implementations must use the tier stored on the account.\r\n * @param expectedResetAt - The expected monthlyResetAt value (for optimistic locking)\r\n * @returns Result indicating whether reset was performed and updated credits\r\n */\r\n atomicMonthlyReset(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n expectedResetAt: Date | string\r\n ): Promise<MonthlyResetResult>;\r\n\r\n // ==================== Subscription Expiry ====================\r\n\r\n /**\r\n * Check and handle subscription expiry with grace period\r\n * Auto-downgrades expired subscriptions after grace period\r\n *\r\n * @param userId - User ID\r\n * @param gracePeriodDays - Days to allow after expiry before downgrade (default: 3)\r\n * @returns Result indicating whether downgrade occurred\r\n */\r\n checkAndHandleSubscriptionExpiry(\r\n userId: string,\r\n gracePeriodDays?: number\r\n ): Promise<SubscriptionExpiryResult>;\r\n\r\n // ==================== Journal Entries ====================\r\n\r\n /**\r\n * Create a journal entry for audit trail\r\n * @param input - Journal entry data\r\n * @returns Created journal entry\r\n */\r\n createJournalEntry(input: CreateJournalEntryInput): Promise<PortableJournalEntry>;\r\n\r\n /**\r\n * Get journal entries for a user\r\n * @param query - Query parameters\r\n * @returns List of journal entries\r\n */\r\n getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]>;\r\n\r\n /**\r\n * Get journal entry count for pagination\r\n * @param query - Query parameters (without limit/offset)\r\n * @returns Count of matching entries\r\n */\r\n getJournalEntriesCount(query: Omit<JournalEntryQuery, \"limit\" | \"offset\">): Promise<number>;\r\n}\r\n\r\n/**\r\n * Factory type for creating repository instances\r\n */\r\nexport type CreditRepositoryFactory = () => ICreditRepository;\r\n\r\n/**\r\n * Narrow a repository to one that implements the full V2 boundary.\r\n *\r\n * V2 methods are optional on {@link ICreditRepository}, so callers that need\r\n * the idempotent/race-safe path must probe rather than assume. A repository\r\n * only counts as V2 when it implements *all four* transitions — a partial\r\n * implementation would leave one path silently unsafe.\r\n */\r\nexport function supportsCreditsV2(\r\n repository: ICreditRepository\r\n): repository is ICreditRepository & ICreditRepositoryV2 {\r\n return (\r\n typeof repository.reserveCreditsV2 === \"function\" &&\r\n typeof repository.commitReservationV2 === \"function\" &&\r\n typeof repository.releaseReservationV2 === \"function\" &&\r\n typeof repository.expireReservationV2 === \"function\"\r\n );\r\n}\r\n\r\n/**\r\n * Narrow a repository to one that credits idempotently against a `paymentRef`.\r\n *\r\n * Deliberately a separate probe from {@link supportsCreditsV2}: serialising\r\n * reservations and enforcing a global payment reference are different\r\n * guarantees resting on different database objects, and an adapter may have one\r\n * without the other.\r\n */\r\nexport function supportsIdempotentCredit(\r\n repository: ICreditRepository\r\n): repository is ICreditRepository & ICreditRepositoryCreditsV2 {\r\n return typeof repository.addCreditsV2 === \"function\";\r\n}\r\n\r\n/**\r\n * Convert PortableUserCredits to client-safe format\r\n * Utility function that implementations can use\r\n */\r\nexport function toClientUserCredits(credits: PortableUserCredits): PortableUserCredits {\r\n // Already in portable format, just ensure all timestamps are ISO strings\r\n return {\r\n userId: credits.userId,\r\n balance: credits.balance,\r\n bonusCredits: credits.bonusCredits ?? 0,\r\n reserved: credits.reserved,\r\n tier: credits.tier,\r\n monthlyLimit: credits.monthlyLimit,\r\n monthlyUsed: credits.monthlyUsed,\r\n monthlyResetAt: toISOString(credits.monthlyResetAt),\r\n subscriptionExpiresAt: credits.subscriptionExpiresAt\r\n ? toISOString(credits.subscriptionExpiresAt)\r\n : null,\r\n ...(credits.expiredHoldBacking !== undefined\r\n ? { expiredHoldBacking: credits.expiredHoldBacking }\r\n : {}),\r\n ...(credits.signupGrant !== undefined ? { signupGrant: credits.signupGrant } : {}),\r\n createdAt: toISOString(credits.createdAt),\r\n updatedAt: toISOString(credits.updatedAt),\r\n };\r\n}\r\n\r\n/**\r\n * Convert any timestamp-like value to ISO string\r\n */\r\nfunction toISOString(value: unknown): string {\r\n if (!value) return new Date().toISOString();\r\n if (typeof value === \"string\") return value;\r\n if (value instanceof Date) return value.toISOString();\r\n // Handle Firestore Timestamp-like objects\r\n if (typeof value === \"object\" && \"toDate\" in value && typeof (value as { toDate: () => Date }).toDate === \"function\") {\r\n return (value as { toDate: () => Date }).toDate().toISOString();\r\n }\r\n return new Date().toISOString();\r\n}\r\n","/**\r\n * One reservation flow for both repository generations.\r\n *\r\n * Adapters that implement the V2 boundary get the idempotent, race-safe path\r\n * and own their journal entry. Adapters that do not (Firestore, custom\r\n * implementations) keep the legacy read-then-write path, with the service\r\n * writing the journal exactly as it always did — so upgrading this package\r\n * does not change their behaviour, and does not double-write anyone's ledger.\r\n */\r\n\r\nimport { getOperationLabel } from \"../config/index.js\";\r\nimport { assertValidCreditAmount, sumAmounts } from \"../core/amount.js\";\r\nimport {\r\n createUnsupportedOperationError,\r\n isInsufficientCreditsError,\r\n} from \"../core/errors.js\";\r\nimport type {\r\n AddCreditsOutcome,\r\n CommitOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n} from \"../core/outcomes.js\";\r\nimport { normalizePaymentRef } from \"../core/payment-ref.js\";\r\nimport type { ICreditRepository } from \"./types.js\";\r\nimport { supportsCreditsV2, supportsIdempotentCredit } from \"./types.js\";\r\nimport type {\r\n AddCreditsV2Input,\r\n ReservationTransitionOptions,\r\n ReserveCreditsV2Input,\r\n} from \"./v2-types.js\";\r\n\r\n/**\r\n * Reserve credits through V2 when available.\r\n *\r\n * A legacy adapter cannot honour an idempotency key — it has no unique index\r\n * to enforce it — so asking for one there is a caller error rather than a\r\n * silently ignored option.\r\n */\r\nexport async function reserveThroughRepository(\r\n repository: ICreditRepository,\r\n input: ReserveCreditsV2Input\r\n): Promise<ReserveOutcome> {\r\n const v2 = supportsCreditsV2(repository);\r\n\r\n // The capability check comes first, so a caller who asked for a guarantee\r\n // this repository cannot give always hears *that*, rather than whichever\r\n // other complaint the arguments happen to trigger. A legacy adapter has no\r\n // unique index to enforce the key, so it cannot deduplicate a replay, and\r\n // reporting `created` for the second delivery would be a lie that costs the\r\n // user real credits.\r\n if (!v2 && input.idempotencyKey !== undefined) {\r\n throw createUnsupportedOperationError(\r\n \"reserveCredits with an idempotencyKey\",\r\n \"This repository does not implement the V2 boundary, so it cannot \" +\r\n \"enforce idempotency keys. Use a V2 repository (check with \" +\r\n \"supportsCreditsV2) or omit the key and accept at-least-once holds.\"\r\n );\r\n }\r\n\r\n assertValidCreditAmount(input.amount, { userId: input.userId });\r\n\r\n if (v2) {\r\n return repository.reserveCreditsV2(input);\r\n }\r\n\r\n try {\r\n const reservation = await repository.reserveCreditsAtomic(\r\n input.userId,\r\n input.amount,\r\n input.operationType,\r\n input.expiresAt\r\n );\r\n return { outcome: \"created\", reservation };\r\n } catch (error) {\r\n // Only a genuine funds shortfall becomes the `insufficient` outcome. The\r\n // previous version asked the repository for the balance and called any\r\n // failure `insufficient` whenever that balance happened to be low — which\r\n // silently rebranded connection drops and driver faults as \"the user is\r\n // out of credits\", and hid real outages behind an upsell.\r\n if (!isInsufficientCreditsError(error)) throw error;\r\n\r\n const credits = await repository.getUserCredits(input.userId);\r\n const available = credits\r\n ? sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved)\r\n : 0;\r\n return {\r\n outcome: \"insufficient\",\r\n available,\r\n required: input.amount,\r\n shortfall: Math.max(sumAmounts(input.amount, -available), 0),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Commit through V2 when available, else the legacy read-then-write path.\r\n *\r\n * **The legacy path cannot promise a single winner.** It reads the status and\r\n * then writes, with no lock and no compare-and-set between the two, so two\r\n * concurrent commits of the same reservation can both observe `reserved` and\r\n * both proceed. The typed outcome still describes what *this* call saw, but on\r\n * a legacy adapter `committed` means \"this call did the work\", not \"this call\r\n * was the only one that did\". Callers needing exactly-once must use a\r\n * repository for which `supportsCreditsV2` returns `true`.\r\n */\r\nexport async function commitThroughRepository(\r\n repository: ICreditRepository,\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n): Promise<CommitOutcome> {\r\n if (supportsCreditsV2(repository)) {\r\n return repository.commitReservationV2(userId, reservationId, options);\r\n }\r\n\r\n const reservation = await repository.getReservation(userId, reservationId);\r\n if (!reservation) return { outcome: \"not_found\", reservationId };\r\n if (reservation.status !== \"reserved\") {\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation,\r\n terminalStatus: reservation.status,\r\n };\r\n }\r\n\r\n await repository.commitReservationAtomic(userId, reservationId);\r\n\r\n const credits = await repository.getUserCredits(userId);\r\n // The ledger balance, not the `balance` column alone: every other journal\r\n // writer — the V2 transitions, add and deduct — records\r\n // `balance + bonusCredits`, and recording less here made the same account's\r\n // audit trail disagree with itself whenever it held bonus credits.\r\n const balanceAfter = credits ? sumAmounts(credits.balance, credits.bonusCredits) : 0;\r\n const entry = await repository.createJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount: reservation.amount,\r\n balanceAfter,\r\n source: \"operation_commit\",\r\n referenceId: reservationId,\r\n referenceType: \"reservation\",\r\n description:\r\n options?.description ??\r\n `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,\r\n // Deterministic fields last: they carry the identity of the event, and a\r\n // caller who happens to send an `operationType` or an `amount` must not\r\n // relabel what actually moved.\r\n metadata: {\r\n ...options?.metadata,\r\n operationType: reservation.operationType,\r\n amount: reservation.amount,\r\n },\r\n });\r\n\r\n return {\r\n outcome: \"committed\",\r\n reservation: { ...reservation, status: \"committed\" },\r\n amount: reservation.amount,\r\n balanceAfter,\r\n journalEntryId: entry.id,\r\n };\r\n}\r\n\r\n/**\r\n * Release through V2 when available, else the legacy read-then-write path.\r\n *\r\n * Same caveat as {@link commitThroughRepository}: on a legacy adapter the\r\n * read-then-write sequence is unguarded, so `released` does not exclude a\r\n * concurrent commit having also run. Only V2 repositories serialise the two.\r\n */\r\nexport async function releaseThroughRepository(\r\n repository: ICreditRepository,\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n): Promise<ReleaseOutcome> {\r\n if (supportsCreditsV2(repository)) {\r\n return repository.releaseReservationV2(userId, reservationId, options);\r\n }\r\n\r\n const reservation = await repository.getReservation(userId, reservationId);\r\n if (!reservation) {\r\n // Legacy adapters vary on whether releasing an unknown reservation throws;\r\n // ask them anyway so that behaviour is preserved, then report not_found.\r\n await repository.releaseReservationAtomic(userId, reservationId);\r\n return { outcome: \"not_found\", reservationId };\r\n }\r\n if (reservation.status !== \"reserved\") {\r\n // The hold is already resolved, so there is nothing to hand back. Legacy\r\n // adapters disagree about whether releasing a processed reservation is an\r\n // error, and `releaseCredits` is specified to be a silent no-op here — so\r\n // the call is still made for adapters that use it to reconcile, and a\r\n // refusal is swallowed rather than propagated. The conflict is not hidden:\r\n // it is what `already_terminal` reports, and `releaseCreditsDetailed`\r\n // surfaces it to callers that ask.\r\n await repository.releaseReservationAtomic(userId, reservationId).catch(() => undefined);\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation,\r\n terminalStatus: reservation.status,\r\n };\r\n }\r\n\r\n await repository.releaseReservationAtomic(userId, reservationId);\r\n\r\n const credits = await repository.getUserCredits(userId);\r\n const entry = await repository.createJournalEntry({\r\n userId,\r\n entryType: \"credit\",\r\n // No credits changed hands — the hold just moved back into \"available\".\r\n amount: 0,\r\n balanceAfter: credits ? sumAmounts(credits.balance, credits.bonusCredits) : 0,\r\n source: \"operation_release\",\r\n referenceId: reservationId,\r\n referenceType: \"reservation\",\r\n description:\r\n options?.description ??\r\n `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,\r\n metadata: {\r\n ...options?.metadata,\r\n operationType: reservation.operationType,\r\n amount: reservation.amount,\r\n },\r\n });\r\n\r\n return {\r\n outcome: \"released\",\r\n reservation: { ...reservation, status: \"released\" },\r\n amount: reservation.amount,\r\n journalEntryId: entry.id,\r\n };\r\n}\r\n\r\n/**\r\n * Credit an account through the idempotent path when the adapter has one.\r\n *\r\n * A legacy adapter cannot resolve `created` / `replayed` / `conflict`: it has\r\n * no global unique index over `payment_ref`, so it cannot tell a re-delivered\r\n * webhook from a second, genuinely different payment that reuses a reference.\r\n * Rather than invent an outcome, this reports `created` — which is what the\r\n * legacy method actually did — and the ambiguity is documented on the return\r\n * type instead of hidden inside it.\r\n *\r\n * The reference is normalised here as well as in the adapters, so a legacy\r\n * adapter that stores the raw string does not persist a whitespace-only\r\n * reference that would then deduplicate nothing.\r\n */\r\nexport async function addCreditsThroughRepository(\r\n repository: ICreditRepository,\r\n input: AddCreditsV2Input\r\n): Promise<AddCreditsOutcome> {\r\n assertValidCreditAmount(input.amount, { userId: input.userId, operation: \"addCredits\" });\r\n const paymentRef = normalizePaymentRef(input.paymentRef);\r\n\r\n if (supportsIdempotentCredit(repository)) {\r\n return repository.addCreditsV2({ ...input, paymentRef });\r\n }\r\n\r\n await repository.addCreditsAtomic(\r\n input.userId,\r\n input.amount,\r\n input.description,\r\n paymentRef,\r\n input.options\r\n );\r\n\r\n // The legacy method returns nothing, so there is no transaction to hand back\r\n // and no way to know whether it deduplicated. Report the one thing that is\r\n // true — a call was made and did not throw — rather than fabricate a record.\r\n return paymentRef ? { outcome: \"created\", paymentRef } : { outcome: \"created\" };\r\n}\r\n","/**\r\n * Core credit operations - framework agnostic\r\n *\r\n * Contains the business logic for credit operations that can be\r\n * used by any adapter or service implementation.\r\n */\r\n\r\nimport type { ICreditRepository } from \"../repository/types.js\";\r\nimport type { PortableReservation } from \"./types.js\";\r\nimport {\r\n commitThroughRepository,\r\n releaseThroughRepository,\r\n reserveThroughRepository,\r\n} from \"../repository/flow.js\";\r\nimport {\r\n createReservationAlreadyProcessedError,\r\n createReservationNotFoundError,\r\n createInsufficientCreditsError,\r\n} from \"./errors.js\";\r\n\r\n/**\r\n * Commit a reservation with journal entry\r\n *\r\n * On a V2 repository the balance move and the journal entry happen in one\r\n * transaction and this function adds nothing of its own — the duplicate entry\r\n * the previous implementation wrote is gone. Legacy adapters keep the old\r\n * read-then-write path, journal included.\r\n *\r\n * @param repository - The credit repository\r\n * @param userId - User ID\r\n * @param reservationId - Reservation to commit\r\n */\r\nexport async function commitReservationWithJournal(\r\n repository: ICreditRepository,\r\n userId: string,\r\n reservationId: string\r\n): Promise<void> {\r\n const outcome = await commitThroughRepository(repository, userId, reservationId);\r\n if (outcome.outcome === \"not_found\") {\r\n throw createReservationNotFoundError(reservationId);\r\n }\r\n if (outcome.outcome === \"already_terminal\" && outcome.terminalStatus !== \"committed\") {\r\n throw createReservationAlreadyProcessedError(reservationId, outcome.terminalStatus);\r\n }\r\n}\r\n\r\n/**\r\n * Release a reservation with journal entry\r\n *\r\n * @param repository - The credit repository\r\n * @param userId - User ID\r\n * @param reservationId - Reservation to release\r\n */\r\nexport async function releaseReservationWithJournal(\r\n repository: ICreditRepository,\r\n userId: string,\r\n reservationId: string\r\n): Promise<void> {\r\n await releaseThroughRepository(repository, userId, reservationId);\r\n}\r\n\r\n/**\r\n * Reserve credits for an operation\r\n *\r\n * @param repository - The credit repository\r\n * @param userId - User ID\r\n * @param amount - Credits to reserve\r\n * @param operationType - Type of operation\r\n * @param expiryMs - Reservation expiry time in milliseconds\r\n * @param idempotencyKey - Optional caller key; a replay with the same\r\n * amount/operation returns the original reservation instead of a second hold\r\n * @returns The reservation\r\n */\r\nexport async function reserveCreditsForOperation(\r\n repository: ICreditRepository,\r\n userId: string,\r\n amount: number,\r\n operationType: string,\r\n expiryMs: number = 5 * 60 * 1000,\r\n idempotencyKey?: string\r\n): Promise<PortableReservation> {\r\n const expiresAt = new Date(Date.now() + expiryMs);\r\n const outcome = await reserveThroughRepository(repository, {\r\n userId,\r\n amount,\r\n operationType,\r\n expiresAt,\r\n idempotencyKey,\r\n });\r\n\r\n if (outcome.outcome === \"created\" || outcome.outcome === \"replayed\") {\r\n return outcome.reservation;\r\n }\r\n if (outcome.outcome === \"insufficient\") {\r\n throw createInsufficientCreditsError(outcome.required, outcome.available);\r\n }\r\n throw createReservationAlreadyProcessedError(\r\n outcome.existing.id,\r\n outcome.existing.status\r\n );\r\n}\r\n","import type { SubscriptionTier, TierConfig } from \"../core/types.js\";\r\nimport {\r\n getConfig,\r\n getConfigOperationCost,\r\n getConfigTierConfig,\r\n getConfigMonthlyLimit,\r\n getValidOperationTypes,\r\n isValidOperationType,\r\n getOperationLabel,\r\n} from \"./index.js\";\r\n\r\n/**\r\n * Get operation costs as a record\r\n * Returns all configured operation costs from the current config\r\n */\r\nexport function getOperationCosts(): Record<string, number> {\r\n return { ...getConfig().operationCosts };\r\n}\r\n\r\n/**\r\n * Get the credit cost for an operation type\r\n * Uses the configuration system for dynamic costs\r\n *\r\n * @param type - Operation type (must be configured in the system)\r\n * @returns Credit cost for the operation\r\n * @throws Error if the operation type is not configured\r\n */\r\nexport function getOperationCost(type: string): number {\r\n return getConfigOperationCost(type);\r\n}\r\n\r\n// Re-export validation helpers for convenience\r\nexport { getValidOperationTypes, isValidOperationType };\r\n\r\n/**\r\n * Get tier configuration\r\n * Uses the configuration system for dynamic configs\r\n *\r\n * @param tier - Subscription tier\r\n * @returns Tier configuration\r\n */\r\nexport function getTierConfig(tier: SubscriptionTier): TierConfig {\r\n return getConfigTierConfig(tier);\r\n}\r\n\r\n/**\r\n * Get monthly credit limit for a tier\r\n * Returns Infinity for unlimited tier\r\n * Uses the configuration system for dynamic limits\r\n *\r\n * @param tier - Subscription tier\r\n * @returns Monthly credit limit\r\n */\r\nexport function getMonthlyLimit(tier: SubscriptionTier): number {\r\n return getConfigMonthlyLimit(tier);\r\n}\r\n\r\n/**\r\n * Get default free credits from configuration\r\n */\r\nexport function getDefaultFreeCredits(): number {\r\n return getConfig().defaultFreeCredits;\r\n}\r\n\r\n/**\r\n * Get reservation expiry time from configuration\r\n */\r\nexport function getReservationExpiryMs(): number {\r\n return getConfig().reservationExpiryMs;\r\n}\r\n\r\n/**\r\n * Information about an operation's cost and display label\r\n */\r\nexport interface OperationCostInfo {\r\n key: string;\r\n cost: number;\r\n label: string;\r\n}\r\n\r\n/**\r\n * Get all operation costs with their display labels.\r\n * Returns a record keyed by operation type with cost + label.\r\n */\r\nexport function getOperationCostsWithLabels(): Record<string, OperationCostInfo> {\r\n const config = getConfig();\r\n const result: Record<string, OperationCostInfo> = {};\r\n for (const [key, cost] of Object.entries(config.operationCosts)) {\r\n result[key] = {\r\n key,\r\n cost,\r\n label: getOperationLabel(key),\r\n };\r\n }\r\n return result;\r\n}\r\n","/**\r\n * Inputs for the V2 reservation boundary.\r\n *\r\n * V2 is additive: every method is optional on {@link ICreditRepository} so\r\n * existing adapters keep compiling and keep working through the legacy\r\n * `*Atomic` methods. Adapters that implement V2 must satisfy the contract\r\n * documented on {@link ICreditRepositoryV2}.\r\n */\r\n\r\nimport type { CreditOperationType } from \"../core/types.js\";\r\nimport type {\r\n AddCreditsOutcome,\r\n CommitOutcome,\r\n ExpireOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n} from \"../core/outcomes.js\";\r\nimport { RESERVED_JOURNAL_KEY_PREFIX } from \"../core/idempotency.js\";\r\nimport type { AddCreditsAtomicOptions } from \"./types.js\";\r\n\r\n/**\r\n * Input for `reserveCreditsV2`.\r\n *\r\n * `userId`, `amount` and `operationType` form the immutable payload bound to\r\n * `idempotencyKey`: replaying the same key with the same payload returns the\r\n * original reservation, replaying it with a different one is a conflict.\r\n * `expiresAt` is deliberately *not* part of the payload — a retry naturally\r\n * computes a later deadline and that must not be treated as a conflict.\r\n */\r\nexport interface ReserveCreditsV2Input {\r\n userId: string;\r\n amount: number;\r\n operationType: CreditOperationType;\r\n expiresAt: Date;\r\n /**\r\n * Caller-supplied key, unique per user. Omit it to get legacy\r\n * (non-idempotent) reserve semantics — every call mints a new hold.\r\n */\r\n idempotencyKey?: string;\r\n}\r\n\r\n/** Optional journal customisation for a V2 state transition. */\r\nexport interface ReservationTransitionOptions {\r\n /** Overrides the generated journal description. */\r\n description?: string;\r\n /** Merged onto the journal entry's metadata. */\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/** Options for `expireReservationV2`. */\r\nexport interface ExpireReservationV2Options extends ReservationTransitionOptions {\r\n /**\r\n * Sweep timestamp. A reservation only expires when `expiresAt <= asOf`;\r\n * otherwise the outcome is `not_due`. Defaults to now.\r\n */\r\n asOf?: Date;\r\n}\r\n\r\n/**\r\n * The V2 surface. Every method must:\r\n *\r\n * 1. run in a single database transaction;\r\n * 2. take row locks in a consistent order — reservation first, then balance —\r\n * so concurrent transitions cannot deadlock;\r\n * 3. transition status with a compare-and-set (`WHERE status = 'reserved'`)\r\n * and report the loser truthfully instead of silently succeeding;\r\n * 4. mutate balances with column expressions, never with values read earlier\r\n * in the transaction;\r\n * 5. write at most one journal entry, inside the same transaction, keyed so a\r\n * retry cannot duplicate it;\r\n * 6. perform no callbacks and no network I/O inside the transaction.\r\n */\r\nexport interface ICreditRepositoryV2 {\r\n reserveCreditsV2(input: ReserveCreditsV2Input): Promise<ReserveOutcome>;\r\n\r\n commitReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<CommitOutcome>;\r\n\r\n releaseReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<ReleaseOutcome>;\r\n\r\n expireReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ExpireReservationV2Options\r\n ): Promise<ExpireOutcome>;\r\n}\r\n\r\n/** Input for `addCreditsV2`. */\r\nexport interface AddCreditsV2Input {\r\n userId: string;\r\n amount: number;\r\n description: string;\r\n /**\r\n * Global idempotency key for the credit event. Empty and whitespace-only\r\n * strings normalise to \"no reference\" — see `normalizePaymentRef`.\r\n */\r\n paymentRef?: string;\r\n options?: AddCreditsAtomicOptions;\r\n}\r\n\r\n/**\r\n * The idempotent credit surface, separate from {@link ICreditRepositoryV2}.\r\n *\r\n * It is its own interface with its own probe because the two capabilities are\r\n * genuinely independent: an adapter can serialise reservations without having a\r\n * unique index over `payment_ref`, and folding this into the reservation probe\r\n * would make `supportsCreditsV2` narrow third-party adapters to a method they\r\n * never wrote.\r\n *\r\n * An implementation must:\r\n *\r\n * 1. run in a single transaction;\r\n * 2. resolve a non-empty reference to exactly one of `created` / `replayed` /\r\n * `conflict`, comparing the canonical payload — not merely the reference's\r\n * presence — before reporting a replay;\r\n * 3. write nothing at all on `replayed` or `conflict`;\r\n * 4. enforce the reference *globally*, so the same reference cannot credit two\r\n * different users;\r\n * 5. fail closed if the uniqueness it relies on is not actually in place.\r\n */\r\nexport interface ICreditRepositoryCreditsV2 {\r\n addCreditsV2(input: AddCreditsV2Input): Promise<AddCreditsOutcome>;\r\n}\r\n\r\n/**\r\n * Deterministic journal key for a reservation transition.\r\n *\r\n * Derived purely from the reservation id and the transition, so a retried\r\n * commit computes the same key and collides with the first entry on the\r\n * journal's partial unique index instead of writing a second row.\r\n */\r\nexport function reservationJournalKey(\r\n reservationId: string,\r\n transition: \"commit\" | \"release\" | \"expire\"\r\n): string {\r\n return `${RESERVED_JOURNAL_KEY_PREFIX}${reservationId}:${transition}`;\r\n}\r\n","/**\r\n * Shared utilities for credit repository implementations\r\n *\r\n * These are pure functions with no server-only dependencies,\r\n * making them safe to use in any repository implementation.\r\n */\r\n\r\nimport { sumAmounts } from \"../core/amount.js\";\r\nimport type { PortableUserCredits } from \"../core/types.js\";\r\nimport type { CreditBalanceUpdate } from \"./types.js\";\r\n\r\n/**\r\n * Generate a unique ID\r\n * Uses timestamp + random string for uniqueness\r\n */\r\nexport function generateId(): string {\r\n return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\r\n}\r\n\r\n/**\r\n * Convert date-like value to Date object\r\n * Handles: Date, string, Firestore Timestamp\r\n */\r\nexport function toDate(value: Date | string | unknown): Date {\r\n if (value instanceof Date) return value;\r\n if (typeof value === \"string\") return new Date(value);\r\n // Handle Firestore Timestamp\r\n if (typeof value === \"object\" && value !== null && \"toDate\" in value) {\r\n return (value as { toDate: () => Date }).toDate();\r\n }\r\n return new Date();\r\n}\r\n\r\n/**\r\n * Get the first day of next month\r\n * Used for calculating monthly reset dates\r\n */\r\nexport function getNextMonthlyReset(from: Date = new Date()): Date {\r\n // From components: `setMonth` on the 31st overflows (31 Jan became 3 March, then\r\n // 1 March after `setDate(1)`), which skipped a whole month.\r\n return new Date(from.getFullYear(), from.getMonth() + 1, 1);\r\n}\r\n\r\n/** Result of validating a balance update. */\r\nexport interface BalanceValidationResult {\r\n valid: boolean;\r\n errors: string[];\r\n}\r\n\r\n/**\r\n * Whether an update leaves a valid account: no negative field, and `reserved` covered\r\n * by `balance + bonusCredits`, which is the floor outstanding holds put under the\r\n * balance. An increment is applied on top of the absolute value when a call names both.\r\n *\r\n * An update is a delta the caller asked for, so one that cannot be honoured is refused,\r\n * as a deduct is. Only tier writes and resets stop at the floor: they state a target.\r\n *\r\n * A negative balance increment is a charge, so it draws only on the balance above\r\n * `expiredHoldBacking`: those credits are expired and leave when their hold closes, so\r\n * charging them would hand the charge back. An absolute balance replaces the field and\r\n * the marks with it, so only an increment on the stored balance meets this limit.\r\n */\r\nexport function validateBalanceUpdate(\r\n current: Pick<PortableUserCredits, \"balance\" | \"bonusCredits\" | \"reserved\" | \"expiredHoldBacking\">,\r\n updates: CreditBalanceUpdate\r\n): BalanceValidationResult {\r\n const next = (base: number, absolute?: number, increment?: number) =>\r\n sumAmounts(absolute ?? base, increment ?? 0);\r\n const balance = next(current.balance, updates.balance, updates.balanceIncrement);\r\n const bonusCredits = next(\r\n current.bonusCredits ?? 0,\r\n updates.bonusCredits,\r\n updates.bonusCreditsIncrement\r\n );\r\n const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);\r\n\r\n const errors: string[] = [];\r\n if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);\r\n const marked = updates.balance === undefined ? (current.expiredHoldBacking ?? 0) : 0;\r\n if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {\r\n errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);\r\n }\r\n if (bonusCredits < 0) errors.push(`BonusCredits cannot be negative: ${bonusCredits}`);\r\n if (reserved < 0) errors.push(`Reserved cannot be negative: ${reserved}`);\r\n\r\n const covering = sumAmounts(balance, bonusCredits);\r\n if (reserved > covering) {\r\n errors.push(`Reserved (${reserved}) exceeds balance + bonusCredits (${covering})`);\r\n }\r\n return { valid: errors.length === 0, errors };\r\n}\r\n\r\n/** Refuse an update {@link validateBalanceUpdate} finds invalid. */\r\nexport function assertValidBalanceUpdate(\r\n current: Pick<PortableUserCredits, \"balance\" | \"bonusCredits\" | \"reserved\" | \"expiredHoldBacking\">,\r\n updates: CreditBalanceUpdate\r\n): void {\r\n const result = validateBalanceUpdate(current, updates);\r\n if (!result.valid) {\r\n throw new Error(`Invalid balance update: ${result.errors.join(\"; \")}`);\r\n }\r\n}\r\n","/**\r\n * Copies at the boundary, so nothing stored is ever handed to a caller.\r\n *\r\n * A real adapter maps a database row into a fresh object every time. This one\r\n * used to return the object it had stored, which made it behave in ways no\r\n * production adapter does: a caller's earlier read mutated under them when a\r\n * later write landed, and a caller that adjusted a returned record corrupted\r\n * the store without going through any write path at all.\r\n *\r\n * That is not a theoretical difference. The service's monthly reset compared\r\n * the post-reset balance against the balance it had read moments earlier, got\r\n * zero because both were the same object, and silently skipped the reset's\r\n * journal entry — a bug that could not reproduce against the SQL adapter.\r\n *\r\n * `metadata` is copied one level deeper because it is the one field callers\r\n * routinely pass in and then keep a reference to.\r\n */\r\n\r\ninterface WithMetadata {\r\n metadata?: Record<string, unknown> | null;\r\n}\r\n\r\n/** A detached copy of one stored record. */\r\nexport function copyRecord<T extends object>(record: T): T;\r\nexport function copyRecord<T extends object>(record: T | undefined): T | undefined;\r\nexport function copyRecord<T extends object>(record: T | undefined): T | undefined {\r\n if (!record) return undefined;\r\n const copy = { ...record };\r\n const metadata = (record as WithMetadata).metadata;\r\n if (metadata && typeof metadata === \"object\") {\r\n (copy as WithMetadata).metadata = { ...metadata };\r\n }\r\n return copy;\r\n}\r\n\r\n/** A detached copy of a list of stored records, and of the list itself. */\r\nexport function copyRecords<T extends object>(records: readonly T[]): T[] {\r\n return records.map((record) => copyRecord(record));\r\n}\r\n","/**\r\n * Promise-chaining mutex keyed by an arbitrary string.\r\n *\r\n * The in-memory repository has no database to serialise it, but its critical\r\n * sections still span `await` points — and every `await` is a scheduling point\r\n * where another caller can interleave. Without a lock, \"read balance, then\r\n * write balance\" in one method is exactly the lost update the SQL adapter uses\r\n * row locks to prevent, so contract tests would pass in memory and fail in\r\n * production. This models those row locks.\r\n */\r\nexport class KeyedMutex {\r\n private readonly tails = new Map<string, Promise<unknown>>();\r\n\r\n /**\r\n * Run `fn` with exclusive access to `key`.\r\n *\r\n * Callers queue in arrival order. The stored tail settles either way, so one\r\n * caller throwing does not wedge the queue behind it.\r\n */\r\n async run<T>(key: string, fn: () => Promise<T>): Promise<T> {\r\n const previous = this.tails.get(key) ?? Promise.resolve();\r\n const result = previous.then(fn, fn);\r\n const tail = result.then(\r\n () => undefined,\r\n () => undefined\r\n );\r\n this.tails.set(key, tail);\r\n\r\n try {\r\n return await result;\r\n } finally {\r\n // Drop the entry once we are the last waiter, so the map does not grow\r\n // one permanent entry per user id ever seen.\r\n if (this.tails.get(key) === tail) {\r\n this.tails.delete(key);\r\n }\r\n }\r\n }\r\n}\r\n","import type {\r\n PortableJournalEntry,\r\n PortableReservation,\r\n PortableTransaction,\r\n PortableUsageLog,\r\n PortableUserCredits,\r\n} from \"../../core/types.js\";\r\nimport { KeyedMutex } from \"./mutex.js\";\r\n\r\n/**\r\n * Backing state for {@link InMemoryCreditRepository}.\r\n *\r\n * Split out so the V2 transition logic can operate on the same state without\r\n * the repository class having to expose its internals publicly.\r\n */\r\nexport class MemoryStore {\r\n readonly users = new Map<string, PortableUserCredits>();\r\n readonly reservations = new Map<string, Map<string, PortableReservation>>();\r\n readonly transactions = new Map<string, PortableTransaction[]>();\r\n usageLogs: PortableUsageLog[] = [];\r\n readonly journalEntries = new Map<string, PortableJournalEntry[]>();\r\n\r\n /** Stands in for the reservations table's partial unique index. */\r\n readonly reservationKeys = new Map<string, string>();\r\n /** Stands in for the journal table's partial unique index. */\r\n readonly journalKeys = new Map<string, string>();\r\n\r\n /** Stands in for the database's per-user row locks. */\r\n readonly locks = new KeyedMutex();\r\n\r\n /**\r\n * Test-only yield point, awaited between reading state and mutating it.\r\n *\r\n * Unset in production, so the critical sections stay synchronous and this\r\n * costs nothing. Tests set it to a real macrotask yield, which forces\r\n * concurrent callers to interleave at exactly the spot a database would have\r\n * them contend — turning the lock above from decoration into the thing the\r\n * concurrency tests actually depend on.\r\n */\r\n schedulingHook?: () => Promise<void>;\r\n\r\n clear(): void {\r\n this.users.clear();\r\n this.reservations.clear();\r\n this.transactions.clear();\r\n this.usageLogs = [];\r\n this.journalEntries.clear();\r\n this.reservationKeys.clear();\r\n this.journalKeys.clear();\r\n }\r\n}\r\n\r\n/**\r\n * Composite map key standing in for a two-column unique index.\r\n *\r\n * The separator is a newline because it cannot appear in a user id and is\r\n * vanishingly unlikely in a caller-supplied idempotency key, so two distinct\r\n * pairs cannot collide into one entry.\r\n */\r\nexport function scopedKey(userId: string, key: string): string {\r\n return `${userId}\\n${key}`;\r\n}\r\n","/**\r\n * Deterministic journal writes for the in-memory V2 transitions.\r\n *\r\n * The SQL adapter gets this from a partial unique index plus an\r\n * `ON CONFLICT DO NOTHING` that re-reads and compares the losing row. There is\r\n * no index here, so the same contract has to be built by hand — and it has to\r\n * be the *same* contract, or the shared reservation tests prove nothing about\r\n * the adapter anyone actually ships.\r\n *\r\n * Two properties matter. A key that already exists must be accepted only when\r\n * the stored row describes the identical event, so a wrong row pre-seeded\r\n * through the public `createJournalEntry` cannot make a transition report a\r\n * charge the ledger does not record. And the check has to happen *before* the\r\n * balances move: this store has no transaction to roll back, so a throw after\r\n * mutation would leave the ledger half-transitioned.\r\n */\r\n\r\nimport { assertRepresentableFields, sameAmount } from \"../../core/amount.js\";\r\nimport { CreditError, CreditErrorCode } from \"../../core/errors.js\";\r\nimport type { CreditSource, PortableJournalEntry } from \"../../core/types.js\";\r\nimport { generateId } from \"../utils.js\";\r\nimport { MemoryStore, scopedKey } from \"./store.js\";\r\n\r\nexport interface JournalWriteInput {\r\n userId: string;\r\n /** The transition writing this entry, so a refusal names what was refused. */\r\n operation: string;\r\n entryType: \"debit\" | \"credit\";\r\n amount: number;\r\n balanceAfter: number;\r\n source: CreditSource;\r\n reservationId: string;\r\n description: string;\r\n metadata?: Record<string, unknown>;\r\n idempotencyKey: string;\r\n}\r\n\r\n/** Name the first field an existing entry disagrees on, or `null` if it matches. */\r\nfunction describeJournalMismatch(\r\n row: PortableJournalEntry,\r\n input: JournalWriteInput\r\n): string | null {\r\n if (row.userId !== input.userId) return \"user_id\";\r\n if (row.entryType !== input.entryType) return \"entry_type\";\r\n if (!sameAmount(row.amount, input.amount)) return \"amount\";\r\n if (!sameAmount(row.balanceAfter, input.balanceAfter)) return \"balance_after\";\r\n if (row.source !== input.source) return \"source\";\r\n if (row.referenceId !== input.reservationId) return \"reference_id\";\r\n if (row.referenceType !== \"reservation\") return \"reference_type\";\r\n\r\n // Only the fields this adapter sets deterministically. Caller-supplied extras\r\n // are free-form and may legitimately differ between a request and its retry,\r\n // so they are not part of the identity of the event.\r\n const existingMeta = (row.metadata ?? {}) as Record<string, unknown>;\r\n const expectedMeta = (input.metadata ?? {}) as Record<string, unknown>;\r\n if (existingMeta.operationType !== expectedMeta.operationType) {\r\n return \"metadata.operationType\";\r\n }\r\n const hadAmount = existingMeta.amount !== undefined;\r\n const wantsAmount = expectedMeta.amount !== undefined;\r\n if (hadAmount !== wantsAmount) return \"metadata.amount\";\r\n if (wantsAmount && !sameAmount(existingMeta.amount, expectedMeta.amount)) {\r\n return \"metadata.amount\";\r\n }\r\n return null;\r\n}\r\n\r\nfunction findEntry(\r\n store: MemoryStore,\r\n userId: string,\r\n entryId: string\r\n): PortableJournalEntry | undefined {\r\n return store.journalEntries.get(userId)?.find((entry) => entry.id === entryId);\r\n}\r\n\r\n/**\r\n * Decide what the journal write will do — without doing it.\r\n *\r\n * Returns the id of an existing, exactly matching entry to reuse, or\r\n * `undefined` when a fresh row is needed. Throws when the key is taken by a\r\n * different event, and it throws *here*, before the caller has touched a single\r\n * balance.\r\n */\r\nexport function planTransitionJournal(\r\n store: MemoryStore,\r\n input: JournalWriteInput\r\n): string | undefined {\r\n // `balanceAfter` is a derived total, and every transition journal write\r\n // passes through this preflight before the ledger moves. Validating it here\r\n // rather than at each call site means a transition added later cannot forget\r\n // it, and a total the numeric column could not hold refuses while the\r\n // balances are still untouched.\r\n assertRepresentableFields(\r\n { amount: input.amount, \"journal balanceAfter\": input.balanceAfter },\r\n { userId: input.userId, reservationId: input.reservationId, operation: input.operation }\r\n );\r\n\r\n const existingId = store.journalKeys.get(scopedKey(input.userId, input.idempotencyKey));\r\n if (!existingId) return undefined;\r\n\r\n const row = findEntry(store, input.userId, existingId);\r\n const mismatch = row\r\n ? describeJournalMismatch(row, input)\r\n : \"the key is registered but its entry is missing\";\r\n if (mismatch) {\r\n throw new CreditError(\r\n `Journal key ${input.idempotencyKey} is already used by a different entry ` +\r\n `(${mismatch}). The transition was refused rather than reported as applied.`,\r\n CreditErrorCode.DATABASE_ERROR,\r\n {\r\n idempotencyKey: input.idempotencyKey,\r\n reservationId: input.reservationId,\r\n mismatch,\r\n }\r\n );\r\n }\r\n return existingId;\r\n}\r\n\r\n/**\r\n * Perform the write the preflight authorised.\r\n *\r\n * `existingId` is whatever {@link planTransitionJournal} returned for this same\r\n * input; passing it through keeps the decision and the write from drifting\r\n * apart, which is the whole point of splitting them.\r\n */\r\nexport function commitTransitionJournal(\r\n store: MemoryStore,\r\n input: JournalWriteInput,\r\n existingId: string | undefined\r\n): string {\r\n if (existingId) return existingId;\r\n\r\n const entry: PortableJournalEntry = {\r\n id: generateId(),\r\n userId: input.userId,\r\n entryType: input.entryType,\r\n amount: input.amount,\r\n balanceAfter: input.balanceAfter,\r\n source: input.source,\r\n referenceId: input.reservationId,\r\n referenceType: \"reservation\",\r\n description: input.description,\r\n metadata: input.metadata,\r\n idempotencyKey: input.idempotencyKey,\r\n createdAt: new Date().toISOString(),\r\n };\r\n\r\n if (!store.journalEntries.has(input.userId)) {\r\n store.journalEntries.set(input.userId, []);\r\n }\r\n store.journalEntries.get(input.userId)!.push(entry);\r\n store.journalKeys.set(scopedKey(input.userId, input.idempotencyKey), entry.id);\r\n return entry.id;\r\n}\r\n","/**\r\n * V2 reservation transitions for the in-memory repository.\r\n *\r\n * These mirror the SQL adapter's guarantees rather than its mechanics: every\r\n * transition runs inside the user's lock (standing in for the row locks),\r\n * re-reads state *after* acquiring it, compares-and-sets the status, and\r\n * writes at most one journal entry keyed so a retry cannot duplicate it.\r\n *\r\n * Modelling that here is what makes the shared contract tests meaningful — a\r\n * naive implementation would pass them by accident because nothing interleaves.\r\n */\r\n\r\nimport {\r\n assertValidCreditAmount,\r\n assertValidStoredAmount,\r\n attributeExpiredBacking,\r\n expiredHoldBackingPatch,\r\n planHoldCommit,\r\n settleExpiredHoldBacking,\r\n sumAmounts,\r\n} from \"../../core/amount.js\";\r\nimport { assertValidIdempotencyKey } from \"../../core/idempotency.js\";\r\nimport {\r\n assertHoldPlaced,\r\n assertKnownReservationStatus,\r\n terminalStatusOf as narrowTerminalStatus,\r\n} from \"../../core/reservation-integrity.js\";\r\nimport { copyRecord } from \"./snapshot.js\";\r\nimport { assertRepresentableFields } from \"../../core/amount.js\";\r\nimport { CreditError, CreditErrorCode } from \"../../core/errors.js\";\r\nimport type {\r\n CommitOutcome,\r\n ExpireOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n TerminalReservationStatus,\r\n} from \"../../core/outcomes.js\";\r\nimport type {\r\n CreditSource,\r\n PortableReservation,\r\n PortableUserCredits,\r\n} from \"../../core/types.js\";\r\nimport { getOperationLabel } from \"../../config/index.js\";\r\nimport { generateId } from \"../utils.js\";\r\nimport {\r\n commitTransitionJournal,\r\n planTransitionJournal,\r\n type JournalWriteInput,\r\n} from \"./transition-journal.js\";\r\nimport type {\r\n ExpireReservationV2Options,\r\n ReservationTransitionOptions,\r\n ReserveCreditsV2Input,\r\n} from \"../v2-types.js\";\r\nimport { reservationJournalKey } from \"../v2-types.js\";\r\nimport { MemoryStore, scopedKey } from \"./store.js\";\r\n\r\n/** Yield at the read/write seam so tests can force real interleaving. */\r\nasync function yieldPoint(store: MemoryStore): Promise<void> {\r\n if (store.schedulingHook) await store.schedulingHook();\r\n}\r\n\r\n/**\r\n * Refuse a transition whose hold is no longer covered by `reserved`.\r\n *\r\n * The SQL adapter enforces this as `reserved >= amount` in the UPDATE's WHERE.\r\n * Clamping with `Math.max(reserved - amount, 0)` instead would look harmless\r\n * and quietly consume the coverage of *other* live holds, so those commits\r\n * would later fail or overdraw. Failing here keeps the damage contained and\r\n * visible, and — because the caller sees `DATABASE_ERROR` rather than\r\n * `INSUFFICIENT_CREDITS` — correctly reads as corruption, not as a user who\r\n * ran out of money.\r\n */\r\nfunction assertReservedCoversHold(\r\n credits: PortableUserCredits,\r\n userId: string,\r\n reservationId: string,\r\n amount: number\r\n): void {\r\n if (credits.reserved >= amount) return;\r\n throw new CreditError(\r\n `Credit balance invariant violated for user ${userId} while processing ` +\r\n `reservation ${reservationId}: reserved (${credits.reserved}) is less ` +\r\n `than the hold (${amount}). The transition was rolled back.`,\r\n CreditErrorCode.DATABASE_ERROR,\r\n { userId, reservationId, amount, reserved: credits.reserved }\r\n );\r\n}\r\n\r\n/**\r\n * Re-validate a persisted amount before the transition touches anything.\r\n *\r\n * Mirrors the SQL adapter: the reservation row was written once and is read\r\n * back on every transition, and a value that is negative, non-finite or off the\r\n * cent grid makes every downstream invariant meaningless — `reserved >= -10` is\r\n * trivially true, and subtracting a negative *adds* credits.\r\n */\r\nfunction assertLockedAmount(reservation: PortableReservation, transition: string): void {\r\n assertValidStoredAmount(reservation.amount, {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n transition,\r\n });\r\n}\r\n\r\n/**\r\n * Narrow a non-`reserved` status, validating instead of casting.\r\n *\r\n * The cast this replaces reported `already_terminal` — a *success* outcome —\r\n * over a row holding any string at all, so a corrupt status read to the caller\r\n * as \"this reservation is resolved\". See `core/reservation-integrity.ts`.\r\n */\r\nfunction terminalStatusOf(\r\n reservation: PortableReservation,\r\n transition: string\r\n): TerminalReservationStatus {\r\n return narrowTerminalStatus(reservation, transition);\r\n}\r\n\r\n/**\r\n * Every check a persisted reservation must pass before a transition touches it.\r\n *\r\n * Ordered ahead of the `already_terminal` and `not_due` early exits on purpose:\r\n * both are success outcomes, and reporting one over a row that is corrupt or\r\n * unbacked tells the caller the reservation is fine.\r\n */\r\nfunction assertTrustworthy(reservation: PortableReservation, transition: string): void {\r\n assertKnownReservationStatus(reservation, transition);\r\n assertLockedAmount(reservation, transition);\r\n assertHoldPlaced(reservation, transition);\r\n}\r\n\r\nfunction openHolds(store: MemoryStore, userId: string): PortableReservation[] {\r\n return [...(store.reservations.get(userId)?.values() ?? [])].filter(\r\n (reservation) => reservation.status === \"reserved\"\r\n );\r\n}\r\n\r\n/**\r\n * Set the marked credits on an account and attribute them to its open holds: the total\r\n * stored is always the sum of the shares stored. An account that never had any reads\r\n * no holds and gains no fields.\r\n */\r\nexport function writeExpiredHoldBacking(\r\n store: MemoryStore,\r\n credits: PortableUserCredits,\r\n next: number\r\n): void {\r\n const stored = credits.expiredHoldBacking;\r\n if (next <= 0 && !stored) return;\r\n const holds = openHolds(store, credits.userId);\r\n const shares = attributeExpiredBacking(holds, next);\r\n for (const hold of holds) {\r\n const share = shares.get(hold.id) ?? 0;\r\n if (share !== (hold.expiredBacking ?? 0)) hold.expiredBacking = share;\r\n }\r\n Object.assign(credits, expiredHoldBackingPatch(sumAmounts(...shares.values()), stored));\r\n}\r\n\r\nfunction getReservation(\r\n store: MemoryStore,\r\n userId: string,\r\n reservationId: string\r\n): PortableReservation | undefined {\r\n return store.reservations.get(userId)?.get(reservationId);\r\n}\r\n\r\n/**\r\n * The *live* stored record, deliberately — the transitions in this module write\r\n * through it. Everything that crosses the repository's public surface goes\r\n * through `copyRecord` instead; see `./snapshot.js`.\r\n */\r\nfunction requireUser(store: MemoryStore, userId: string): PortableUserCredits {\r\n const credits = store.users.get(userId);\r\n if (!credits) {\r\n throw new CreditError(\r\n `User credits not found for user ${userId}`,\r\n CreditErrorCode.USER_NOT_FOUND,\r\n { userId }\r\n );\r\n }\r\n return credits;\r\n}\r\n\r\nexport async function reserveCreditsV2(\r\n store: MemoryStore,\r\n input: ReserveCreditsV2Input\r\n): Promise<ReserveOutcome> {\r\n assertValidCreditAmount(input.amount, { userId: input.userId });\r\n assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });\r\n\r\n return store.locks.run(input.userId, async () => {\r\n if (input.idempotencyKey) {\r\n const existingId = store.reservationKeys.get(\r\n scopedKey(input.userId, input.idempotencyKey)\r\n );\r\n if (existingId) {\r\n const existing = getReservation(store, input.userId, existingId);\r\n if (existing) {\r\n // Before the payload comparison, and before any outcome is chosen: a\r\n // row is only adoptable as a replay if *its own* reserve placed the\r\n // hold. Adopting an unbacked row would report `replayed` over credits\r\n // nothing ever held. Mirrors the SQL adapter.\r\n assertHoldPlaced(existing, \"reserve replay\");\r\n // `expiresAt` is excluded on purpose: a retry legitimately computes a\r\n // later deadline and that must not read as a conflict.\r\n const samePayload =\r\n existing.amount === input.amount &&\r\n existing.operationType === input.operationType;\r\n return samePayload\r\n ? { outcome: \"replayed\", reservation: copyRecord(existing) }\r\n : {\r\n outcome: \"idempotency_conflict\",\r\n idempotencyKey: input.idempotencyKey,\r\n existing: copyRecord(existing),\r\n };\r\n }\r\n }\r\n }\r\n\r\n const credits = requireUser(store, input.userId);\r\n const available = sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved);\r\n await yieldPoint(store);\r\n if (available < input.amount) {\r\n return {\r\n outcome: \"insufficient\",\r\n available,\r\n required: input.amount,\r\n shortfall: sumAmounts(input.amount, -available),\r\n };\r\n }\r\n\r\n const now = new Date().toISOString();\r\n const reservation: PortableReservation = {\r\n id: generateId(),\r\n userId: input.userId,\r\n amount: input.amount,\r\n operationType: input.operationType,\r\n status: \"reserved\",\r\n createdAt: now,\r\n expiresAt: input.expiresAt.toISOString(),\r\n idempotencyKey: input.idempotencyKey,\r\n // The hold-origin fact. Written here and nowhere else, in the same\r\n // critical section that raises `reserved` below, so the row and the hold\r\n // become visible together. `createReservation` cannot set it.\r\n holdPlacedAt: now,\r\n };\r\n\r\n if (!store.reservations.has(input.userId)) {\r\n store.reservations.set(input.userId, new Map());\r\n }\r\n // The derived hold is checked before the reservation row exists, not after:\r\n // a refusal here must leave no reservation, no key registration and no\r\n // counter change behind.\r\n const nextReserved = sumAmounts(credits.reserved, input.amount);\r\n assertRepresentableFields(\r\n { reserved: nextReserved },\r\n { userId: input.userId, operation: \"reserveCredits\" }\r\n );\r\n\r\n store.reservations.get(input.userId)!.set(reservation.id, reservation);\r\n if (input.idempotencyKey) {\r\n store.reservationKeys.set(\r\n scopedKey(input.userId, input.idempotencyKey),\r\n reservation.id\r\n );\r\n }\r\n\r\n credits.reserved = nextReserved;\r\n credits.updatedAt = now;\r\n\r\n return { outcome: \"created\", reservation: copyRecord(reservation) };\r\n });\r\n}\r\n\r\nexport async function commitReservationV2(\r\n store: MemoryStore,\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n): Promise<CommitOutcome> {\r\n return store.locks.run(userId, async () => {\r\n const reservation = getReservation(store, userId, reservationId);\r\n if (!reservation) return { outcome: \"not_found\", reservationId };\r\n // Ahead of the early exits: `already_terminal` and `not_due` are success\r\n // outcomes, and reporting one over a row that is unusable tells the caller\r\n // the reservation is fine. Mirrors the SQL adapter.\r\n assertTrustworthy(reservation, \"commit\");\r\n\r\n if (reservation.status !== \"reserved\") {\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation: copyRecord(reservation),\r\n terminalStatus: terminalStatusOf(reservation, \"commit\"),\r\n };\r\n }\r\n\r\n const credits = requireUser(store, userId);\r\n const amount = reservation.amount;\r\n await yieldPoint(store);\r\n assertReservedCoversHold(credits, userId, reservationId, amount);\r\n const paid = planHoldCommit(\r\n credits.balance,\r\n credits.bonusCredits,\r\n credits.expiredHoldBacking ?? 0,\r\n reservation.expiredBacking ?? 0,\r\n amount\r\n );\r\n if (!paid.covered) {\r\n throw new CreditError(\r\n `Insufficient credits to commit reservation ${reservationId}`,\r\n CreditErrorCode.INSUFFICIENT_CREDITS,\r\n { userId, reservationId, required: amount }\r\n );\r\n }\r\n\r\n // Project the post-transition balances before applying them. The store has\r\n // no transaction to roll back, so everything that can refuse — the journal\r\n // key check below — has to refuse while the ledger is still untouched. The\r\n // projected values are then assigned verbatim, so the number the journal\r\n // records and the number the balance ends up holding cannot drift apart.\r\n const nextBalance = paid.balance;\r\n const nextBonusCredits = paid.bonusCredits;\r\n const balanceAfter = sumAmounts(nextBalance, nextBonusCredits);\r\n\r\n const journal: JournalWriteInput = {\r\n userId,\r\n operation: \"commitReservation\",\r\n entryType: \"debit\",\r\n amount,\r\n balanceAfter,\r\n source: \"operation_commit\",\r\n reservationId,\r\n description:\r\n options?.description ??\r\n `Committed ${amount} credits for ${getOperationLabel(reservation.operationType)}`,\r\n // Caller metadata first: the deterministic fields carry the identity of\r\n // the event that the collision check compares on.\r\n metadata: { ...options?.metadata, operationType: reservation.operationType, amount },\r\n idempotencyKey: reservationJournalKey(reservationId, \"commit\"),\r\n };\r\n const nextReserved = sumAmounts(credits.reserved, -amount);\r\n const nextMonthlyUsed = sumAmounts(credits.monthlyUsed, amount);\r\n assertRepresentableFields(\r\n {\r\n balance: nextBalance,\r\n bonusCredits: nextBonusCredits,\r\n reserved: nextReserved,\r\n monthlyUsed: nextMonthlyUsed,\r\n },\r\n { userId, operation: \"commitReservation\" }\r\n );\r\n\r\n const existingEntryId = planTransitionJournal(store, journal);\r\n\r\n reservation.status = \"committed\";\r\n reservation.completedAt = new Date().toISOString();\r\n\r\n // Drain `balance` before `bonusCredits`, matching the SQL adapter.\r\n credits.balance = nextBalance;\r\n credits.bonusCredits = nextBonusCredits;\r\n credits.reserved = nextReserved;\r\n credits.monthlyUsed = nextMonthlyUsed;\r\n Object.assign(\r\n credits,\r\n expiredHoldBackingPatch(paid.expiredHoldBacking, credits.expiredHoldBacking)\r\n );\r\n credits.updatedAt = new Date().toISOString();\r\n\r\n const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);\r\n\r\n return {\r\n outcome: \"committed\",\r\n reservation: copyRecord(reservation),\r\n amount,\r\n balanceAfter,\r\n journalEntryId,\r\n };\r\n });\r\n}\r\n\r\nexport async function releaseReservationV2(\r\n store: MemoryStore,\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n): Promise<ReleaseOutcome> {\r\n return store.locks.run(userId, async () => {\r\n const reservation = getReservation(store, userId, reservationId);\r\n if (!reservation) return { outcome: \"not_found\", reservationId };\r\n // Ahead of the early exits: `already_terminal` and `not_due` are success\r\n // outcomes, and reporting one over a row that is unusable tells the caller\r\n // the reservation is fine. Mirrors the SQL adapter.\r\n assertTrustworthy(reservation, \"release\");\r\n\r\n if (reservation.status !== \"reserved\") {\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation: copyRecord(reservation),\r\n terminalStatus: terminalStatusOf(reservation, \"release\"),\r\n };\r\n }\r\n\r\n await yieldPoint(store);\r\n const journalEntryId = finishUnspent(store, reservation, {\r\n status: \"released\",\r\n source: \"operation_release\",\r\n transition: \"release\",\r\n verb: \"Released\",\r\n options,\r\n });\r\n\r\n return {\r\n outcome: \"released\",\r\n reservation: copyRecord(reservation),\r\n amount: reservation.amount,\r\n journalEntryId,\r\n };\r\n });\r\n}\r\n\r\nexport async function expireReservationV2(\r\n store: MemoryStore,\r\n userId: string,\r\n reservationId: string,\r\n options?: ExpireReservationV2Options\r\n): Promise<ExpireOutcome> {\r\n const asOf = options?.asOf ?? new Date();\r\n return store.locks.run(userId, async () => {\r\n const reservation = getReservation(store, userId, reservationId);\r\n if (!reservation) return { outcome: \"not_found\", reservationId };\r\n // Ahead of the early exits: `already_terminal` and `not_due` are success\r\n // outcomes, and reporting one over a row that is unusable tells the caller\r\n // the reservation is fine. Mirrors the SQL adapter.\r\n assertTrustworthy(reservation, \"expire\");\r\n\r\n if (reservation.status !== \"reserved\") {\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation: copyRecord(reservation),\r\n terminalStatus: terminalStatusOf(reservation, \"expire\"),\r\n };\r\n }\r\n if (new Date(reservation.expiresAt).getTime() > asOf.getTime()) {\r\n return { outcome: \"not_due\", reservation: copyRecord(reservation) };\r\n }\r\n\r\n await yieldPoint(store);\r\n const journalEntryId = finishUnspent(store, reservation, {\r\n status: \"expired\",\r\n source: \"reservation_expired\",\r\n transition: \"expire\",\r\n verb: \"Expired\",\r\n options,\r\n });\r\n\r\n return {\r\n outcome: \"expired\",\r\n reservation: copyRecord(reservation),\r\n amount: reservation.amount,\r\n journalEntryId,\r\n };\r\n });\r\n}\r\n\r\ninterface UnspentTransition {\r\n status: \"released\" | \"expired\";\r\n source: CreditSource;\r\n transition: \"release\" | \"expire\";\r\n verb: string;\r\n options?: ReservationTransitionOptions;\r\n}\r\n\r\n/**\r\n * Hand a hold back unspent and journal it.\r\n *\r\n * `amount` is 0 because no credits changed hands — the hold only moves back\r\n * into \"available\". The hold size stays in metadata, matching the SQL adapter.\r\n */\r\nfunction finishUnspent(\r\n store: MemoryStore,\r\n reservation: PortableReservation,\r\n transition: UnspentTransition\r\n): string {\r\n const credits = requireUser(store, reservation.userId);\r\n assertReservedCoversHold(\r\n credits,\r\n reservation.userId,\r\n reservation.id,\r\n reservation.amount\r\n );\r\n\r\n // Handing a hold back removes only this hold's share of the credits kept solely to\r\n // back holds. The journal check still runs before the mutation, so a key collision\r\n // leaves `reserved`, the balance, the status and `completedAt` untouched.\r\n const backing = settleExpiredHoldBacking(\r\n credits.balance,\r\n credits.expiredHoldBacking ?? 0,\r\n reservation.expiredBacking ?? 0\r\n );\r\n const nextBalance = sumAmounts(credits.balance, -backing.used);\r\n const journal: JournalWriteInput = {\r\n userId: reservation.userId,\r\n operation: `${transition.transition}Reservation`,\r\n entryType: \"credit\",\r\n amount: 0,\r\n balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),\r\n source: transition.source,\r\n reservationId: reservation.id,\r\n description:\r\n transition.options?.description ??\r\n `${transition.verb} ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,\r\n metadata: {\r\n ...transition.options?.metadata,\r\n operationType: reservation.operationType,\r\n amount: reservation.amount,\r\n ...(backing.used > 0 ? { expiredCreditsRemoved: backing.used } : {}),\r\n },\r\n idempotencyKey: reservationJournalKey(reservation.id, transition.transition),\r\n };\r\n const nextReserved = sumAmounts(credits.reserved, -reservation.amount);\r\n assertRepresentableFields(\r\n { reserved: nextReserved },\r\n { userId: reservation.userId, operation: `${transition.transition}Reservation` }\r\n );\r\n\r\n const existingEntryId = planTransitionJournal(store, journal);\r\n\r\n reservation.status = transition.status;\r\n reservation.completedAt = new Date().toISOString();\r\n credits.reserved = nextReserved;\r\n credits.balance = nextBalance;\r\n Object.assign(credits, expiredHoldBackingPatch(backing.remaining, credits.expiredHoldBacking));\r\n credits.updatedAt = new Date().toISOString();\r\n\r\n return commitTransitionJournal(store, journal, existingEntryId);\r\n}\r\n","/**\n * In-Memory Credit Repository Implementation\n *\n * A database-agnostic implementation of ICreditRepository for testing and prototyping.\n * All data is stored in memory and lost when the process restarts.\n *\n * Usage:\n * - Unit tests without database dependency\n * - Local development and prototyping\n * - Reference implementation for custom repositories\n */\n\nimport type {\n PortableUserCredits,\n PortableReservation,\n PortableTransaction,\n PortableJournalEntry,\n PortableUsageLog,\n SubscriptionTier,\n ReservationStatus,\n MonthlyResetResult,\n SubscriptionExpiryResult,\n} from \"../../core/types.js\";\nimport type {\n ICreditRepository,\n CreateReservationInput,\n CreateTransactionInput,\n CreateUsageLogInput,\n CreateJournalEntryInput,\n UsageLogQuery,\n JournalEntryQuery,\n CreditBalanceUpdate,\n TierUpdateInput,\n AddCreditsAtomicOptions,\n CreditPolicyOptions,\n} from \"../types.js\";\nimport type {\n AddCreditsOutcome,\n CommitOutcome,\n ExpireOutcome,\n ReleaseOutcome,\n ReserveOutcome,\n} from \"../../core/outcomes.js\";\nimport type {\n AddCreditsV2Input,\n ExpireReservationV2Options,\n ReservationTransitionOptions,\n ReserveCreditsV2Input,\n} from \"../v2-types.js\";\nimport {\n assertUnreferencedDirectTransaction,\n createPaymentRefConflictError,\n describePaymentMismatch,\n normalizePaymentRef,\n type PaymentEventPayload,\n} from \"../../core/payment-ref.js\";\nimport {\n createInsufficientCreditsError,\n createReservationAlreadyProcessedError,\n createReservationNotFoundError,\n} from \"../../core/errors.js\";\nimport { assertValidBalanceUpdate, generateId, toDate, getNextMonthlyReset } from \"../utils.js\";\nimport { copyRecord, copyRecords } from \"./snapshot.js\";\nimport {\n assertRepresentableAmount,\n assertRepresentableFields,\n assertRepresentableTierAmount,\n assertValidBalanceTarget,\n assertValidTierBalanceInput,\n planBalanceCap,\n planBalanceTarget,\n spendableBalance,\n storedMonthlyLimit,\n assertValidCreditAmount,\n sumAmounts,\n} from \"../../core/amount.js\";\nimport {\n assertPublicJournalKey,\n assertValidIdempotencyKey,\n} from \"../../core/idempotency.js\";\nimport {\n assertDirectStatusWriteAllowed,\n assertUnkeyedDirectReservation,\n} from \"../../core/reservation-integrity.js\";\nimport { CreditError, CreditErrorCode } from \"../../core/errors.js\";\nimport { MemoryStore, scopedKey } from \"./store.js\";\nimport {\n commitReservationV2,\n expireReservationV2,\n releaseReservationV2,\n reserveCreditsV2,\n writeExpiredHoldBacking,\n} from \"./v2.js\";\nimport {\n getConfigMonthlyLimit,\n getConfigTierConfig,\n getDefaultTier,\n isFreeTier,\n monthlyResetBalance,\n} from \"../../config/index.js\";\n\n/**\n * In-Memory implementation of ICreditRepository\n *\n * Implements all repository methods using Map-based storage.\n * Useful for testing and as a reference implementation.\n */\n/** Apply an increment, or report `undefined` when the caller sent none. */\n/** Accept either half of the `Date | string` timestamp inputs the API takes. */\nfunction isoOf(value: Date | string): string {\n return value instanceof Date ? value.toISOString() : value;\n}\n\nexport class InMemoryCreditRepository implements ICreditRepository {\n private readonly store = new MemoryStore();\n private readonly options: CreditPolicyOptions;\n\n constructor(options: CreditPolicyOptions = {}) {\n this.options = options;\n }\n\n // ==================== User Credits ====================\n\n /**\n * A copy, never the live record.\n *\n * Handing out the stored object made this adapter behave unlike any real\n * one: a caller holding a \"before\" snapshot saw it mutate under them. The\n * service's monthly reset compared `resetResult.credits.balance` against the\n * balance it had read moments earlier, got zero because both were the same\n * object, and silently skipped the reset's journal entry — while the SQL\n * adapter, which maps each row into a fresh object, wrote it.\n */\n async getUserCredits(userId: string): Promise<PortableUserCredits | null> {\n const credits = this.store.users.get(userId);\n return copyRecord(credits) ?? null;\n }\n\n async initializeUserCredits(\n userId: string,\n tier: SubscriptionTier,\n initialBalance: number\n ): Promise<PortableUserCredits> {\n // Create only: a late second call must not wipe an account already in use.\n const existing = this.store.users.get(userId);\n return copyRecord(existing ?? this.seedUser(userId, tier, initialBalance));\n }\n\n /**\n * The synchronous body of `initializeUserCredits`.\n *\n * Kept `await`-free on purpose: `addCreditsV2` seeds a missing user *inside*\n * its critical section, and an `await` between the payment-reference check\n * and the transaction write opens a gap another caller can run through — two\n * simultaneous first deliveries of the same reference would then both credit.\n */\n private seedUser(\n userId: string,\n tier: SubscriptionTier,\n initialBalance: number\n ): PortableUserCredits {\n assertRepresentableAmount(initialBalance, \"initialBalance\", { userId });\n // Derived from tier configuration rather than supplied by the caller, and\n // therefore just as capable of being unrepresentable: a tier configured\n // with `monthlyCredits: 1.005` writes a number the column cannot hold.\n const configured = getConfigMonthlyLimit(tier);\n assertRepresentableTierAmount(configured, \"monthlyLimit\", { userId, tier });\n // Stored through the shared contract so both adapters hold the same number\n // for an unlimited tier. See `storedMonthlyLimit`.\n const monthlyLimit = storedMonthlyLimit(configured);\n\n const now = new Date().toISOString();\n const credits: PortableUserCredits = {\n userId,\n balance: initialBalance,\n bonusCredits: 0,\n reserved: 0,\n tier,\n monthlyLimit,\n monthlyUsed: 0,\n monthlyResetAt: getNextMonthlyReset().toISOString(),\n subscriptionExpiresAt: null,\n createdAt: now,\n updatedAt: now,\n };\n this.store.users.set(userId, credits);\n return credits;\n }\n\n async updateUserCredits(userId: string, updates: CreditBalanceUpdate): Promise<void> {\n assertRepresentableFields(\n {\n balance: updates.balance,\n bonusCredits: updates.bonusCredits,\n reserved: updates.reserved,\n monthlyLimit: updates.monthlyLimit,\n monthlyUsed: updates.monthlyUsed,\n balanceIncrement: updates.balanceIncrement,\n bonusCreditsIncrement: updates.bonusCreditsIncrement,\n reservedIncrement: updates.reservedIncrement,\n monthlyUsedIncrement: updates.monthlyUsedIncrement,\n },\n { userId, operation: \"updateUserCredits\" }\n );\n\n const credits = this.store.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n assertValidBalanceUpdate(credits, updates);\n\n // The entire record is projected onto a candidate before anything is\n // written back. Assigning the absolute fields first and validating the\n // increments afterwards would leave a refused update half-applied — an\n // explicit `monthlyUsed: 5` would stick even though the derived `balance`\n // it was sent with was rejected — and this store has no transaction to\n // undo it. Every numeric field is checked on the candidate, so the record\n // either takes the whole update or none of it.\n const next: PortableUserCredits = { ...credits };\n\n if (updates.balance !== undefined) {\n next.balance = updates.balance;\n }\n if (updates.bonusCredits !== undefined) next.bonusCredits = updates.bonusCredits;\n if (updates.reserved !== undefined) next.reserved = updates.reserved;\n if (updates.tier !== undefined) next.tier = updates.tier;\n if (updates.monthlyLimit !== undefined) next.monthlyLimit = updates.monthlyLimit;\n if (updates.monthlyUsed !== undefined) next.monthlyUsed = updates.monthlyUsed;\n if (updates.monthlyResetAt !== undefined) {\n next.monthlyResetAt = isoOf(updates.monthlyResetAt);\n }\n if (updates.subscriptionExpiresAt !== undefined) {\n next.subscriptionExpiresAt =\n updates.subscriptionExpiresAt === null\n ? null\n : isoOf(updates.subscriptionExpiresAt);\n }\n\n // Increments are checked as *results*, not just as arguments: adding a\n // legal 0.01 to a legal 9999999999.99 produces a number the column cannot\n // hold. They apply on top of whatever the absolute fields just set; the SQL\n // adapter is built to match, and the SQL package's\n // `__tests__/integration/adapter-parity.test.ts` pins the two together.\n //\n // `sumAmounts`, not `+`: a float sum can land off the cent grid the ledger\n // validates against, and rejecting a legal increment as `INVALID_AMOUNT` is\n // a worse failure than the overflow this check exists for.\n if (updates.balanceIncrement !== undefined) {\n next.balance = sumAmounts(next.balance, updates.balanceIncrement);\n }\n if (updates.bonusCreditsIncrement !== undefined) {\n next.bonusCredits = sumAmounts(next.bonusCredits, updates.bonusCreditsIncrement);\n }\n if (updates.reservedIncrement !== undefined) {\n next.reserved = sumAmounts(next.reserved, updates.reservedIncrement);\n }\n if (updates.monthlyUsedIncrement !== undefined) {\n next.monthlyUsed = sumAmounts(next.monthlyUsed, updates.monthlyUsedIncrement);\n }\n\n assertRepresentableFields(\n {\n balance: next.balance,\n bonusCredits: next.bonusCredits,\n reserved: next.reserved,\n monthlyUsed: next.monthlyUsed,\n },\n { userId, operation: \"updateUserCredits\" }\n );\n assertRepresentableTierAmount(next.monthlyLimit, \"monthlyLimit\", {\n userId,\n operation: \"updateUserCredits\",\n });\n\n // An absolute balance replaces what the marked credits were part of. An increment\n // cannot reach them: the validator refused it.\n if ((credits.expiredHoldBacking ?? 0) > 0 && updates.balance !== undefined) {\n writeExpiredHoldBacking(this.store, next, 0);\n }\n\n next.updatedAt = new Date().toISOString();\n this.store.users.set(userId, next);\n }\n\n async updateUserTier(userId: string, input: TierUpdateInput): Promise<void> {\n assertRepresentableFields(\n { monthlyLimit: input.monthlyLimit, balance: input.balance, monthlyUsed: input.monthlyUsed },\n { userId, operation: \"updateUserTier\" }\n );\n assertValidTierBalanceInput(input, { userId, operation: \"updateUserTier\" });\n\n const credits = this.store.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n // Decided from the tier the account holds now, before this write replaces it.\n if (input.balanceCap !== undefined && !isFreeTier(credits.tier)) {\n const capped = planBalanceCap(\n credits.balance,\n input.balanceCap,\n credits.reserved,\n credits.bonusCredits,\n credits.expiredHoldBacking\n );\n credits.balance = capped.balance;\n writeExpiredHoldBacking(this.store, credits, capped.expiredHoldBacking);\n }\n\n credits.tier = input.tier;\n credits.monthlyLimit = input.monthlyLimit;\n // A tier write may lower the balance, but never below what still backs the\n // outstanding holds — see `backedBalanceFloor`. Same clamp as the SQL\n // adapter, so a downgrade cannot strand a live reservation.\n if (input.balance !== undefined) {\n const planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);\n credits.balance = planned.balance;\n writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);\n }\n if (input.monthlyUsed !== undefined) credits.monthlyUsed = input.monthlyUsed;\n if (input.subscriptionExpiresAt !== undefined) {\n if (input.subscriptionExpiresAt === null) {\n credits.subscriptionExpiresAt = null;\n } else {\n credits.subscriptionExpiresAt = input.subscriptionExpiresAt instanceof Date\n ? input.subscriptionExpiresAt.toISOString()\n : input.subscriptionExpiresAt;\n }\n }\n credits.updatedAt = new Date().toISOString();\n\n this.store.users.set(userId, credits);\n }\n\n // ==================== Reservations ====================\n\n async createReservation(input: CreateReservationInput): Promise<PortableReservation> {\n // Same refusal as the SQL adapter: a row written with an amount the ledger\n // cannot honour has no repair path once it exists, and every transition\n // that later locks it would have to refuse anyway.\n assertValidCreditAmount(input.amount, { userId: input.userId, operation: \"createReservation\" });\n assertUnkeyedDirectReservation(input);\n\n const now = new Date().toISOString();\n const reservation: PortableReservation = {\n id: generateId(),\n userId: input.userId,\n amount: input.amount,\n operationType: input.operationType,\n status: \"reserved\",\n createdAt: now,\n expiresAt: input.expiresAt.toISOString(),\n };\n\n if (!this.store.reservations.has(input.userId)) {\n this.store.reservations.set(input.userId, new Map());\n }\n this.store.reservations.get(input.userId)!.set(reservation.id, reservation);\n\n return copyRecord(reservation);\n }\n\n async getReservation(\n userId: string,\n reservationId: string\n ): Promise<PortableReservation | null> {\n const userReservations = this.store.reservations.get(userId);\n if (!userReservations) return null;\n return copyRecord(userReservations.get(reservationId)) ?? null;\n }\n\n async updateReservationStatus(\n userId: string,\n reservationId: string,\n status: ReservationStatus,\n completedAt?: Date\n ): Promise<void> {\n const userReservations = this.store.reservations.get(userId);\n if (!userReservations) {\n throw new Error(`No reservations found for user ${userId}`);\n }\n\n const reservation = userReservations.get(reservationId);\n if (!reservation) {\n throw new Error(`Reservation ${reservationId} not found`);\n }\n\n // A backed hold's status is owned by the transition that settles it, and no\n // row may be reopened: this method assigns a status and nothing else, so on\n // a V2 row it would change the status without the ledger movement the\n // status stands for. See `assertDirectStatusWriteAllowed`.\n assertDirectStatusWriteAllowed(reservation, status);\n\n reservation.status = status;\n if (completedAt) reservation.completedAt = completedAt.toISOString();\n\n userReservations.set(reservationId, reservation);\n }\n\n // ==================== Atomic Operations ====================\n\n // ==================== V2 boundary ====================\n //\n // As in the Drizzle adapter, V2 is the real implementation and the legacy\n // `*Atomic` methods are adapters over it, so both APIs share one set of\n // guarantees.\n\n async reserveCreditsV2(input: ReserveCreditsV2Input): Promise<ReserveOutcome> {\n return reserveCreditsV2(this.store, input);\n }\n\n async commitReservationV2(\n userId: string,\n reservationId: string,\n options?: ReservationTransitionOptions\n ): Promise<CommitOutcome> {\n return commitReservationV2(this.store, userId, reservationId, options);\n }\n\n async releaseReservationV2(\n userId: string,\n reservationId: string,\n options?: ReservationTransitionOptions\n ): Promise<ReleaseOutcome> {\n return releaseReservationV2(this.store, userId, reservationId, options);\n }\n\n async expireReservationV2(\n userId: string,\n reservationId: string,\n options?: ExpireReservationV2Options\n ): Promise<ExpireOutcome> {\n return expireReservationV2(this.store, userId, reservationId, options);\n }\n\n // ==================== Legacy atomic operations ====================\n\n async reserveCreditsAtomic(\n userId: string,\n amount: number,\n operationType: string,\n expiresAt: Date\n ): Promise<PortableReservation> {\n const outcome = await this.reserveCreditsV2({ userId, amount, operationType, expiresAt });\n if (outcome.outcome === \"created\" || outcome.outcome === \"replayed\") {\n return outcome.reservation;\n }\n if (outcome.outcome === \"insufficient\") {\n throw createInsufficientCreditsError(outcome.required, outcome.available);\n }\n throw createReservationAlreadyProcessedError(outcome.existing.id, outcome.existing.status);\n }\n\n async commitReservationAtomic(userId: string, reservationId: string): Promise<void> {\n const outcome = await this.commitReservationV2(userId, reservationId);\n if (outcome.outcome === \"committed\") return;\n if (outcome.outcome === \"not_found\") throw createReservationNotFoundError(reservationId);\n // Re-delivering a commit for an already-committed reservation stays a no-op.\n if (outcome.terminalStatus === \"committed\") return;\n throw createReservationAlreadyProcessedError(reservationId, outcome.terminalStatus);\n }\n\n async releaseReservationAtomic(userId: string, reservationId: string): Promise<void> {\n const outcome = await this.releaseReservationV2(userId, reservationId);\n if (outcome.outcome === \"not_found\") throw createReservationNotFoundError(reservationId);\n // Releasing an already-terminal reservation is a no-op, as before.\n }\n\n /**\n * The stored transaction carrying this reference, in *any* user's ledger.\n *\n * Deliberately not scoped to one user. The SQL adapter's unique index covers\n * `payment_ref` alone, so searching only the crediting user's own history made\n * the same reference credit two different accounts here while the SQL adapter\n * treated the second delivery as a duplicate. See `core/payment-ref.ts`.\n */\n private findByPaymentRef(paymentRef: string): PortableTransaction | undefined {\n for (const recorded of this.store.transactions.values()) {\n const hit = recorded.find((transaction) => transaction.paymentRef === paymentRef);\n if (hit) return hit;\n }\n return undefined;\n }\n\n /** The journal entry written alongside a stored purchase, if there is one. */\n private journalForTransaction(\n transaction: PortableTransaction\n ): PortableJournalEntry | undefined {\n return this.store.journalEntries\n .get(transaction.userId)\n ?.find((entry) => entry.referenceId === transaction.id);\n }\n\n /**\n * Credit an account, resolving `paymentRef` to created / replayed / conflict.\n *\n * The reference is checked and the transaction is recorded with no `await`\n * between them. `createTransaction` is `async` but its body is synchronous, so\n * under JavaScript's run-to-completion semantics no other caller can observe\n * the gap — which is what stands in for the SQL adapter's unique index.\n *\n * A `replayed` or `conflict` outcome writes nothing at all: the check runs\n * before the balance moves, not after.\n */\n async addCreditsV2(input: AddCreditsV2Input): Promise<AddCreditsOutcome> {\n const { userId, amount, description, options } = input;\n assertValidCreditAmount(amount, { userId, operation: \"addCredits\" });\n\n // Empty and whitespace-only strings are not references. Normalised in one\n // place so this adapter and the SQL one agree on which calls carry one.\n const paymentRef = normalizePaymentRef(input.paymentRef);\n const payload: PaymentEventPayload = {\n userId,\n amount,\n type: \"purchase\",\n source: options?.source ?? \"purchase\",\n referenceType: options?.referenceType ?? \"transaction\",\n };\n\n if (paymentRef) {\n const existing = this.findByPaymentRef(paymentRef);\n if (existing) {\n const journal = this.journalForTransaction(existing);\n // Presence is not enough. A reference that arrives again with a\n // different user, amount or source is a *different* credit event, and\n // reporting it as a replay would credit the first amount and call the\n // second one done.\n const mismatch = describePaymentMismatch(\n {\n userId: existing.userId,\n amount: existing.amount,\n type: existing.type,\n source: journal?.source,\n referenceType: journal?.referenceType,\n },\n payload\n );\n return mismatch === null\n ? { outcome: \"replayed\", paymentRef, transaction: copyRecord(existing) }\n : { outcome: \"conflict\", paymentRef, existing: copyRecord(existing), mismatch };\n }\n }\n\n // After the reference check, never before it: a replay or a conflict must\n // write nothing, including the account row this creates. An absent user is\n // created at tier defaults, matching the SQL adapter's `ensureUserCredits`\n // — the first credit for a not-yet-seeded user (a webhook that outran\n // provisioning) lands instead of throwing in one adapter and landing in the\n // other.\n if (!this.store.users.has(userId)) {\n const tier = getDefaultTier();\n // `seedUser`, not `await initializeUserCredits(...)`: an `await` here sat\n // between the reference check above and the transaction write below, and\n // two simultaneous first deliveries of one reference both ran through\n // that gap and both credited. The seed is synchronous so the critical\n // section stays unbroken under run-to-completion.\n this.seedUser(userId, tier, storedMonthlyLimit(getConfigMonthlyLimit(tier)));\n }\n const credits = this.store.users.get(userId)!;\n\n // Project first, mutate second. The transaction write below validates the\n // derived `newBalance`, and if that throws after the balance has already\n // moved the store is left holding half of an operation that reported\n // failure. There is no rollback here, so nothing may change until every\n // derived value is known to be storable.\n const previousTotal = sumAmounts(credits.balance, credits.bonusCredits);\n const nextBonusCredits = sumAmounts(credits.bonusCredits, amount);\n const newTotal = sumAmounts(credits.balance, nextBonusCredits);\n assertRepresentableFields(\n { previousBalance: previousTotal, bonusCredits: nextBonusCredits, newBalance: newTotal },\n { userId, operation: \"addCredits\" }\n );\n\n credits.bonusCredits = nextBonusCredits;\n credits.updatedAt = new Date().toISOString();\n this.store.users.set(userId, credits);\n\n // Recorded through the internal writer: the public `createTransaction`\n // refuses a `paymentRef` precisely so that only this path — which moved the\n // balance above — can claim one.\n const transaction = this.recordTransaction({\n userId,\n type: \"purchase\",\n amount,\n description,\n paymentRef,\n previousBalance: previousTotal,\n newBalance: newTotal,\n });\n\n // Journal entry (parity with the Drizzle adapter): a single journal can then\n // serve as the app's credit ledger, including revenue-attribution metadata.\n const journalMetadata = {\n ...(paymentRef ? { paymentRef } : {}),\n ...(options?.metadata ?? {}),\n };\n const entry = await this.createJournalEntry({\n userId,\n entryType: \"credit\",\n amount,\n balanceAfter: newTotal,\n source: payload.source,\n referenceId: transaction.id ?? paymentRef ?? \"unknown\",\n referenceType: payload.referenceType,\n description,\n metadata: Object.keys(journalMetadata).length > 0 ? journalMetadata : undefined,\n });\n\n return paymentRef\n ? { outcome: \"created\", paymentRef, transaction, journalEntryId: entry.id }\n : { outcome: \"created\", transaction, journalEntryId: entry.id };\n }\n\n /**\n * The legacy signature, on top of {@link addCreditsV2}.\n *\n * `Promise<void>` has nowhere to report a conflict, and silently returning is\n * indistinguishable from \"credited\" — the exact failure this round is closing\n * — so a conflict throws. A genuine replay still returns quietly, because\n * that *is* the idempotent no-op the caller asked for.\n */\n async addCreditsAtomic(\n userId: string,\n amount: number,\n description: string,\n paymentRef?: string,\n options?: AddCreditsAtomicOptions\n ): Promise<void> {\n const outcome = await this.addCreditsV2({\n userId,\n amount,\n description,\n paymentRef,\n options,\n });\n if (outcome.outcome === \"conflict\") {\n throw createPaymentRefConflictError(outcome.paymentRef, {\n userId,\n mismatch: outcome.mismatch,\n existingUserId: outcome.existing.userId,\n });\n }\n }\n\n async deductCreditsAtomic(\n userId: string,\n amount: number\n ): Promise<{ previousBalance: number; newBalance: number }> {\n // `amount > 0` alone let 1.005, Infinity and out-of-range values through to\n // arithmetic the `numeric(12, 2)` adapter could never have stored.\n assertValidCreditAmount(amount, { userId, operation: \"deductCredits\" });\n\n const credits = this.store.users.get(userId);\n if (!credits) {\n throw new Error(`User credits not found for userId: ${userId}`);\n }\n\n const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);\n const available = Math.min(\n sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved),\n sumAmounts(spendable, credits.bonusCredits)\n );\n if (available < amount) {\n throw new Error(\n `Insufficient credits. Available: ${available}, requested: ${amount}`\n );\n }\n\n // Drain balance first, then bonusCredits — same policy as commit. Credits kept\n // only for holds have expired: a deduct cannot land on them.\n const balanceDeduction = Math.min(spendable, amount);\n const bonusDeduction = sumAmounts(amount, -balanceDeduction);\n\n // Projected and checked before anything moves, for the same reason as\n // `addCreditsAtomic`: there is no rollback in this store.\n const previousBalance = sumAmounts(credits.balance, credits.bonusCredits);\n const nextBalance = sumAmounts(credits.balance, -balanceDeduction);\n const nextBonusCredits = sumAmounts(credits.bonusCredits, -bonusDeduction);\n const newBalance = sumAmounts(previousBalance, -amount);\n assertRepresentableFields(\n { previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },\n { userId, operation: \"deductCredits\" }\n );\n\n credits.balance = nextBalance;\n credits.bonusCredits = nextBonusCredits;\n credits.updatedAt = new Date().toISOString();\n this.store.users.set(userId, credits);\n\n return { previousBalance, newBalance };\n }\n\n // ==================== Transactions ====================\n\n async createTransaction(input: CreateTransactionInput): Promise<PortableTransaction> {\n // A record with a `paymentRef` would occupy the global payment boundary\n // without crediting anyone — a later addCredits with the same reference\n // then reports `replayed` and credits nothing. Referenced payments go\n // through addCredits; see `assertUnreferencedDirectTransaction`.\n assertUnreferencedDirectTransaction(input);\n // Always absent after the guard; spelled as the normalised value so a blank\n // string is stored as \"no reference\", matching the SQL adapter.\n return this.recordTransaction({\n ...input,\n paymentRef: normalizePaymentRef(input.paymentRef),\n });\n }\n\n /** The unguarded writer, for `addCreditsV2` — which has moved the balance. */\n private recordTransaction(input: CreateTransactionInput): PortableTransaction {\n // Ledger records, not movements: a correction may be negative and a balance\n // may be below zero, so these check what `numeric(12, 2)` can hold.\n assertRepresentableAmount(input.amount, \"transaction amount\", { userId: input.userId });\n assertRepresentableAmount(input.previousBalance, \"transaction previousBalance\", {\n userId: input.userId,\n });\n assertRepresentableAmount(input.newBalance, \"transaction newBalance\", {\n userId: input.userId,\n });\n\n const transaction: PortableTransaction = {\n id: generateId(),\n userId: input.userId,\n type: input.type,\n amount: input.amount,\n description: input.description,\n paymentRef: input.paymentRef,\n previousBalance: input.previousBalance,\n newBalance: input.newBalance,\n createdAt: new Date().toISOString(),\n };\n\n if (!this.store.transactions.has(input.userId)) {\n this.store.transactions.set(input.userId, []);\n }\n this.store.transactions.get(input.userId)!.push(transaction);\n\n return copyRecord(transaction);\n }\n\n async getTransactions(\n userId: string,\n limit = 50,\n offset = 0\n ): Promise<PortableTransaction[]> {\n const userTransactions = this.store.transactions.get(userId) ?? [];\n // Sort by createdAt descending (most recent first)\n const sorted = [...userTransactions].sort((a, b) => {\n const aDate = toDate(a.createdAt).getTime();\n const bDate = toDate(b.createdAt).getTime();\n return bDate - aDate;\n });\n return copyRecords(sorted.slice(offset, offset + limit));\n }\n\n // ==================== Usage Logs ====================\n\n async logUsage(input: CreateUsageLogInput): Promise<PortableUsageLog> {\n assertRepresentableAmount(input.creditsUsed, \"creditsUsed\", { userId: input.userId });\n\n const log: PortableUsageLog = {\n id: generateId(),\n userId: input.userId,\n operationType: input.operationType,\n provider: input.provider,\n creditsUsed: input.creditsUsed,\n success: input.success,\n errorMessage: input.errorMessage,\n resourceId: input.resourceId,\n resourceType: input.resourceType,\n requestId: input.requestId,\n metadata: input.metadata ? { ...input.metadata } : input.metadata,\n createdAt: new Date().toISOString(),\n };\n\n this.store.usageLogs.push(log);\n return copyRecord(log);\n }\n\n async getUsageLogs(query: UsageLogQuery): Promise<PortableUsageLog[]> {\n let results = [...this.store.usageLogs];\n\n // Apply filters\n if (query.userId) {\n results = results.filter((log) => log.userId === query.userId);\n }\n if (query.operationType) {\n results = results.filter((log) => log.operationType === query.operationType);\n }\n if (query.success !== undefined) {\n results = results.filter((log) => log.success === query.success);\n }\n if (query.startDate) {\n const startTime = query.startDate.getTime();\n results = results.filter(\n (log) => toDate(log.createdAt).getTime() >= startTime\n );\n }\n if (query.endDate) {\n const endTime = query.endDate.getTime();\n results = results.filter(\n (log) => toDate(log.createdAt).getTime() <= endTime\n );\n }\n\n // Sort by createdAt descending\n results.sort((a, b) => {\n return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();\n });\n\n // Apply pagination\n const offset = query.offset ?? 0;\n const limit = query.limit ?? 50;\n return copyRecords(results.slice(offset, offset + limit));\n }\n\n async getUsageLogsCount(\n query: Omit<UsageLogQuery, \"limit\" | \"offset\">\n ): Promise<number> {\n const results = await this.getUsageLogs({ ...query, limit: Infinity, offset: 0 });\n return results.length;\n }\n\n // ==================== Journal Entries ====================\n\n async createJournalEntry(input: CreateJournalEntryInput): Promise<PortableJournalEntry> {\n return this.recordJournalEntry(input);\n }\n\n /**\n * The synchronous body of `createJournalEntry`, so `atomicMonthlyReset` can\n * write its journal line inside the same run-to-completion step as the\n * balance write — an `await` between the two is exactly the gap that loses\n * the line when the second half fails.\n */\n private recordJournalEntry(input: CreateJournalEntryInput): PortableJournalEntry {\n assertRepresentableAmount(input.amount, \"journal amount\", { userId: input.userId });\n assertRepresentableAmount(input.balanceAfter, \"journal balanceAfter\", {\n userId: input.userId,\n });\n assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });\n assertPublicJournalKey(input.idempotencyKey, input.userId);\n\n // The SQL adapter has a partial unique index on `(user_id, idempotency_key)`.\n // Without the equivalent here, a key written through this public method was\n // invisible to the V2 transitions, which would then happily write a *second*\n // row under the same deterministic key — the ledger would record one event\n // twice and the two adapters would disagree about what happened.\n const key =\n input.idempotencyKey !== undefined\n ? scopedKey(input.userId, input.idempotencyKey)\n : undefined;\n if (key !== undefined && this.store.journalKeys.has(key)) {\n throw new CreditError(\n `Journal idempotency key ${input.idempotencyKey} is already used for user ` +\n `${input.userId}. The unique constraint refuses a second entry.`,\n CreditErrorCode.DATABASE_ERROR,\n { userId: input.userId, idempotencyKey: input.idempotencyKey }\n );\n }\n\n const entry: PortableJournalEntry = {\n id: generateId(),\n userId: input.userId,\n entryType: input.entryType,\n amount: input.amount,\n balanceAfter: input.balanceAfter,\n source: input.source,\n referenceId: input.referenceId,\n referenceType: input.referenceType,\n description: input.description,\n metadata: input.metadata ? { ...input.metadata } : input.metadata,\n idempotencyKey: input.idempotencyKey,\n createdAt: new Date().toISOString(),\n };\n\n if (!this.store.journalEntries.has(input.userId)) {\n this.store.journalEntries.set(input.userId, []);\n }\n this.store.journalEntries.get(input.userId)!.push(entry);\n if (key !== undefined) this.store.journalKeys.set(key, entry.id);\n\n return copyRecord(entry);\n }\n\n async getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]> {\n let results = this.store.journalEntries.get(query.userId) ?? [];\n\n // Apply filters\n if (query.source) {\n results = results.filter((entry) => entry.source === query.source);\n }\n if (query.referenceType) {\n results = results.filter((entry) => entry.referenceType === query.referenceType);\n }\n if (query.startDate) {\n const startTime = query.startDate.getTime();\n results = results.filter(\n (entry) => toDate(entry.createdAt).getTime() >= startTime\n );\n }\n if (query.endDate) {\n const endTime = query.endDate.getTime();\n results = results.filter(\n (entry) => toDate(entry.createdAt).getTime() <= endTime\n );\n }\n\n // Sort by createdAt descending\n results = [...results].sort((a, b) => {\n return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();\n });\n\n // Apply pagination\n const offset = query.offset ?? 0;\n const limit = query.limit ?? 50;\n return copyRecords(results.slice(offset, offset + limit));\n }\n\n async getJournalEntriesCount(\n query: Omit<JournalEntryQuery, \"limit\" | \"offset\">\n ): Promise<number> {\n const results = await this.getJournalEntries({\n ...query,\n limit: Infinity,\n offset: 0,\n });\n return results.length;\n }\n\n // ==================== Cleanup Operations ====================\n\n /**\n * Expire every hold whose deadline has passed.\n *\n * Each candidate goes through the guarded {@link expireReservationV2}, which\n * re-checks the status and the deadline under the user's lock. The previous\n * implementation released the hold and then overwrote the status to\n * `expired`, so a commit landing in between got its credits handed back and\n * was then relabelled.\n */\n async findAndExpireReservations(\n _batchSize = 100,\n _maxIterations = 100\n ): Promise<{\n expiredCount: number;\n creditsReleased: number;\n errors: string[];\n }> {\n const asOf = new Date();\n let expiredCount = 0;\n let creditsReleased = 0;\n const errors: string[] = [];\n\n // Snapshot the candidates first: expiring mutates the maps being iterated.\n const candidates: Array<{ userId: string; reservationId: string }> = [];\n for (const [userId, userReservations] of this.store.reservations) {\n for (const [reservationId, reservation] of userReservations) {\n if (\n reservation.status === \"reserved\" &&\n toDate(reservation.expiresAt).getTime() < asOf.getTime()\n ) {\n candidates.push({ userId, reservationId });\n }\n }\n }\n\n for (const candidate of candidates) {\n try {\n const outcome = await this.expireReservationV2(\n candidate.userId,\n candidate.reservationId,\n { asOf }\n );\n if (outcome.outcome === \"expired\") {\n expiredCount++;\n creditsReleased = sumAmounts(creditsReleased, outcome.amount);\n }\n } catch (error) {\n errors.push(\n `Failed to expire reservation ${candidate.reservationId}: ${error}`\n );\n }\n }\n\n return { expiredCount, creditsReleased, errors };\n }\n\n // ==================== Atomic Monthly Reset ====================\n\n /**\n * @param _tier - Not trusted. The caller read it earlier and the account may\n * have changed tier since, so the reset uses the tier stored on the account.\n */\n async atomicMonthlyReset(\n userId: string,\n _tier: SubscriptionTier,\n expectedResetAt: Date | string\n ): Promise<MonthlyResetResult> {\n const credits = this.store.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n // Optimistic locking: check if expectedResetAt matches current value\n const currentResetAt = toDate(credits.monthlyResetAt).getTime();\n const expected = toDate(expectedResetAt).getTime();\n\n if (currentResetAt !== expected) {\n // Another request already performed the reset\n return { wasReset: false, credits: { ...credits } };\n }\n\n const tier = credits.tier;\n // The resolver decides on credits the account may spend, not on those kept\n // only for holds; otherwise \"keep the balance\" would unmark them.\n const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);\n const decision = this.options.resolveMonthlyReset?.({ tier, balance: spendable });\n if (decision === null) {\n return { wasReset: false, credits: { ...credits } };\n }\n if (decision) {\n assertValidBalanceTarget(decision.balance, \"resolveMonthlyReset balance\", { userId, tier });\n if (decision.monthlyLimit !== undefined) {\n assertValidBalanceTarget(decision.monthlyLimit, \"resolveMonthlyReset monthlyLimit\", {\n userId,\n tier,\n });\n }\n } else {\n assertRepresentableTierAmount(getConfigMonthlyLimit(tier), \"monthlyLimit\", { userId, tier });\n }\n\n // Perform the reset\n const nextReset = getNextMonthlyReset();\n\n // The unlimited contract, from the one place that defines it. This used to\n // read `newBalance === Infinity ? credits.balance : newBalance` — \"leave an\n // unlimited balance alone\" — which never restored an unlimited account whose\n // balance had been driven to zero. See `monthlyResetBalance`.\n // Floored at what still backs the outstanding holds: a reset that cut\n // `balance + bonusCredits` below `reserved` would strand every live\n // reservation at commit time with INSUFFICIENT_CREDITS. See\n // `backedBalanceFloor`.\n const previousBalance = credits.balance;\n let target: number;\n if (decision) {\n target = decision.balance;\n } else {\n const configured = monthlyResetBalance(tier);\n target =\n configured.kind === \"atLeast\" ? Math.max(spendable, configured.value) : configured.value;\n }\n const planned = planBalanceTarget(target, credits.reserved, credits.bonusCredits);\n const newBalance = planned.balance;\n\n // The journal line is part of the reset, not a follow-up. It is validated\n // before anything mutates and written synchronously after, so no failure or\n // interleaving can consume the CAS and lose the line — which is exactly\n // what happened when the service wrote it as a separate call: the retry saw\n // `wasReset: false` and the reset entry was gone for good.\n const change = sumAmounts(newBalance, -previousBalance);\n const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);\n assertRepresentableAmount(Math.abs(change), \"journal amount\", { userId });\n assertRepresentableAmount(balanceAfter, \"journal balanceAfter\", { userId });\n\n credits.balance = newBalance;\n writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);\n if (decision?.monthlyLimit !== undefined) credits.monthlyLimit = decision.monthlyLimit;\n credits.monthlyUsed = 0;\n credits.monthlyResetAt = nextReset.toISOString();\n credits.updatedAt = new Date().toISOString();\n this.store.users.set(userId, credits);\n\n if (change !== 0) {\n this.recordJournalEntry({\n userId,\n entryType: change > 0 ? \"credit\" : \"debit\",\n amount: Math.abs(change),\n balanceAfter,\n source: \"monthly_reset\",\n referenceId: `reset-${Date.now()}`,\n referenceType: \"reset\",\n description: `Monthly credit reset for ${tier} tier.`,\n metadata: { tier, previousBalance, newBalance },\n });\n }\n\n return { wasReset: true, credits: { ...credits }, journaled: true };\n }\n\n // ==================== Subscription Expiry ====================\n\n async checkAndHandleSubscriptionExpiry(\n userId: string,\n gracePeriodDays = 3\n ): Promise<SubscriptionExpiryResult> {\n const credits = this.store.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n // Free tier doesn't expire\n if (isFreeTier(credits.tier) || !credits.subscriptionExpiresAt) {\n return {\n wasDowngraded: false,\n inGracePeriod: false,\n graceDaysRemaining: 0,\n credits: { ...credits },\n };\n }\n\n const now = new Date();\n const expiresAt = toDate(credits.subscriptionExpiresAt);\n const daysSinceExpiry =\n (now.getTime() - expiresAt.getTime()) / (1000 * 60 * 60 * 24);\n\n if (daysSinceExpiry <= 0) {\n // Not expired yet\n return {\n wasDowngraded: false,\n inGracePeriod: false,\n graceDaysRemaining: 0,\n credits: { ...credits },\n };\n }\n\n if (daysSinceExpiry <= gracePeriodDays) {\n // In grace period\n return {\n wasDowngraded: false,\n inGracePeriod: true,\n graceDaysRemaining: Math.ceil(gracePeriodDays - daysSinceExpiry),\n credits: { ...credits },\n };\n }\n\n // Grace period expired - downgrade to the configured default tier\n const defaultTier = getDefaultTier();\n let limit: number;\n if (this.options.getDowngradeBalanceCap) {\n limit = this.options.getDowngradeBalanceCap();\n assertValidBalanceTarget(limit, \"getDowngradeBalanceCap\", { userId, tier: defaultTier });\n } else {\n // `getConfigMonthlyLimit`, not the raw `monthlyCredits`: in tier *config*\n // zero means unlimited, so reading the field directly would downgrade onto\n // an unlimited default tier with a limit and a balance of zero.\n const configured = getConfigMonthlyLimit(defaultTier);\n assertRepresentableTierAmount(configured, \"monthlyLimit\", {\n userId,\n tier: defaultTier,\n });\n limit = storedMonthlyLimit(configured);\n }\n\n // The downgrade may cut the balance to the new tier's limit, but never\n // below what still backs the outstanding holds — see `backedBalanceFloor`.\n const previousTier = credits.tier;\n const previousBalance = credits.balance;\n const planned = planBalanceCap(\n credits.balance,\n limit,\n credits.reserved,\n credits.bonusCredits,\n credits.expiredHoldBacking\n );\n const newBalance = planned.balance;\n\n // The journal line is part of the downgrade, not a follow-up: validated\n // before anything mutates, written synchronously after. Written separately\n // by the service, a failure landed after the tier write, and no retry ever\n // fired again — the row was no longer eligible, the audit line gone.\n const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);\n assertRepresentableAmount(balanceAfter, \"journal balanceAfter\", { userId });\n\n credits.tier = defaultTier;\n credits.monthlyLimit = limit;\n credits.balance = newBalance;\n writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);\n credits.subscriptionExpiresAt = null;\n credits.updatedAt = new Date().toISOString();\n this.store.users.set(userId, credits);\n\n this.recordJournalEntry({\n userId,\n entryType: \"debit\",\n amount: 0, // No credits deducted, just tier change\n balanceAfter,\n source: \"subscription_downgrade\",\n referenceId: `downgrade-${Date.now()}`,\n referenceType: \"subscription\",\n description: `Subscription expired. Downgraded from ${previousTier} to ${defaultTier} tier.`,\n metadata: { previousTier, previousBalance, newBalance },\n });\n\n return {\n wasDowngraded: true,\n inGracePeriod: false,\n graceDaysRemaining: 0,\n credits: { ...credits },\n journaled: true,\n };\n }\n\n // ==================== Testing Utilities ====================\n\n /**\n * Clear all data (useful for testing)\n */\n clear(): void {\n this.store.clear();\n }\n\n /**\n * Install a yield point inside every V2 critical section (testing only).\n *\n * Unset, the critical sections run start-to-finish synchronously, so\n * concurrent callers never interleave and a concurrency test would pass even\n * with the locking removed. Setting this to a real yield (a macrotask or a\n * barrier) makes callers genuinely overlap, so the tests exercise the lock\n * instead of the event loop. Never call this in production code.\n */\n setSchedulingHook(hook: (() => Promise<void>) | undefined): void {\n this.store.schedulingHook = hook;\n }\n\n /**\n * Get all users (useful for testing/debugging)\n */\n getAllUsers(): PortableUserCredits[] {\n return copyRecords(Array.from(this.store.users.values()));\n }\n\n /**\n * Get all reservations for a user (useful for testing)\n */\n getAllReservations(userId: string): PortableReservation[] {\n const userReservations = this.store.reservations.get(userId);\n if (!userReservations) return [];\n return copyRecords(Array.from(userReservations.values()));\n }\n}\n\n/**\n * Create a new in-memory repository instance\n * Each call creates a fresh, isolated instance\n */\nexport function createInMemoryCreditRepository(\n options: CreditPolicyOptions = {}\n): InMemoryCreditRepository {\n return new InMemoryCreditRepository(options);\n}\n","import type { ICreditsAuthProvider, CreditsUser } from \"./types.js\";\r\n\r\n/**\r\n * Timing-safe string comparison to prevent timing attacks\r\n */\r\nfunction timingSafeEqual(a: string, b: string): boolean {\r\n if (!a || !b) {\r\n return false;\r\n }\r\n\r\n // Pad strings to same length to prevent length-based timing attacks\r\n const maxLength = Math.max(a.length, b.length);\r\n const paddedA = a.padEnd(maxLength);\r\n const paddedB = b.padEnd(maxLength);\r\n\r\n let result = 0;\r\n for (let i = 0; i < maxLength; i++) {\r\n result |= paddedA.charCodeAt(i) ^ paddedB.charCodeAt(i);\r\n }\r\n\r\n // Also check that lengths were originally equal\r\n return result === 0 && a.length === b.length;\r\n}\r\n\r\n/**\r\n * Verify a Bearer token from an Authorization header\r\n *\r\n * @param header - The Authorization header value (e.g., \"Bearer secret123\")\r\n * @param secret - The expected secret value\r\n * @returns True if token is valid\r\n */\r\nexport function verifyBearerToken(header: string | null, secret: string): boolean {\r\n if (!header || !secret) {\r\n return false;\r\n }\r\n\r\n // Extract token from \"Bearer {token}\" format\r\n const token = header.replace(/^Bearer\\s+/i, \"\");\r\n\r\n // Use timing-safe comparison\r\n return timingSafeEqual(token, secret);\r\n}\r\n\r\n/**\r\n * API Key auth provider for admin routes\r\n *\r\n * Uses Bearer token authentication for external API access\r\n * The API key is stored in CREDITS_ADMIN_API_KEY environment variable\r\n */\r\nexport class ApiKeyCreditsProvider implements ICreditsAuthProvider {\r\n private readonly apiKey: string | undefined;\r\n private isValidated = false;\r\n\r\n constructor(authHeader?: string | null, apiKey?: string) {\r\n this.apiKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;\r\n\r\n // Validate the provided auth header using timing-safe comparison\r\n if (authHeader && this.apiKey) {\r\n this.isValidated = verifyBearerToken(authHeader, this.apiKey);\r\n }\r\n }\r\n\r\n async getCurrentUser(): Promise<CreditsUser | null> {\r\n if (!this.isValidated) {\r\n return null;\r\n }\r\n\r\n // API key auth returns a special \"admin\" user\r\n return {\r\n id: \"api-admin\",\r\n email: \"api@admin.internal\",\r\n name: \"API Admin\",\r\n };\r\n }\r\n\r\n async verifyAdminAccess(): Promise<boolean> {\r\n return this.isValidated;\r\n }\r\n}\r\n\r\n/**\r\n * Verify an API key from a request header\r\n * @param authHeader - Authorization header value\r\n * @param apiKey - Optional API key to use (defaults to CREDITS_ADMIN_API_KEY env var)\r\n * @returns True if API key is valid\r\n */\r\nexport function verifyAdminApiKey(authHeader: string | null, apiKey?: string): boolean {\r\n if (!authHeader) {\r\n return false;\r\n }\r\n\r\n const expectedKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;\r\n if (!expectedKey) {\r\n console.warn(\"CREDITS_ADMIN_API_KEY not configured\");\r\n return false;\r\n }\r\n\r\n // Use timing-safe comparison to prevent timing attacks\r\n return verifyBearerToken(authHeader, expectedKey);\r\n}\r\n\r\n/**\r\n * Create an API key auth provider from a request\r\n * @param request - Request object with headers\r\n * @param apiKey - Optional API key to use\r\n * @returns API key auth provider\r\n */\r\nexport function createApiKeyProvider(request: Request, apiKey?: string): ApiKeyCreditsProvider {\r\n const authHeader = request.headers.get(\"Authorization\");\r\n return new ApiKeyCreditsProvider(authHeader, apiKey);\r\n}\r\n","import type {\r\n PortableUserCredits,\r\n PortableReservation,\r\n CreditCheckResult,\r\n CreditOperationType,\r\n SubscriptionTier,\r\n PortableUsageLog,\r\n PortableJournalEntry,\r\n UsageHistoryEntry,\r\n UsageHistoryResponse,\r\n DeductCreditsResult,\r\n} from \"../core/types.js\";\r\nimport { toDate } from \"../core/types.js\";\r\nimport type {\r\n AddCreditsOutcome,\r\n CommitOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n} from \"../core/outcomes.js\";\r\nimport {\r\n CreditErrorCode,\r\n createIdempotencyConflictError,\r\n createInsufficientCreditsError,\r\n createReservationAlreadyProcessedError,\r\n createReservationNotFoundError,\r\n isCreditError,\r\n} from \"../core/errors.js\";\r\nimport {\r\n assertValidCreditAmount,\r\n storedMonthlyLimit,\r\n sumAmounts,\r\n} from \"../core/amount.js\";\r\nimport type { ReservationTransitionOptions } from \"../repository/v2-types.js\";\r\nimport {\r\n addCreditsThroughRepository,\r\n commitThroughRepository,\r\n releaseThroughRepository,\r\n reserveThroughRepository,\r\n} from \"../repository/flow.js\";\r\nimport type {\r\n ICreditRepository,\r\n CreateUsageLogInput,\r\n JournalEntryQuery,\r\n AddCreditsAtomicOptions,\r\n} from \"../repository/types.js\";\r\nimport { toClientUserCredits } from \"../repository/types.js\";\r\nimport {\r\n getConfig,\r\n getConfigMonthlyLimit,\r\n getOperationLabel,\r\n isFreeTier,\r\n isUnlimitedTier,\r\n getDefaultTier,\r\n getUnlimitedSentinelBalance,\r\n} from \"../config/index.js\";\r\n\r\n/**\r\n * Check if a date is past the monthly reset date\r\n */\r\nfunction isPastMonthlyReset(resetAt: unknown): boolean {\r\n if (!resetAt) return false;\r\n const resetDate = toDate(resetAt);\r\n return new Date() >= resetDate;\r\n}\r\n\r\n/**\r\n * Notification callback type for low balance notifications\r\n */\r\nexport type LowBalanceNotificationCallback = (userId: string, balance: number) => Promise<void>;\r\n\r\n/**\r\n * Notification callback type for subscription expired notifications\r\n */\r\nexport type SubscriptionExpiredNotificationCallback = (userId: string, wasDowngraded: boolean) => Promise<void>;\r\n\r\n/**\r\n * Options for reserving credits.\r\n */\r\nexport interface ReserveCreditsOptions {\r\n /**\r\n * Time-to-live for the reservation, in milliseconds, before the cleanup\r\n * sweep may expire it. Defaults to `config.reservationExpiryMs` (suitable\r\n * for synchronous, in-request operations). Long-running async jobs\r\n * (e.g. background media generation) should pass a TTL matching their\r\n * own lifecycle so the reservation is not expired while the job is still\r\n * legitimately in flight.\r\n */\r\n ttlMs?: number;\r\n /**\r\n * Caller-supplied idempotency key, unique per user.\r\n *\r\n * Retrying a reserve with the same key and the same amount/operation returns\r\n * the original reservation instead of placing a second hold — the fix for a\r\n * webhook or job runner delivering the same request twice. Reusing a key\r\n * with a *different* amount or operation is a\r\n * `CreditErrorCode.IDEMPOTENCY_CONFLICT`.\r\n *\r\n * Requires a repository implementing the V2 boundary (see\r\n * `supportsCreditsV2`). A legacy adapter has no unique index to enforce the\r\n * key, so it is ignored there rather than silently promised.\r\n */\r\n idempotencyKey?: string;\r\n}\r\n\r\n/**\r\n * Options for a one-shot atomic credit deduction via `deductCredits`.\r\n */\r\nexport interface DeductCreditsOptions {\r\n /**\r\n * Operation type for cost tracking / journal + usage-log labeling.\r\n * Falls back to a generic \"adjustment\" description when omitted.\r\n */\r\n operationType?: CreditOperationType;\r\n /** Resource ID to attach to the usage log (e.g. the generated asset's id) */\r\n resourceId?: string;\r\n /** Resource type to attach to the usage log */\r\n resourceType?: string;\r\n /** Request ID to attach to the usage log, for tracing */\r\n requestId?: string;\r\n /** Additional metadata recorded on the journal entry */\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Credits service with dependency injection for repository\r\n *\r\n * Provides business logic for credit operations, delegating\r\n * database operations to the injected repository.\r\n */\r\nexport class CreditsService {\r\n private lowBalanceCallback?: LowBalanceNotificationCallback;\r\n private subscriptionExpiredCallback?: SubscriptionExpiredNotificationCallback;\r\n\r\n constructor(private readonly repository: ICreditRepository) {}\r\n\r\n /**\r\n * Set callback for low balance notifications\r\n */\r\n setLowBalanceCallback(callback: LowBalanceNotificationCallback): void {\r\n this.lowBalanceCallback = callback;\r\n }\r\n\r\n /**\r\n * Set callback for subscription expired notifications\r\n */\r\n setSubscriptionExpiredCallback(callback: SubscriptionExpiredNotificationCallback): void {\r\n this.subscriptionExpiredCallback = callback;\r\n }\r\n\r\n /**\r\n * Get user credits, performing monthly reset and subscription expiry checks if needed\r\n *\r\n * This method uses atomic operations to prevent race conditions:\r\n * 1. Checks subscription expiry with grace period\r\n * 2. Atomically performs monthly reset if needed (with optimistic locking)\r\n *\r\n * @param userId - User ID\r\n * @returns User credits or null if not found\r\n */\r\n async getUserCredits(userId: string): Promise<PortableUserCredits | null> {\r\n let data = await this.repository.getUserCredits(userId);\r\n\r\n if (!data) {\r\n return null;\r\n }\r\n\r\n // Step 1: Check subscription expiry (for non-free tiers)\r\n if (!isFreeTier(data.tier) && data.subscriptionExpiresAt) {\r\n const expiryResult = await this.repository.checkAndHandleSubscriptionExpiry(\r\n userId,\r\n getConfig().subscriptionGracePeriodDays\r\n );\r\n\r\n if (expiryResult.wasDowngraded && !expiryResult.journaled) {\r\n // Legacy path for repositories that do not journal the downgrade\r\n // themselves. This write sits outside the downgrade's atomic step: if\r\n // it fails after the tier write committed, the account is downgraded\r\n // with no audit line and no retry ever fires (the row is no longer\r\n // eligible). Repositories that can journal atomically return\r\n // `journaled: true` and this block is skipped.\r\n await this.repository.createJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount: 0, // No credits deducted, just tier change\r\n // The ledger balance, matching every other journal writer: the V2\r\n // transitions and add/deduct all record `balance + bonusCredits`, so\r\n // recording `balance` alone made the audit trail disagree with itself\r\n // for any user holding bonus credits.\r\n balanceAfter: sumAmounts(\r\n expiryResult.credits.balance,\r\n expiryResult.credits.bonusCredits\r\n ),\r\n source: \"subscription_downgrade\",\r\n referenceId: `downgrade-${Date.now()}`,\r\n referenceType: \"subscription\",\r\n // The tier actually landed on, not a hard-coded \"free\": the downgrade\r\n // target is configurable, so naming `free` here could describe a\r\n // transition that never happened.\r\n description: `Subscription expired. Downgraded from ${data.tier} to ${expiryResult.credits.tier} tier.`,\r\n metadata: {\r\n previousTier: data.tier,\r\n previousBalance: data.balance,\r\n newBalance: expiryResult.credits.balance,\r\n },\r\n });\r\n }\r\n\r\n // Trigger subscription expired notification (non-blocking) — on every\r\n // downgrade, whichever side wrote the journal.\r\n if (expiryResult.wasDowngraded && this.subscriptionExpiredCallback) {\r\n this.subscriptionExpiredCallback(userId, true).catch((error) => {\r\n console.error(\"[Credits] Failed to send subscription expired notification:\", error);\r\n });\r\n }\r\n\r\n // Use the potentially updated credits\r\n data = expiryResult.credits;\r\n }\r\n\r\n // Step 2: Check if monthly reset is needed (use atomic operation)\r\n if (isPastMonthlyReset(data.monthlyResetAt)) {\r\n // Convert monthlyResetAt to a compatible type (Date or string)\r\n const expectedResetAt = toDate(data.monthlyResetAt);\r\n const resetResult = await this.repository.atomicMonthlyReset(\r\n userId,\r\n data.tier,\r\n expectedResetAt\r\n );\r\n\r\n if (resetResult.wasReset && !resetResult.journaled) {\r\n // Legacy path for repositories that do not journal the reset\r\n // themselves. This write sits outside the reset's atomic step: if it\r\n // fails after the CAS was consumed, the journal line is lost for good.\r\n // Repositories that can journal atomically return `journaled: true`\r\n // and this block is skipped.\r\n const balanceChange = sumAmounts(resetResult.credits.balance, -data.balance);\r\n if (balanceChange !== 0) {\r\n await this.repository.createJournalEntry({\r\n userId,\r\n entryType: balanceChange > 0 ? \"credit\" : \"debit\",\r\n amount: Math.abs(balanceChange),\r\n balanceAfter: sumAmounts(\r\n resetResult.credits.balance,\r\n resetResult.credits.bonusCredits\r\n ),\r\n source: \"monthly_reset\",\r\n referenceId: `reset-${Date.now()}`,\r\n referenceType: \"reset\",\r\n // The tier the reset ran on, which a concurrent downgrade may have\r\n // changed since `data` was read.\r\n description: `Monthly credit reset for ${resetResult.credits.tier} tier.`,\r\n metadata: {\r\n tier: resetResult.credits.tier,\r\n previousBalance: data.balance,\r\n newBalance: resetResult.credits.balance,\r\n },\r\n });\r\n }\r\n }\r\n\r\n // Use the potentially updated credits\r\n data = resetResult.credits;\r\n }\r\n\r\n return toClientUserCredits(data);\r\n }\r\n\r\n /**\r\n * Initialize credits for a new user with free tier\r\n * @param userId - User ID\r\n * @returns Initialized user credits\r\n */\r\n async initializeUserCredits(userId: string): Promise<PortableUserCredits> {\r\n const credits = await this.repository.initializeUserCredits(\r\n userId,\r\n getDefaultTier(),\r\n getConfig().defaultFreeCredits\r\n );\r\n return toClientUserCredits(credits);\r\n }\r\n\r\n /**\r\n * Get or create user credits\r\n * Initializes with free tier if not exists\r\n * @param userId - User ID\r\n * @returns User credits\r\n */\r\n async getOrCreateUserCredits(userId: string): Promise<PortableUserCredits> {\r\n const existing = await this.getUserCredits(userId);\r\n if (existing) {\r\n return existing;\r\n }\r\n return this.initializeUserCredits(userId);\r\n }\r\n\r\n /**\r\n * Check if user has sufficient credits for an operation\r\n * @param userId - User ID\r\n * @param requiredCredits - Credits required\r\n * @returns Credit check result\r\n */\r\n async checkCredits(userId: string, requiredCredits: number): Promise<CreditCheckResult> {\r\n const credits = await this.getOrCreateUserCredits(userId);\r\n\r\n // Available = balance + bonusCredits - reserved\r\n const totalBalance = sumAmounts(credits.balance, credits.bonusCredits);\r\n const available = sumAmounts(totalBalance, -credits.reserved);\r\n const hasCredits = available >= requiredCredits;\r\n\r\n return {\r\n hasCredits,\r\n balance: totalBalance,\r\n required: requiredCredits,\r\n shortfall: hasCredits ? 0 : sumAmounts(requiredCredits, -available),\r\n };\r\n }\r\n\r\n /**\r\n * Reserve credits for an operation (phase 1 of two-phase commit)\r\n * Creates a reservation and locks the credits\r\n * @param userId - User ID\r\n * @param amount - Credits to reserve\r\n * @param operationType - Operation type for tracking\r\n * @param options - Optional settings (e.g. a custom `ttlMs` for long-running async jobs)\r\n * @returns Reservation object\r\n * @throws Error if insufficient credits\r\n */\r\n async reserveCredits(\r\n userId: string,\r\n amount: number,\r\n operationType: CreditOperationType,\r\n options?: ReserveCreditsOptions\r\n ): Promise<PortableReservation> {\r\n const outcome = await this.reserveCreditsDetailed(userId, amount, operationType, options);\r\n if (outcome.outcome === \"created\" || outcome.outcome === \"replayed\") {\r\n return outcome.reservation;\r\n }\r\n if (outcome.outcome === \"insufficient\") {\r\n throw createInsufficientCreditsError(outcome.required, outcome.available);\r\n }\r\n throw createIdempotencyConflictError(outcome.idempotencyKey, {\r\n userId,\r\n requested: { amount, operationType },\r\n existing: {\r\n amount: outcome.existing.amount,\r\n operationType: outcome.existing.operationType,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Reserve credits and get the typed outcome instead of an exception.\r\n *\r\n * Prefer this over {@link reserveCredits} in async callers: it distinguishes\r\n * a fresh hold from an idempotent replay, and reports insufficient funds as\r\n * a value rather than as control flow.\r\n */\r\n async reserveCreditsDetailed(\r\n userId: string,\r\n amount: number,\r\n operationType: CreditOperationType,\r\n options?: ReserveCreditsOptions\r\n ): Promise<ReserveOutcome> {\r\n const ttlMs = options?.ttlMs ?? getConfig().reservationExpiryMs;\r\n const expiresAt = new Date(Date.now() + ttlMs);\r\n return reserveThroughRepository(this.repository, {\r\n userId,\r\n amount,\r\n operationType,\r\n expiresAt,\r\n idempotencyKey: options?.idempotencyKey,\r\n });\r\n }\r\n\r\n /**\r\n * Commit a reservation (phase 2 of two-phase commit - success).\r\n *\r\n * Idempotent: re-delivering a commit for an already-committed reservation is\r\n * a no-op. Committing one that was released or expired throws, because the\r\n * credits are no longer held and pretending otherwise would hide a real bug.\r\n */\r\n async commitCredits(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<void> {\r\n const outcome = await this.commitCreditsDetailed(userId, reservationId, options);\r\n if (outcome.outcome === \"not_found\") {\r\n throw createReservationNotFoundError(reservationId);\r\n }\r\n if (outcome.outcome === \"already_terminal\" && outcome.terminalStatus !== \"committed\") {\r\n throw createReservationAlreadyProcessedError(reservationId, outcome.terminalStatus);\r\n }\r\n }\r\n\r\n /**\r\n * Commit a reservation and get the typed outcome.\r\n *\r\n * Exactly one concurrent caller sees `committed`; that call moved the balance\r\n * and wrote the single journal entry. On a V2 repository the journal is\r\n * written inside the same transaction, so this method adds none of its own —\r\n * the duplicate entry the previous implementation wrote is gone.\r\n *\r\n * The low-balance notification fires only for the winner, and only after the\r\n * transaction has committed, so no callback runs inside a database\r\n * transaction.\r\n */\r\n async commitCreditsDetailed(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<CommitOutcome> {\r\n const outcome = await commitThroughRepository(\r\n this.repository,\r\n userId,\r\n reservationId,\r\n options\r\n );\r\n\r\n if (outcome.outcome === \"committed\" && this.lowBalanceCallback) {\r\n this.lowBalanceCallback(userId, outcome.balanceAfter).catch((error) => {\r\n console.error(\"[Credits] Failed to send low balance notification:\", error);\r\n });\r\n }\r\n\r\n return outcome;\r\n }\r\n\r\n /**\r\n * Release a reservation (phase 2 of two-phase commit - failure).\r\n *\r\n * Throws `RESERVATION_NOT_FOUND` for an unknown reservation, matching the\r\n * pre-V2 behaviour — a release naming a reservation that does not exist is a\r\n * caller bug, and swallowing it hides the bug while the credits stay held.\r\n *\r\n * Releasing one that is already terminal — `released`, `expired`, or\r\n * `committed` — is a no-op, which is the behaviour this method has always\r\n * had. A release that loses a race to a concurrent commit is the common\r\n * case in a retry-heavy caller, and callers written against the pre-V2\r\n * contract handle it by not handling it.\r\n *\r\n * That does mean this method cannot tell you that the credits were spent\r\n * rather than returned. Use {@link releaseCreditsDetailed}, which reports\r\n * `already_terminal` with `terminalStatus: 'committed'`, when you need to\r\n * branch on it.\r\n */\r\n async releaseCredits(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<void> {\r\n const outcome = await this.releaseCreditsDetailed(userId, reservationId, options);\r\n if (outcome.outcome === \"not_found\") {\r\n throw createReservationNotFoundError(reservationId);\r\n }\r\n }\r\n\r\n /** Release a reservation and get the typed outcome. */\r\n async releaseCreditsDetailed(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<ReleaseOutcome> {\r\n return releaseThroughRepository(this.repository, userId, reservationId, options);\r\n }\r\n\r\n /**\r\n * Log usage for audit trail\r\n * @param log - Usage log data\r\n */\r\n async logUsage(log: Omit<PortableUsageLog, \"id\" | \"createdAt\">): Promise<void> {\r\n await this.repository.logUsage(log as CreateUsageLogInput);\r\n }\r\n\r\n /**\r\n * Add credits to user account (for purchases, bonuses, etc.)\r\n * @param userId - User ID\r\n * @param amount - Credits to add\r\n * @param description - Transaction description\r\n * @param paymentRef - Optional payment reference\r\n * @param options - Optional journal source / reference type / extra metadata\r\n * for the credit journal entry (e.g. revenue-attribution fields).\r\n * @returns Whether this delivery credited the account (`created`), repeated an\r\n * earlier one (`replayed`), or reused the reference for a different credit\r\n * event (`conflict`, which credited nothing). On a repository without\r\n * `addCreditsV2` the result is always `created` and carries no\r\n * deduplication guarantee — see {@link addCreditsThroughRepository}.\r\n */\r\n async addCredits(\r\n userId: string,\r\n amount: number,\r\n description: string,\r\n paymentRef?: string,\r\n options?: AddCreditsAtomicOptions\r\n ): Promise<AddCreditsOutcome> {\r\n return addCreditsThroughRepository(this.repository, {\r\n userId,\r\n amount,\r\n description,\r\n paymentRef,\r\n options,\r\n });\r\n }\r\n\r\n /**\r\n * One-shot atomic \"deduct-if-sufficient\" credit charge.\r\n *\r\n * Unlike the reserve/commit/release two-phase flow, this performs a single\r\n * atomic check-and-deduct against the repository — suited for synchronous,\r\n * in-request operations that don't need a hold across an async job. Exactly\r\n * one call is made to `repository.deductCreditsAtomic`, which enforces\r\n * atomicity so concurrent callers can never both succeed against the same\r\n * limited balance.\r\n *\r\n * On success, creates a journal entry (same audit trail as `commitCredits`)\r\n * and, if `options.operationType` is provided, a usage log entry via the\r\n * same `logUsage` path.\r\n *\r\n * @param userId - User ID\r\n * @param amount - Credits to deduct (must be > 0)\r\n * @param options - Optional usage-log / journal metadata\r\n * @returns Typed result: `{ success: true, newBalance }` or\r\n * `{ success: false, reason: 'insufficient', available, required, shortfall }`\r\n * @throws Error if amount is not a positive number\r\n */\r\n async deductCredits(\r\n userId: string,\r\n amount: number,\r\n options?: DeductCreditsOptions\r\n ): Promise<DeductCreditsResult> {\r\n // `amount > 0` accepted 1.005, Infinity and values past what a\r\n // `numeric(12, 2)` column can hold; each of those reached the repository's\r\n // raw arithmetic. Validate here, *before* the try, so a rejected amount can\r\n // never be mistaken for a shortfall by the catch below.\r\n assertValidCreditAmount(amount, { userId, operation: \"deductCredits\" });\r\n\r\n let deduction: { previousBalance: number; newBalance: number };\r\n try {\r\n deduction = await this.repository.deductCreditsAtomic(userId, amount);\r\n } catch (error) {\r\n // A rejected amount is a caller bug, not a balance problem, and must not\r\n // be reshaped into `{ success: false, reason: 'insufficient' }`.\r\n if (isCreditError(error) && error.code === CreditErrorCode.INVALID_AMOUNT) throw error;\r\n\r\n // The repository layer throws on insufficient funds (and on missing\r\n // user docs, which we surface as unavailable rather than a shortfall).\r\n const credits = await this.repository.getUserCredits(userId);\r\n const available = credits\r\n ? sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved)\r\n : 0;\r\n\r\n if (available >= amount) {\r\n // Not an insufficient-credits condition (e.g. missing user record) —\r\n // this is unexpected, so let the original error propagate.\r\n throw error;\r\n }\r\n\r\n return {\r\n success: false,\r\n reason: \"insufficient\",\r\n available,\r\n required: amount,\r\n shortfall: sumAmounts(amount, -available),\r\n };\r\n }\r\n\r\n const operationType = options?.operationType;\r\n await this.repository.createJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount,\r\n balanceAfter: deduction.newBalance,\r\n source: \"operation_commit\",\r\n referenceId: options?.requestId ?? `deduct-${Date.now()}`,\r\n referenceType: \"adjustment\",\r\n description: operationType\r\n ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}`\r\n : `Deducted ${amount} credits`,\r\n metadata: {\r\n ...options?.metadata,\r\n ...(operationType && { operationType }),\r\n },\r\n });\r\n\r\n if (operationType) {\r\n await this.logUsage({\r\n userId,\r\n operationType,\r\n provider: \"gemini\",\r\n creditsUsed: amount,\r\n success: true,\r\n resourceId: options?.resourceId,\r\n resourceType: options?.resourceType,\r\n requestId: options?.requestId,\r\n metadata: options?.metadata,\r\n });\r\n }\r\n\r\n // Trigger low balance notification (non-blocking) — same hook commitCredits uses.\r\n if (this.lowBalanceCallback) {\r\n this.lowBalanceCallback(userId, deduction.newBalance).catch((error) => {\r\n console.error(\"[Credits] Failed to send low balance notification:\", error);\r\n });\r\n }\r\n\r\n return { success: true, newBalance: deduction.newBalance };\r\n }\r\n\r\n /**\r\n * Update user subscription tier\r\n * @param userId - User ID\r\n * @param tier - New subscription tier\r\n * @param expiresAt - Subscription expiry date (optional)\r\n */\r\n async updateTier(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n expiresAt?: Date\r\n ): Promise<void> {\r\n const monthlyLimit = getConfigMonthlyLimit(tier);\r\n const unlimited = isUnlimitedTier(tier);\r\n const free = isFreeTier(tier);\r\n\r\n await this.repository.updateUserTier(userId, {\r\n tier,\r\n // Through the shared contract, like every path that resolves the tier's\r\n // limit from configuration. (The repository's `updateUserCredits` and\r\n // `updateUserTier` still store a caller-supplied `monthlyLimit` verbatim;\r\n // they are given a value, not a tier, so there is no sentinel to map.)\r\n // This used to store 0 for an unlimited tier under a \"0 means unlimited\"\r\n // convention that nothing actually implemented — no read path anywhere in\r\n // the library branches on it — so upgrading a user to unlimited gave them\r\n // an allowance of zero.\r\n monthlyLimit: storedMonthlyLimit(monthlyLimit),\r\n // Reset balance to new tier limit if upgrading\r\n balance: !free\r\n ? (unlimited ? getUnlimitedSentinelBalance() : monthlyLimit)\r\n : undefined,\r\n monthlyUsed: !free ? 0 : undefined,\r\n subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null,\r\n });\r\n }\r\n\r\n /**\r\n * Get usage logs with optional filtering\r\n * @param userId - Optional user ID filter\r\n * @param limit - Max results\r\n * @param offset - Skip results\r\n * @returns List of usage logs\r\n */\r\n async getUsageLogs(\r\n userId?: string,\r\n limit = 50,\r\n offset = 0\r\n ): Promise<PortableUsageLog[]> {\r\n return this.repository.getUsageLogs({ userId, limit, offset });\r\n }\r\n\r\n /**\r\n * Get user-friendly usage history\r\n * Combines journal entries into a user-facing format\r\n *\r\n * @param userId - User ID\r\n * @param limit - Max results per page\r\n * @param offset - Skip results for pagination\r\n * @returns Paginated usage history response\r\n */\r\n async getUsageHistory(\r\n userId: string,\r\n limit = 20,\r\n offset = 0\r\n ): Promise<UsageHistoryResponse> {\r\n // Get journal entries\r\n const [entries, total] = await Promise.all([\r\n this.repository.getJournalEntries({ userId, limit, offset }),\r\n this.repository.getJournalEntriesCount({ userId }),\r\n ]);\r\n\r\n // Convert journal entries to user-friendly format\r\n const historyEntries: UsageHistoryEntry[] = entries.map((entry) => {\r\n const type = this.mapSourceToHistoryType(entry.source);\r\n const creditsChange = entry.entryType === \"credit\" ? entry.amount : -entry.amount;\r\n\r\n return {\r\n id: entry.id,\r\n type,\r\n creditsChange,\r\n balanceAfter: entry.balanceAfter,\r\n description: entry.description,\r\n createdAt: typeof entry.createdAt === \"string\"\r\n ? entry.createdAt\r\n : toDate(entry.createdAt).toISOString(),\r\n };\r\n });\r\n\r\n return {\r\n entries: historyEntries,\r\n pagination: {\r\n total,\r\n limit,\r\n offset,\r\n hasMore: offset + entries.length < total,\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * Map journal entry source to user-friendly history type\r\n */\r\n private mapSourceToHistoryType(\r\n source: PortableJournalEntry[\"source\"]\r\n ): UsageHistoryEntry[\"type\"] {\r\n switch (source) {\r\n case \"operation_commit\":\r\n case \"reservation_expired\":\r\n return \"usage\";\r\n case \"purchase\":\r\n return \"purchase\";\r\n case \"subscription_grant\":\r\n case \"subscription_upgrade\":\r\n case \"bonus\":\r\n return \"bonus\";\r\n case \"monthly_reset\":\r\n return \"reset\";\r\n case \"refund\":\r\n case \"operation_release\":\r\n return \"refund\";\r\n case \"admin_adjustment\":\r\n case \"subscription_downgrade\":\r\n case \"expiry\":\r\n default:\r\n return \"adjustment\";\r\n }\r\n }\r\n\r\n /**\r\n * Get journal entries directly (for admin or debugging)\r\n * @param query - Journal entry query parameters\r\n * @returns List of journal entries\r\n */\r\n async getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]> {\r\n return this.repository.getJournalEntries(query);\r\n }\r\n\r\n /**\r\n * Get the underlying repository (for advanced use cases)\r\n */\r\n getRepository(): ICreditRepository {\r\n return this.repository;\r\n }\r\n}\r\n\r\n/**\r\n * Create a credits service with a repository\r\n */\r\nexport function createCreditsService(repository: ICreditRepository): CreditsService {\r\n return new CreditsService(repository);\r\n}\r\n","/**\r\n * Generic credits adapter - framework agnostic\r\n *\r\n * This adapter uses standard JavaScript APIs and works in any\r\n * JavaScript environment (Node.js, browser, etc.).\r\n */\r\n\r\nimport type { PortableReservation, WithCreditsOptions } from \"../core/types.js\";\r\nimport type { ActionResult, CreditsAdapter, CreditsAdapterConfig, CreditActionHandler } from \"./types.js\";\r\nimport {\r\n commitReservationWithJournal,\r\n releaseReservationWithJournal,\r\n reserveCreditsForOperation,\r\n} from \"../core/operations.js\";\r\n\r\n/**\r\n * Generate a unique request ID\r\n */\r\nfunction generateRequestId(): string {\r\n const timestamp = Date.now();\r\n const randomPart = Math.random().toString(36).substring(2, 14);\r\n return `req_${timestamp}_${randomPart}`;\r\n}\r\n\r\n/**\r\n * Create a generic credits adapter\r\n *\r\n * This adapter:\r\n * - Authenticates users via the provided auth provider\r\n * - Reserves credits before action execution\r\n * - Commits credits on success, releases on failure\r\n * - Runs an operation configured with a cost of 0 without touching the ledger\r\n * - Logs usage asynchronously via the deferred executor\r\n */\r\nexport function createGenericAdapter(config: CreditsAdapterConfig): CreditsAdapter {\r\n const { repository, authProvider, deferred, operationCosts } = config;\r\n\r\n function getOperationCost(operationType: string): number {\r\n const cost = operationCosts[operationType];\r\n if (cost === undefined) {\r\n throw new Error(\r\n `Unknown operation type: \"${operationType}\". ` +\r\n `Configured types: ${Object.keys(operationCosts).join(\", \")}`\r\n );\r\n }\r\n return cost;\r\n }\r\n\r\n function logUsage(params: {\r\n userId: string;\r\n operationType: string;\r\n creditsUsed: number;\r\n success: boolean;\r\n errorMessage?: string;\r\n resourceId?: string;\r\n resourceType?: string;\r\n requestId: string;\r\n }): void {\r\n deferred.defer(async () => {\r\n await repository.logUsage({\r\n userId: params.userId,\r\n operationType: params.operationType,\r\n provider: \"gemini\",\r\n creditsUsed: params.creditsUsed,\r\n success: params.success,\r\n errorMessage: params.errorMessage,\r\n resourceId: params.resourceId,\r\n resourceType: params.resourceType,\r\n requestId: params.requestId,\r\n });\r\n });\r\n }\r\n\r\n return {\r\n withCredits<TInput, TOutput>(\r\n options: WithCreditsOptions,\r\n handler: CreditActionHandler<TInput, TOutput>\r\n ): (data: TInput) => Promise<ActionResult<TOutput>> {\r\n return async (data: TInput): Promise<ActionResult<TOutput>> => {\r\n const requestId = generateRequestId();\r\n\r\n // Authenticate\r\n const user = await authProvider.getCurrentUser();\r\n if (!user?.id) {\r\n return { success: false, error: \"Authentication required\" };\r\n }\r\n\r\n // Handle preview mode - skip credit handling entirely\r\n if (isPreviewMode(data)) {\r\n const dummyReservation = createDummyReservation(user.id, options.operationType);\r\n return handler(user, data, dummyReservation);\r\n }\r\n\r\n // Calculate cost and reserve credits\r\n let reservation: PortableReservation;\r\n let isFree = false;\r\n try {\r\n const cost = options.customCost ?? getOperationCost(options.operationType);\r\n // A reservation row must hold a positive amount, so a free operation\r\n // has no hold to place, commit or release.\r\n isFree = options.customCost === undefined && cost === 0;\r\n reservation = isFree\r\n ? { ...createDummyReservation(user.id, options.operationType), id: \"free-operation\" }\r\n : await reserveCreditsForOperation(\r\n repository,\r\n user.id,\r\n cost,\r\n options.operationType\r\n );\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"Failed to reserve credits\";\r\n logUsage({\r\n userId: user.id,\r\n operationType: options.operationType,\r\n creditsUsed: 0,\r\n success: false,\r\n errorMessage: message,\r\n resourceId: options.resourceId,\r\n resourceType: options.resourceType,\r\n requestId,\r\n });\r\n return { success: false, error: message };\r\n }\r\n\r\n // Execute action\r\n try {\r\n const result = await handler(user, data, reservation);\r\n\r\n if (result.success) {\r\n if (!isFree) await commitReservationWithJournal(repository, user.id, reservation.id);\r\n logUsage({\r\n userId: user.id,\r\n operationType: options.operationType,\r\n creditsUsed: reservation.amount,\r\n success: true,\r\n resourceId: options.resourceId,\r\n resourceType: options.resourceType,\r\n requestId,\r\n });\r\n } else {\r\n if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);\r\n logUsage({\r\n userId: user.id,\r\n operationType: options.operationType,\r\n creditsUsed: 0,\r\n success: false,\r\n errorMessage: result.error,\r\n resourceId: options.resourceId,\r\n resourceType: options.resourceType,\r\n requestId,\r\n });\r\n }\r\n\r\n return result;\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"An unexpected error occurred\";\r\n if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);\r\n logUsage({\r\n userId: user.id,\r\n operationType: options.operationType,\r\n creditsUsed: 0,\r\n success: false,\r\n errorMessage: message,\r\n resourceId: options.resourceId,\r\n resourceType: options.resourceType,\r\n requestId,\r\n });\r\n console.error(\"Action error:\", error);\r\n return { success: false, error: message };\r\n }\r\n };\r\n },\r\n };\r\n}\r\n\r\nfunction isPreviewMode(data: unknown): boolean {\r\n return (\r\n typeof data === \"object\" &&\r\n data !== null &&\r\n \"preview\" in data &&\r\n (data as Record<string, unknown>).preview === true\r\n );\r\n}\r\n\r\nfunction createDummyReservation(userId: string, operationType: string): PortableReservation {\r\n return {\r\n id: \"preview-mode\",\r\n userId,\r\n amount: 0,\r\n operationType,\r\n status: \"released\",\r\n createdAt: new Date().toISOString(),\r\n expiresAt: new Date().toISOString(),\r\n };\r\n}\r\n","import { getConfig } from \"../config/index.js\";\r\n\r\n/**\r\n * Notification event types\r\n */\r\nexport type CreditNotificationEvent =\r\n | {\r\n type: \"low_balance\";\r\n userId: string;\r\n balance: number;\r\n threshold: number;\r\n }\r\n | {\r\n type: \"balance_depleted\";\r\n userId: string;\r\n }\r\n | {\r\n type: \"subscription_expiring\";\r\n userId: string;\r\n expiresAt: Date;\r\n daysRemaining: number;\r\n }\r\n | {\r\n type: \"subscription_expired\";\r\n userId: string;\r\n wasDowngraded: boolean;\r\n };\r\n\r\n/**\r\n * Notification handler interface\r\n * Implementations can send emails, push notifications, etc.\r\n */\r\nexport interface ICreditNotificationHandler {\r\n /**\r\n * Handle a notification event\r\n * @param event - The notification event\r\n */\r\n handleNotification(event: CreditNotificationEvent): Promise<void>;\r\n}\r\n\r\n/**\r\n * Notification cooldown tracker\r\n * Prevents spam by tracking last notification time per user per event type\r\n */\r\ninterface NotificationCooldownState {\r\n lastLowBalanceNotification?: Date;\r\n lastDepletedNotification?: Date;\r\n lastExpiringNotification?: Date;\r\n}\r\n\r\n/**\r\n * In-memory cooldown tracker\r\n * In production, this should be stored in a persistent store\r\n */\r\nconst cooldownState = new Map<string, NotificationCooldownState>();\r\n\r\n/**\r\n * Check if a notification is allowed (not in cooldown)\r\n */\r\nfunction isNotificationAllowed(\r\n userId: string,\r\n eventType: \"lowBalance\" | \"depleted\" | \"expiring\"\r\n): boolean {\r\n const state = cooldownState.get(userId);\r\n if (!state) return true;\r\n\r\n const now = Date.now();\r\n let lastNotification: Date | undefined;\r\n\r\n switch (eventType) {\r\n case \"lowBalance\":\r\n lastNotification = state.lastLowBalanceNotification;\r\n break;\r\n case \"depleted\":\r\n lastNotification = state.lastDepletedNotification;\r\n break;\r\n case \"expiring\":\r\n lastNotification = state.lastExpiringNotification;\r\n break;\r\n }\r\n\r\n if (!lastNotification) return true;\r\n return now - lastNotification.getTime() > getConfig().lowBalanceNotificationCooldownHours * 60 * 60 * 1000;\r\n}\r\n\r\n/**\r\n * Record that a notification was sent\r\n */\r\nfunction recordNotification(\r\n userId: string,\r\n eventType: \"lowBalance\" | \"depleted\" | \"expiring\"\r\n): void {\r\n const state = cooldownState.get(userId) || {};\r\n const now = new Date();\r\n\r\n switch (eventType) {\r\n case \"lowBalance\":\r\n state.lastLowBalanceNotification = now;\r\n break;\r\n case \"depleted\":\r\n state.lastDepletedNotification = now;\r\n break;\r\n case \"expiring\":\r\n state.lastExpiringNotification = now;\r\n break;\r\n }\r\n\r\n cooldownState.set(userId, state);\r\n}\r\n\r\n/**\r\n * Default notification handlers registry\r\n * Handlers can be registered at application startup\r\n */\r\nconst notificationHandlers: ICreditNotificationHandler[] = [];\r\n\r\n/**\r\n * Register a notification handler\r\n */\r\nexport function registerNotificationHandler(\r\n handler: ICreditNotificationHandler\r\n): void {\r\n notificationHandlers.push(handler);\r\n}\r\n\r\n/**\r\n * Clear all notification handlers (for testing)\r\n */\r\nexport function clearNotificationHandlers(): void {\r\n notificationHandlers.length = 0;\r\n}\r\n\r\n/**\r\n * Clear cooldown state (for testing)\r\n */\r\nexport function clearCooldownState(): void {\r\n cooldownState.clear();\r\n}\r\n\r\n/**\r\n * Dispatch a notification event to all handlers\r\n * Runs asynchronously without blocking\r\n */\r\nasync function dispatchNotification(\r\n event: CreditNotificationEvent\r\n): Promise<void> {\r\n if (notificationHandlers.length === 0) {\r\n // No handlers registered - log for debugging\r\n console.debug(`[Credits] No notification handlers registered for: ${event.type}`);\r\n return;\r\n }\r\n\r\n // Run handlers in parallel\r\n await Promise.allSettled(\r\n notificationHandlers.map((handler) =>\r\n handler.handleNotification(event).catch((error) => {\r\n console.error(`[Credits] Notification handler error:`, error);\r\n })\r\n )\r\n );\r\n}\r\n\r\n/**\r\n * Check and trigger low balance notification if needed\r\n * Call this after commit operations\r\n *\r\n * @param userId - User ID\r\n * @param newBalance - Balance after the operation\r\n * @param threshold - Custom threshold (defaults to 10)\r\n */\r\nexport async function checkAndNotifyLowBalance(\r\n userId: string,\r\n newBalance: number,\r\n threshold = getConfig().lowBalanceThreshold\r\n): Promise<void> {\r\n // Check if balance is depleted\r\n if (newBalance <= 0 && isNotificationAllowed(userId, \"depleted\")) {\r\n recordNotification(userId, \"depleted\");\r\n await dispatchNotification({\r\n type: \"balance_depleted\",\r\n userId,\r\n });\r\n return; // Don't also send low balance notification\r\n }\r\n\r\n // Check if balance is below threshold\r\n if (newBalance > 0 && newBalance <= threshold && isNotificationAllowed(userId, \"lowBalance\")) {\r\n recordNotification(userId, \"lowBalance\");\r\n await dispatchNotification({\r\n type: \"low_balance\",\r\n userId,\r\n balance: newBalance,\r\n threshold,\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * Trigger subscription expiring notification\r\n *\r\n * @param userId - User ID\r\n * @param expiresAt - When subscription expires\r\n * @param daysRemaining - Days until expiry\r\n */\r\nexport async function notifySubscriptionExpiring(\r\n userId: string,\r\n expiresAt: Date,\r\n daysRemaining: number\r\n): Promise<void> {\r\n if (!isNotificationAllowed(userId, \"expiring\")) {\r\n return;\r\n }\r\n\r\n recordNotification(userId, \"expiring\");\r\n await dispatchNotification({\r\n type: \"subscription_expiring\",\r\n userId,\r\n expiresAt,\r\n daysRemaining,\r\n });\r\n}\r\n\r\n/**\r\n * Trigger subscription expired notification\r\n *\r\n * @param userId - User ID\r\n * @param wasDowngraded - Whether the user was downgraded to free tier\r\n */\r\nexport async function notifySubscriptionExpired(\r\n userId: string,\r\n wasDowngraded: boolean\r\n): Promise<void> {\r\n await dispatchNotification({\r\n type: \"subscription_expired\",\r\n userId,\r\n wasDowngraded,\r\n });\r\n}\r\n\r\n/**\r\n * Console notification handler for development/testing\r\n * Logs notifications to console\r\n */\r\nexport class ConsoleNotificationHandler implements ICreditNotificationHandler {\r\n async handleNotification(event: CreditNotificationEvent): Promise<void> {\r\n switch (event.type) {\r\n case \"low_balance\":\r\n console.log(\r\n `[Credits Notification] Low balance alert for user ${event.userId}: ` +\r\n `${event.balance} credits remaining (threshold: ${event.threshold})`\r\n );\r\n break;\r\n case \"balance_depleted\":\r\n console.log(\r\n `[Credits Notification] Balance depleted for user ${event.userId}`\r\n );\r\n break;\r\n case \"subscription_expiring\":\r\n console.log(\r\n `[Credits Notification] Subscription expiring for user ${event.userId}: ` +\r\n `${event.daysRemaining} days remaining`\r\n );\r\n break;\r\n case \"subscription_expired\":\r\n console.log(\r\n `[Credits Notification] Subscription expired for user ${event.userId}: ` +\r\n `downgraded=${event.wasDowngraded}`\r\n );\r\n break;\r\n }\r\n }\r\n}\r\n","/**\r\n * SDK types for the credits system\r\n *\r\n * These types are used by the client SDKs for REST API consumption.\r\n */\r\n\r\n// Re-export core types for SDK consumers\r\nexport type {\r\n PortableUserCredits as UserCredits,\r\n PortableReservation as CreditReservation,\r\n CreditCheckResult,\r\n SubscriptionTier,\r\n} from \"../core/types.js\";\r\n\r\n/**\r\n * Configuration for CreditsClient\r\n */\r\nexport interface CreditsClientConfig {\r\n /** Base URL of the credits API (e.g., \"https://api.example.com\") */\r\n baseUrl: string;\r\n /** Function to get the auth token for session-based auth */\r\n getAuthToken?: () => Promise<string>;\r\n /** Optional custom fetch function for testing */\r\n fetch?: typeof fetch;\r\n}\r\n\r\n/**\r\n * Configuration for AdminCreditsClient\r\n */\r\nexport interface AdminCreditsClientConfig {\r\n /** Base URL of the credits API */\r\n baseUrl: string;\r\n /** Admin API key */\r\n apiKey: string;\r\n /** Optional custom fetch function for testing */\r\n fetch?: typeof fetch;\r\n}\r\n\r\n/**\r\n * Result of a credit reservation\r\n */\r\nexport interface ReservationResult {\r\n /** The reservation ID */\r\n reservationId: string;\r\n /** Amount of credits reserved */\r\n amount: number;\r\n /** When the reservation expires (ISO 8601) */\r\n expiresAt: string;\r\n}\r\n\r\n/**\r\n * Pagination options for API requests\r\n */\r\nexport interface PaginationOptions {\r\n /** Page number (1-indexed) */\r\n page?: number;\r\n /** Number of items per page */\r\n limit?: number;\r\n}\r\n\r\n/**\r\n * Usage history entry\r\n */\r\nexport interface UsageHistoryEntry {\r\n /** Unique ID of the usage log */\r\n id: string;\r\n /** Type of operation */\r\n operationType: string;\r\n /** AI provider used */\r\n provider: string;\r\n /** Credits consumed */\r\n creditsUsed: number;\r\n /** Whether the operation was successful */\r\n success: boolean;\r\n /** Error message if unsuccessful */\r\n errorMessage?: string;\r\n /** ID of the related resource */\r\n resourceId?: string;\r\n /** Type of the related resource */\r\n resourceType?: string;\r\n /** When the operation occurred (ISO 8601) */\r\n createdAt: string;\r\n}\r\n\r\n/**\r\n * Response from usage history endpoint\r\n */\r\nexport interface UsageHistoryResponse {\r\n /** List of usage entries */\r\n entries: UsageHistoryEntry[];\r\n /** Whether there are more results */\r\n hasMore: boolean;\r\n /** Total count (if available) */\r\n total?: number;\r\n}\r\n\r\n/**\r\n * Generic API response wrapper\r\n */\r\nexport interface ApiResponse<T> {\r\n success: boolean;\r\n data?: T;\r\n error?: {\r\n code: string;\r\n message: string;\r\n details?: unknown;\r\n };\r\n}\r\n\r\n/**\r\n * SDK error codes\r\n */\r\nexport const SDKErrorCode = {\r\n NETWORK_ERROR: \"NETWORK_ERROR\",\r\n AUTHENTICATION_ERROR: \"AUTHENTICATION_ERROR\",\r\n AUTHORIZATION_ERROR: \"AUTHORIZATION_ERROR\",\r\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\r\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\r\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\r\n VALIDATION_ERROR: \"VALIDATION_ERROR\",\r\n SERVER_ERROR: \"SERVER_ERROR\",\r\n UNKNOWN_ERROR: \"UNKNOWN_ERROR\",\r\n} as const;\r\n\r\nexport type SDKErrorCodeType = (typeof SDKErrorCode)[keyof typeof SDKErrorCode];\r\n","/**\r\n * SDK error classes\r\n *\r\n * Provides typed error handling for SDK operations.\r\n */\r\n\r\nimport { SDKErrorCode, type SDKErrorCodeType } from \"./types.js\";\r\n\r\n/**\r\n * Base error class for SDK errors\r\n */\r\nexport class CreditsSDKError extends Error {\r\n public readonly code: SDKErrorCodeType;\r\n public readonly details?: Record<string, unknown>;\r\n public readonly statusCode?: number;\r\n\r\n constructor(\r\n message: string,\r\n code: SDKErrorCodeType,\r\n options?: {\r\n details?: Record<string, unknown>;\r\n statusCode?: number;\r\n cause?: Error;\r\n }\r\n ) {\r\n super(message, { cause: options?.cause });\r\n this.name = \"CreditsSDKError\";\r\n this.code = code;\r\n this.details = options?.details;\r\n this.statusCode = options?.statusCode;\r\n }\r\n\r\n /**\r\n * Check if this is a specific error type\r\n */\r\n is(code: SDKErrorCodeType): boolean {\r\n return this.code === code;\r\n }\r\n\r\n /**\r\n * Create a JSON representation for logging\r\n */\r\n toJSON(): Record<string, unknown> {\r\n return {\r\n name: this.name,\r\n message: this.message,\r\n code: this.code,\r\n details: this.details,\r\n statusCode: this.statusCode,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when a network request fails\r\n */\r\nexport class NetworkError extends CreditsSDKError {\r\n constructor(message: string, cause?: Error) {\r\n super(message, SDKErrorCode.NETWORK_ERROR, { cause });\r\n this.name = \"NetworkError\";\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when authentication fails\r\n */\r\nexport class AuthenticationError extends CreditsSDKError {\r\n constructor(message: string = \"Authentication required\") {\r\n super(message, SDKErrorCode.AUTHENTICATION_ERROR, { statusCode: 401 });\r\n this.name = \"AuthenticationError\";\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when authorization fails\r\n */\r\nexport class AuthorizationError extends CreditsSDKError {\r\n constructor(message: string = \"Access denied\") {\r\n super(message, SDKErrorCode.AUTHORIZATION_ERROR, { statusCode: 403 });\r\n this.name = \"AuthorizationError\";\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when credits are insufficient\r\n */\r\nexport class InsufficientCreditsError extends CreditsSDKError {\r\n public readonly available: number;\r\n public readonly required: number;\r\n\r\n constructor(available: number, required: number) {\r\n super(\r\n `Insufficient credits: available ${available}, required ${required}`,\r\n SDKErrorCode.INSUFFICIENT_CREDITS,\r\n {\r\n statusCode: 402,\r\n details: { available, required },\r\n }\r\n );\r\n this.name = \"InsufficientCreditsError\";\r\n this.available = available;\r\n this.required = required;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when a reservation is not found\r\n */\r\nexport class ReservationNotFoundError extends CreditsSDKError {\r\n public readonly reservationId: string;\r\n\r\n constructor(reservationId: string) {\r\n super(\r\n `Reservation not found: ${reservationId}`,\r\n SDKErrorCode.RESERVATION_NOT_FOUND,\r\n {\r\n statusCode: 404,\r\n details: { reservationId },\r\n }\r\n );\r\n this.name = \"ReservationNotFoundError\";\r\n this.reservationId = reservationId;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when a reservation has expired\r\n */\r\nexport class ReservationExpiredError extends CreditsSDKError {\r\n public readonly reservationId: string;\r\n\r\n constructor(reservationId: string) {\r\n super(\r\n `Reservation expired: ${reservationId}`,\r\n SDKErrorCode.RESERVATION_EXPIRED,\r\n {\r\n statusCode: 410,\r\n details: { reservationId },\r\n }\r\n );\r\n this.name = \"ReservationExpiredError\";\r\n this.reservationId = reservationId;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown for validation failures\r\n */\r\nexport class ValidationError extends CreditsSDKError {\r\n public readonly field?: string;\r\n\r\n constructor(message: string, field?: string) {\r\n super(message, SDKErrorCode.VALIDATION_ERROR, {\r\n statusCode: 400,\r\n details: field ? { field } : undefined,\r\n });\r\n this.name = \"ValidationError\";\r\n this.field = field;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown for server errors\r\n */\r\nexport class ServerError extends CreditsSDKError {\r\n constructor(message: string = \"Internal server error\") {\r\n super(message, SDKErrorCode.SERVER_ERROR, { statusCode: 500 });\r\n this.name = \"ServerError\";\r\n }\r\n}\r\n\r\n/**\r\n * Parse an API error response into the appropriate SDK error\r\n */\r\nexport function parseApiError(\r\n statusCode: number,\r\n body: { error?: { code?: string; message?: string; details?: unknown } }\r\n): CreditsSDKError {\r\n const errorCode = body.error?.code || \"\";\r\n const errorMessage = body.error?.message || \"An error occurred\";\r\n const details = body.error?.details as Record<string, unknown> | undefined;\r\n\r\n switch (statusCode) {\r\n case 401:\r\n return new AuthenticationError(errorMessage);\r\n case 403:\r\n return new AuthorizationError(errorMessage);\r\n case 402:\r\n // Parse insufficient credits details\r\n if (details?.available !== undefined && details?.required !== undefined) {\r\n return new InsufficientCreditsError(\r\n details.available as number,\r\n details.required as number\r\n );\r\n }\r\n return new CreditsSDKError(errorMessage, SDKErrorCode.INSUFFICIENT_CREDITS, {\r\n statusCode,\r\n details,\r\n });\r\n case 404:\r\n if (errorCode === \"RESERVATION_NOT_FOUND\" && details?.reservationId) {\r\n return new ReservationNotFoundError(details.reservationId as string);\r\n }\r\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_NOT_FOUND, {\r\n statusCode,\r\n details,\r\n });\r\n case 410:\r\n if (errorCode === \"RESERVATION_EXPIRED\" && details?.reservationId) {\r\n return new ReservationExpiredError(details.reservationId as string);\r\n }\r\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_EXPIRED, {\r\n statusCode,\r\n details,\r\n });\r\n case 400:\r\n case 422:\r\n return new ValidationError(errorMessage, details?.field as string | undefined);\r\n case 500:\r\n default:\r\n if (statusCode >= 500) {\r\n return new ServerError(errorMessage);\r\n }\r\n return new CreditsSDKError(errorMessage, SDKErrorCode.UNKNOWN_ERROR, {\r\n statusCode,\r\n details,\r\n });\r\n }\r\n}\r\n","/**\r\n * CreditsClient - REST API client for the credits system\r\n *\r\n * Provides a type-safe interface for consuming credits API endpoints\r\n * from external services or other applications.\r\n *\r\n * @example\r\n * ```typescript\r\n * const client = new CreditsClient({\r\n * baseUrl: process.env.CREDITS_API_URL,\r\n * getAuthToken: async () => session.accessToken,\r\n * });\r\n *\r\n * // Check if user has sufficient credits\r\n * const check = await client.checkAvailability(\"story_generation\");\r\n * if (!check.hasCredits) {\r\n * console.log(\"Insufficient credits\");\r\n * return;\r\n * }\r\n *\r\n * // Two-phase commit pattern\r\n * const reservation = await client.reserve(\"story_generation\");\r\n * try {\r\n * const result = await doExpensiveWork();\r\n * await client.commit(reservation.reservationId);\r\n * return result;\r\n * } catch (error) {\r\n * await client.release(reservation.reservationId);\r\n * throw error;\r\n * }\r\n * ```\r\n */\r\n\r\nimport type {\r\n CreditsClientConfig,\r\n UserCredits,\r\n CreditCheckResult,\r\n ReservationResult,\r\n UsageHistoryResponse,\r\n PaginationOptions,\r\n} from \"./types.js\";\r\nimport { parseApiError, NetworkError } from \"./errors.js\";\r\nimport { sumAmounts } from \"../core/amount.js\";\r\n\r\nexport class CreditsClient {\r\n private readonly baseUrl: string;\r\n private readonly getAuthToken?: () => Promise<string>;\r\n private readonly fetchFn: typeof fetch;\r\n\r\n constructor(config: CreditsClientConfig) {\r\n // Ensure baseUrl doesn't have trailing slash\r\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\r\n this.getAuthToken = config.getAuthToken;\r\n this.fetchFn = config.fetch ?? fetch;\r\n }\r\n\r\n /**\r\n * Make an authenticated request to the API\r\n */\r\n private async request<T>(\r\n path: string,\r\n options: {\r\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\r\n body?: unknown;\r\n params?: Record<string, string | number | undefined>;\r\n } = {}\r\n ): Promise<T> {\r\n const { method = \"GET\", body, params } = options;\r\n\r\n // Build URL with query params\r\n let url = `${this.baseUrl}${path}`;\r\n if (params) {\r\n const searchParams = new URLSearchParams();\r\n for (const [key, value] of Object.entries(params)) {\r\n if (value !== undefined) {\r\n searchParams.set(key, String(value));\r\n }\r\n }\r\n const queryString = searchParams.toString();\r\n if (queryString) {\r\n url += `?${queryString}`;\r\n }\r\n }\r\n\r\n // Build headers\r\n const headers: Record<string, string> = {\r\n \"Content-Type\": \"application/json\",\r\n };\r\n\r\n // Add auth token if available\r\n if (this.getAuthToken) {\r\n try {\r\n const token = await this.getAuthToken();\r\n if (token) {\r\n headers[\"Authorization\"] = `Bearer ${token}`;\r\n }\r\n } catch {\r\n // Auth token not available, proceed without it\r\n }\r\n }\r\n\r\n // Make the request\r\n let response: Response;\r\n try {\r\n response = await this.fetchFn(url, {\r\n method,\r\n headers,\r\n body: body ? JSON.stringify(body) : undefined,\r\n });\r\n } catch (error) {\r\n throw new NetworkError(\r\n error instanceof Error ? error.message : \"Network request failed\",\r\n error instanceof Error ? error : undefined\r\n );\r\n }\r\n\r\n // Parse response\r\n const responseBody = (await response.json()) as {\r\n data?: T;\r\n error?: { code?: string; message?: string; details?: unknown };\r\n };\r\n\r\n // Handle errors\r\n if (!response.ok) {\r\n throw parseApiError(response.status, responseBody);\r\n }\r\n\r\n // Return data\r\n return responseBody.data as T;\r\n }\r\n\r\n /**\r\n * Get the current user's credit balance\r\n *\r\n * @returns User's credit information\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async getBalance(): Promise<UserCredits> {\r\n return this.request<UserCredits>(\"/api/v1/credits/balance\");\r\n }\r\n\r\n /**\r\n * Check if the user has sufficient credits for an operation\r\n *\r\n * @param operationType - Type of operation to check\r\n * @param customCost - Optional custom cost to check against\r\n * @returns Credit availability status\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async checkAvailability(\r\n operationType: string,\r\n customCost?: number\r\n ): Promise<CreditCheckResult> {\r\n return this.request<CreditCheckResult>(\"/api/v1/credits/availability\", {\r\n params: {\r\n operationType,\r\n customCost: customCost?.toString(),\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Reserve credits for an operation\r\n *\r\n * This is the first step in the two-phase commit pattern.\r\n * Reserved credits are held for 5 minutes before expiring.\r\n *\r\n * @param operationType - Type of operation\r\n * @param options - Optional reservation options\r\n * @returns Reservation details including ID and expiry\r\n * @throws InsufficientCreditsError if not enough credits\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async reserve(\r\n operationType: string,\r\n options?: {\r\n customCost?: number;\r\n resourceId?: string;\r\n resourceType?: string;\r\n }\r\n ): Promise<ReservationResult> {\r\n return this.request<ReservationResult>(\"/api/v1/credits/reserve\", {\r\n method: \"POST\",\r\n body: {\r\n operationType,\r\n ...options,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Commit a reservation (deduct credits)\r\n *\r\n * This is the second step in the two-phase commit pattern.\r\n * Call this after your operation succeeds.\r\n *\r\n * @param reservationId - ID of the reservation to commit\r\n * @throws ReservationNotFoundError if reservation doesn't exist\r\n * @throws ReservationExpiredError if reservation has expired\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async commit(reservationId: string): Promise<void> {\r\n await this.request<{ message: string }>(\"/api/v1/credits/commit\", {\r\n method: \"POST\",\r\n body: { reservationId },\r\n });\r\n }\r\n\r\n /**\r\n * Release a reservation (refund credits)\r\n *\r\n * Call this if your operation fails after reserving credits.\r\n *\r\n * @param reservationId - ID of the reservation to release\r\n * @throws ReservationNotFoundError if reservation doesn't exist\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async release(reservationId: string): Promise<void> {\r\n await this.request<{ message: string }>(\"/api/v1/credits/release\", {\r\n method: \"POST\",\r\n body: { reservationId },\r\n });\r\n }\r\n\r\n /**\r\n * Get usage history for the current user\r\n *\r\n * @param options - Optional pagination options\r\n * @returns Paginated list of usage entries\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async getHistory(options?: PaginationOptions): Promise<UsageHistoryResponse> {\r\n return this.request<UsageHistoryResponse>(\"/api/v1/credits/history\", {\r\n params: {\r\n page: options?.page,\r\n limit: options?.limit,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Get the available credits (balance + bonus - reserved)\r\n *\r\n * Convenience method that calculates available credits from balance.\r\n *\r\n * @returns Number of available credits\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async getAvailableCredits(): Promise<number> {\r\n const balance = await this.getBalance();\r\n return sumAmounts(balance.balance, balance.bonusCredits, -balance.reserved);\r\n }\r\n\r\n /**\r\n * Execute an operation with automatic credit handling\r\n *\r\n * This is a convenience method that handles the full two-phase commit\r\n * pattern automatically.\r\n *\r\n * @param operationType - Type of operation\r\n * @param operation - The operation to execute\r\n * @param options - Optional reservation options\r\n * @returns Result of the operation\r\n * @throws InsufficientCreditsError if not enough credits\r\n * @throws AuthenticationError if not authenticated\r\n *\r\n * @example\r\n * ```typescript\r\n * const result = await client.withCredits(\r\n * \"story_generation\",\r\n * async () => {\r\n * return generateStory(data);\r\n * }\r\n * );\r\n * ```\r\n */\r\n async withCredits<T>(\r\n operationType: string,\r\n operation: () => Promise<T>,\r\n options?: {\r\n customCost?: number;\r\n resourceId?: string;\r\n resourceType?: string;\r\n }\r\n ): Promise<T> {\r\n const reservation = await this.reserve(operationType, options);\r\n\r\n try {\r\n const result = await operation();\r\n await this.commit(reservation.reservationId);\r\n return result;\r\n } catch (error) {\r\n // Always attempt to release on failure\r\n try {\r\n await this.release(reservation.reservationId);\r\n } catch {\r\n // Ignore release errors, original error is more important\r\n }\r\n throw error;\r\n }\r\n }\r\n}\r\n","/**\r\n * AdminCreditsClient - Admin REST API client for the credits system\r\n *\r\n * Provides administrative access to the credits system for managing\r\n * user credits, configurations, and subscriptions.\r\n *\r\n * @example\r\n * ```typescript\r\n * const adminClient = new AdminCreditsClient({\r\n * baseUrl: process.env.CREDITS_API_URL,\r\n * apiKey: process.env.CREDITS_ADMIN_API_KEY,\r\n * });\r\n *\r\n * // Add credits to a user\r\n * await adminClient.addCredits(\"user-123\", 100, \"Support credit grant\");\r\n *\r\n * // Update subscription tier\r\n * await adminClient.updateSubscription(\"user-123\", \"premium\", \"2027-01-01T00:00:00Z\");\r\n *\r\n * // Update operation costs\r\n * await adminClient.updateCosts({\r\n * story_generation: 8,\r\n * image_generation: 12,\r\n * });\r\n * ```\r\n */\r\n\r\nimport type { AdminCreditsClientConfig, UserCredits } from \"./types.js\";\r\nimport { parseApiError, NetworkError } from \"./errors.js\";\r\n\r\n/**\r\n * Credits configuration returned by the API\r\n */\r\nexport interface CreditsConfig {\r\n operationCosts: Record<string, number>;\r\n tierConfigs: Record<\r\n string,\r\n {\r\n monthlyLimit: number;\r\n features: string[];\r\n }\r\n >;\r\n}\r\n\r\n/**\r\n * Response from listing users\r\n */\r\nexport interface ListUsersResponse {\r\n users: Array<{\r\n userId: string;\r\n balance: number;\r\n bonusCredits?: number;\r\n tier: string;\r\n monthlyUsed?: number;\r\n }>;\r\n hasMore: boolean;\r\n total?: number;\r\n}\r\n\r\n/**\r\n * Options for listing users\r\n */\r\nexport interface ListUsersOptions {\r\n page?: number;\r\n limit?: number;\r\n tier?: string;\r\n search?: string;\r\n}\r\n\r\nexport class AdminCreditsClient {\r\n private readonly baseUrl: string;\r\n private readonly apiKey: string;\r\n private readonly fetchFn: typeof fetch;\r\n\r\n constructor(config: AdminCreditsClientConfig) {\r\n // Ensure baseUrl doesn't have trailing slash\r\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\r\n this.apiKey = config.apiKey;\r\n this.fetchFn = config.fetch ?? fetch;\r\n }\r\n\r\n /**\r\n * Make an authenticated admin request to the API\r\n */\r\n private async request<T>(\r\n path: string,\r\n options: {\r\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\r\n body?: unknown;\r\n params?: Record<string, string | number | undefined>;\r\n } = {}\r\n ): Promise<T> {\r\n const { method = \"GET\", body, params } = options;\r\n\r\n // Build URL with query params\r\n let url = `${this.baseUrl}${path}`;\r\n if (params) {\r\n const searchParams = new URLSearchParams();\r\n for (const [key, value] of Object.entries(params)) {\r\n if (value !== undefined) {\r\n searchParams.set(key, String(value));\r\n }\r\n }\r\n const queryString = searchParams.toString();\r\n if (queryString) {\r\n url += `?${queryString}`;\r\n }\r\n }\r\n\r\n // Build headers with admin API key\r\n const headers: Record<string, string> = {\r\n \"Content-Type\": \"application/json\",\r\n \"x-api-key\": this.apiKey,\r\n };\r\n\r\n // Make the request\r\n let response: Response;\r\n try {\r\n response = await this.fetchFn(url, {\r\n method,\r\n headers,\r\n body: body ? JSON.stringify(body) : undefined,\r\n });\r\n } catch (error) {\r\n throw new NetworkError(\r\n error instanceof Error ? error.message : \"Network request failed\",\r\n error instanceof Error ? error : undefined\r\n );\r\n }\r\n\r\n // Parse response\r\n const responseBody = (await response.json()) as {\r\n data?: T;\r\n error?: { code?: string; message?: string; details?: unknown };\r\n };\r\n\r\n // Handle errors\r\n if (!response.ok) {\r\n throw parseApiError(response.status, responseBody);\r\n }\r\n\r\n // Return data\r\n return responseBody.data as T;\r\n }\r\n\r\n /**\r\n * Get a specific user's credits\r\n *\r\n * @param userId - ID of the user\r\n * @returns User's credit information\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async getUserCredits(userId: string): Promise<UserCredits> {\r\n return this.request<UserCredits>(`/api/admin/credits/users/${userId}`);\r\n }\r\n\r\n /**\r\n * Add credits to a user's account\r\n *\r\n * Credits are added to bonusCredits, which don't expire on monthly reset.\r\n *\r\n * @param userId - ID of the user\r\n * @param amount - Amount of credits to add\r\n * @param description - Reason for adding credits\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async addCredits(\r\n userId: string,\r\n amount: number,\r\n description: string\r\n ): Promise<void> {\r\n await this.request<{ message: string; newBalance: number }>(\r\n `/api/admin/credits/users/${userId}/add`,\r\n {\r\n method: \"POST\",\r\n body: { amount, description },\r\n }\r\n );\r\n }\r\n\r\n /**\r\n * Update a user's subscription tier\r\n *\r\n * @param userId - ID of the user\r\n * @param tier - New subscription tier\r\n * @param expiresAt - Optional expiration date (ISO 8601)\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async updateSubscription(\r\n userId: string,\r\n tier: string,\r\n expiresAt?: string\r\n ): Promise<void> {\r\n const body: { tier: string; expiresAt?: string } = { tier };\r\n if (expiresAt) {\r\n body.expiresAt = expiresAt;\r\n }\r\n\r\n await this.request<{ message: string }>(\r\n `/api/admin/credits/users/${userId}/subscription`,\r\n {\r\n method: \"PUT\",\r\n body,\r\n }\r\n );\r\n }\r\n\r\n /**\r\n * Get the current credits configuration\r\n *\r\n * @returns Credits system configuration\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async getConfig(): Promise<CreditsConfig> {\r\n return this.request<CreditsConfig>(\"/api/admin/credits/config\");\r\n }\r\n\r\n /**\r\n * Update operation costs\r\n *\r\n * @param costs - New operation costs (operation type -> cost)\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async updateCosts(costs: Record<string, number>): Promise<void> {\r\n await this.request<{ message: string }>(\"/api/admin/credits/config/costs\", {\r\n method: \"PUT\",\r\n body: { operationCosts: costs },\r\n });\r\n }\r\n\r\n /**\r\n * List all users with their credits\r\n *\r\n * @param options - Pagination and filtering options\r\n * @returns Paginated list of users\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async listUsers(options?: ListUsersOptions): Promise<ListUsersResponse> {\r\n return this.request<ListUsersResponse>(\"/api/admin/credits/users\", {\r\n params: {\r\n page: options?.page,\r\n limit: options?.limit,\r\n tier: options?.tier,\r\n search: options?.search,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Deduct credits from a user's account\r\n *\r\n * @param userId - ID of the user\r\n * @param amount - Amount of credits to deduct\r\n * @param description - Reason for deducting credits\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async deductCredits(\r\n userId: string,\r\n amount: number,\r\n description: string\r\n ): Promise<void> {\r\n await this.request<{ message: string; newBalance: number }>(\r\n `/api/admin/credits/users/${userId}/deduct`,\r\n {\r\n method: \"POST\",\r\n body: { amount, description },\r\n }\r\n );\r\n }\r\n\r\n /**\r\n * Get usage statistics for a user\r\n *\r\n * @param userId - ID of the user\r\n * @param options - Optional date range\r\n * @returns Usage statistics\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async getUserStats(\r\n userId: string,\r\n options?: { startDate?: string; endDate?: string }\r\n ): Promise<{\r\n totalCreditsUsed: number;\r\n operationBreakdown: Record<string, number>;\r\n successRate: number;\r\n }> {\r\n return this.request<{\r\n totalCreditsUsed: number;\r\n operationBreakdown: Record<string, number>;\r\n successRate: number;\r\n }>(`/api/admin/credits/users/${userId}/stats`, {\r\n params: {\r\n startDate: options?.startDate,\r\n endDate: options?.endDate,\r\n },\r\n });\r\n }\r\n}\r\n","import { sumAmounts } from \"../core/amount.js\";\r\n/**\r\n * Credit error utilities\r\n *\r\n * Provides functions to parse and identify credit-related errors\r\n * for consistent error handling across the application.\r\n */\r\n\r\n/**\r\n * Parsed information from a credit error message\r\n */\r\nexport interface CreditErrorInfo {\r\n /** Credits currently available */\r\n available: number;\r\n /** Credits required for the operation */\r\n required: number;\r\n /** Difference between required and available */\r\n shortfall: number;\r\n /** Original error message */\r\n rawMessage: string;\r\n}\r\n\r\n/**\r\n * Parse a credit error message to extract available/required credits\r\n *\r\n * @param error - Error message string\r\n * @returns Parsed credit info or null if not a valid credit error format\r\n *\r\n * @example\r\n * ```ts\r\n * const error = \"Insufficient credits. Available: 3, Required: 10\";\r\n * const info = parseCreditError(error);\r\n * // { available: 3, required: 10, shortfall: 7, rawMessage: \"...\" }\r\n * ```\r\n */\r\nexport function parseCreditError(error: string): CreditErrorInfo | null {\r\n // Pattern: Available: X, Required: Y (case insensitive, flexible whitespace)\r\n const match = error.match(/Available:\\s*(\\d+)\\s*,\\s*Required:\\s*(\\d+)/i);\r\n\r\n if (!match) {\r\n return null;\r\n }\r\n\r\n const available = parseInt(match[1], 10);\r\n const required = parseInt(match[2], 10);\r\n\r\n // Validate parsed numbers\r\n if (isNaN(available) || isNaN(required)) {\r\n return null;\r\n }\r\n\r\n return {\r\n available,\r\n required,\r\n shortfall: sumAmounts(required, -available),\r\n rawMessage: error,\r\n };\r\n}\r\n\r\n/**\r\n * Check if an error message is a credit-related error\r\n *\r\n * @param error - Error message string\r\n * @returns true if the error is credit-related\r\n *\r\n * @example\r\n * ```ts\r\n * if (isCreditError(error)) {\r\n * showCreditErrorDialog(parseCreditError(error));\r\n * }\r\n * ```\r\n */\r\nexport function isCreditErrorMessage(error: string): boolean {\r\n const lowerError = error.toLowerCase();\r\n return (\r\n lowerError.includes(\"insufficient credits\") ||\r\n lowerError.includes(\"not enough credits\")\r\n );\r\n}\r\n","/**\r\n * Credit utility functions\r\n *\r\n * Common helper functions for credit calculations\r\n */\r\n\r\nimport type { PortableReservation } from \"../core/index.js\";\r\nimport { sumAmounts } from \"../core/amount.js\";\r\n\r\nexport {\r\n parseCreditError,\r\n isCreditErrorMessage,\r\n type CreditErrorInfo,\r\n} from \"./error-utils.js\";\r\n\r\n/**\r\n * Calculate total available credits from balance and bonus credits\r\n *\r\n * @param credits - Object containing balance and bonusCredits properties\r\n * @returns Total credits (balance + bonusCredits)\r\n *\r\n * @example\r\n * ```ts\r\n * const total = getTotalCredits({ balance: 100, bonusCredits: 50 });\r\n * // Returns 150\r\n * ```\r\n */\r\nexport function getTotalCredits(credits: {\r\n balance: number;\r\n bonusCredits: number;\r\n}): number {\r\n return sumAmounts(credits.balance, credits.bonusCredits);\r\n}\r\n\r\n/**\r\n * Generate a unique request ID\r\n *\r\n * Format: req_{timestamp}_{randomPart}\r\n * Example: req_1706234567890_a1b2c3d4e5f6\r\n *\r\n * @returns URL-safe request ID string\r\n */\r\nexport function generateRequestId(): string {\r\n const timestamp = Date.now();\r\n const randomPart = Math.random().toString(36).substring(2, 14);\r\n return `req_${timestamp}_${randomPart}`;\r\n}\r\n\r\n/**\r\n * Check if the input data indicates preview mode\r\n *\r\n * Preview mode skips actual credit deduction - used for dry runs\r\n * and UI previews where the user wants to see results without committing.\r\n *\r\n * @param data - The input data to check\r\n * @returns true if data has preview: true\r\n */\r\nexport function isPreviewMode(data: unknown): boolean {\r\n return (\r\n typeof data === \"object\" &&\r\n data !== null &&\r\n \"preview\" in data &&\r\n (data as Record<string, unknown>).preview === true\r\n );\r\n}\r\n\r\n/**\r\n * Create a dummy reservation for preview mode\r\n *\r\n * Used when `isPreviewMode(data)` returns true to provide\r\n * a valid reservation object without actual credit operations.\r\n *\r\n * @param userId - The user ID\r\n * @param operationType - The operation type\r\n * @returns A dummy PortableReservation with id \"preview-mode\"\r\n */\r\nexport function createDummyReservation(\r\n userId: string,\r\n operationType: string\r\n): PortableReservation {\r\n const now = new Date().toISOString();\r\n return {\r\n id: \"preview-mode\",\r\n userId,\r\n amount: 0,\r\n operationType,\r\n status: \"released\",\r\n createdAt: now,\r\n expiresAt: now,\r\n };\r\n}\r\n"],"mappings":"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","names":["cents","toDate","terminalStatusOf","toDate","getOperationCost","generateRequestId","isPreviewMode","createDummyReservation"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/errors.ts","../src/core/amount.ts","../src/core/types.ts","../src/core/outcomes.ts","../src/core/reservation-integrity.ts","../src/core/payment-ref.ts","../src/core/idempotency.ts","../src/core/error-classify.ts","../src/core/deferred.ts","../src/config/index.ts","../src/config/provider.ts","../src/repository/types.ts","../src/repository/flow.ts","../src/core/operations.ts","../src/config/costs.ts","../src/repository/v2-types.ts","../src/repository/utils.ts","../src/repository/memory/snapshot.ts","../src/repository/memory/mutex.ts","../src/repository/memory/store.ts","../src/repository/memory/transition-journal.ts","../src/repository/memory/v2.ts","../src/repository/memory/index.ts","../src/auth/api-key.ts","../src/service/credits-service.ts","../src/adapters/generic.ts","../src/notifications/index.ts","../src/sdk/types.ts","../src/sdk/errors.ts","../src/sdk/client.ts","../src/sdk/admin-client.ts","../src/utils/error-utils.ts","../src/utils/index.ts"],"sourcesContent":["import { sumAmounts } from \"./amount.js\";\r\n/**\r\n * Credit system error classes - framework agnostic\r\n *\r\n * Provides typed error handling for credit operations.\r\n * These errors can be used across different environments.\r\n */\r\n\r\n/**\r\n * Error codes for credit operations\r\n */\r\nexport const CreditErrorCode = {\r\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\r\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\r\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\r\n RESERVATION_ALREADY_PROCESSED: \"RESERVATION_ALREADY_PROCESSED\",\r\n USER_NOT_FOUND: \"USER_NOT_FOUND\",\r\n INVALID_OPERATION_TYPE: \"INVALID_OPERATION_TYPE\",\r\n CONFIGURATION_ERROR: \"CONFIGURATION_ERROR\",\r\n DATABASE_ERROR: \"DATABASE_ERROR\",\r\n /** Caller reused an idempotency key with a different immutable payload. */\r\n IDEMPOTENCY_CONFLICT: \"IDEMPOTENCY_CONFLICT\",\r\n /**\r\n * Retryable infrastructure failure — serialization/deadlock, lock timeout,\r\n * connection loss, pool exhaustion. Distinct from `DATABASE_ERROR`, which is\r\n * a deterministic failure that will fail again identically on retry.\r\n */\r\n TRANSIENT_ERROR: \"TRANSIENT_ERROR\",\r\n /** The configured repository adapter does not implement the requested V2 method. */\r\n UNSUPPORTED_OPERATION: \"UNSUPPORTED_OPERATION\",\r\n /** Amount was not a finite positive number. */\r\n INVALID_AMOUNT: \"INVALID_AMOUNT\",\r\n /** Idempotency key was supplied but empty or whitespace-only. */\r\n INVALID_IDEMPOTENCY_KEY: \"INVALID_IDEMPOTENCY_KEY\",\r\n /**\r\n * A persisted reservation holds a status this library does not define, so no\r\n * transition can reason about it. Quarantined rather than cast; nothing moved.\r\n */\r\n CORRUPT_RESERVATION_STATUS: \"CORRUPT_RESERVATION_STATUS\",\r\n /**\r\n * A persisted reservation does not record that its hold was placed\r\n * atomically, so the credits it claims are not known to be reserved. Moving\r\n * it would spend another hold's coverage; nothing moved.\r\n */\r\n UNBACKED_RESERVATION: \"UNBACKED_RESERVATION\",\r\n} as const;\r\n\r\nexport type CreditErrorCodeType = (typeof CreditErrorCode)[keyof typeof CreditErrorCode];\r\n\r\n/**\r\n * Custom error class for credit operations\r\n *\r\n * Provides structured error information including:\r\n * - Error code for programmatic handling\r\n * - Human-readable message\r\n * - Optional details for debugging\r\n */\r\nexport class CreditError extends Error {\r\n public readonly code: CreditErrorCodeType;\r\n public readonly details?: Record<string, unknown>;\r\n\r\n constructor(\r\n message: string,\r\n code: CreditErrorCodeType,\r\n details?: Record<string, unknown>\r\n ) {\r\n super(message);\r\n this.name = \"CreditError\";\r\n this.code = code;\r\n this.details = details;\r\n\r\n // Maintains proper stack trace for where error was thrown\r\n if (Error.captureStackTrace) {\r\n Error.captureStackTrace(this, CreditError);\r\n }\r\n }\r\n\r\n /**\r\n * Convert to JSON-serializable object\r\n */\r\n toJSON(): { name: string; code: string; message: string; details?: Record<string, unknown> } {\r\n return {\r\n name: this.name,\r\n code: this.code,\r\n message: this.message,\r\n ...(this.details && { details: this.details }),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Type guard to check if an error is a CreditError\r\n */\r\nexport function isCreditError(error: unknown): error is CreditError {\r\n return error instanceof CreditError;\r\n}\r\n\r\n/**\r\n * Check if an error represents insufficient credits\r\n *\r\n * Works with both CreditError and regular Error instances.\r\n * Useful for handling errors from different sources.\r\n */\r\nexport function isInsufficientCreditsError(error: unknown): boolean {\r\n if (isCreditError(error)) {\r\n return error.code === CreditErrorCode.INSUFFICIENT_CREDITS;\r\n }\r\n\r\n if (error instanceof Error) {\r\n return error.message.toLowerCase().includes(\"insufficient credits\");\r\n }\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Create an insufficient credits error\r\n */\r\nexport function createInsufficientCreditsError(\r\n required: number,\r\n available: number\r\n): CreditError {\r\n return new CreditError(\r\n `Insufficient credits: available ${available}, required ${required}`,\r\n CreditErrorCode.INSUFFICIENT_CREDITS,\r\n { required, available, shortfall: sumAmounts(required, -available) }\r\n );\r\n}\r\n\r\n/**\r\n * Create a reservation not found error\r\n */\r\nexport function createReservationNotFoundError(reservationId: string): CreditError {\r\n return new CreditError(\r\n `Reservation ${reservationId} not found`,\r\n CreditErrorCode.RESERVATION_NOT_FOUND,\r\n { reservationId }\r\n );\r\n}\r\n\r\n/**\r\n * Create a reservation expired error\r\n */\r\nexport function createReservationExpiredError(reservationId: string): CreditError {\r\n return new CreditError(\r\n `Reservation ${reservationId} has expired`,\r\n CreditErrorCode.RESERVATION_EXPIRED,\r\n { reservationId }\r\n );\r\n}\r\n\r\n/**\r\n * Create a reservation already processed error\r\n */\r\nexport function createReservationAlreadyProcessedError(\r\n reservationId: string,\r\n status: string\r\n): CreditError {\r\n return new CreditError(\r\n `Reservation ${reservationId} has already been ${status}`,\r\n CreditErrorCode.RESERVATION_ALREADY_PROCESSED,\r\n { reservationId, status }\r\n );\r\n}\r\n\r\n/**\r\n * Create a user not found error\r\n */\r\nexport function createUserNotFoundError(userId: string): CreditError {\r\n return new CreditError(\r\n `User ${userId} not found`,\r\n CreditErrorCode.USER_NOT_FOUND,\r\n { userId }\r\n );\r\n}\r\n\r\n/**\r\n * Create an invalid operation type error\r\n */\r\nexport function createInvalidOperationTypeError(\r\n operationType: string,\r\n validTypes: string[]\r\n): CreditError {\r\n return new CreditError(\r\n `Invalid operation type: ${operationType}. Valid types: ${validTypes.join(\", \")}`,\r\n CreditErrorCode.INVALID_OPERATION_TYPE,\r\n { operationType, validTypes }\r\n );\r\n}\r\n\r\n/**\r\n * Create an idempotency conflict error\r\n *\r\n * Raised when a caller reuses an idempotency key with a different immutable\r\n * payload (different amount or operation type). This is a caller bug, never a\r\n * retryable condition — the details carry both payloads so it can be diffed.\r\n */\r\nexport function createIdempotencyConflictError(\r\n idempotencyKey: string,\r\n details?: Record<string, unknown>\r\n): CreditError {\r\n return new CreditError(\r\n `Idempotency key ${idempotencyKey} was reused with a different payload`,\r\n CreditErrorCode.IDEMPOTENCY_CONFLICT,\r\n { idempotencyKey, ...details }\r\n );\r\n}\r\n\r\n/**\r\n * Create a transient (retryable) error\r\n */\r\nexport function createTransientError(\r\n message: string,\r\n details?: Record<string, unknown>\r\n): CreditError {\r\n return new CreditError(message, CreditErrorCode.TRANSIENT_ERROR, details);\r\n}\r\n\r\n/**\r\n * Create an error for a V2 method the configured adapter does not implement\r\n */\r\nexport function createUnsupportedOperationError(\r\n operation: string,\r\n hint?: string\r\n): CreditError {\r\n return new CreditError(\r\n hint\r\n ? `Repository does not support ${operation}. ${hint}`\r\n : `Repository does not support ${operation}`,\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n { operation, ...(hint ? { hint } : {}) }\r\n );\r\n}\r\n\r\n/**\r\n * Create an invalid amount error\r\n */\r\nexport function createInvalidAmountError(amount: unknown): CreditError {\r\n return new CreditError(\r\n `Credit amount must be a finite positive number (got ${String(amount)})`,\r\n CreditErrorCode.INVALID_AMOUNT,\r\n { amount }\r\n );\r\n}\r\n\r\n/**\r\n * Check if an error is an idempotency conflict\r\n */\r\nexport function isIdempotencyConflictError(error: unknown): boolean {\r\n return isCreditError(error) && error.code === CreditErrorCode.IDEMPOTENCY_CONFLICT;\r\n}\r\n\r\n/**\r\n * Check if an error is safe to retry\r\n *\r\n * Only `TRANSIENT_ERROR` qualifies. Use {@link classifyDatabaseError} first to\r\n * turn a raw driver error into a classified `CreditError`.\r\n */\r\nexport function isTransientError(error: unknown): boolean {\r\n return isCreditError(error) && error.code === CreditErrorCode.TRANSIENT_ERROR;\r\n}\r\n","/**\r\n * Validation and exact comparison for credit amounts.\r\n *\r\n * Every amount ends up in a `numeric(12, 2)` column, so the ledger can only\r\n * represent values on the cent grid, with at most ten integer digits. A value\r\n * outside that grid is not \"close enough\" — PostgreSQL silently rounds it on\r\n * write, and the row that comes back no longer equals what the caller asked\r\n * for. Rejecting it before any row is touched is the only way the ledger and\r\n * the caller can agree on what happened.\r\n *\r\n * These live in core rather than in an adapter so every repository — shipped\r\n * or third-party — validates identically.\r\n */\r\n\r\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\r\nimport type { ExpiredBackingFrontier, PortableReservation } from \"./types.js\";\r\n\r\n/** `numeric(12, 2)`: 12 significant digits, 2 of them after the point. */\r\nexport const CREDIT_AMOUNT_SCALE = 2;\r\n/** Largest representable amount: ten integer digits plus two decimals. */\r\nexport const CREDIT_AMOUNT_MAX = 9_999_999_999.99;\r\nconst MAX_CENTS = 999_999_999_999;\r\n\r\n/**\r\n * Convert an amount to whole cents, or `null` if it is not exactly on the grid.\r\n *\r\n * `Math.round(value * 100) === value * 100` is the tempting check and it is\r\n * wrong: `1.005 * 100` is `100.49999999999999` in binary float, so a genuinely\r\n * invalid value can pass and a valid one can fail. Rounding first and then\r\n * confirming the value round-trips (`value === cents / 100`) is exact for\r\n * everything in range, because a cent count below 2^53 divided by 100 is the\r\n * nearest double to that decimal, which is what the literal parsed to.\r\n */\r\nexport function toCents(value: number): number | null {\r\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\r\n const cents = Math.round(value * 100);\r\n if (!Number.isSafeInteger(cents)) return null;\r\n return value === cents / 100 ? cents : null;\r\n}\r\n\r\n/**\r\n * Add amounts on the cent grid, exactly.\r\n *\r\n * Deriving a total with `+` is not safe in a ledger that validates against the\r\n * grid: `0.10 + 0.20` is `0.30000000000000004` and `0.30 - 0.10` is\r\n * `0.19999999999999998`, neither of which is a value `numeric(12, 2)` can hold.\r\n * Two perfectly legal columns would therefore produce a total the validator\r\n * rejects with `INVALID_AMOUNT`, blaming the caller for a rounding artefact.\r\n * Summing whole cents is exact for every value in range, and `cents / 100` is\r\n * the nearest double to that decimal — the same one the literal would parse to.\r\n *\r\n * Subtract by negating: `sumAmounts(total, -amount)`. Negation is exact.\r\n *\r\n * This is arithmetic, not validation. If any input is itself off the grid the\r\n * float sum is returned unchanged — deliberately, so a corrupt value is never\r\n * rounded into legitimacy here — but that is not a guarantee the result will be\r\n * rejected downstream: two off-grid inputs can cancel, and `sumAmounts(0.005,\r\n * -0.005)` is a perfectly representable `0`. Inputs read from storage must be\r\n * validated as inputs, with `assertValidStoredAmount` or\r\n * `assertValidStoredAmountRaw`, before they are summed. Checking only the\r\n * derived result is not enough and never was.\r\n */\r\nexport function sumAmounts(...values: number[]): number {\r\n let cents = 0;\r\n for (const value of values) {\r\n const valueCents = toCents(value);\r\n if (valueCents === null) return floatSum(values);\r\n cents += valueCents;\r\n }\r\n return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);\r\n}\r\n\r\nfunction floatSum(values: number[]): number {\r\n let total = 0;\r\n for (const value of values) total += value;\r\n return total;\r\n}\r\n\r\n/**\r\n * Multiply a price by a whole quantity, in cents.\r\n *\r\n * `1.1 * 3` is `3.3000000000000003`, which the amount validators refuse: the price\r\n * was legal, and the float product blames the caller for a rounding artefact. A\r\n * fractional quantity has no exact result on the grid and is refused rather than\r\n * rounded, so the rounding policy stays with whoever prices fractions.\r\n */\r\nexport function multiplyAmount(unit: number, quantity: number): number {\r\n const cents = toCents(unit);\r\n const product = cents === null ? Number.NaN : cents * quantity;\r\n if (\r\n cents === null ||\r\n cents < 0 ||\r\n !Number.isSafeInteger(quantity) ||\r\n quantity < 0 ||\r\n !Number.isSafeInteger(product) ||\r\n product > MAX_CENTS\r\n ) {\r\n throw new CreditError(\r\n `Cannot price ${quantity} units at ${unit}: the price must be a non-negative ` +\r\n `amount exact to ${CREDIT_AMOUNT_SCALE} decimals, the quantity a whole number, ` +\r\n `and the result at most ${CREDIT_AMOUNT_MAX}`,\r\n CreditErrorCode.INVALID_AMOUNT,\r\n { unit, quantity, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE }\r\n );\r\n }\r\n return product / 100;\r\n}\r\n\r\n/** Below this, a difference from the grid is noise at any size. */\r\nconst FLOAT_NOISE_FLOOR = 1e-6;\r\n/** 64 to 128 units in the last place: what a run of float additions leaves behind. */\r\nconst FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;\r\n\r\n/**\r\n * How far from the grid a value can be and still be noise. One unit in the last place is\r\n * 2e-6 at 9e9, so a fixed threshold that suits small amounts calls a sum of two legal\r\n * amounts off the grid at the top of the range. At `CREDIT_AMOUNT_MAX` this is about\r\n * 1.4e-4, 35 times less than the half cent that separates a value from the next one.\r\n */\r\nfunction floatNoise(value: number): number {\r\n return Math.max(FLOAT_NOISE_FLOOR, Math.abs(value) * FLOAT_NOISE_RELATIVE);\r\n}\r\n\r\n/**\r\n * The cent-grid value a float landed next to, or the value unchanged.\r\n *\r\n * Documents written by older versions can hold the result of IEEE 754 additions\r\n * (`FieldValue.increment` stored `0.30000000000000004`), and an application can pass\r\n * a raw float difference to a ledger record. That is noise, not an amount. A value\r\n * genuinely off the grid, such as `0.005`, is not noise and comes back as it went in.\r\n */\r\nexport function canonicalAmount(value: number): number {\r\n if (typeof value !== \"number\" || !Number.isFinite(value)) return value;\r\n const cents = Math.round(value * 100);\r\n if (!Number.isSafeInteger(cents)) return value;\r\n const onGrid = cents / 100 + 0;\r\n return Math.abs(value - onGrid) < floatNoise(value) ? onGrid : value;\r\n}\r\n\r\n/**\r\n * Read a stored amount the account owns as a cent-grid value, rounding down.\r\n *\r\n * A stored `24.999` makes every later sum off-grid, the validators then refuse the\r\n * commit or release that needs it, and the holds on that account lock. Rounding down\r\n * unlocks them. A value within {@link canonicalAmount}'s noise of the next cent reads as\r\n * that cent, because a float cannot tell the two apart, so this can add at most that noise:\r\n * 1e-6 up to 7e7, and about 1.4e-4 at the maximum. A non-finite value is left for the\r\n * validators to refuse.\r\n */\r\nexport function storedAmountFloor(value: number): number {\r\n const canonical = canonicalAmount(value);\r\n if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;\r\n return Math.floor(canonical * 100) / 100;\r\n}\r\n\r\n/**\r\n * Read a stored amount the account owes as a cent-grid value, rounding up, so what\r\n * stands against the balance is never understated. See {@link storedAmountFloor}.\r\n */\r\nexport function storedAmountCeil(value: number): number {\r\n const canonical = canonicalAmount(value);\r\n if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;\r\n return Math.ceil(canonical * 100) / 100;\r\n}\r\n\r\n/** Amounts that may be *moved*: strictly positive and on the cent grid. */\r\nexport function isValidCreditAmount(value: unknown): value is number {\r\n if (typeof value !== \"number\") return false;\r\n const cents = toCents(value);\r\n return cents !== null && cents > 0 && cents <= MAX_CENTS;\r\n}\r\n\r\n/**\r\n * Reject an unusable amount before any row is written.\r\n *\r\n * @param context - merged into the error details, e.g. `{ userId }`.\r\n */\r\nexport function assertValidCreditAmount(\r\n value: unknown,\r\n context?: Record<string, unknown>\r\n): asserts value is number {\r\n if (isValidCreditAmount(value)) return;\r\n throw new CreditError(\r\n creditAmountReason(value),\r\n CreditErrorCode.INVALID_AMOUNT,\r\n { amount: value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }\r\n );\r\n}\r\n\r\n/** Say precisely which rule the value broke, so the caller can fix it. */\r\nfunction creditAmountReason(value: unknown): string {\r\n if (typeof value !== \"number\") {\r\n return `Credit amount must be a number (got ${typeof value})`;\r\n }\r\n if (!Number.isFinite(value)) {\r\n return `Credit amount must be finite (got ${String(value)})`;\r\n }\r\n if (value <= 0) {\r\n return `Credit amount must be greater than zero (got ${value})`;\r\n }\r\n if (value > CREDIT_AMOUNT_MAX) {\r\n return `Credit amount ${value} exceeds the maximum of ${CREDIT_AMOUNT_MAX}`;\r\n }\r\n return `Credit amount ${value} is not exact to ${CREDIT_AMOUNT_SCALE} decimal places`;\r\n}\r\n\r\n/**\r\n * Amounts the ledger can *record*: on the cent grid and in range, but free to\r\n * be zero or negative.\r\n *\r\n * Not every numeric column holds something spendable. A release journal entry\r\n * records `amount: 0` because no credits changed hands, and `balanceAfter` goes\r\n * negative for an account that was corrected or allowed to overdraw. Forcing\r\n * those through {@link isValidCreditAmount} would reject legitimate ledger\r\n * rows; skipping validation entirely would let `Infinity` or a third decimal\r\n * reach the column and be silently rounded or rejected by the driver.\r\n */\r\nexport function isRepresentableAmount(value: unknown): value is number {\r\n if (typeof value !== \"number\") return false;\r\n const cents = toCents(value);\r\n return cents !== null && Math.abs(cents) <= MAX_CENTS;\r\n}\r\n\r\n/**\r\n * Reject a ledger value the `numeric(12, 2)` column cannot hold exactly.\r\n *\r\n * @param field - the column being written, so the error names the culprit.\r\n */\r\nexport function assertRepresentableAmount(\r\n value: unknown,\r\n field: string,\r\n context?: Record<string, unknown>\r\n): asserts value is number {\r\n if (isRepresentableAmount(value)) return;\r\n throw new CreditError(\r\n `${field} must be a finite number exact to ${CREDIT_AMOUNT_SCALE} decimal places ` +\r\n `within ±${CREDIT_AMOUNT_MAX} (got ${String(value)})`,\r\n CreditErrorCode.INVALID_AMOUNT,\r\n { field, value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }\r\n );\r\n}\r\n\r\n/**\r\n * Check a whole set of optional ledger fields at once.\r\n *\r\n * The balance writers each take a handful of independently optional numbers,\r\n * and validating them one `if` at a time is where one quietly gets missed.\r\n * Undefined entries are skipped — they mean \"leave this column alone\".\r\n */\r\nexport function assertRepresentableFields(\r\n fields: Record<string, unknown>,\r\n context?: Record<string, unknown>\r\n): void {\r\n for (const [field, value] of Object.entries(fields)) {\r\n if (value === undefined) continue;\r\n assertRepresentableAmount(value, field, context);\r\n }\r\n}\r\n\r\n/**\r\n * The lowest `balance` that keeps every outstanding hold committable.\r\n *\r\n * Commit's funding guard is `balance + bonusCredits >= amount`, and every live\r\n * hold is counted in `reserved`, so any write that lowers `balance` must keep\r\n * `balance + bonusCredits >= reserved` — otherwise a hold that was fully backed\r\n * when it was placed throws `INSUFFICIENT_CREDITS` at commit time and is\r\n * stranded. Monthly resets, tier downgrades and explicit tier writes all lower\r\n * `balance` to a configured target; this is the floor they may not cut through.\r\n *\r\n * The floor is `reserved - bonusCredits`, never below zero: bonus credits also\r\n * fund commits, so they count toward the backing.\r\n */\r\nexport function backedBalanceFloor(reserved: number, bonusCredits: number): number {\r\n const floor = Math.max(0, sumAmounts(reserved, -bonusCredits));\r\n // A floor beyond the representable range means the row itself is corrupt —\r\n // e.g. a large negative `bonusCredits` pushing `reserved - bonusCredits`\r\n // past the `numeric(12,2)` cap. Refuse loudly: writing it would succeed in\r\n // memory and be rejected by the SQL column, splitting the adapters.\r\n assertRepresentableAmount(floor, \"backedBalanceFloor\", { reserved, bonusCredits });\r\n return floor;\r\n}\r\n\r\n/**\r\n * The balance left by a cap: lowered to `cap` at most, never raised, and never\r\n * cut through the credits backing outstanding holds (see\r\n * {@link backedBalanceFloor}). A balance already under `cap` is left alone,\r\n * which is the point of computing this from the stored row inside a transaction\r\n * rather than writing a value the caller read earlier.\r\n */\r\nexport function cappedBalance(\r\n balance: number,\r\n cap: number,\r\n reserved: number,\r\n bonusCredits: number\r\n): number {\r\n return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));\r\n}\r\n\r\n/** A balance a lowering write lands on, and how much of it only backs holds. */\r\nexport interface PlannedBalance {\r\n balance: number;\r\n expiredHoldBacking: number;\r\n}\r\n\r\n/**\r\n * The expired credits actually held in a balance: `0 <= held <= balance`, whatever\r\n * is stored. Every read of `expiredHoldBacking` goes through this. Trusting the stored\r\n * value made a negative one mint credits: `balance - (-10)` read as spendable, and a\r\n * close that \"used\" -10 of backing added 10 to the balance.\r\n */\r\nexport function heldExpiredBacking(balance: number, stored?: number): number {\r\n if (!(stored !== undefined && stored > 0)) return 0;\r\n return Math.max(0, Math.min(stored, balance));\r\n}\r\n\r\n/** The part of a balance not kept only for holds. A missing field reads as 0. */\r\nexport function spendableBalance(balance: number, expiredHoldBacking?: number): number {\r\n return sumAmounts(balance, -heldExpiredBacking(balance, expiredHoldBacking));\r\n}\r\n\r\n/**\r\n * An absolute balance write: `target`, floored at what backs outstanding holds.\r\n * Whatever the floor kept above `target` is not the account's to spend.\r\n */\r\nexport function planBalanceTarget(\r\n target: number,\r\n reserved: number,\r\n bonusCredits: number\r\n): PlannedBalance {\r\n // A negative target would be floored at 0 and leave `-target` marked on a balance\r\n // of 0, and the rest of the ledger cannot honour an unrepresentable one.\r\n assertValidBalanceTarget(target, \"balance target\", { reserved, bonusCredits });\r\n const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));\r\n return { balance, expiredHoldBacking: sumAmounts(balance, -target) };\r\n}\r\n\r\n/**\r\n * A cap on the spendable part of a balance. Like {@link cappedBalance} it never\r\n * raises the balance and never cuts through what backs holds, and it records the\r\n * part kept above the cap so a later release cannot hand it back. Credits already\r\n * marked stay marked, so applying a cap twice changes nothing.\r\n */\r\nexport function planBalanceCap(\r\n balance: number,\r\n cap: number,\r\n reserved: number,\r\n bonusCredits: number,\r\n expiredHoldBacking?: number\r\n): PlannedBalance {\r\n const spendable = spendableBalance(balance, expiredHoldBacking);\r\n const target = Math.min(spendable, cap);\r\n // Marked credits belong to the holds they back. A cap adds to them when the floor\r\n // needs it and never takes them away, even if later bonus credits lowered the floor.\r\n const kept = Math.max(\r\n sumAmounts(balance, -spendable),\r\n sumAmounts(backedBalanceFloor(reserved, bonusCredits), -target)\r\n );\r\n const next = Math.min(balance, sumAmounts(target, kept));\r\n return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };\r\n}\r\n\r\n/**\r\n * What a closing hold takes out of the credits kept only for holds: its own share\r\n * and nothing of another hold's. `holdBacking` comes from {@link holdExpiredBacking}.\r\n */\r\nexport function settleExpiredHoldBacking(\r\n balance: number,\r\n expiredHoldBacking: number,\r\n holdBacking: number\r\n): { used: number; remaining: number } {\r\n const held = heldExpiredBacking(balance, expiredHoldBacking);\r\n const used = Math.max(0, Math.min(held, holdBacking));\r\n return { used, remaining: sumAmounts(held, -used) };\r\n}\r\n\r\n/** The balances a commit lands on, and whether the account could pay it. */\r\nexport interface PlannedHoldCommit {\r\n covered: boolean;\r\n balance: number;\r\n bonusCredits: number;\r\n expiredHoldBacking: number;\r\n}\r\n\r\n/**\r\n * Pay a hold: first its own share of the marked credits, then spendable balance,\r\n * then bonus. Marked credits backing other holds are never touched, so what this\r\n * commit costs does not depend on which holds closed before it.\r\n */\r\nexport function planHoldCommit(\r\n balance: number,\r\n bonusCredits: number,\r\n expiredHoldBacking: number,\r\n holdBacking: number,\r\n amount: number\r\n): PlannedHoldCommit {\r\n const backing = settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking);\r\n const rest = sumAmounts(amount, -backing.used);\r\n const fromSpendable = Math.min(spendableBalance(balance, expiredHoldBacking), rest);\r\n const fromBonus = sumAmounts(rest, -fromSpendable);\r\n return {\r\n covered: fromBonus <= bonusCredits,\r\n balance: sumAmounts(balance, -backing.used, -fromSpendable),\r\n bonusCredits: sumAmounts(bonusCredits, -fromBonus),\r\n expiredHoldBacking: backing.remaining,\r\n };\r\n}\r\n\r\n/** The fields of an open hold that decide its share of the marked credits. */\r\nexport type ExpiredBackingHold = Pick<\r\n PortableReservation,\r\n \"id\" | \"amount\" | \"holdPlacedAt\" | \"expiredBacking\"\r\n>;\r\n\r\n/** A hold whose stored share has to change for a new total. */\r\nexport interface ExpiredBackingRewrite {\r\n id: string;\r\n expiredBacking: number;\r\n}\r\n\r\n/** The marked credits on an account and where they end among its open holds. */\r\nexport interface ExpiredBackingAllocation {\r\n total: number;\r\n frontier: ExpiredBackingFrontier | null;\r\n /** Stored shares to write with this total. Absent when none changes. */\r\n rewrites?: ExpiredBackingRewrite[];\r\n}\r\n\r\n/**\r\n * A frontier before every hold: no hold shares through it. It records that the account's\r\n * marks are placed, so the next close does not walk the holds again.\r\n */\r\nexport const NO_EXPIRED_BACKING_PREFIX: ExpiredBackingFrontier = {\r\n holdPlacedAt: \"\",\r\n id: \"\",\r\n share: 0,\r\n};\r\n\r\nfunction compareHolds(\r\n a: { holdPlacedAt: string; id: string },\r\n b: { holdPlacedAt: string; id: string }\r\n): number {\r\n if (a.holdPlacedAt !== b.holdPlacedAt) return a.holdPlacedAt < b.holdPlacedAt ? -1 : 1;\r\n if (a.id === b.id) return 0;\r\n return a.id < b.id ? -1 : 1;\r\n}\r\n\r\n/**\r\n * The share a 2.2.0 hold document recorded for itself: the order of record for that hold.\r\n * Clocks are not, since the server that placed a hold may lag the one before it. Rounded\r\n * up, like every amount that stands against the balance, and never above the hold. Zero\r\n * means no record, which leaves the hold to the frontier.\r\n */\r\nexport function recordedExpiredBacking(hold: ExpiredBackingHold): number {\r\n const stored = hold.expiredBacking;\r\n if (typeof stored !== \"number\" || !Number.isFinite(stored) || stored <= 0) return 0;\r\n if (!isValidCreditAmount(hold.amount)) return 0;\r\n return Math.min(storedAmountCeil(stored), hold.amount);\r\n}\r\n\r\n/**\r\n * Place `total` marked credits on the open holds, so that what each hold takes does not\r\n * depend on the order they close in.\r\n *\r\n * Shares a 2.2.0 hold document stored stand as recorded: they need not be a prefix of\r\n * anything, and they sit on documents that already exist. The rest goes to the other\r\n * holds oldest first, each up to its amount. That part is a prefix, so one\r\n * {@link ExpiredBackingFrontier} on the account describes it and no hold document changes\r\n * however many there are. A new hold carries no share, so no record is ever added.\r\n *\r\n * A total below the records trims them newest first, and only when every other hold has\r\n * nothing, and a total the other holds cannot take fills what the records left free. Those\r\n * are the only hold writes: `rewrites`, at most one per record, each record being an open\r\n * hold from before the upgrade.\r\n *\r\n * Marks no hold can carry stay marked. They were out of reach before this ran, and\r\n * dropping them would make them spendable; the next absolute balance write replaces\r\n * them. A row without `holdPlacedAt` is a record, not a hold, and a row with an unusable\r\n * amount cannot close, so neither takes a share.\r\n */\r\nexport function allocateExpiredBacking(\r\n holds: readonly ExpiredBackingHold[],\r\n total: number\r\n): ExpiredBackingAllocation {\r\n const placed = holds\r\n .filter((hold) => hold.holdPlacedAt && isValidCreditAmount(hold.amount))\r\n .map((hold) => ({ ...hold, holdPlacedAt: hold.holdPlacedAt as string }))\r\n .sort(compareHolds);\r\n const wanted = Math.max(0, total);\r\n if (wanted === 0) return { total: 0, frontier: null };\r\n\r\n const recorded = placed.filter((hold) => recordedExpiredBacking(hold) > 0);\r\n const shares = new Map(recorded.map((hold) => [hold.id, recordedExpiredBacking(hold)]));\r\n let left = sumAmounts(wanted, ...[...shares.values()].map((share) => -share));\r\n\r\n for (const hold of [...recorded].reverse()) {\r\n if (left >= 0) break;\r\n const share = shares.get(hold.id) as number;\r\n const cut = Math.min(share, -left);\r\n shares.set(hold.id, sumAmounts(share, -cut));\r\n left = sumAmounts(left, cut);\r\n }\r\n\r\n let frontier: ExpiredBackingFrontier = NO_EXPIRED_BACKING_PREFIX;\r\n for (const hold of placed) {\r\n if (left <= 0) break;\r\n if (shares.has(hold.id)) continue;\r\n const share = Math.min(hold.amount, left);\r\n frontier = { holdPlacedAt: hold.holdPlacedAt, id: hold.id, share };\r\n left = sumAmounts(left, -share);\r\n }\r\n\r\n for (const hold of recorded) {\r\n if (left <= 0) break;\r\n const share = shares.get(hold.id) as number;\r\n const added = Math.min(sumAmounts(hold.amount, -share), left);\r\n shares.set(hold.id, sumAmounts(share, added));\r\n left = sumAmounts(left, -added);\r\n }\r\n\r\n const rewrites = recorded\r\n .filter((hold) => shares.get(hold.id) !== hold.expiredBacking)\r\n .map((hold) => ({ id: hold.id, expiredBacking: shares.get(hold.id) as number }));\r\n return { total: wanted, frontier, ...(rewrites.length > 0 ? { rewrites } : {}) };\r\n}\r\n\r\n/**\r\n * This hold's share of the marked credits: its own record if it has one, otherwise where\r\n * it falls against the account's frontier. Pass the `rewrites` of an allocation made in\r\n * this same step, since the document does not hold them yet.\r\n */\r\nexport function holdExpiredBacking(\r\n hold: ExpiredBackingHold,\r\n frontier?: ExpiredBackingFrontier | null,\r\n rewrites?: readonly ExpiredBackingRewrite[]\r\n): number {\r\n const rewritten = rewrites?.find((rewrite) => rewrite.id === hold.id);\r\n const recorded = recordedExpiredBacking(\r\n rewritten ? { ...hold, expiredBacking: rewritten.expiredBacking } : hold\r\n );\r\n if (recorded > 0) return recorded;\r\n if (!frontier || !hold.holdPlacedAt) return 0;\r\n const order = compareHolds({ holdPlacedAt: hold.holdPlacedAt, id: hold.id }, frontier);\r\n if (order > 0) return 0;\r\n return order < 0 ? hold.amount : Math.min(frontier.share, hold.amount);\r\n}\r\n\r\n/**\r\n * When a new hold counts as placed: `now`, or just after the frontier if this server's\r\n * clock is behind the server that placed it. A hold that sorted before the frontier\r\n * would read as fully marked, taking expired credits that belong to older holds.\r\n */\r\nexport function holdPlacedAtAfter(\r\n now: string,\r\n frontier?: ExpiredBackingFrontier | null\r\n): string {\r\n if (!frontier?.holdPlacedAt) return now;\r\n const earliest = new Date(Date.parse(frontier.holdPlacedAt) + 1).toISOString();\r\n return now > earliest ? now : earliest;\r\n}\r\n\r\n/** The account fields that hold the marked credits. */\r\nexport interface ExpiredBackingFields {\r\n expiredHoldBacking?: number;\r\n expiredBackingFrontier?: ExpiredBackingFrontier | null;\r\n}\r\n\r\n/**\r\n * The fields to write for a new marked total: nothing when it was and stays 0, so an\r\n * account that never had any keeps a document without the fields. A stored value that\r\n * is not the new one is overwritten, which is what repairs a bad one. A `frontier` left\r\n * undefined keeps the stored one.\r\n */\r\nexport function expiredBackingPatch(\r\n next: { total: number; frontier?: ExpiredBackingFrontier | null },\r\n stored: ExpiredBackingFields\r\n): ExpiredBackingFields {\r\n const total = Math.max(0, next.total);\r\n const patch: ExpiredBackingFields = {};\r\n const storedTotal = stored.expiredHoldBacking;\r\n if (total > 0 || (storedTotal !== undefined && storedTotal !== 0)) {\r\n patch.expiredHoldBacking = total;\r\n }\r\n if (total <= 0) {\r\n if (stored.expiredBackingFrontier) patch.expiredBackingFrontier = null;\r\n } else if (next.frontier) {\r\n patch.expiredBackingFrontier = next.frontier;\r\n }\r\n return patch;\r\n}\r\n\r\n/**\r\n * Validate a balance target supplied by the application: a cap or the result of\r\n * a monthly-reset decision. It must be storable and cannot be negative.\r\n */\r\nexport function assertValidBalanceTarget(\r\n value: unknown,\r\n field: string,\r\n context?: Record<string, unknown>\r\n): asserts value is number {\r\n assertRepresentableAmount(value, field, context);\r\n if (value < 0) {\r\n throw new CreditError(\r\n `${field} cannot be negative (got ${value})`,\r\n CreditErrorCode.INVALID_AMOUNT,\r\n { field, value, ...context }\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Validate how a tier write changes the balance. `balance` sets a value the\r\n * caller computed and `balanceCap` lowers the stored value, so naming both has\r\n * no single meaning and is refused.\r\n */\r\nexport function assertValidTierBalanceInput(\r\n input: { balance?: number; balanceCap?: number },\r\n context?: Record<string, unknown>\r\n): void {\r\n if (input.balanceCap === undefined) {\r\n if (input.balance !== undefined) assertValidBalanceTarget(input.balance, \"balance\", context);\r\n return;\r\n }\r\n if (input.balance !== undefined) {\r\n throw new CreditError(\r\n \"A tier update cannot set both balance and balanceCap\",\r\n CreditErrorCode.INVALID_AMOUNT,\r\n { balance: input.balance, balanceCap: input.balanceCap, ...context }\r\n );\r\n }\r\n assertValidBalanceTarget(input.balanceCap, \"balanceCap\", context);\r\n}\r\n\r\n/**\r\n * Validate a limit derived from tier configuration.\r\n *\r\n * `Infinity` is this codebase's sentinel for an unlimited tier, so it is\r\n * accepted here; {@link storedMonthlyLimit} decides what actually reaches the\r\n * column. Every other value ends up in `numeric(12, 2)` and has to fit, so a\r\n * tier configured with `monthlyCredits: 1.005` is caught here rather than\r\n * silently rounded to `1.01` by PostgreSQL.\r\n */\r\nexport function assertRepresentableTierAmount(\r\n value: unknown,\r\n field: string,\r\n context?: Record<string, unknown>\r\n): void {\r\n if (value === Number.POSITIVE_INFINITY) return;\r\n assertRepresentableAmount(value, field, context);\r\n}\r\n\r\n/**\r\n * Validate a stored amount in the representation storage actually returned.\r\n *\r\n * Checking the mapped float is not enough, because the mapping is lossy in both\r\n * directions. A `numeric` column with no scale constraint can hold\r\n * `9999999999.9900001`; `Number()` rounds that to `9999999999.99`, which is a\r\n * perfectly valid amount, so a validator looking at the mapped value sees\r\n * nothing wrong and the transition proceeds — while PostgreSQL goes on doing\r\n * exact arithmetic with the digits JavaScript threw away. `NaN` and values past\r\n * double range are worse: the mapper turns them into `0`, which misreports what\r\n * the row holds.\r\n *\r\n * So this runs on the driver's own output — a string, for `numeric` — and uses\r\n * exact integer arithmetic. It returns the amount as a float only after\r\n * establishing that the float is a faithful representation of it.\r\n *\r\n * @returns the stored amount, exactly representable as a JS number.\r\n */\r\n/**\r\n * How an unlimited tier's monthly limit is stored.\r\n *\r\n * `getConfigMonthlyLimit` returns `Infinity` for an unlimited tier, and no\r\n * numeric column can hold that. Both adapters therefore store the ceiling of\r\n * the representable range instead: any real usage compares below it, so\r\n * \"unlimited\" keeps behaving like unlimited, and a reader that knows nothing\r\n * about the sentinel sees a very large allowance rather than a wrong one.\r\n *\r\n * This exists so the two adapters cannot drift. The SQL adapter previously\r\n * stored `0` here, which reads as *no* allowance — the exact opposite — while\r\n * the in-memory adapter kept `Infinity`.\r\n */\r\nexport function storedMonthlyLimit(limit: number): number {\r\n return Number.isFinite(limit) ? limit : CREDIT_AMOUNT_MAX;\r\n}\r\n\r\nexport function assertValidStoredAmountRaw(\r\n raw: unknown,\r\n context: Record<string, unknown>\r\n): number {\r\n const cents = numericToCents(raw);\r\n if (cents !== null && cents > 0n && cents <= BigInt(MAX_CENTS)) {\r\n return Number(cents) / 100;\r\n }\r\n throw new CreditError(\r\n `Stored credit amount ${String(raw)} is not a valid amount, so this ` +\r\n \"transition was refused before any state changed. The row must be \" +\r\n \"repaired before it can be committed, released or expired.\",\r\n CreditErrorCode.INVALID_AMOUNT,\r\n {\r\n amount: typeof raw === \"string\" ? raw : String(raw),\r\n reason: \"corrupt_stored_amount\",\r\n max: CREDIT_AMOUNT_MAX,\r\n scale: CREDIT_AMOUNT_SCALE,\r\n ...context,\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Reject a *persisted* amount that the ledger cannot honour.\r\n *\r\n * A reservation row is written once and read back on every transition, and\r\n * nothing stops a direct SQL write, a botched migration, or an older version of\r\n * this library from leaving an amount the invariants cannot reason about. A\r\n * negative one is the dangerous case: `reserved >= amount` is trivially true\r\n * for it, and `balance - (-10)` *adds* credits, so a transition that trusts the\r\n * stored value mints money. Every transition therefore re-validates what it\r\n * locked before it changes anything.\r\n *\r\n * Deliberately fails release and expire as well as commit. Returning a hold of\r\n * an unknown size is not obviously safer than spending one — it hands back\r\n * coverage that may never have been taken — so a corrupt row stops moving\r\n * entirely and waits for an operator, rather than being partially honoured.\r\n *\r\n * This is the float-typed form, used by the in-memory adapter where the stored\r\n * value *is* a JS number and no conversion has happened. A repository whose\r\n * storage returns some other representation must use\r\n * {@link assertValidStoredAmountRaw} instead, or the conversion will hide the\r\n * corruption from the check.\r\n */\r\nexport function assertValidStoredAmount(\r\n value: unknown,\r\n context: Record<string, unknown>\r\n): asserts value is number {\r\n if (isValidCreditAmount(value)) return;\r\n throw new CreditError(\r\n `Stored credit amount ${String(value)} is not a valid amount, so this ` +\r\n \"transition was refused before any state changed. The row must be \" +\r\n \"repaired before it can be committed, released or expired.\",\r\n CreditErrorCode.INVALID_AMOUNT,\r\n { amount: value, reason: \"corrupt_stored_amount\", max: CREDIT_AMOUNT_MAX, ...context }\r\n );\r\n}\r\n\r\n/**\r\n * Parse a value read back from a `numeric` column into exact cents.\r\n *\r\n * Drivers return `numeric` as a string precisely so no precision is lost in\r\n * transit; comparing the parsed floats would throw that away again. Returns\r\n * `null` for anything unparseable so callers can treat it as \"not equal\"\r\n * rather than crashing on malformed ledger data.\r\n */\r\nexport function numericToCents(value: unknown): bigint | null {\r\n if (typeof value === \"number\") {\r\n const cents = toCents(value);\r\n return cents === null ? null : BigInt(cents);\r\n }\r\n if (typeof value !== \"string\") return null;\r\n\r\n const match = /^([+-]?)(\\d+)(?:\\.(\\d*))?$/.exec(value.trim());\r\n if (!match) return null;\r\n\r\n const [, sign, whole, fraction = \"\"] = match;\r\n // More than two decimals is only acceptable when the extra digits are zeros;\r\n // anything else never came from a numeric(12,2) column.\r\n if (/[1-9]/.test(fraction.slice(CREDIT_AMOUNT_SCALE))) return null;\r\n\r\n const cents =\r\n BigInt(whole) * 100n + BigInt(fraction.slice(0, CREDIT_AMOUNT_SCALE).padEnd(CREDIT_AMOUNT_SCALE, \"0\"));\r\n return sign === \"-\" ? -cents : cents;\r\n}\r\n\r\n/**\r\n * Exact equality for two amounts that may be numbers or `numeric` strings.\r\n *\r\n * Unparseable input is *not* equal to anything, including itself — a\r\n * comparison against corrupt data must never come back \"matches\".\r\n */\r\nexport function sameAmount(a: unknown, b: unknown): boolean {\r\n const left = numericToCents(a);\r\n const right = numericToCents(b);\r\n return left !== null && right !== null && left === right;\r\n}\r\n","import { sumAmounts } from \"./amount.js\";\r\n/**\r\n * Portable credits types - framework and database agnostic\r\n *\r\n * These types use standard JavaScript types (string, Date) instead of\r\n * Firebase-specific types (Timestamp). This makes the credits system\r\n * portable to other environments without Firebase dependencies.\r\n *\r\n * Note: Firestore Timestamp is handled by the credits-firestore adapter.\r\n */\r\n\r\n// ==================== Utility Functions ====================\r\n\r\n/**\r\n * Calculate available credits from balance, bonus, and reserved amounts.\r\n *\r\n * Available = balance + bonusCredits - reserved\r\n *\r\n * @param balance - Regular monthly credits\r\n * @param bonusCredits - Purchased/admin credits\r\n * @param reserved - Credits locked for in-flight operations\r\n * @returns Available credits for new operations\r\n */\r\nexport function calculateAvailableCredits(\r\n balance: number,\r\n bonusCredits: number,\r\n reserved: number\r\n): number {\r\n return sumAmounts(balance, bonusCredits, -reserved);\r\n}\r\n\r\n/**\r\n * Convert various timestamp formats to ISO string.\r\n *\r\n * Handles:\r\n * - Date objects\r\n * - ISO string (returned as-is)\r\n * - Timestamp-like objects with toDate() method (Firebase)\r\n * - undefined/null (returns current time)\r\n *\r\n * @param value - Value to convert\r\n * @returns ISO 8601 string\r\n */\r\nexport function toPortableTimestamp(value: unknown): string {\r\n if (!value) {\r\n return new Date().toISOString();\r\n }\r\n\r\n if (typeof value === \"string\") {\r\n return value;\r\n }\r\n\r\n if (value instanceof Date) {\r\n return value.toISOString();\r\n }\r\n\r\n if (\r\n typeof value === \"object\" &&\r\n value !== null &&\r\n \"toDate\" in value &&\r\n typeof (value as { toDate: () => Date }).toDate === \"function\"\r\n ) {\r\n return (value as { toDate: () => Date }).toDate().toISOString();\r\n }\r\n\r\n return new Date().toISOString();\r\n}\r\n\r\n/**\r\n * Convert various timestamp formats to Date object.\r\n *\r\n * Handles:\r\n * - Date objects (returned as-is)\r\n * - ISO string\r\n * - Timestamp-like objects with toDate() method (Firebase)\r\n * - Invalid values (returns current date)\r\n *\r\n * @param value - Value to convert\r\n * @returns Date object\r\n */\r\nexport function toDate(value: unknown): Date {\r\n if (value instanceof Date) {\r\n return value;\r\n }\r\n\r\n if (typeof value === \"string\") {\r\n return new Date(value);\r\n }\r\n\r\n if (\r\n typeof value === \"object\" &&\r\n value !== null &&\r\n \"toDate\" in value &&\r\n typeof (value as { toDate: () => Date }).toDate === \"function\"\r\n ) {\r\n return (value as { toDate: () => Date }).toDate();\r\n }\r\n\r\n return new Date();\r\n}\r\n\r\n// ==================== Subscription Types ====================\r\n\r\n/**\r\n * Built-in tier ids (autocomplete) — apps may add more via config.\r\n */\r\nexport type BuiltinTier = \"free\" | \"basic\" | \"premium\" | \"unlimited\";\r\n\r\n/**\r\n * Subscription tiers for the credits system.\r\n *\r\n * Any built-in OR any config-defined tier id. The `(string & {})` keeps builtin\r\n * autocomplete while accepting arbitrary configured tiers.\r\n */\r\nexport type SubscriptionTier = BuiltinTier | (string & {});\r\n\r\n// ==================== Operation Types ====================\r\n\r\n/**\r\n * Operation types for cost tracking (provider-agnostic)\r\n *\r\n * Dynamic string type - any operation configured in the system is valid.\r\n */\r\nexport type CreditOperationType = string;\r\n\r\n/**\r\n * AI providers\r\n */\r\nexport type AIProviderType = \"gemini\";\r\n\r\n/**\r\n * Resource type for usage logging\r\n */\r\nexport type ResourceType = string;\r\n\r\n// ==================== User Credits ====================\r\n\r\n/**\r\n * Portable user credits balance\r\n *\r\n * Uses ISO string timestamps instead of Firebase Timestamp.\r\n * This type is safe for JSON serialization and works across frameworks.\r\n */\r\nexport interface PortableUserCredits {\r\n userId: string;\r\n /** Current available balance (monthly credits, reset each month) */\r\n balance: number;\r\n /** Bonus credits from purchases/admin (never reset, persist until used) */\r\n bonusCredits: number;\r\n /** Credits currently reserved for in-flight operations */\r\n reserved: number;\r\n /** User's subscription tier */\r\n tier: SubscriptionTier;\r\n /** Monthly credit limit based on tier (0 = unlimited) */\r\n monthlyLimit: number;\r\n /** Credits used this month (resets monthly) */\r\n monthlyUsed: number;\r\n /** When monthly credits reset (start of next month) - ISO 8601 */\r\n monthlyResetAt: string;\r\n /** Subscription expiry (null/undefined for free tier) - ISO 8601 or null */\r\n subscriptionExpiresAt?: string | null;\r\n /**\r\n * The part of `balance` a balance-lowering write kept only to back outstanding\r\n * holds. A commit consumes it; a release or expiry removes it from `balance`.\r\n * Absent means 0. See `planBalanceCap`.\r\n */\r\n expiredHoldBacking?: number;\r\n /**\r\n * Where those credits end among the open holds, so a hold's share needs no write\r\n * to the hold. Internal to the repositories: `toClientUserCredits` drops it.\r\n */\r\n expiredBackingFrontier?: ExpiredBackingFrontier | null;\r\n /**\r\n * Set when the account was created with a signup-grant claim: the amount\r\n * granted, 0 when the claim was already taken. Absent on any other account.\r\n */\r\n signupGrant?: number;\r\n /** Creation timestamp - ISO 8601 */\r\n createdAt: string;\r\n /** Last update timestamp - ISO 8601 */\r\n updatedAt: string;\r\n}\r\n\r\n// ==================== Reservation Types ====================\r\n\r\n/**\r\n * The newest open hold carrying marked credits (`expiredHoldBacking`).\r\n *\r\n * Credits are marked oldest hold first, so a single point describes every share:\r\n * a hold older than this carries its full amount, this hold carries `share`, and a\r\n * newer one carries nothing. A marked credit leaves the balance when its hold closes,\r\n * however it closes. See `allocateExpiredBacking` and `holdExpiredBacking`.\r\n */\r\nexport interface ExpiredBackingFrontier {\r\n /** The hold's `holdPlacedAt`. */\r\n holdPlacedAt: string;\r\n /** The hold's id, which orders holds placed in the same millisecond. */\r\n id: string;\r\n share: number;\r\n}\r\n\r\n/**\r\n * Credit reservation status for two-phase commit\r\n */\r\nexport type ReservationStatus = \"reserved\" | \"committed\" | \"released\" | \"expired\";\r\n\r\n/**\r\n * Portable credit reservation\r\n *\r\n * Used for two-phase commit to prevent double-spending.\r\n */\r\nexport interface PortableReservation {\r\n id: string;\r\n userId: string;\r\n /** Amount of credits reserved */\r\n amount: number;\r\n /** Operation type for tracking */\r\n operationType: CreditOperationType;\r\n /** Current status of reservation */\r\n status: ReservationStatus;\r\n /** When reservation was created - ISO 8601 */\r\n createdAt: string;\r\n /** When reservation expires (for cleanup) - ISO 8601 */\r\n expiresAt: string;\r\n /** When reservation was committed/released - ISO 8601 or undefined */\r\n completedAt?: string;\r\n /**\r\n * Caller-supplied idempotency key (V2). Absent on legacy reservations.\r\n * Unique per user; a replay with the same key returns this same reservation.\r\n */\r\n idempotencyKey?: string;\r\n /**\r\n * When the atomic reserve placed this hold - ISO 8601, or absent.\r\n *\r\n * Written by `reserveCreditsV2` inside the same transaction that increases\r\n * `reserved`, so its presence is proof that the hold exists. `createReservation`\r\n * never sets it, and the V2 transitions refuse to move a row without it.\r\n * See `assertHoldPlaced` in `core/reservation-integrity.ts`.\r\n */\r\n holdPlacedAt?: string;\r\n /**\r\n * This hold's share of the marked credits, stored by 2.2.0. A hold placed since carries\r\n * none and takes its share from the account's frontier. See `holdExpiredBacking`.\r\n */\r\n expiredBacking?: number;\r\n}\r\n\r\n// ==================== Result Types ====================\r\n\r\n/**\r\n * Result of a credit check operation\r\n */\r\nexport interface CreditCheckResult {\r\n /** Whether user has sufficient credits */\r\n hasCredits: boolean;\r\n /** Current balance (balance + bonusCredits) */\r\n balance: number;\r\n /** Required credits for operation */\r\n required: number;\r\n /** Shortfall if insufficient */\r\n shortfall: number;\r\n}\r\n\r\n/**\r\n * Result of a one-shot atomic credit deduction (see `CreditsService.deductCredits`).\r\n *\r\n * Discriminated on `success` so callers get exhaustive narrowing:\r\n * - `success: true` -> `newBalance` is populated, `available`/`shortfall` are not present.\r\n * - `success: false` -> `available`/`shortfall` describe the shortfall, no mutation occurred.\r\n */\r\nexport type DeductCreditsResult =\r\n | {\r\n success: true;\r\n /** Combined balance (balance + bonusCredits) after the deduction */\r\n newBalance: number;\r\n }\r\n | {\r\n success: false;\r\n reason: \"insufficient\";\r\n /** Combined balance (balance + bonusCredits - reserved) available before the attempt */\r\n available: number;\r\n /** Credits that were requested */\r\n required: number;\r\n /** How many more credits would have been needed */\r\n shortfall: number;\r\n };\r\n\r\n/**\r\n * Result of monthly reset operation\r\n */\r\nexport interface MonthlyResetResult {\r\n /** Whether the reset was performed */\r\n wasReset: boolean;\r\n /** The user credits after the operation */\r\n credits: PortableUserCredits;\r\n /**\r\n * `true` when the repository wrote the reset's journal entry inside the same\r\n * atomic step as the balance write. The service then skips its own journal\r\n * call — a separate call sits outside the reset's atomicity, and if it fails\r\n * after the CAS was consumed, the line is lost for good (no retry ever sees\r\n * the reset again). Absent/`false` means the repository did not journal and\r\n * the service falls back to the legacy non-atomic write.\r\n */\r\n journaled?: boolean;\r\n}\r\n\r\n/**\r\n * Result of subscription expiry check\r\n */\r\nexport interface SubscriptionExpiryResult {\r\n /** Whether the subscription was downgraded */\r\n wasDowngraded: boolean;\r\n /** Whether user is in grace period */\r\n inGracePeriod: boolean;\r\n /** Days remaining in grace period (0 if not in grace) */\r\n graceDaysRemaining: number;\r\n /** The user credits after the operation */\r\n credits: PortableUserCredits;\r\n /**\r\n * `true` when the repository wrote the downgrade's journal entry inside the\r\n * same atomic step as the tier write. The service then skips its own journal\r\n * call — a separate call sits outside the downgrade's atomicity, and if it\r\n * fails after the tier write committed, the account is downgraded with no\r\n * audit line and no retry ever fires (the row is no longer eligible).\r\n * Absent/`false` means the repository did not journal and the service falls\r\n * back to the legacy non-atomic write.\r\n */\r\n journaled?: boolean;\r\n}\r\n\r\n// ==================== Transaction Types ====================\r\n\r\n/**\r\n * Credit transaction type\r\n */\r\nexport type TransactionType = \"purchase\" | \"subscription\" | \"bonus\" | \"refund\" | \"adjustment\";\r\n\r\n/**\r\n * Portable credit transaction\r\n */\r\nexport interface PortableTransaction {\r\n id: string;\r\n userId: string;\r\n type: TransactionType;\r\n /** Amount of credits (positive for additions, negative for deductions) */\r\n amount: number;\r\n description: string;\r\n /** Reference to payment provider */\r\n paymentRef?: string;\r\n previousBalance: number;\r\n newBalance: number;\r\n createdAt: string;\r\n}\r\n\r\n// ==================== Journal Types ====================\r\n\r\n/**\r\n * Source of a credit journal entry\r\n */\r\nexport type CreditSource =\r\n | \"operation_commit\"\r\n | \"operation_release\"\r\n | \"purchase\"\r\n | \"subscription_grant\"\r\n | \"subscription_upgrade\"\r\n | \"subscription_downgrade\"\r\n | \"monthly_reset\"\r\n | \"bonus\"\r\n | \"refund\"\r\n | \"admin_adjustment\"\r\n | \"expiry\"\r\n | \"reservation_expired\";\r\n\r\n/**\r\n * Type of reference for a journal entry\r\n */\r\nexport type JournalReferenceType =\r\n | \"reservation\"\r\n | \"transaction\"\r\n | \"adjustment\"\r\n | \"reset\"\r\n | \"subscription\";\r\n\r\n/**\r\n * Portable credit journal entry for audit trail\r\n */\r\nexport interface PortableJournalEntry {\r\n id: string;\r\n userId: string;\r\n /** Type of entry - debit decreases balance, credit increases balance */\r\n entryType: \"debit\" | \"credit\";\r\n /** Amount of credits involved */\r\n amount: number;\r\n /** `balance + bonusCredits` after this entry */\r\n balanceAfter: number;\r\n /**\r\n * `balance + bonusCredits` before this entry, read in the same atomic step.\r\n * Absent on entries written before 3.0.0.\r\n */\r\n balanceBefore?: number;\r\n /** Source of the credit change */\r\n source: CreditSource;\r\n /** Reference ID (e.g., reservationId, transactionId) */\r\n referenceId: string;\r\n /** Type of reference */\r\n referenceType: JournalReferenceType;\r\n /** Human-readable description */\r\n description: string;\r\n /** Additional metadata */\r\n metadata?: Record<string, unknown>;\r\n /**\r\n * Deterministic key that makes this entry unique per user (V2). Absent on\r\n * legacy entries; present entries cannot be written twice.\r\n */\r\n idempotencyKey?: string;\r\n /** When this entry was created - ISO 8601 */\r\n createdAt: string;\r\n}\r\n\r\n// ==================== Usage Types ====================\r\n\r\n/**\r\n * Portable usage log entry\r\n */\r\nexport interface PortableUsageLog {\r\n id: string;\r\n userId: string;\r\n operationType: CreditOperationType;\r\n provider: AIProviderType;\r\n creditsUsed: number;\r\n success: boolean;\r\n errorMessage?: string;\r\n resourceId?: string;\r\n resourceType?: ResourceType;\r\n requestId?: string;\r\n metadata?: Record<string, unknown>;\r\n createdAt: string;\r\n}\r\n\r\n/**\r\n * User-friendly usage history entry\r\n */\r\nexport interface UsageHistoryEntry {\r\n id: string;\r\n /** Type of entry for display */\r\n type: \"usage\" | \"purchase\" | \"bonus\" | \"reset\" | \"refund\" | \"adjustment\";\r\n /** Positive = earned, Negative = spent */\r\n creditsChange: number;\r\n /** Balance after this entry */\r\n balanceAfter: number;\r\n /** Human-readable description */\r\n description: string;\r\n /** When this occurred - ISO 8601 */\r\n createdAt: string;\r\n}\r\n\r\n/**\r\n * Paginated usage history response\r\n */\r\nexport interface UsageHistoryResponse {\r\n entries: UsageHistoryEntry[];\r\n pagination: {\r\n total: number;\r\n limit: number;\r\n offset: number;\r\n hasMore: boolean;\r\n };\r\n}\r\n\r\n// ==================== Tier Configuration ====================\r\n\r\n/**\r\n * Tier configuration for credit limits and pricing\r\n */\r\nexport interface TierConfig {\r\n tier: SubscriptionTier;\r\n monthlyCredits: number;\r\n priceUsd: number;\r\n features: string[];\r\n /** Free/default tier marker — no subscription expiry, balance untouched on tier change. Defaults to priceUsd === 0. */\r\n isFree?: boolean;\r\n /** Unlimited tier marker. Defaults to monthlyCredits === 0. */\r\n unlimited?: boolean;\r\n /** Tier assigned to brand-new users. Defaults to the tier flagged isFree. */\r\n isDefault?: boolean;\r\n}\r\n\r\n// ==================== Options Types ====================\r\n\r\n/**\r\n * Options for withCredits HOF\r\n */\r\nexport interface WithCreditsOptions {\r\n /** Operation type for cost lookup */\r\n operationType: CreditOperationType;\r\n /** AI provider (defaults to \"gemini\") */\r\n provider?: AIProviderType;\r\n /**\r\n * Custom cost override (use with caution). Must be a movable amount: only a\r\n * cost of 0 in `operationCosts` makes an operation free, so a computed\r\n * override that lands on 0 is refused rather than run for nothing.\r\n */\r\n customCost?: number;\r\n /** Resource ID for usage logging */\r\n resourceId?: string;\r\n /** Resource type for usage logging */\r\n resourceType?: ResourceType;\r\n}\r\n\r\n// ==================== Constants ====================\r\n","/**\r\n * Typed outcomes for the V2 reservation boundary.\r\n *\r\n * The legacy `*Atomic` repository methods signal \"you lost the race\" by\r\n * throwing (or, worse, by silently returning). V2 makes every terminal\r\n * transition an explicit, serialisable value so callers can tell a winner\r\n * from a duplicate delivery without string-matching an error message.\r\n *\r\n * Every outcome is discriminated on `outcome` (never `status`, which is the\r\n * reservation's own lifecycle field and would collide).\r\n */\r\n\r\nimport type {\r\n PortableReservation,\r\n PortableTransaction,\r\n ReservationStatus,\r\n} from \"./types.js\";\r\n\r\n/** Statuses a reservation can no longer transition out of. */\r\nexport type TerminalReservationStatus = Exclude<ReservationStatus, \"reserved\">;\r\n\r\n/**\r\n * Result of `reserveCreditsV2`.\r\n *\r\n * - `created` — this caller minted the hold.\r\n * - `replayed` — same `(userId, idempotencyKey)` with an identical immutable\r\n * payload; the original reservation is returned and no new hold was placed.\r\n * - `insufficient` — the guarded hold could not be placed; nothing was written.\r\n * - `idempotency_conflict` — the key was reused with a different\r\n * amount/operation. Nothing was written; the existing reservation is\r\n * returned so the caller can diff it.\r\n */\r\nexport type ReserveOutcome =\r\n | { outcome: \"created\"; reservation: PortableReservation }\r\n | { outcome: \"replayed\"; reservation: PortableReservation }\r\n | {\r\n outcome: \"insufficient\";\r\n available: number;\r\n required: number;\r\n shortfall: number;\r\n }\r\n | {\r\n outcome: \"idempotency_conflict\";\r\n idempotencyKey: string;\r\n existing: PortableReservation;\r\n };\r\n\r\n/**\r\n * Result of `commitReservationV2`.\r\n *\r\n * Exactly one concurrent caller ever observes `committed`; that call — and\r\n * only that call — moved the balance and wrote the journal entry.\r\n */\r\nexport type CommitOutcome =\r\n | {\r\n outcome: \"committed\";\r\n reservation: PortableReservation;\r\n amount: number;\r\n /** `balance + bonusCredits` after the deduction, read from the same UPDATE. */\r\n balanceAfter: number;\r\n journalEntryId: string;\r\n }\r\n | {\r\n outcome: \"already_terminal\";\r\n reservation: PortableReservation;\r\n terminalStatus: TerminalReservationStatus;\r\n }\r\n | { outcome: \"not_found\"; reservationId: string };\r\n\r\n/** Result of `releaseReservationV2`. Mirrors {@link CommitOutcome}. */\r\nexport type ReleaseOutcome =\r\n | {\r\n outcome: \"released\";\r\n reservation: PortableReservation;\r\n amount: number;\r\n journalEntryId: string;\r\n }\r\n | {\r\n outcome: \"already_terminal\";\r\n reservation: PortableReservation;\r\n terminalStatus: TerminalReservationStatus;\r\n }\r\n | { outcome: \"not_found\"; reservationId: string };\r\n\r\n/**\r\n * Result of `expireReservationV2`.\r\n *\r\n * `not_due` means the reservation is still `reserved` but its `expiresAt` is\r\n * in the future relative to the sweep's `asOf` — a no-op, not a failure.\r\n */\r\nexport type ExpireOutcome =\r\n | {\r\n outcome: \"expired\";\r\n reservation: PortableReservation;\r\n amount: number;\r\n journalEntryId: string;\r\n }\r\n | { outcome: \"not_due\"; reservation: PortableReservation }\r\n | {\r\n outcome: \"already_terminal\";\r\n reservation: PortableReservation;\r\n terminalStatus: TerminalReservationStatus;\r\n }\r\n | { outcome: \"not_found\"; reservationId: string };\r\n\r\n/**\r\n * Result of crediting an account against a `paymentRef`.\r\n *\r\n * - `created` — this delivery credited the account.\r\n * - `replayed` — the reference was already used for the *same* credit event;\r\n * nothing was written and the original transaction is returned.\r\n * - `conflict` — the reference was already used for a *different* event\r\n * (another user, another amount, another source). Nothing was written;\r\n * `mismatch` names the first field that disagreed.\r\n *\r\n * A call with no `paymentRef` is always `created`: without a reference there is\r\n * nothing to deduplicate on, and every delivery is a distinct credit.\r\n *\r\n * **`created` is only a single-delivery guarantee on an adapter that implements\r\n * `addCreditsV2`.** Routed through a legacy adapter, `created` means \"the call\r\n * was made and did not throw\" — there is no global unique index behind it, so a\r\n * re-delivered reference credits again. `supportsIdempotentCredit` is the probe\r\n * that tells the two apart, and it is also why `transaction` is optional here:\r\n * the legacy method returns nothing, and inventing a transaction to fill the\r\n * field would be a fabricated audit record.\r\n */\r\nexport type AddCreditsOutcome =\r\n | {\r\n outcome: \"created\";\r\n /** The reference this credit was recorded under, if there was one. */\r\n paymentRef?: string;\r\n /** Absent only on the legacy fallback path; see above. */\r\n transaction?: PortableTransaction;\r\n /** Absent only on the legacy fallback path; see above. */\r\n journalEntryId?: string;\r\n }\r\n | {\r\n outcome: \"replayed\";\r\n paymentRef: string;\r\n transaction: PortableTransaction;\r\n }\r\n | {\r\n outcome: \"conflict\";\r\n paymentRef: string;\r\n existing: PortableTransaction;\r\n /** The first field of the canonical payload that disagreed. */\r\n mismatch: string;\r\n };\r\n\r\n/** True when this delivery actually moved credits. */\r\nexport function isCreditedOutcome(\r\n outcome: AddCreditsOutcome\r\n): outcome is Extract<AddCreditsOutcome, { outcome: \"created\" }> {\r\n return outcome.outcome === \"created\";\r\n}\r\n\r\n/** True when this caller performed the state transition. */\r\nexport function isWinningOutcome(\r\n outcome: CommitOutcome | ReleaseOutcome | ExpireOutcome\r\n): boolean {\r\n return (\r\n outcome.outcome === \"committed\" ||\r\n outcome.outcome === \"released\" ||\r\n outcome.outcome === \"expired\"\r\n );\r\n}\r\n\r\n/** Narrow a reserve outcome to the two cases that yield a usable reservation. */\r\nexport function isReservedOutcome(\r\n outcome: ReserveOutcome\r\n): outcome is Extract<ReserveOutcome, { reservation: PortableReservation }> {\r\n return outcome.outcome === \"created\" || outcome.outcome === \"replayed\";\r\n}\r\n","/**\r\n * Trust checks for a *persisted* reservation row.\r\n *\r\n * A reservation is written once and read back on every transition, and nothing\r\n * between those two moments belongs to this library: a direct `UPDATE`, a\r\n * half-finished migration, a legacy adapter or a future version can all leave a\r\n * row this code never wrote. Two of the fields it reads back are load-bearing.\r\n *\r\n * **`status`** decides whether a transition runs at all. It used to be cast —\r\n * `reservation.status as TerminalReservationStatus` — so a row holding\r\n * `'gremlin'` was reported as `already_terminal` with `terminalStatus:\r\n * 'gremlin'`: a *success* outcome, telling the caller the reservation had been\r\n * resolved, over a row nothing in this library can resolve. The cast is a lie\r\n * the type system cannot catch, so the value is checked against the closed set\r\n * instead and anything outside it quarantines the row.\r\n *\r\n * **`holdPlacedAt`** decides whether the hold behind the row exists. See\r\n * {@link assertHoldPlaced}.\r\n *\r\n * Both refusals happen before any state changes, and neither one repairs\r\n * anything: a corrupt row stops moving and waits for an operator. Silently\r\n * reconciling it would need an audit trail and a recovery procedure, and\r\n * guessing is how a bad row becomes a wrong balance.\r\n */\r\n\r\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\r\nimport type { TerminalReservationStatus } from \"./outcomes.js\";\r\nimport type { PortableReservation, ReservationStatus } from \"./types.js\";\r\n\r\n/** Every status this library knows how to reason about. */\r\nexport const RESERVATION_STATUSES: readonly ReservationStatus[] = [\r\n \"reserved\",\r\n \"committed\",\r\n \"released\",\r\n \"expired\",\r\n];\r\n\r\n/** The statuses a reservation can no longer transition out of. */\r\nexport const TERMINAL_RESERVATION_STATUSES: readonly TerminalReservationStatus[] = [\r\n \"committed\",\r\n \"released\",\r\n \"expired\",\r\n];\r\n\r\nexport function isReservationStatus(value: unknown): value is ReservationStatus {\r\n return RESERVATION_STATUSES.includes(value as ReservationStatus);\r\n}\r\n\r\nexport function isTerminalReservationStatus(\r\n value: unknown\r\n): value is TerminalReservationStatus {\r\n return TERMINAL_RESERVATION_STATUSES.includes(value as TerminalReservationStatus);\r\n}\r\n\r\n/** The subset of a reservation these checks read, so callers can pass a row. */\r\nexport interface ReservationIntegrityView {\r\n id: string;\r\n userId: string;\r\n status: unknown;\r\n /** Set by the atomic reserve, in the same transaction as the hold. */\r\n holdPlacedAt?: string | null;\r\n idempotencyKey?: string | null;\r\n}\r\n\r\n/**\r\n * Refuse a transition over a status this library does not define.\r\n *\r\n * Runs ahead of every early exit, including the `already_terminal` one, for the\r\n * reason in the module header: `already_terminal` is a success outcome, and a\r\n * success outcome over an unknown status is the failure mode being prevented.\r\n */\r\nexport function assertKnownReservationStatus(\r\n reservation: ReservationIntegrityView,\r\n transition: string\r\n): asserts reservation is ReservationIntegrityView & { status: ReservationStatus } {\r\n if (isReservationStatus(reservation.status)) return;\r\n throw new CreditError(\r\n `Reservation ${reservation.id} holds the status ${JSON.stringify(reservation.status)}, ` +\r\n `which is not one of ${RESERVATION_STATUSES.join(\", \")}. The ${transition} was ` +\r\n \"refused before any state changed; the row must be repaired by an operator.\",\r\n CreditErrorCode.CORRUPT_RESERVATION_STATUS,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n transition,\r\n status: typeof reservation.status === \"string\" ? reservation.status : String(reservation.status),\r\n allowed: [...RESERVATION_STATUSES],\r\n reason: \"corrupt_reservation_status\",\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Narrow a non-`reserved` status for the `already_terminal` outcomes.\r\n *\r\n * Validates rather than casts. `assertKnownReservationStatus` has usually run\r\n * already; this repeats the check because the cast it replaces was the defect,\r\n * and a second closed-set test costs nothing next to a database round trip.\r\n */\r\nexport function terminalStatusOf(\r\n reservation: ReservationIntegrityView,\r\n transition: string\r\n): TerminalReservationStatus {\r\n assertKnownReservationStatus(reservation, transition);\r\n if (isTerminalReservationStatus(reservation.status)) return reservation.status;\r\n throw new CreditError(\r\n `Reservation ${reservation.id} is ${reservation.status}, which is not a terminal ` +\r\n `status, but the ${transition} tried to report it as one.`,\r\n CreditErrorCode.CORRUPT_RESERVATION_STATUS,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n transition,\r\n status: reservation.status,\r\n reason: \"not_terminal\",\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Refuse a transition over a hold that was never atomically placed.\r\n *\r\n * `holdPlacedAt` is the single invariant behind the V2 boundary: **a row is\r\n * only a reservation if the same atomic operation that wrote it also increased\r\n * `reserved` by its amount.** The atomic reserve sets the field inside that one\r\n * transaction, so the fact and the hold commit together or not at all.\r\n *\r\n * Without it, `createReservation` — a plain row writer that does not touch\r\n * `reserved` — could mint a keyed row, and a later `reserveCreditsV2` with that\r\n * key would adopt it as a `replayed` hold that no credits back. The caller then\r\n * commits it, and the commit passes its `reserved >= amount` guard by consuming\r\n * coverage that belongs to a *different*, real hold: two holds, one payment.\r\n *\r\n * So this is checked before adopting a replay and before every transition, and\r\n * `createReservation` refuses a key outright — the two halves of the same rule.\r\n */\r\nexport function assertHoldPlaced(\r\n reservation: ReservationIntegrityView,\r\n transition: string\r\n): void {\r\n if (reservation.holdPlacedAt) return;\r\n throw new CreditError(\r\n `Reservation ${reservation.id} does not record that its hold was placed atomically ` +\r\n \"(holdPlacedAt is absent), so the credits it claims to hold may never have been \" +\r\n `reserved. The ${transition} was refused before any state changed: honouring it ` +\r\n \"would spend coverage belonging to another reservation. Rows written by \" +\r\n \"createReservation are not V2 reservations; place holds with reserveCredits.\",\r\n CreditErrorCode.UNBACKED_RESERVATION,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n transition,\r\n idempotencyKey: reservation.idempotencyKey ?? undefined,\r\n reason: \"hold_not_placed\",\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Refuse an idempotency key on the direct, non-atomic reservation writer.\r\n *\r\n * The other half of {@link assertHoldPlaced}. `createReservation` inserts a row\r\n * and does not touch `reserved`, so a key registered here would name a hold\r\n * that does not exist. Rejecting the key at the boundary means the unbacked\r\n * keyed row cannot be created in the first place, rather than only being\r\n * refused later by every transition that tries to move it.\r\n *\r\n * The amount check that runs alongside this is about a value the ledger cannot\r\n * honour; this is about a *claim* the writer cannot honour.\r\n */\r\nexport function assertUnkeyedDirectReservation(input: {\r\n userId: string;\r\n idempotencyKey?: string;\r\n}): void {\r\n if (input.idempotencyKey === undefined || input.idempotencyKey === null) return;\r\n throw new CreditError(\r\n \"createReservation cannot accept an idempotencyKey: it writes the reservation \" +\r\n \"row without placing the hold, so the key would name credits that were never \" +\r\n \"reserved, and a later reserveCredits with the same key would adopt it as a \" +\r\n \"replay. Call reserveCredits (or reserveCreditsV2) with the key instead — it \" +\r\n \"claims the key and places the hold in one transaction.\",\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n {\r\n userId: input.userId,\r\n idempotencyKey: input.idempotencyKey,\r\n operation: \"createReservation\",\r\n reason: \"keyed_direct_reservation\",\r\n }\r\n );\r\n}\r\n\r\n/** True when the row records an atomically placed hold. */\r\nexport function hasPlacedHold(reservation: Pick<PortableReservation, \"holdPlacedAt\">): boolean {\r\n return Boolean(reservation.holdPlacedAt);\r\n}\r\n\r\n/**\r\n * Refuse a direct status write that would bypass the V2 state machine.\r\n *\r\n * `updateReservationStatus` assigns a status and nothing else: it does not\r\n * check the current status, move the balance, adjust `reserved`, or journal.\r\n * On a row whose hold was atomically placed, that is not an update — it is a\r\n * bypass. Writing `committed` onto a live hold strands `reserved` forever (the\r\n * real commit then reports `already_terminal` and never debits), and writing\r\n * `reserved` onto a terminal row re-arms a settled reservation so it can be\r\n * settled again.\r\n *\r\n * Two refusals, mirroring the createReservation/reserveCredits split:\r\n *\r\n * - A row bearing `holdPlacedAt` belongs to the V2 transitions. Every status it\r\n * will ever hold is assigned by commit/release/expire, atomically with the\r\n * ledger movement that status implies.\r\n * - No row, V2 or legacy, may be *reopened* — status can never be assigned back\r\n * to `reserved`, because nothing here re-places the hold that status claims.\r\n *\r\n * Rows written by `createReservation` (no hold, no key) remain freely\r\n * annotatable with terminal statuses: their statuses never carried ledger\r\n * meaning, and the V2 transitions refuse them anyway.\r\n */\r\nexport function assertDirectStatusWriteAllowed(\r\n reservation: ReservationIntegrityView,\r\n status: ReservationStatus\r\n): void {\r\n if (status === \"reserved\") {\r\n throw new CreditError(\r\n `Reservation ${reservation.id} cannot have its status set back to \"reserved\": ` +\r\n \"updateReservationStatus does not place a hold, so the reopened row would claim \" +\r\n \"credits nothing reserved. Create a new reservation with reserveCredits instead.\",\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n status,\r\n reason: \"reopen_reservation\",\r\n }\r\n );\r\n }\r\n if (!reservation.holdPlacedAt) return;\r\n throw new CreditError(\r\n `Reservation ${reservation.id} is backed by an atomically placed hold, so its ` +\r\n \"status may only be assigned by the transition that settles it. Writing \" +\r\n `\"${status}\" directly would change the status without moving the credits it ` +\r\n \"stands for, stranding the hold. Use commitReservation / releaseReservation / \" +\r\n \"expireReservation (or their V2 forms) instead.\",\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n status,\r\n reason: \"direct_status_write_on_backed_hold\",\r\n }\r\n );\r\n}\r\n","/**\r\n * `paymentRef` as a global, semantic idempotency boundary.\r\n *\r\n * A payment reference is the identity of a *credit event* — one webhook, one\r\n * invoice, one charge. It is deliberately not scoped to a user: a processor's\r\n * reference is unique across the processor's whole account, and the SQL adapter\r\n * has always enforced it with a unique index over `payment_ref` alone. Two\r\n * problems followed from treating it as anything less.\r\n *\r\n * **It was scoped per user in memory and globally in SQL.** The in-memory\r\n * adapter searched only the crediting user's own transactions, so replaying a\r\n * reference against a *different* user credited both accounts, while the SQL\r\n * adapter silently no-opped. Same call, two ledgers, opposite results — and the\r\n * memory one is the direction that invents money.\r\n *\r\n * **It was compared on presence, never on payload.** A reference that arrived a\r\n * second time carrying a different amount was accepted as a replay of the\r\n * first, so a corrected or spoofed webhook credited the original amount and\r\n * reported success. Presence is only sound when the payload is known to match;\r\n * otherwise the honest answer is a conflict.\r\n *\r\n * So a reference now resolves to exactly one of three outcomes — `created`,\r\n * `replayed`, `conflict` — in both adapters, and `conflict` mutates nothing.\r\n */\r\n\r\nimport { sameAmount } from \"./amount.js\";\r\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\r\nimport type {\r\n CreditSource,\r\n JournalReferenceType,\r\n PortableTransaction,\r\n} from \"./types.js\";\r\n\r\n/**\r\n * The fields that define the credit event a reference names.\r\n *\r\n * `description` is excluded on purpose: it is human-facing copy that a retry\r\n * may legitimately regenerate (\"Purchase 100 credits\" versus \"Purchase of 100\r\n * credits\") without the event being different. Everything that decides *how\r\n * much lands where* is in.\r\n */\r\nexport interface PaymentEventPayload {\r\n userId: string;\r\n amount: number;\r\n /** The transaction row's `type` — the operation the reference names. */\r\n type: PortableTransaction[\"type\"];\r\n /** The journal entry's `source` — where the credits came from. */\r\n source: CreditSource;\r\n /** The journal entry's `referenceType`. */\r\n referenceType: JournalReferenceType;\r\n}\r\n\r\n/**\r\n * The stored side of the comparison, as either adapter can produce it.\r\n *\r\n * `amount` is `unknown` because SQL returns `numeric` as a string, and\r\n * comparing the mapped `Number` is exactly the rounding hole that let\r\n * `9999999999.9900001` match a request for `9999999999.99`.\r\n */\r\nexport interface StoredPaymentEvent {\r\n userId: string;\r\n amount: unknown;\r\n type: string;\r\n /** Absent when no journal entry could be located for the stored transaction. */\r\n source?: string;\r\n referenceType?: string;\r\n}\r\n\r\n/**\r\n * Normalise a payment reference to \"a reference\" or \"no reference\".\r\n *\r\n * Empty and whitespace-only strings are *not* references: they cannot identify\r\n * a payment, and treating them as one produced the divergence where SQL skipped\r\n * the duplicate check (falsy) but still stored the value, so the replay hit the\r\n * unique index and threw while memory quietly credited twice.\r\n *\r\n * Unlike an idempotency key, a blank reference is normalised rather than\r\n * rejected. A key is the caller's explicit request for exactly-once and a blank\r\n * one means they got it wrong; `paymentRef` is an optional annotation on a\r\n * credit, and an absent one is a normal, supported call.\r\n */\r\nexport function normalizePaymentRef(ref?: string | null): string | undefined {\r\n if (typeof ref !== \"string\") return undefined;\r\n const trimmed = ref.trim();\r\n return trimmed.length > 0 ? trimmed : undefined;\r\n}\r\n\r\n/**\r\n * Name the first field on which a stored credit event disagrees, or `null`.\r\n *\r\n * `null` means the delivery is a genuine replay and must change nothing.\r\n * Anything else is a conflict: the same reference was used for a different\r\n * event, and crediting on it would either double-credit or credit the wrong\r\n * amount to the wrong account.\r\n *\r\n * `source` and `referenceType` are compared only when the stored side has them.\r\n * They live on the journal entry, and a transaction written before this library\r\n * journalled its purchases has none to compare against — treating that absence\r\n * as a mismatch would turn every pre-existing reference into a permanent\r\n * conflict, which is a refusal to credit rather than a protection against one.\r\n */\r\nexport function describePaymentMismatch(\r\n stored: StoredPaymentEvent,\r\n expected: PaymentEventPayload\r\n): string | null {\r\n if (stored.userId !== expected.userId) return \"userId\";\r\n if (!sameAmount(stored.amount, expected.amount)) return \"amount\";\r\n if (stored.type !== expected.type) return \"type\";\r\n if (stored.source !== undefined && stored.source !== expected.source) return \"source\";\r\n if (stored.referenceType !== undefined && stored.referenceType !== expected.referenceType) {\r\n return \"referenceType\";\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * Refuse a payment reference on the direct, record-only transaction writer.\r\n *\r\n * `createTransaction` writes a ledger *record*: no balance moves and no journal\r\n * entry is created. But a `paymentRef` on that record still occupies the global\r\n * unique boundary, and `addCreditsV2` treats the row that holds a reference as\r\n * the authoritative credit event — a later delivery of the real payment with\r\n * the same reference then matches the record on user/amount/type, reports\r\n * `replayed`, and credits nothing, forever. The record would have consumed the\r\n * payment.\r\n *\r\n * Same shape as `assertUnkeyedDirectReservation`: the claim is refused at the\r\n * boundary, so the row that could impersonate a credit event cannot be written\r\n * in the first place. Credits that carry a reference go through\r\n * `addCreditsAtomic` / `addCreditsV2`, which claim the reference and move the\r\n * balance in one atomic step.\r\n */\r\nexport function assertUnreferencedDirectTransaction(input: {\r\n userId: string;\r\n paymentRef?: string;\r\n}): void {\r\n if (normalizePaymentRef(input.paymentRef) === undefined) return;\r\n throw new CreditError(\r\n \"createTransaction cannot accept a paymentRef: it records a transaction \" +\r\n \"without crediting the account, and the reference would then be treated \" +\r\n \"as an already-delivered payment — a later addCredits call with the same \" +\r\n \"reference reports `replayed` and credits nothing. Record unreferenced \" +\r\n \"transactions here, or deliver referenced payments through addCredits \" +\r\n \"(addCreditsAtomic / addCreditsV2), which claims the reference and moves \" +\r\n \"the balance atomically.\",\r\n CreditErrorCode.UNSUPPORTED_OPERATION,\r\n {\r\n userId: input.userId,\r\n paymentRef: input.paymentRef,\r\n operation: \"createTransaction\",\r\n reason: \"referenced_direct_transaction\",\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * The conflict, as an error, for the legacy `addCreditsAtomic` signature.\r\n *\r\n * That method returns `void`, so it has nowhere to put a `conflict` outcome.\r\n * Swallowing one would be the original bug in a new place — the SQL adapter\r\n * used to no-op on a cross-user reference reuse, which reads to the caller as\r\n * \"credited\" — so the legacy path throws instead. Callers that want to *handle*\r\n * a conflict rather than catch it should use `addCreditsV2`.\r\n */\r\nexport function createPaymentRefConflictError(\r\n paymentRef: string,\r\n details: { userId: string; mismatch: string; existingUserId?: string }\r\n): CreditError {\r\n return new CreditError(\r\n `Payment reference ${paymentRef} has already been used for a different credit ` +\r\n `event (${details.mismatch} differs). Nothing was credited. A payment reference ` +\r\n \"identifies one credit event globally; reusing it for another user, amount or \" +\r\n \"source is refused rather than silently ignored.\",\r\n CreditErrorCode.IDEMPOTENCY_CONFLICT,\r\n { paymentRef, reason: \"payment_ref_conflict\", ...details }\r\n );\r\n}\r\n","/**\r\n * Validation for caller-supplied idempotency keys.\r\n *\r\n * The keys are enforced by a *partial* unique index (`WHERE idempotency_key IS\r\n * NOT NULL`), so \"no key\" and \"a key\" are genuinely different states and the\r\n * boundary between them has to be unambiguous. An empty string sat in between:\r\n * the SQL adapter stored it — occupying a row in the unique index — while every\r\n * `if (key)` check read it as absent, so the key was written but never\r\n * deduplicated anything, and the in-memory adapter ignored it outright. Two\r\n * adapters, two behaviours, neither of them the one the caller asked for.\r\n *\r\n * Rejecting is preferred over trimming. Normalising `\" job-1 \"` to `\"job-1\"`\r\n * would mean two callers who sent different strings silently share a hold, and\r\n * a caller whose key is accidentally whitespace would rather hear about it than\r\n * have it quietly repaired.\r\n */\r\n\r\nimport { CreditError, CreditErrorCode } from \"./errors.js\";\r\n\r\n/** A usable key: a string with at least one non-whitespace character. */\r\nexport function isValidIdempotencyKey(value: unknown): value is string {\r\n return typeof value === \"string\" && value.trim().length > 0;\r\n}\r\n\r\n/**\r\n * Reject an unusable idempotency key before anything is written.\r\n *\r\n * `undefined` is allowed and means \"no key\" — that is the legacy, at-least-once\r\n * behaviour, and it is a valid choice. Only a key that was *supplied* and\r\n * cannot work is an error.\r\n */\r\nexport function assertValidIdempotencyKey(\r\n value: unknown,\r\n context?: Record<string, unknown>\r\n): asserts value is string | undefined {\r\n if (value === undefined || value === null) return;\r\n if (isValidIdempotencyKey(value)) return;\r\n throw new CreditError(\r\n typeof value === \"string\"\r\n ? \"An idempotency key was supplied but is empty or whitespace-only. It \" +\r\n \"cannot deduplicate anything, so it was rejected rather than stored \" +\r\n \"as a key that never matches. Omit the key for at-least-once \" +\r\n \"behaviour, or send a meaningful one.\"\r\n : `Idempotency key must be a string (got ${typeof value})`,\r\n CreditErrorCode.INVALID_IDEMPOTENCY_KEY,\r\n { idempotencyKey: value, ...context }\r\n );\r\n}\r\n\r\n/**\r\n * Namespace the V2 reservation transitions own. Public writes may not use it.\r\n *\r\n * A transition that finds its deterministic key already present treats an\r\n * exactly-matching row as its own retry and reuses it. That is correct against\r\n * a row the transition itself wrote — and a hole if any caller can write one.\r\n * A caller who pre-seeds `reservation:<id>:commit` with a plausible-looking\r\n * entry would have the commit adopt it: the reservation flips to committed and\r\n * the balance moves, while the only journal row is the caller's, never written\r\n * by the transition and not necessarily describing what it did.\r\n *\r\n * Reserving the prefix removes the premise. Nothing outside the transitions can\r\n * create a key in it, so an existing key is either the transition's own retry\r\n * or corruption — and corruption is refused.\r\n */\r\nexport const RESERVED_JOURNAL_KEY_PREFIX = \"reservation:\";\r\n\r\n/** True when a key belongs to the namespace the V2 transitions own. */\r\nexport function isReservedJournalKey(key: unknown): boolean {\r\n return typeof key === \"string\" && key.startsWith(RESERVED_JOURNAL_KEY_PREFIX);\r\n}\r\n\r\n/**\r\n * Refuse a public journal write that claims a key in the reserved namespace.\r\n *\r\n * Called from the adapters' public `createJournalEntry`, never from the\r\n * transitions themselves — they are the ones the namespace belongs to.\r\n */\r\nexport function assertPublicJournalKey(key: unknown, userId: string): void {\r\n if (!isReservedJournalKey(key)) return;\r\n throw new CreditError(\r\n `Idempotency keys starting with \"${RESERVED_JOURNAL_KEY_PREFIX}\" are reserved for ` +\r\n 'the V2 reservation transitions and cannot be written directly. Use a key of ' +\r\n 'your own, or commit/release/expire the reservation through the V2 methods.',\r\n CreditErrorCode.INVALID_IDEMPOTENCY_KEY,\r\n { userId, idempotencyKey: key, reason: \"reserved_namespace\" }\r\n );\r\n}\r\n","/**\r\n * Driver-error classification.\r\n *\r\n * Consuming apps need to react differently to \"the user is out of credits\"\r\n * (show a paywall), \"the database blinked\" (retry), and \"our SQL is wrong\"\r\n * (page someone). Drivers signal that distinction through a SQLSTATE on the\r\n * thrown object, so this maps SQLSTATE classes onto {@link CreditErrorCode}\r\n * without taking a dependency on any particular driver.\r\n */\r\n\r\nimport {\r\n CreditError,\r\n CreditErrorCode,\r\n isCreditError,\r\n} from \"./errors.js\";\r\n\r\n/**\r\n * PostgreSQL SQLSTATEs whose failure is a property of the moment, not of the\r\n * statement — the identical call may well succeed on retry.\r\n *\r\n * - `40001` serialization_failure, `40P01` deadlock_detected, `40000` transaction_rollback\r\n * - `55P03` lock_not_available, `57014` query_canceled (statement timeout)\r\n * - `53300` too_many_connections, `53200` out_of_memory, `53400` configuration_limit_exceeded\r\n * - `08xxx` connection exception class\r\n */\r\nconst TRANSIENT_SQLSTATES = new Set([\r\n \"40000\",\r\n \"40001\",\r\n \"40P01\",\r\n \"55P03\",\r\n \"57014\",\r\n \"57P01\",\r\n \"57P02\",\r\n \"57P03\",\r\n \"53200\",\r\n \"53300\",\r\n \"53400\",\r\n]);\r\n\r\n/** Node driver-level codes that mean \"the socket/pool failed\", not \"the query is wrong\". */\r\nconst TRANSIENT_DRIVER_CODES = new Set([\r\n \"ECONNRESET\",\r\n \"ECONNREFUSED\",\r\n \"EPIPE\",\r\n \"ETIMEDOUT\",\r\n \"ENOTFOUND\",\r\n \"EHOSTUNREACH\",\r\n]);\r\n\r\n/**\r\n * `22003 numeric_value_out_of_range` — the arithmetic overflowed the column.\r\n *\r\n * Validating the operands cannot prevent this: `9999999999.99 + 0.01` is two\r\n * perfectly valid amounts whose *sum* the `numeric(12, 2)` column cannot hold.\r\n * It is a deterministic domain failure, not a broken query, so it is reported\r\n * as one instead of as a generic `DATABASE_ERROR`.\r\n */\r\nconst NUMERIC_OVERFLOW = \"22003\";\r\n\r\n/**\r\n * Read a SQLSTATE-ish `code` off an unknown thrown value.\r\n *\r\n * The chain matters. Wrapping drivers — and Drizzle itself, in places — rethrow\r\n * with the original error on `cause` and no `code` of their own, so looking only\r\n * at the outermost object reports \"no SQLSTATE\" for an error that plainly has\r\n * one. The walk is bounded and tracks what it has seen, since a `cause` graph\r\n * can be circular.\r\n */\r\nexport function getSqlState(error: unknown): string | undefined {\r\n const seen = new Set<unknown>();\r\n let current: unknown = error;\r\n for (let depth = 0; depth < 8; depth += 1) {\r\n if (typeof current !== \"object\" || current === null || seen.has(current)) return undefined;\r\n seen.add(current);\r\n const code = (current as { code?: unknown }).code;\r\n if (typeof code === \"string\") return code;\r\n current = (current as { cause?: unknown }).cause;\r\n }\r\n return undefined;\r\n}\r\n\r\n/**\r\n * True when the SQLSTATE is a unique-violation (`23505`).\r\n *\r\n * Callers that race on a partial unique index use this to tell \"someone else\r\n * inserted the same idempotency key\" from a genuine schema error.\r\n */\r\nexport function isUniqueViolation(error: unknown): boolean {\r\n return getSqlState(error) === \"23505\";\r\n}\r\n\r\n/** True when the error is worth retrying. */\r\nexport function isTransientDatabaseError(error: unknown): boolean {\r\n const code = getSqlState(error);\r\n if (!code) return false;\r\n if (TRANSIENT_SQLSTATES.has(code)) return true;\r\n if (TRANSIENT_DRIVER_CODES.has(code)) return true;\r\n // Whole connection-exception class (08000, 08003, 08006, 08001, 08004, 08007, 08P01).\r\n return code.startsWith(\"08\");\r\n}\r\n\r\n/**\r\n * Wrap an unknown thrown value in a classified {@link CreditError}.\r\n *\r\n * Already-classified `CreditError`s pass through untouched so a domain outcome\r\n * (insufficient credits, idempotency conflict) is never downgraded to\r\n * `DATABASE_ERROR` by a generic catch further up the stack.\r\n */\r\nexport function classifyDatabaseError(\r\n error: unknown,\r\n context?: Record<string, unknown>\r\n): CreditError {\r\n if (isCreditError(error)) return error;\r\n\r\n const sqlState = getSqlState(error);\r\n const message = error instanceof Error ? error.message : String(error);\r\n const details = { ...context, ...(sqlState ? { sqlState } : {}) };\r\n\r\n if (isTransientDatabaseError(error)) {\r\n return new CreditError(message, CreditErrorCode.TRANSIENT_ERROR, details);\r\n }\r\n\r\n if (sqlState === NUMERIC_OVERFLOW) {\r\n return new CreditError(message, CreditErrorCode.INVALID_AMOUNT, {\r\n ...details,\r\n reason: \"amount_out_of_range\",\r\n });\r\n }\r\n\r\n return new CreditError(message, CreditErrorCode.DATABASE_ERROR, details);\r\n}\r\n","/**\r\n * Deferred execution abstraction - framework agnostic\r\n *\r\n * Provides a way to schedule work to run after the current operation completes.\r\n * Different frameworks can provide their own implementations:\r\n * - Next.js: uses `after()` from `next/server`\r\n * - Generic: uses `setImmediate()` or `setTimeout()`\r\n * - Testing: can use synchronous execution\r\n */\r\n\r\n/**\r\n * Interface for deferred execution\r\n *\r\n * Implementations should schedule the function to run asynchronously\r\n * without blocking the current operation.\r\n */\r\nexport interface DeferredExecutor {\r\n /**\r\n * Schedule a function to run asynchronously\r\n *\r\n * The function should be executed after the current operation completes.\r\n * Errors should be caught and logged, not propagated.\r\n *\r\n * @param fn - Async function to execute\r\n */\r\n defer(fn: () => Promise<void>): void;\r\n}\r\n\r\n/**\r\n * Generic deferred executor using setImmediate/setTimeout\r\n *\r\n * Works in Node.js and browser environments.\r\n * Suitable for non-framework environments or testing.\r\n */\r\nexport const genericDeferred: DeferredExecutor = {\r\n defer(fn: () => Promise<void>): void {\r\n // Use setImmediate in Node.js, setTimeout as fallback\r\n const scheduler = typeof setImmediate !== \"undefined\" ? setImmediate : setTimeout;\r\n scheduler(() => {\r\n fn().catch((error) => {\r\n console.error(\"[Credits] Deferred task error:\", error);\r\n });\r\n });\r\n },\r\n};\r\n\r\n/**\r\n * Synchronous deferred executor\r\n *\r\n * Executes the function immediately (fire-and-forget).\r\n * Useful for testing or when deferred execution isn't needed.\r\n */\r\nexport const synchronousDeferred: DeferredExecutor = {\r\n defer(fn: () => Promise<void>): void {\r\n fn().catch((error) => {\r\n console.error(\"[Credits] Deferred task error:\", error);\r\n });\r\n },\r\n};\r\n\r\n/**\r\n * Create a custom deferred executor\r\n *\r\n * Allows frameworks to provide their own scheduling mechanism.\r\n *\r\n * @example\r\n * ```typescript\r\n * // Next.js adapter\r\n * import { after } from 'next/server';\r\n *\r\n * const nextJsDeferred = createDeferredExecutor((fn) => after(fn));\r\n * ```\r\n *\r\n * @param schedule - Function that schedules async work\r\n * @returns DeferredExecutor instance\r\n */\r\nexport function createDeferredExecutor(\r\n schedule: (fn: () => Promise<void>) => void\r\n): DeferredExecutor {\r\n return {\r\n defer: schedule,\r\n };\r\n}\r\n\r\n/**\r\n * No-op deferred executor\r\n *\r\n * Discards deferred work. Useful when you want to disable\r\n * background tasks (e.g., in certain test scenarios).\r\n */\r\nexport const noopDeferred: DeferredExecutor = {\r\n defer(_fn: () => Promise<void>): void {\r\n // Intentionally do nothing\r\n },\r\n};\r\n","import { z } from \"zod\";\r\nimport type { SubscriptionTier, TierConfig } from \"../core/types.js\";\r\nimport { getConfigProvider } from \"./provider.js\";\r\n\r\n/**\r\n * Schema for tier configuration\r\n */\r\nconst tierConfigSchema = z.object({\r\n tier: z.string().min(1),\r\n monthlyCredits: z.number().min(0),\r\n priceUsd: z.number().min(0),\r\n features: z.array(z.string()),\r\n isFree: z.boolean().optional(),\r\n unlimited: z.boolean().optional(),\r\n isDefault: z.boolean().optional(),\r\n});\r\n\r\n/**\r\n * Schema for credit system configuration\r\n */\r\nconst creditSystemConfigSchema = z.object({\r\n /**\r\n * Operation costs in credits\r\n * Keys are operation type names (any non-empty string)\r\n * Values are non-negative numbers representing credit cost. 0 marks a free\r\n * operation: `withCredits` runs it without touching the ledger. Amounts that\r\n * move credits stay strictly positive, so `reserveCredits` refuses a 0.\r\n */\r\n operationCosts: z.record(\r\n z.string().min(1),\r\n z.number().nonnegative()\r\n ),\r\n\r\n /**\r\n * Display labels for operation types (English canonical labels).\r\n * Falls back to auto-generating from snake_case keys if not provided.\r\n */\r\n operationLabels: z.record(\r\n z.string().min(1),\r\n z.string().min(1)\r\n ).optional().default({}),\r\n\r\n /**\r\n * Tier configurations\r\n */\r\n tierConfigs: z.record(\r\n z.string().min(1),\r\n tierConfigSchema\r\n ),\r\n\r\n /**\r\n * Reservation expiry time in milliseconds\r\n */\r\n reservationExpiryMs: z.number().positive().default(5 * 60 * 1000),\r\n\r\n /**\r\n * Default credits for new users (free tier). 0 means no signup grant.\r\n */\r\n defaultFreeCredits: z.number().min(0).default(25),\r\n\r\n /**\r\n * Grace period in days after subscription expires before downgrade\r\n */\r\n subscriptionGracePeriodDays: z.number().min(0).default(3),\r\n\r\n /**\r\n * Low balance notification threshold\r\n */\r\n lowBalanceThreshold: z.number().min(0).default(10),\r\n\r\n /**\r\n * Cooldown in hours between low balance notifications\r\n */\r\n lowBalanceNotificationCooldownHours: z.number().positive().default(24),\r\n\r\n /**\r\n * Feature flags for optional features\r\n */\r\n features: z.object({\r\n journalEntries: z.boolean().default(true),\r\n notifications: z.boolean().default(true),\r\n subscriptionExpiry: z.boolean().default(true),\r\n usageHistory: z.boolean().default(true),\r\n }).default({\r\n journalEntries: true,\r\n notifications: true,\r\n subscriptionExpiry: true,\r\n usageHistory: true,\r\n }),\r\n});\r\n\r\n/**\r\n * Credit system configuration type\r\n */\r\nexport type CreditSystemConfig = z.infer<typeof creditSystemConfigSchema>;\r\n\r\n/**\r\n * Default configuration\r\n */\r\nconst DEFAULT_CONFIG: CreditSystemConfig = {\r\n operationCosts: {\r\n story_generation: 5,\r\n conversation: 2,\r\n image_generation: 10,\r\n template_generation: 10,\r\n },\r\n operationLabels: {\r\n story_generation: \"Story generation\",\r\n conversation: \"Conversation\",\r\n image_generation: \"Image generation\",\r\n template_generation: \"Template generation\",\r\n },\r\n tierConfigs: {\r\n free: {\r\n tier: \"free\",\r\n monthlyCredits: 25,\r\n priceUsd: 0,\r\n isFree: true,\r\n isDefault: true,\r\n features: [\r\n \"25 credits per month\",\r\n \"Basic story generation\",\r\n \"Standard image quality\",\r\n ],\r\n },\r\n basic: {\r\n tier: \"basic\",\r\n monthlyCredits: 200,\r\n priceUsd: 9.99,\r\n features: [\r\n \"200 credits per month\",\r\n \"All story features\",\r\n \"High quality images\",\r\n \"Priority support\",\r\n ],\r\n },\r\n premium: {\r\n tier: \"premium\",\r\n monthlyCredits: 500,\r\n priceUsd: 19.99,\r\n features: [\r\n \"500 credits per month\",\r\n \"All features\",\r\n \"Highest quality images\",\r\n \"Early access to new features\",\r\n \"Priority support\",\r\n ],\r\n },\r\n unlimited: {\r\n tier: \"unlimited\",\r\n monthlyCredits: 0, // 0 = unlimited\r\n unlimited: true,\r\n priceUsd: 49.99,\r\n features: [\r\n \"Unlimited credits\",\r\n \"All features\",\r\n \"Highest quality images\",\r\n \"Early access to new features\",\r\n \"Dedicated support\",\r\n ],\r\n },\r\n },\r\n reservationExpiryMs: 5 * 60 * 1000, // 5 minutes\r\n defaultFreeCredits: 25,\r\n subscriptionGracePeriodDays: 3,\r\n lowBalanceThreshold: 10,\r\n lowBalanceNotificationCooldownHours: 24,\r\n features: {\r\n journalEntries: true,\r\n notifications: true,\r\n subscriptionExpiry: true,\r\n usageHistory: true,\r\n },\r\n};\r\n\r\n/**\r\n * Current configuration (can be overridden)\r\n */\r\nlet currentConfig: CreditSystemConfig = DEFAULT_CONFIG;\r\n\r\n/**\r\n * Load configuration from environment variables\r\n * Can be extended to load from other sources (file, remote config, etc.)\r\n *\r\n * Operation costs are loaded from CREDITS_OPERATION_COSTS as JSON:\r\n * CREDITS_OPERATION_COSTS={\"story_generation\":5,\"image_generation\":10}\r\n */\r\nexport function loadConfigFromEnv(): Partial<CreditSystemConfig> {\r\n const envConfig: Partial<CreditSystemConfig> = {};\r\n\r\n // Load operation costs from env as JSON\r\n const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;\r\n if (operationCostsEnv) {\r\n try {\r\n const parsed = JSON.parse(operationCostsEnv);\r\n if (typeof parsed === \"object\" && parsed !== null) {\r\n envConfig.operationCosts = {\r\n ...DEFAULT_CONFIG.operationCosts,\r\n ...parsed,\r\n };\r\n }\r\n } catch {\r\n console.warn(\"[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON\");\r\n }\r\n }\r\n\r\n // Load other settings from env\r\n const reservationExpiry = process.env.CREDITS_RESERVATION_EXPIRY_MS;\r\n if (reservationExpiry) {\r\n envConfig.reservationExpiryMs = parseInt(reservationExpiry, 10);\r\n }\r\n\r\n const defaultCredits = process.env.CREDITS_DEFAULT_FREE;\r\n if (defaultCredits) {\r\n envConfig.defaultFreeCredits = parseInt(defaultCredits, 10);\r\n }\r\n\r\n const gracePeriod = process.env.CREDITS_GRACE_PERIOD_DAYS;\r\n if (gracePeriod) {\r\n envConfig.subscriptionGracePeriodDays = parseInt(gracePeriod, 10);\r\n }\r\n\r\n const lowBalanceThreshold = process.env.CREDITS_LOW_BALANCE_THRESHOLD;\r\n if (lowBalanceThreshold) {\r\n envConfig.lowBalanceThreshold = parseInt(lowBalanceThreshold, 10);\r\n }\r\n\r\n // Load feature flags\r\n const featuresEnv = process.env.CREDITS_FEATURES;\r\n if (featuresEnv) {\r\n try {\r\n const features = JSON.parse(featuresEnv);\r\n envConfig.features = {\r\n ...DEFAULT_CONFIG.features,\r\n ...features,\r\n };\r\n } catch {\r\n console.warn(\"[Credits Config] Failed to parse CREDITS_FEATURES env var\");\r\n }\r\n }\r\n\r\n return envConfig;\r\n}\r\n\r\n/**\r\n * Initialize configuration\r\n * Merges default config with environment overrides\r\n */\r\nexport function initializeConfig(overrides?: Partial<CreditSystemConfig>): CreditSystemConfig {\r\n const envConfig = loadConfigFromEnv();\r\n\r\n const mergedConfig = {\r\n ...DEFAULT_CONFIG,\r\n ...envConfig,\r\n ...overrides,\r\n operationCosts: {\r\n ...DEFAULT_CONFIG.operationCosts,\r\n ...envConfig.operationCosts,\r\n ...overrides?.operationCosts,\r\n },\r\n operationLabels: {\r\n ...DEFAULT_CONFIG.operationLabels,\r\n ...envConfig.operationLabels,\r\n ...overrides?.operationLabels,\r\n },\r\n tierConfigs: {\r\n ...DEFAULT_CONFIG.tierConfigs,\r\n ...envConfig.tierConfigs,\r\n ...overrides?.tierConfigs,\r\n },\r\n features: {\r\n ...DEFAULT_CONFIG.features,\r\n ...envConfig.features,\r\n ...overrides?.features,\r\n },\r\n };\r\n\r\n // Validate configuration\r\n const result = creditSystemConfigSchema.safeParse(mergedConfig);\r\n if (!result.success) {\r\n console.error(\"[Credits Config] Invalid configuration:\", result.error.issues);\r\n throw new Error(\"Invalid credit system configuration\");\r\n }\r\n\r\n currentConfig = result.data;\r\n return currentConfig;\r\n}\r\n\r\n/**\r\n * Get the current configuration.\r\n * If an external provider is registered, delegates to it.\r\n * Otherwise falls back to the local currentConfig.\r\n */\r\nexport function getConfig(): CreditSystemConfig {\r\n const provider = getConfigProvider();\r\n if (provider) {\r\n return provider.getConfig();\r\n }\r\n return currentConfig;\r\n}\r\n\r\n/**\r\n * Get all valid operation types from configuration\r\n * Returns the keys of the operationCosts object\r\n */\r\nexport function getValidOperationTypes(): string[] {\r\n return Object.keys(getConfig().operationCosts);\r\n}\r\n\r\n/**\r\n * Check if an operation type is valid (configured in the system)\r\n * @param type - The operation type to check\r\n * @returns true if the operation type is configured\r\n */\r\nexport function isValidOperationType(type: string): boolean {\r\n return type in getConfig().operationCosts;\r\n}\r\n\r\n/**\r\n * Get operation cost from configuration\r\n * @throws Error if the operation type is not configured\r\n */\r\nexport function getConfigOperationCost(operationType: string): number {\r\n const cost = getConfig().operationCosts[operationType];\r\n if (cost === undefined) {\r\n throw new Error(\r\n `Unknown operation type: ${operationType}. Valid types: ${getValidOperationTypes().join(\", \")}`\r\n );\r\n }\r\n return cost;\r\n}\r\n\r\n/**\r\n * Get all valid tier ids from configuration\r\n * Returns the keys of the tierConfigs object\r\n */\r\nexport function getValidTiers(): string[] {\r\n return Object.keys(getConfig().tierConfigs);\r\n}\r\n\r\n/**\r\n * Check if a tier id is valid (configured in the system)\r\n */\r\nexport function isValidTier(tier: string): boolean {\r\n return tier in getConfig().tierConfigs;\r\n}\r\n\r\n/**\r\n * Check if a tier is the free/default tier.\r\n * Falls back to priceUsd === 0 when the isFree flag is not set.\r\n */\r\nexport function isFreeTier(tier: SubscriptionTier): boolean {\r\n const c = getConfig().tierConfigs[tier];\r\n if (!c) return false;\r\n return c.isFree ?? c.priceUsd === 0;\r\n}\r\n\r\n/**\r\n * Check if a tier is unlimited.\r\n * Falls back to monthlyCredits === 0 when the unlimited flag is not set.\r\n */\r\nexport function isUnlimitedTier(tier: SubscriptionTier): boolean {\r\n const c = getConfig().tierConfigs[tier];\r\n if (!c) return false;\r\n return c.unlimited ?? c.monthlyCredits === 0;\r\n}\r\n\r\n/**\r\n * Get the tier assigned to brand-new users.\r\n * Prefers the tier flagged isDefault, then the first isFree tier, then \"free\".\r\n */\r\nexport function getDefaultTier(): SubscriptionTier {\r\n const entries = Object.entries(getConfig().tierConfigs);\r\n const def =\r\n entries.find(([, c]) => c.isDefault)?.[0] ??\r\n entries.find(([, c]) => c.isFree ?? c.priceUsd === 0)?.[0];\r\n return def ?? \"free\";\r\n}\r\n\r\n/**\r\n * Magic balance assigned to unlimited tiers on upgrade.\r\n * Kept at 999999 for back-compat with stored/displayed balances.\r\n */\r\nexport const UNLIMITED_BALANCE_SENTINEL = 999999;\r\n\r\n/**\r\n * Get the magic balance assigned to unlimited tiers on upgrade.\r\n */\r\nexport function getUnlimitedSentinelBalance(): number {\r\n return UNLIMITED_BALANCE_SENTINEL;\r\n}\r\n\r\n/**\r\n * What a monthly reset must leave in `balance`, as a storable instruction.\r\n *\r\n * This is the one place the unlimited contract is defined, because it is the\r\n * one thing the two adapters kept disagreeing about. The canonical persisted\r\n * representation of an unlimited tier is:\r\n *\r\n * - `monthlyLimit` = `storedMonthlyLimit(Infinity)`, i.e. the top of the\r\n * representable range. Never `0`, which reads as *no* allowance.\r\n * - `balance` = at least {@link UNLIMITED_BALANCE_SENTINEL}, which is what\r\n * `updateTier` writes on upgrade.\r\n *\r\n * The reset used to leave an unlimited balance strictly alone (\"`Infinity` is\r\n * not storable, so change nothing\"), which is correct for a healthy account and\r\n * a trap for a degraded one: an unlimited user whose balance had been driven to\r\n * `0` — by a spend, or by the older code that stored `0` as the limit — stayed\r\n * at `0` through every subsequent reset and could never buy anything again. So\r\n * the instruction is `atLeast`, not \"leave alone\": it restores a degraded\r\n * balance to the sentinel and leaves a topped-up one (a purchase on top of an\r\n * unlimited plan) untouched. It is expressed as a floor rather than as a number\r\n * so the SQL adapter can apply it as `greatest(balance, n)` inside its guarded\r\n * UPDATE instead of writing a literal read before the lock.\r\n */\r\nexport type MonthlyResetBalance =\r\n /** Store exactly this value. */\r\n | { kind: \"absolute\"; value: number }\r\n /** Raise the stored balance to this value; leave anything higher alone. */\r\n | { kind: \"atLeast\"; value: number };\r\n\r\nexport function monthlyResetBalance(tier: SubscriptionTier): MonthlyResetBalance {\r\n if (isUnlimitedTier(tier)) {\r\n return { kind: \"atLeast\", value: UNLIMITED_BALANCE_SENTINEL };\r\n }\r\n return { kind: \"absolute\", value: getConfigMonthlyLimit(tier) };\r\n}\r\n\r\n/**\r\n * Get tier configuration from configuration.\r\n * Falls back to the default tier (with a warning) if the tier is unknown.\r\n */\r\nexport function getConfigTierConfig(tier: SubscriptionTier): TierConfig {\r\n const cfg = getConfig().tierConfigs[tier];\r\n if (cfg) return cfg;\r\n const fallback = getDefaultTier();\r\n console.warn(\r\n `[Credits Config] Unknown tier \"${tier}\". Falling back to \"${fallback}\". Valid: ${getValidTiers().join(\", \")}`\r\n );\r\n return getConfig().tierConfigs[fallback]!;\r\n}\r\n\r\n/**\r\n * Get monthly limit for a tier from configuration\r\n * Returns Infinity for unlimited tier\r\n */\r\nexport function getConfigMonthlyLimit(tier: SubscriptionTier): number {\r\n return isUnlimitedTier(tier) ? Infinity : getConfigTierConfig(tier).monthlyCredits;\r\n}\r\n\r\n/**\r\n * Runtime zod validator for tier ids. Validates against the LIVE config keys\r\n * at parse time, so apps that add tiers via config get correct validation.\r\n */\r\nexport const tierSchema = z\r\n .string()\r\n .refine(isValidTier, { message: \"Unknown subscription tier\" }) as unknown as z.ZodType<SubscriptionTier>;\r\n\r\n/**\r\n * Parse/validate a tier id against the live config keys.\r\n * @throws ZodError if the tier is not configured\r\n */\r\nexport function parseTier(value: unknown): SubscriptionTier {\r\n return tierSchema.parse(value);\r\n}\r\n\r\n/**\r\n * Check if a feature is enabled\r\n */\r\nexport function isFeatureEnabled(feature: keyof CreditSystemConfig[\"features\"]): boolean {\r\n return getConfig().features[feature];\r\n}\r\n\r\n/**\r\n * Convert a snake_case string to Title Case.\r\n * E.g., \"story_generation\" -> \"Story Generation\"\r\n */\r\nfunction snakeCaseToTitleCase(str: string): string {\r\n return str\r\n .replace(/_/g, \" \")\r\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\r\n}\r\n\r\n/**\r\n * Get the display label for an operation type.\r\n * Falls back to converting snake_case to Title Case if no label is configured.\r\n */\r\nexport function getOperationLabel(operationType: string): string {\r\n const labels = getConfig().operationLabels;\r\n if (labels && operationType in labels) {\r\n return labels[operationType];\r\n }\r\n return snakeCaseToTitleCase(operationType);\r\n}\r\n\r\n/**\r\n * Get all operation labels as a record.\r\n * For any operation in operationCosts that lacks a label, generates one from the key.\r\n */\r\nexport function getOperationLabels(): Record<string, string> {\r\n const config = getConfig();\r\n const result: Record<string, string> = {};\r\n for (const key of Object.keys(config.operationCosts)) {\r\n result[key] = config.operationLabels?.[key]\r\n ?? snakeCaseToTitleCase(key);\r\n }\r\n return result;\r\n}\r\n\r\n/**\r\n * Reset configuration to defaults (for testing)\r\n */\r\nexport function resetConfig(): void {\r\n currentConfig = DEFAULT_CONFIG;\r\n}\r\n\r\n// Initialize configuration on module load\r\ninitializeConfig();\r\n","import type { CreditSystemConfig } from \"./index.js\";\r\n\r\n/** Interface for external config providers (e.g. Firestore-backed) */\r\nexport interface ICreditConfigProvider {\r\n getConfig(): CreditSystemConfig;\r\n}\r\n\r\nlet configProvider: ICreditConfigProvider | null = null;\r\n\r\n/** Register an external config provider. Called once at app startup. */\r\nexport function registerConfigProvider(provider: ICreditConfigProvider): void {\r\n configProvider = provider;\r\n}\r\n\r\n/** Get the registered provider, or null if none registered. */\r\nexport function getConfigProvider(): ICreditConfigProvider | null {\r\n return configProvider;\r\n}\r\n\r\n/** Clear registered provider (for testing). */\r\nexport function clearConfigProvider(): void {\r\n configProvider = null;\r\n}\r\n","import type {\r\n ICreditRepositoryCreditsV2,\r\n ICreditRepositoryV2,\r\n} from \"./v2-types.js\";\r\nimport type {\r\n PortableUserCredits,\r\n PortableReservation,\r\n PortableTransaction,\r\n PortableUsageLog,\r\n PortableJournalEntry,\r\n CreditOperationType,\r\n SubscriptionTier,\r\n ReservationStatus,\r\n AIProviderType,\r\n MonthlyResetResult,\r\n SubscriptionExpiryResult,\r\n CreditSource,\r\n JournalReferenceType,\r\n} from \"../core/types.js\";\r\n\r\n/**\r\n * Input for creating a credit reservation\r\n */\r\nexport interface CreateReservationInput {\r\n userId: string;\r\n amount: number;\r\n operationType: CreditOperationType;\r\n expiresAt: Date;\r\n /**\r\n * @deprecated Rejected — this writer does not place the hold.\r\n *\r\n * `createReservation` inserts a row and leaves `reserved` alone, so a keyed\r\n * row written here names a hold that does not exist, and `reserveCreditsV2`\r\n * would adopt it as a `replayed` reservation whose commit then spends another\r\n * hold's coverage. Both adapters throw `UNSUPPORTED_OPERATION` when this is\r\n * set. Pass the key to `reserveCredits` / `reserveCreditsV2` instead, which\r\n * claims the key and places the hold in one transaction.\r\n *\r\n * The field is kept rather than deleted so the refusal reaches a JavaScript\r\n * caller as a typed error instead of a silently ignored property.\r\n */\r\n idempotencyKey?: string;\r\n}\r\n\r\n/**\r\n * Input for creating a credit transaction\r\n */\r\nexport interface CreateTransactionInput {\r\n userId: string;\r\n type: PortableTransaction[\"type\"];\r\n amount: number;\r\n description: string;\r\n paymentRef?: string;\r\n previousBalance: number;\r\n newBalance: number;\r\n}\r\n\r\n/**\r\n * Input for logging usage\r\n */\r\nexport interface CreateUsageLogInput {\r\n userId: string;\r\n operationType: CreditOperationType;\r\n provider: AIProviderType;\r\n creditsUsed: number;\r\n success: boolean;\r\n errorMessage?: string;\r\n resourceId?: string;\r\n resourceType?: string;\r\n requestId?: string;\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Query options for usage logs\r\n */\r\nexport interface UsageLogQuery {\r\n userId?: string;\r\n operationType?: CreditOperationType;\r\n success?: boolean;\r\n limit?: number;\r\n offset?: number;\r\n startDate?: Date;\r\n endDate?: Date;\r\n}\r\n\r\n/**\r\n * Input for creating a journal entry\r\n */\r\nexport interface CreateJournalEntryInput {\r\n userId: string;\r\n entryType: \"debit\" | \"credit\";\r\n amount: number;\r\n /** `balance + bonusCredits` after this entry. */\r\n balanceAfter: number;\r\n /** `balance + bonusCredits` before this entry. */\r\n balanceBefore?: number;\r\n source: CreditSource;\r\n referenceId: string;\r\n referenceType: JournalReferenceType;\r\n description: string;\r\n metadata?: Record<string, unknown>;\r\n /** Deterministic key making this entry unique per user (V2, optional). */\r\n idempotencyKey?: string;\r\n}\r\n\r\n/**\r\n * The journal entry of a one-shot deduct, written in the deduct's own atomic step.\r\n */\r\nexport interface DeductJournalInput {\r\n referenceId: string;\r\n description: string;\r\n /** Defaults to `\"operation_commit\"`. */\r\n source?: CreditSource;\r\n /** Defaults to `\"adjustment\"`. */\r\n referenceType?: JournalReferenceType;\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Query options for journal entries\r\n */\r\nexport interface JournalEntryQuery {\r\n userId: string;\r\n source?: CreditSource;\r\n referenceType?: JournalReferenceType;\r\n limit?: number;\r\n offset?: number;\r\n startDate?: Date;\r\n endDate?: Date;\r\n}\r\n\r\n/**\r\n * Partial update for user credits balance\r\n */\r\nexport interface CreditBalanceUpdate {\r\n balance?: number;\r\n bonusCredits?: number;\r\n reserved?: number;\r\n tier?: SubscriptionTier;\r\n monthlyLimit?: number;\r\n monthlyUsed?: number;\r\n monthlyResetAt?: Date | string;\r\n subscriptionExpiresAt?: Date | string | null;\r\n /** Increment balance by this amount (alternative to absolute value) */\r\n balanceIncrement?: number;\r\n /** Increment bonusCredits by this amount (alternative to absolute value) */\r\n bonusCreditsIncrement?: number;\r\n /** Increment reserved by this amount (alternative to absolute value) */\r\n reservedIncrement?: number;\r\n /** Increment monthlyUsed by this amount */\r\n monthlyUsedIncrement?: number;\r\n}\r\n\r\n/**\r\n * Input for tier update\r\n */\r\nexport interface TierUpdateInput {\r\n tier: SubscriptionTier;\r\n monthlyLimit: number;\r\n balance?: number;\r\n /**\r\n * Atomically lowers balance to at most this, only when the account's current tier is not free.\r\n * Ignored for an account already free. Cannot be combined with `balance`.\r\n * Like every balance reduction, it stops at the credits backing outstanding holds.\r\n */\r\n balanceCap?: number;\r\n monthlyUsed?: number;\r\n subscriptionExpiresAt?: Date | string | null;\r\n}\r\n\r\n/**\r\n * Free-tier policy an application hands to a repository, for the decisions that\r\n * have to be made inside the write, from the account's current state.\r\n */\r\nexport interface CreditPolicyOptions {\r\n /**\r\n * Decides a due monthly reset, inside the reset transaction, from the account's current state.\r\n * null: no reset and no write (wasReset false, monthlyResetAt unchanged).\r\n * Takes precedence over getMonthlyLimit. monthlyLimit is written only when returned.\r\n * A throw aborts the transaction.\r\n *\r\n * Must be synchronous and free of side effects: a contended transaction runs it again.\r\n * The balance is validated, and floored at the credits backing outstanding holds.\r\n */\r\n resolveMonthlyReset?: (credits: { tier: SubscriptionTier; balance: number }) =>\r\n { balance: number; monthlyLimit?: number } | null;\r\n /**\r\n * Balance cap and monthlyLimit on a subscription-expiry downgrade.\r\n * Default: DEFAULT_FREE_CREDITS in the Firestore adapter, the default tier's\r\n * configured limit in the in-memory adapter.\r\n */\r\n getDowngradeBalanceCap?: () => number;\r\n}\r\n\r\n/**\r\n * Optional journal customization for `addCreditsAtomic`.\r\n *\r\n * By default a credit add records a journal entry with `source: \"purchase\"`,\r\n * `referenceType: \"transaction\"`, and `{ paymentRef }` metadata. These options\r\n * let callers override that — e.g. tag a signup grant as `source: \"bonus\"`, or\r\n * carry revenue-attribution fields (gross amount, credits granted) so a single\r\n * journal can serve as the app's revenue ledger.\r\n */\r\nexport interface AddCreditsAtomicOptions {\r\n /** Journal entry source (defaults to `\"purchase\"`). */\r\n source?: CreditSource;\r\n /** Journal reference type (defaults to `\"transaction\"`). */\r\n referenceType?: JournalReferenceType;\r\n /**\r\n * Extra metadata merged onto the journal entry. When `paymentRef` is present\r\n * it is merged in too (explicit keys here win on collision).\r\n */\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Repository interface for credits database operations\r\n *\r\n * Implementations can use any database (Firestore, PostgreSQL, etc.)\r\n * All methods should handle their own error handling and transactions\r\n */\r\nexport interface ICreditRepository\r\n extends Partial<ICreditRepositoryV2>,\r\n Partial<ICreditRepositoryCreditsV2> {\r\n // ==================== User Credits ====================\r\n\r\n /**\r\n * Get user credits balance\r\n * @param userId - User ID\r\n * @returns User credits or null if not found\r\n */\r\n getUserCredits(userId: string): Promise<PortableUserCredits | null>;\r\n\r\n /**\r\n * Initialize credits for a new user\r\n * @param userId - User ID\r\n * @param tier - Initial subscription tier\r\n * @param initialBalance - Initial credit balance\r\n * @returns Initialized user credits\r\n */\r\n initializeUserCredits(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n initialBalance: number\r\n ): Promise<PortableUserCredits>;\r\n\r\n /**\r\n * Update user credits balance\r\n * @param userId - User ID\r\n * @param updates - Partial updates to apply\r\n */\r\n updateUserCredits(userId: string, updates: CreditBalanceUpdate): Promise<void>;\r\n\r\n /**\r\n * Update user subscription tier\r\n * @param userId - User ID\r\n * @param input - Tier update data\r\n */\r\n updateUserTier(userId: string, input: TierUpdateInput): Promise<void>;\r\n\r\n // ==================== Reservations ====================\r\n\r\n /**\r\n * Create a reservation *record* — not a hold.\r\n *\r\n * Writes a row and leaves `reserved` untouched, so the row is not backed by\r\n * credits and no settlement path will move money for it: the commit /\r\n * release / expire transitions (legacy and V2 alike) refuse it with\r\n * `UNBACKED_RESERVATION`, and it refuses an `idempotencyKey` so it can never\r\n * be adopted as a V2 replay. Use it for imported or annotation-only records.\r\n *\r\n * To place a hold that can actually be committed, call `reserveCreditsAtomic`\r\n * or `reserveCreditsV2` — they raise `reserved` and write the row in one\r\n * atomic step.\r\n *\r\n * @param input - Reservation data\r\n * @returns Created reservation record\r\n */\r\n createReservation(input: CreateReservationInput): Promise<PortableReservation>;\r\n\r\n /**\r\n * Get a reservation by ID\r\n * @param userId - User ID\r\n * @param reservationId - Reservation ID\r\n * @returns Reservation or null\r\n */\r\n getReservation(userId: string, reservationId: string): Promise<PortableReservation | null>;\r\n\r\n /**\r\n * Annotate a reservation *record* with a status.\r\n *\r\n * Assigns a status and nothing else — no balance movement, no `reserved`\r\n * adjustment, no journal entry. It therefore refuses two writes that would\r\n * make the status lie about the ledger: any write to a row whose hold was\r\n * atomically placed (those statuses belong to commit/release/expire), and\r\n * any write back to `reserved` (nothing here re-places a hold). Rows written\r\n * by `createReservation` accept terminal statuses freely.\r\n *\r\n * @param userId - User ID\r\n * @param reservationId - Reservation ID\r\n * @param status - New status\r\n * @param completedAt - Completion timestamp\r\n */\r\n updateReservationStatus(\r\n userId: string,\r\n reservationId: string,\r\n status: ReservationStatus,\r\n completedAt?: Date\r\n ): Promise<void>;\r\n\r\n // ==================== Atomic Operations ====================\r\n\r\n /**\r\n * Reserve credits atomically (creates reservation + updates balance in transaction)\r\n * @param userId - User ID\r\n * @param amount - Credits to reserve\r\n * @param operationType - Operation type for tracking\r\n * @param expiresAt - Reservation expiry time\r\n * @returns Created reservation\r\n * @throws Error if insufficient credits\r\n */\r\n reserveCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n operationType: CreditOperationType,\r\n expiresAt: Date\r\n ): Promise<PortableReservation>;\r\n\r\n /**\r\n * Commit a reservation atomically (deducts credits + marks reservation committed)\r\n * @param userId - User ID\r\n * @param reservationId - Reservation ID\r\n * @throws Error if reservation not found or not in reserved state\r\n */\r\n commitReservationAtomic(userId: string, reservationId: string): Promise<void>;\r\n\r\n /**\r\n * Release a reservation atomically (releases reserved credits + marks reservation released)\r\n * @param userId - User ID\r\n * @param reservationId - Reservation ID\r\n */\r\n releaseReservationAtomic(userId: string, reservationId: string): Promise<void>;\r\n\r\n /**\r\n * Add credits atomically (creates transaction + updates balance)\r\n * @param userId - User ID\r\n * @param amount - Credits to add\r\n * @param description - Transaction description\r\n * @param paymentRef - Optional payment reference\r\n * @param options - Optional journal source / reference type / extra metadata\r\n * for the credit journal entry (e.g. revenue attribution fields). Lets\r\n * callers record a purchase's gross amount, a bonus/refund source, etc.,\r\n * instead of the default `source: \"purchase\"` + `{ paymentRef }` metadata.\r\n */\r\n addCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n description: string,\r\n paymentRef?: string,\r\n options?: AddCreditsAtomicOptions\r\n ): Promise<void>;\r\n\r\n /**\r\n * Deduct credits from a user's balance atomically.\r\n *\r\n * Splits the deduction across `balance` and `bonusCredits`, draining\r\n * `balance` (monthly, resets each cycle) first so persistent bonus\r\n * credits survive longer. Runs in a single atomic transaction so\r\n * concurrent deducts cannot drive either field negative.\r\n *\r\n * Callers are responsible for writing any audit record (journal /\r\n * transaction) — this method only moves credits.\r\n *\r\n * @param userId - User ID\r\n * @param amount - Credits to deduct (positive)\r\n * @returns Combined totals (balance + bonusCredits) before and after.\r\n * @throws If user has no credits doc or insufficient credits\r\n */\r\n deductCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n journal?: DeductJournalInput\r\n ): Promise<{ previousBalance: number; newBalance: number; journalEntryId?: string }>;\r\n\r\n // ==================== Transactions ====================\r\n\r\n /**\r\n * Create a credit transaction *record* — no balance moves.\r\n *\r\n * Refuses a `paymentRef`: the reference is the global identity of a credit\r\n * event, and a record that carried one without crediting anyone would make a\r\n * later `addCredits` with the same reference report `replayed` and credit\r\n * nothing. Deliver referenced payments through `addCreditsAtomic` /\r\n * `addCreditsV2`, which claim the reference and move the balance atomically.\r\n *\r\n * @param input - Transaction data\r\n * @returns Created transaction\r\n */\r\n createTransaction(input: CreateTransactionInput): Promise<PortableTransaction>;\r\n\r\n /**\r\n * Get user's transaction history\r\n * @param userId - User ID\r\n * @param limit - Max results\r\n * @param offset - Skip results\r\n * @returns List of transactions\r\n */\r\n getTransactions(\r\n userId: string,\r\n limit?: number,\r\n offset?: number\r\n ): Promise<PortableTransaction[]>;\r\n\r\n // ==================== Usage Logs ====================\r\n\r\n /**\r\n * Log a usage event\r\n * @param input - Usage log data\r\n * @returns Created usage log\r\n */\r\n logUsage(input: CreateUsageLogInput): Promise<PortableUsageLog>;\r\n\r\n /**\r\n * Query usage logs\r\n * @param query - Query parameters\r\n * @returns List of usage logs\r\n */\r\n getUsageLogs(query: UsageLogQuery): Promise<PortableUsageLog[]>;\r\n\r\n /**\r\n * Get usage log count (for pagination)\r\n * @param query - Query parameters (without limit/offset)\r\n * @returns Count of matching logs\r\n */\r\n getUsageLogsCount(query: Omit<UsageLogQuery, \"limit\" | \"offset\">): Promise<number>;\r\n\r\n // ==================== Cleanup Operations ====================\r\n\r\n /**\r\n * Find and expire reservations past their expiration time\r\n * Used by cron job to clean up stale reservations\r\n * @param batchSize - Maximum number of reservations to process per batch (default: 100)\r\n * @param maxIterations - Maximum number of pagination iterations to prevent infinite loops (default: 100)\r\n * @returns Cleanup results with counts and errors\r\n */\r\n findAndExpireReservations(batchSize?: number, maxIterations?: number): Promise<{\r\n expiredCount: number;\r\n creditsReleased: number;\r\n errors: string[];\r\n }>;\r\n\r\n // ==================== Atomic Monthly Reset ====================\r\n\r\n /**\r\n * Atomically perform monthly reset if needed\r\n * Uses optimistic locking to prevent race conditions\r\n *\r\n * @param userId - User ID\r\n * @param tier - The tier the caller last read. Not trusted: it may be stale by the\r\n * time the write runs, so implementations must use the tier stored on the account.\r\n * @param expectedResetAt - The expected monthlyResetAt value (for optimistic locking)\r\n * @returns Result indicating whether reset was performed and updated credits\r\n */\r\n atomicMonthlyReset(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n expectedResetAt: Date | string\r\n ): Promise<MonthlyResetResult>;\r\n\r\n // ==================== Subscription Expiry ====================\r\n\r\n /**\r\n * Check and handle subscription expiry with grace period\r\n * Auto-downgrades expired subscriptions after grace period\r\n *\r\n * @param userId - User ID\r\n * @param gracePeriodDays - Days to allow after expiry before downgrade (default: 3)\r\n * @returns Result indicating whether downgrade occurred\r\n */\r\n checkAndHandleSubscriptionExpiry(\r\n userId: string,\r\n gracePeriodDays?: number\r\n ): Promise<SubscriptionExpiryResult>;\r\n\r\n // ==================== Journal Entries ====================\r\n\r\n /**\r\n * Create a journal entry for audit trail\r\n * @param input - Journal entry data\r\n * @returns Created journal entry\r\n */\r\n createJournalEntry(input: CreateJournalEntryInput): Promise<PortableJournalEntry>;\r\n\r\n /**\r\n * Get journal entries for a user\r\n * @param query - Query parameters\r\n * @returns List of journal entries\r\n */\r\n getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]>;\r\n\r\n /**\r\n * Get journal entry count for pagination\r\n * @param query - Query parameters (without limit/offset)\r\n * @returns Count of matching entries\r\n */\r\n getJournalEntriesCount(query: Omit<JournalEntryQuery, \"limit\" | \"offset\">): Promise<number>;\r\n}\r\n\r\n/**\r\n * Factory type for creating repository instances\r\n */\r\nexport type CreditRepositoryFactory = () => ICreditRepository;\r\n\r\n/**\r\n * Narrow a repository to one that implements the full V2 boundary.\r\n *\r\n * V2 methods are optional on {@link ICreditRepository}, so callers that need\r\n * the idempotent/race-safe path must probe rather than assume. A repository\r\n * only counts as V2 when it implements *all four* transitions — a partial\r\n * implementation would leave one path silently unsafe.\r\n */\r\nexport function supportsCreditsV2(\r\n repository: ICreditRepository\r\n): repository is ICreditRepository & ICreditRepositoryV2 {\r\n return (\r\n typeof repository.reserveCreditsV2 === \"function\" &&\r\n typeof repository.commitReservationV2 === \"function\" &&\r\n typeof repository.releaseReservationV2 === \"function\" &&\r\n typeof repository.expireReservationV2 === \"function\"\r\n );\r\n}\r\n\r\n/**\r\n * Narrow a repository to one that credits idempotently against a `paymentRef`.\r\n *\r\n * Deliberately a separate probe from {@link supportsCreditsV2}: serialising\r\n * reservations and enforcing a global payment reference are different\r\n * guarantees resting on different database objects, and an adapter may have one\r\n * without the other.\r\n */\r\nexport function supportsIdempotentCredit(\r\n repository: ICreditRepository\r\n): repository is ICreditRepository & ICreditRepositoryCreditsV2 {\r\n return typeof repository.addCreditsV2 === \"function\";\r\n}\r\n\r\n/**\r\n * Convert PortableUserCredits to client-safe format\r\n * Utility function that implementations can use\r\n */\r\nexport function toClientUserCredits(credits: PortableUserCredits): PortableUserCredits {\r\n // Already in portable format, just ensure all timestamps are ISO strings\r\n return {\r\n userId: credits.userId,\r\n balance: credits.balance,\r\n bonusCredits: credits.bonusCredits ?? 0,\r\n reserved: credits.reserved,\r\n tier: credits.tier,\r\n monthlyLimit: credits.monthlyLimit,\r\n monthlyUsed: credits.monthlyUsed,\r\n monthlyResetAt: toISOString(credits.monthlyResetAt),\r\n subscriptionExpiresAt: credits.subscriptionExpiresAt\r\n ? toISOString(credits.subscriptionExpiresAt)\r\n : null,\r\n ...(credits.expiredHoldBacking !== undefined\r\n ? { expiredHoldBacking: credits.expiredHoldBacking }\r\n : {}),\r\n ...(credits.signupGrant !== undefined ? { signupGrant: credits.signupGrant } : {}),\r\n createdAt: toISOString(credits.createdAt),\r\n updatedAt: toISOString(credits.updatedAt),\r\n };\r\n}\r\n\r\n/**\r\n * Convert any timestamp-like value to ISO string\r\n */\r\nfunction toISOString(value: unknown): string {\r\n if (!value) return new Date().toISOString();\r\n if (typeof value === \"string\") return value;\r\n if (value instanceof Date) return value.toISOString();\r\n // Handle Firestore Timestamp-like objects\r\n if (typeof value === \"object\" && \"toDate\" in value && typeof (value as { toDate: () => Date }).toDate === \"function\") {\r\n return (value as { toDate: () => Date }).toDate().toISOString();\r\n }\r\n return new Date().toISOString();\r\n}\r\n","/**\r\n * One reservation flow for both repository generations.\r\n *\r\n * Adapters that implement the V2 boundary get the idempotent, race-safe path\r\n * and own their journal entry. Adapters that do not (Firestore, custom\r\n * implementations) keep the legacy read-then-write path, with the service\r\n * writing the journal exactly as it always did — so upgrading this package\r\n * does not change their behaviour, and does not double-write anyone's ledger.\r\n */\r\n\r\nimport { getOperationLabel } from \"../config/index.js\";\r\nimport { assertValidCreditAmount, sumAmounts } from \"../core/amount.js\";\r\nimport {\r\n createUnsupportedOperationError,\r\n isInsufficientCreditsError,\r\n} from \"../core/errors.js\";\r\nimport type {\r\n AddCreditsOutcome,\r\n CommitOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n} from \"../core/outcomes.js\";\r\nimport { normalizePaymentRef } from \"../core/payment-ref.js\";\r\nimport type { ICreditRepository } from \"./types.js\";\r\nimport { supportsCreditsV2, supportsIdempotentCredit } from \"./types.js\";\r\nimport type {\r\n AddCreditsV2Input,\r\n ReservationTransitionOptions,\r\n ReserveCreditsV2Input,\r\n} from \"./v2-types.js\";\r\n\r\n/**\r\n * Reserve credits through V2 when available.\r\n *\r\n * A legacy adapter cannot honour an idempotency key — it has no unique index\r\n * to enforce it — so asking for one there is a caller error rather than a\r\n * silently ignored option.\r\n */\r\nexport async function reserveThroughRepository(\r\n repository: ICreditRepository,\r\n input: ReserveCreditsV2Input\r\n): Promise<ReserveOutcome> {\r\n const v2 = supportsCreditsV2(repository);\r\n\r\n // The capability check comes first, so a caller who asked for a guarantee\r\n // this repository cannot give always hears *that*, rather than whichever\r\n // other complaint the arguments happen to trigger. A legacy adapter has no\r\n // unique index to enforce the key, so it cannot deduplicate a replay, and\r\n // reporting `created` for the second delivery would be a lie that costs the\r\n // user real credits.\r\n if (!v2 && input.idempotencyKey !== undefined) {\r\n throw createUnsupportedOperationError(\r\n \"reserveCredits with an idempotencyKey\",\r\n \"This repository does not implement the V2 boundary, so it cannot \" +\r\n \"enforce idempotency keys. Use a V2 repository (check with \" +\r\n \"supportsCreditsV2) or omit the key and accept at-least-once holds.\"\r\n );\r\n }\r\n\r\n assertValidCreditAmount(input.amount, { userId: input.userId });\r\n\r\n if (v2) {\r\n return repository.reserveCreditsV2(input);\r\n }\r\n\r\n try {\r\n const reservation = await repository.reserveCreditsAtomic(\r\n input.userId,\r\n input.amount,\r\n input.operationType,\r\n input.expiresAt\r\n );\r\n return { outcome: \"created\", reservation };\r\n } catch (error) {\r\n // Only a genuine funds shortfall becomes the `insufficient` outcome. The\r\n // previous version asked the repository for the balance and called any\r\n // failure `insufficient` whenever that balance happened to be low — which\r\n // silently rebranded connection drops and driver faults as \"the user is\r\n // out of credits\", and hid real outages behind an upsell.\r\n if (!isInsufficientCreditsError(error)) throw error;\r\n\r\n const credits = await repository.getUserCredits(input.userId);\r\n const available = credits\r\n ? sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved)\r\n : 0;\r\n return {\r\n outcome: \"insufficient\",\r\n available,\r\n required: input.amount,\r\n shortfall: Math.max(sumAmounts(input.amount, -available), 0),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Commit through V2 when available, else the legacy read-then-write path.\r\n *\r\n * **The legacy path cannot promise a single winner.** It reads the status and\r\n * then writes, with no lock and no compare-and-set between the two, so two\r\n * concurrent commits of the same reservation can both observe `reserved` and\r\n * both proceed. The typed outcome still describes what *this* call saw, but on\r\n * a legacy adapter `committed` means \"this call did the work\", not \"this call\r\n * was the only one that did\". Callers needing exactly-once must use a\r\n * repository for which `supportsCreditsV2` returns `true`.\r\n */\r\nexport async function commitThroughRepository(\r\n repository: ICreditRepository,\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n): Promise<CommitOutcome> {\r\n if (supportsCreditsV2(repository)) {\r\n return repository.commitReservationV2(userId, reservationId, options);\r\n }\r\n\r\n const reservation = await repository.getReservation(userId, reservationId);\r\n if (!reservation) return { outcome: \"not_found\", reservationId };\r\n if (reservation.status !== \"reserved\") {\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation,\r\n terminalStatus: reservation.status,\r\n };\r\n }\r\n\r\n await repository.commitReservationAtomic(userId, reservationId);\r\n\r\n const credits = await repository.getUserCredits(userId);\r\n // The ledger balance, not the `balance` column alone: every other journal\r\n // writer — the V2 transitions, add and deduct — records\r\n // `balance + bonusCredits`, and recording less here made the same account's\r\n // audit trail disagree with itself whenever it held bonus credits.\r\n const balanceAfter = credits ? sumAmounts(credits.balance, credits.bonusCredits) : 0;\r\n const entry = await repository.createJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount: reservation.amount,\r\n balanceAfter,\r\n source: \"operation_commit\",\r\n referenceId: reservationId,\r\n referenceType: \"reservation\",\r\n description:\r\n options?.description ??\r\n `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,\r\n // Deterministic fields last: they carry the identity of the event, and a\r\n // caller who happens to send an `operationType` or an `amount` must not\r\n // relabel what actually moved.\r\n metadata: {\r\n ...options?.metadata,\r\n operationType: reservation.operationType,\r\n amount: reservation.amount,\r\n },\r\n });\r\n\r\n return {\r\n outcome: \"committed\",\r\n reservation: { ...reservation, status: \"committed\" },\r\n amount: reservation.amount,\r\n balanceAfter,\r\n journalEntryId: entry.id,\r\n };\r\n}\r\n\r\n/**\r\n * Release through V2 when available, else the legacy read-then-write path.\r\n *\r\n * Same caveat as {@link commitThroughRepository}: on a legacy adapter the\r\n * read-then-write sequence is unguarded, so `released` does not exclude a\r\n * concurrent commit having also run. Only V2 repositories serialise the two.\r\n */\r\nexport async function releaseThroughRepository(\r\n repository: ICreditRepository,\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n): Promise<ReleaseOutcome> {\r\n if (supportsCreditsV2(repository)) {\r\n return repository.releaseReservationV2(userId, reservationId, options);\r\n }\r\n\r\n const reservation = await repository.getReservation(userId, reservationId);\r\n if (!reservation) {\r\n // Legacy adapters vary on whether releasing an unknown reservation throws;\r\n // ask them anyway so that behaviour is preserved, then report not_found.\r\n await repository.releaseReservationAtomic(userId, reservationId);\r\n return { outcome: \"not_found\", reservationId };\r\n }\r\n if (reservation.status !== \"reserved\") {\r\n // The hold is already resolved, so there is nothing to hand back. Legacy\r\n // adapters disagree about whether releasing a processed reservation is an\r\n // error, and `releaseCredits` is specified to be a silent no-op here — so\r\n // the call is still made for adapters that use it to reconcile, and a\r\n // refusal is swallowed rather than propagated. The conflict is not hidden:\r\n // it is what `already_terminal` reports, and `releaseCreditsDetailed`\r\n // surfaces it to callers that ask.\r\n await repository.releaseReservationAtomic(userId, reservationId).catch(() => undefined);\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation,\r\n terminalStatus: reservation.status,\r\n };\r\n }\r\n\r\n await repository.releaseReservationAtomic(userId, reservationId);\r\n\r\n const credits = await repository.getUserCredits(userId);\r\n const entry = await repository.createJournalEntry({\r\n userId,\r\n entryType: \"credit\",\r\n // No credits changed hands — the hold just moved back into \"available\".\r\n amount: 0,\r\n balanceAfter: credits ? sumAmounts(credits.balance, credits.bonusCredits) : 0,\r\n source: \"operation_release\",\r\n referenceId: reservationId,\r\n referenceType: \"reservation\",\r\n description:\r\n options?.description ??\r\n `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,\r\n metadata: {\r\n ...options?.metadata,\r\n operationType: reservation.operationType,\r\n amount: reservation.amount,\r\n },\r\n });\r\n\r\n return {\r\n outcome: \"released\",\r\n reservation: { ...reservation, status: \"released\" },\r\n amount: reservation.amount,\r\n journalEntryId: entry.id,\r\n };\r\n}\r\n\r\n/**\r\n * Credit an account through the idempotent path when the adapter has one.\r\n *\r\n * A legacy adapter cannot resolve `created` / `replayed` / `conflict`: it has\r\n * no global unique index over `payment_ref`, so it cannot tell a re-delivered\r\n * webhook from a second, genuinely different payment that reuses a reference.\r\n * Rather than invent an outcome, this reports `created` — which is what the\r\n * legacy method actually did — and the ambiguity is documented on the return\r\n * type instead of hidden inside it.\r\n *\r\n * The reference is normalised here as well as in the adapters, so a legacy\r\n * adapter that stores the raw string does not persist a whitespace-only\r\n * reference that would then deduplicate nothing.\r\n */\r\nexport async function addCreditsThroughRepository(\r\n repository: ICreditRepository,\r\n input: AddCreditsV2Input\r\n): Promise<AddCreditsOutcome> {\r\n assertValidCreditAmount(input.amount, { userId: input.userId, operation: \"addCredits\" });\r\n const paymentRef = normalizePaymentRef(input.paymentRef);\r\n\r\n if (supportsIdempotentCredit(repository)) {\r\n return repository.addCreditsV2({ ...input, paymentRef });\r\n }\r\n\r\n await repository.addCreditsAtomic(\r\n input.userId,\r\n input.amount,\r\n input.description,\r\n paymentRef,\r\n input.options\r\n );\r\n\r\n // The legacy method returns nothing, so there is no transaction to hand back\r\n // and no way to know whether it deduplicated. Report the one thing that is\r\n // true — a call was made and did not throw — rather than fabricate a record.\r\n return paymentRef ? { outcome: \"created\", paymentRef } : { outcome: \"created\" };\r\n}\r\n","/**\r\n * Core credit operations - framework agnostic\r\n *\r\n * Contains the business logic for credit operations that can be\r\n * used by any adapter or service implementation.\r\n */\r\n\r\nimport type { ICreditRepository } from \"../repository/types.js\";\r\nimport type { PortableReservation } from \"./types.js\";\r\nimport {\r\n commitThroughRepository,\r\n releaseThroughRepository,\r\n reserveThroughRepository,\r\n} from \"../repository/flow.js\";\r\nimport {\r\n createReservationAlreadyProcessedError,\r\n createReservationNotFoundError,\r\n createInsufficientCreditsError,\r\n} from \"./errors.js\";\r\n\r\n/**\r\n * Commit a reservation with journal entry\r\n *\r\n * On a V2 repository the balance move and the journal entry happen in one\r\n * transaction and this function adds nothing of its own — the duplicate entry\r\n * the previous implementation wrote is gone. Legacy adapters keep the old\r\n * read-then-write path, journal included.\r\n *\r\n * @param repository - The credit repository\r\n * @param userId - User ID\r\n * @param reservationId - Reservation to commit\r\n */\r\nexport async function commitReservationWithJournal(\r\n repository: ICreditRepository,\r\n userId: string,\r\n reservationId: string\r\n): Promise<void> {\r\n const outcome = await commitThroughRepository(repository, userId, reservationId);\r\n if (outcome.outcome === \"not_found\") {\r\n throw createReservationNotFoundError(reservationId);\r\n }\r\n if (outcome.outcome === \"already_terminal\" && outcome.terminalStatus !== \"committed\") {\r\n throw createReservationAlreadyProcessedError(reservationId, outcome.terminalStatus);\r\n }\r\n}\r\n\r\n/**\r\n * Release a reservation with journal entry\r\n *\r\n * @param repository - The credit repository\r\n * @param userId - User ID\r\n * @param reservationId - Reservation to release\r\n */\r\nexport async function releaseReservationWithJournal(\r\n repository: ICreditRepository,\r\n userId: string,\r\n reservationId: string\r\n): Promise<void> {\r\n await releaseThroughRepository(repository, userId, reservationId);\r\n}\r\n\r\n/**\r\n * Reserve credits for an operation\r\n *\r\n * @param repository - The credit repository\r\n * @param userId - User ID\r\n * @param amount - Credits to reserve\r\n * @param operationType - Type of operation\r\n * @param expiryMs - Reservation expiry time in milliseconds\r\n * @param idempotencyKey - Optional caller key; a replay with the same\r\n * amount/operation returns the original reservation instead of a second hold\r\n * @returns The reservation\r\n */\r\nexport async function reserveCreditsForOperation(\r\n repository: ICreditRepository,\r\n userId: string,\r\n amount: number,\r\n operationType: string,\r\n expiryMs: number = 5 * 60 * 1000,\r\n idempotencyKey?: string\r\n): Promise<PortableReservation> {\r\n const expiresAt = new Date(Date.now() + expiryMs);\r\n const outcome = await reserveThroughRepository(repository, {\r\n userId,\r\n amount,\r\n operationType,\r\n expiresAt,\r\n idempotencyKey,\r\n });\r\n\r\n if (outcome.outcome === \"created\" || outcome.outcome === \"replayed\") {\r\n return outcome.reservation;\r\n }\r\n if (outcome.outcome === \"insufficient\") {\r\n throw createInsufficientCreditsError(outcome.required, outcome.available);\r\n }\r\n throw createReservationAlreadyProcessedError(\r\n outcome.existing.id,\r\n outcome.existing.status\r\n );\r\n}\r\n","import type { SubscriptionTier, TierConfig } from \"../core/types.js\";\r\nimport {\r\n getConfig,\r\n getConfigOperationCost,\r\n getConfigTierConfig,\r\n getConfigMonthlyLimit,\r\n getValidOperationTypes,\r\n isValidOperationType,\r\n getOperationLabel,\r\n} from \"./index.js\";\r\n\r\n/**\r\n * Get operation costs as a record\r\n * Returns all configured operation costs from the current config\r\n */\r\nexport function getOperationCosts(): Record<string, number> {\r\n return { ...getConfig().operationCosts };\r\n}\r\n\r\n/**\r\n * Get the credit cost for an operation type\r\n * Uses the configuration system for dynamic costs\r\n *\r\n * @param type - Operation type (must be configured in the system)\r\n * @returns Credit cost for the operation\r\n * @throws Error if the operation type is not configured\r\n */\r\nexport function getOperationCost(type: string): number {\r\n return getConfigOperationCost(type);\r\n}\r\n\r\n// Re-export validation helpers for convenience\r\nexport { getValidOperationTypes, isValidOperationType };\r\n\r\n/**\r\n * Get tier configuration\r\n * Uses the configuration system for dynamic configs\r\n *\r\n * @param tier - Subscription tier\r\n * @returns Tier configuration\r\n */\r\nexport function getTierConfig(tier: SubscriptionTier): TierConfig {\r\n return getConfigTierConfig(tier);\r\n}\r\n\r\n/**\r\n * Get monthly credit limit for a tier\r\n * Returns Infinity for unlimited tier\r\n * Uses the configuration system for dynamic limits\r\n *\r\n * @param tier - Subscription tier\r\n * @returns Monthly credit limit\r\n */\r\nexport function getMonthlyLimit(tier: SubscriptionTier): number {\r\n return getConfigMonthlyLimit(tier);\r\n}\r\n\r\n/**\r\n * Get default free credits from configuration\r\n */\r\nexport function getDefaultFreeCredits(): number {\r\n return getConfig().defaultFreeCredits;\r\n}\r\n\r\n/**\r\n * Get reservation expiry time from configuration\r\n */\r\nexport function getReservationExpiryMs(): number {\r\n return getConfig().reservationExpiryMs;\r\n}\r\n\r\n/**\r\n * Information about an operation's cost and display label\r\n */\r\nexport interface OperationCostInfo {\r\n key: string;\r\n cost: number;\r\n label: string;\r\n}\r\n\r\n/**\r\n * Get all operation costs with their display labels.\r\n * Returns a record keyed by operation type with cost + label.\r\n */\r\nexport function getOperationCostsWithLabels(): Record<string, OperationCostInfo> {\r\n const config = getConfig();\r\n const result: Record<string, OperationCostInfo> = {};\r\n for (const [key, cost] of Object.entries(config.operationCosts)) {\r\n result[key] = {\r\n key,\r\n cost,\r\n label: getOperationLabel(key),\r\n };\r\n }\r\n return result;\r\n}\r\n","/**\r\n * Inputs for the V2 reservation boundary.\r\n *\r\n * V2 is additive: every method is optional on {@link ICreditRepository} so\r\n * existing adapters keep compiling and keep working through the legacy\r\n * `*Atomic` methods. Adapters that implement V2 must satisfy the contract\r\n * documented on {@link ICreditRepositoryV2}.\r\n */\r\n\r\nimport type { CreditOperationType } from \"../core/types.js\";\r\nimport type {\r\n AddCreditsOutcome,\r\n CommitOutcome,\r\n ExpireOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n} from \"../core/outcomes.js\";\r\nimport { RESERVED_JOURNAL_KEY_PREFIX } from \"../core/idempotency.js\";\r\nimport type { AddCreditsAtomicOptions } from \"./types.js\";\r\n\r\n/**\r\n * Input for `reserveCreditsV2`.\r\n *\r\n * `userId`, `amount` and `operationType` form the immutable payload bound to\r\n * `idempotencyKey`: replaying the same key with the same payload returns the\r\n * original reservation, replaying it with a different one is a conflict.\r\n * `expiresAt` is deliberately *not* part of the payload — a retry naturally\r\n * computes a later deadline and that must not be treated as a conflict.\r\n */\r\nexport interface ReserveCreditsV2Input {\r\n userId: string;\r\n amount: number;\r\n operationType: CreditOperationType;\r\n expiresAt: Date;\r\n /**\r\n * Caller-supplied key, unique per user. Omit it to get legacy\r\n * (non-idempotent) reserve semantics — every call mints a new hold.\r\n */\r\n idempotencyKey?: string;\r\n}\r\n\r\n/** Optional journal customisation for a V2 state transition. */\r\nexport interface ReservationTransitionOptions {\r\n /** Overrides the generated journal description. */\r\n description?: string;\r\n /** Merged onto the journal entry's metadata. */\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/** Options for `expireReservationV2`. */\r\nexport interface ExpireReservationV2Options extends ReservationTransitionOptions {\r\n /**\r\n * Sweep timestamp. A reservation only expires when `expiresAt <= asOf`;\r\n * otherwise the outcome is `not_due`. Defaults to now.\r\n */\r\n asOf?: Date;\r\n}\r\n\r\n/**\r\n * The V2 surface. Every method must:\r\n *\r\n * 1. run in a single database transaction;\r\n * 2. take row locks in a consistent order — reservation first, then balance —\r\n * so concurrent transitions cannot deadlock;\r\n * 3. transition status with a compare-and-set (`WHERE status = 'reserved'`)\r\n * and report the loser truthfully instead of silently succeeding;\r\n * 4. mutate balances with column expressions, never with values read earlier\r\n * in the transaction;\r\n * 5. write at most one journal entry, inside the same transaction, keyed so a\r\n * retry cannot duplicate it;\r\n * 6. perform no callbacks and no network I/O inside the transaction.\r\n */\r\nexport interface ICreditRepositoryV2 {\r\n reserveCreditsV2(input: ReserveCreditsV2Input): Promise<ReserveOutcome>;\r\n\r\n commitReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<CommitOutcome>;\r\n\r\n releaseReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<ReleaseOutcome>;\r\n\r\n expireReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ExpireReservationV2Options\r\n ): Promise<ExpireOutcome>;\r\n}\r\n\r\n/** Input for `addCreditsV2`. */\r\nexport interface AddCreditsV2Input {\r\n userId: string;\r\n amount: number;\r\n description: string;\r\n /**\r\n * Global idempotency key for the credit event. Empty and whitespace-only\r\n * strings normalise to \"no reference\" — see `normalizePaymentRef`.\r\n */\r\n paymentRef?: string;\r\n options?: AddCreditsAtomicOptions;\r\n}\r\n\r\n/**\r\n * The idempotent credit surface, separate from {@link ICreditRepositoryV2}.\r\n *\r\n * It is its own interface with its own probe because the two capabilities are\r\n * genuinely independent: an adapter can serialise reservations without having a\r\n * unique index over `payment_ref`, and folding this into the reservation probe\r\n * would make `supportsCreditsV2` narrow third-party adapters to a method they\r\n * never wrote.\r\n *\r\n * An implementation must:\r\n *\r\n * 1. run in a single transaction;\r\n * 2. resolve a non-empty reference to exactly one of `created` / `replayed` /\r\n * `conflict`, comparing the canonical payload — not merely the reference's\r\n * presence — before reporting a replay;\r\n * 3. write nothing at all on `replayed` or `conflict`;\r\n * 4. enforce the reference *globally*, so the same reference cannot credit two\r\n * different users;\r\n * 5. fail closed if the uniqueness it relies on is not actually in place.\r\n */\r\nexport interface ICreditRepositoryCreditsV2 {\r\n addCreditsV2(input: AddCreditsV2Input): Promise<AddCreditsOutcome>;\r\n}\r\n\r\n/**\r\n * Deterministic journal key for a reservation transition.\r\n *\r\n * Derived purely from the reservation id and the transition, so a retried\r\n * commit computes the same key and collides with the first entry on the\r\n * journal's partial unique index instead of writing a second row.\r\n */\r\nexport function reservationJournalKey(\r\n reservationId: string,\r\n transition: \"commit\" | \"release\" | \"expire\"\r\n): string {\r\n return `${RESERVED_JOURNAL_KEY_PREFIX}${reservationId}:${transition}`;\r\n}\r\n","/**\r\n * Shared utilities for credit repository implementations\r\n *\r\n * These are pure functions with no server-only dependencies,\r\n * making them safe to use in any repository implementation.\r\n */\r\n\r\nimport { heldExpiredBacking, sumAmounts } from \"../core/amount.js\";\r\nimport type { PortableUserCredits } from \"../core/types.js\";\r\nimport type { CreditBalanceUpdate } from \"./types.js\";\r\n\r\n/**\r\n * Generate a unique ID\r\n * Uses timestamp + random string for uniqueness\r\n */\r\nexport function generateId(): string {\r\n return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\r\n}\r\n\r\n/**\r\n * Convert date-like value to Date object\r\n * Handles: Date, string, Firestore Timestamp\r\n */\r\nexport function toDate(value: Date | string | unknown): Date {\r\n if (value instanceof Date) return value;\r\n if (typeof value === \"string\") return new Date(value);\r\n // Handle Firestore Timestamp\r\n if (typeof value === \"object\" && value !== null && \"toDate\" in value) {\r\n return (value as { toDate: () => Date }).toDate();\r\n }\r\n return new Date();\r\n}\r\n\r\n/**\r\n * Get the first day of next month\r\n * Used for calculating monthly reset dates\r\n */\r\nexport function getNextMonthlyReset(from: Date = new Date()): Date {\r\n // From components: `setMonth` on the 31st overflows (31 Jan became 3 March, then\r\n // 1 March after `setDate(1)`), which skipped a whole month.\r\n return new Date(from.getFullYear(), from.getMonth() + 1, 1);\r\n}\r\n\r\n/** Result of validating a balance update. */\r\nexport interface BalanceValidationResult {\r\n valid: boolean;\r\n errors: string[];\r\n}\r\n\r\n/**\r\n * Whether an update leaves a valid account: no negative field, and `reserved` covered\r\n * by `balance + bonusCredits`, which is the floor outstanding holds put under the\r\n * balance. An increment is applied on top of the absolute value when a call names both.\r\n *\r\n * An update is a delta the caller asked for, so one that cannot be honoured is refused,\r\n * as a deduct is. Only tier writes and resets stop at the floor: they state a target.\r\n *\r\n * A negative balance increment is a charge, so it draws only on the balance above\r\n * `expiredHoldBacking`: those credits are expired and leave when their hold closes, so\r\n * charging them would hand the charge back. An absolute balance replaces the field and\r\n * the marks with it, so only an increment on the stored balance meets this limit.\r\n */\r\nexport function validateBalanceUpdate(\r\n current: Pick<PortableUserCredits, \"balance\" | \"bonusCredits\" | \"reserved\" | \"expiredHoldBacking\">,\r\n updates: CreditBalanceUpdate\r\n): BalanceValidationResult {\r\n const next = (base: number, absolute?: number, increment?: number) =>\r\n sumAmounts(absolute ?? base, increment ?? 0);\r\n const balance = next(current.balance, updates.balance, updates.balanceIncrement);\r\n const bonusCredits = next(\r\n current.bonusCredits ?? 0,\r\n updates.bonusCredits,\r\n updates.bonusCreditsIncrement\r\n );\r\n const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);\r\n\r\n const errors: string[] = [];\r\n if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);\r\n const marked =\r\n updates.balance === undefined\r\n ? heldExpiredBacking(current.balance, current.expiredHoldBacking)\r\n : 0;\r\n if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {\r\n errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);\r\n }\r\n if (bonusCredits < 0) errors.push(`BonusCredits cannot be negative: ${bonusCredits}`);\r\n if (reserved < 0) errors.push(`Reserved cannot be negative: ${reserved}`);\r\n\r\n const covering = sumAmounts(balance, bonusCredits);\r\n if (reserved > covering) {\r\n errors.push(`Reserved (${reserved}) exceeds balance + bonusCredits (${covering})`);\r\n }\r\n return { valid: errors.length === 0, errors };\r\n}\r\n\r\n/** Refuse an update {@link validateBalanceUpdate} finds invalid. */\r\nexport function assertValidBalanceUpdate(\r\n current: Pick<PortableUserCredits, \"balance\" | \"bonusCredits\" | \"reserved\" | \"expiredHoldBacking\">,\r\n updates: CreditBalanceUpdate\r\n): void {\r\n const result = validateBalanceUpdate(current, updates);\r\n if (!result.valid) {\r\n throw new Error(`Invalid balance update: ${result.errors.join(\"; \")}`);\r\n }\r\n}\r\n","/**\r\n * Copies at the boundary, so nothing stored is ever handed to a caller.\r\n *\r\n * A real adapter maps a database row into a fresh object every time. This one\r\n * used to return the object it had stored, which made it behave in ways no\r\n * production adapter does: a caller's earlier read mutated under them when a\r\n * later write landed, and a caller that adjusted a returned record corrupted\r\n * the store without going through any write path at all.\r\n *\r\n * That is not a theoretical difference. The service's monthly reset compared\r\n * the post-reset balance against the balance it had read moments earlier, got\r\n * zero because both were the same object, and silently skipped the reset's\r\n * journal entry — a bug that could not reproduce against the SQL adapter.\r\n *\r\n * `metadata` is copied one level deeper because it is the one field callers\r\n * routinely pass in and then keep a reference to.\r\n */\r\n\r\ninterface WithMetadata {\r\n metadata?: Record<string, unknown> | null;\r\n}\r\n\r\n/** A detached copy of one stored record. */\r\nexport function copyRecord<T extends object>(record: T): T;\r\nexport function copyRecord<T extends object>(record: T | undefined): T | undefined;\r\nexport function copyRecord<T extends object>(record: T | undefined): T | undefined {\r\n if (!record) return undefined;\r\n const copy = { ...record };\r\n const metadata = (record as WithMetadata).metadata;\r\n if (metadata && typeof metadata === \"object\") {\r\n (copy as WithMetadata).metadata = { ...metadata };\r\n }\r\n return copy;\r\n}\r\n\r\n/** A detached copy of a list of stored records, and of the list itself. */\r\nexport function copyRecords<T extends object>(records: readonly T[]): T[] {\r\n return records.map((record) => copyRecord(record));\r\n}\r\n","/**\r\n * Promise-chaining mutex keyed by an arbitrary string.\r\n *\r\n * The in-memory repository has no database to serialise it, but its critical\r\n * sections still span `await` points — and every `await` is a scheduling point\r\n * where another caller can interleave. Without a lock, \"read balance, then\r\n * write balance\" in one method is exactly the lost update the SQL adapter uses\r\n * row locks to prevent, so contract tests would pass in memory and fail in\r\n * production. This models those row locks.\r\n */\r\nexport class KeyedMutex {\r\n private readonly tails = new Map<string, Promise<unknown>>();\r\n\r\n /**\r\n * Run `fn` with exclusive access to `key`.\r\n *\r\n * Callers queue in arrival order. The stored tail settles either way, so one\r\n * caller throwing does not wedge the queue behind it.\r\n */\r\n async run<T>(key: string, fn: () => Promise<T>): Promise<T> {\r\n const previous = this.tails.get(key) ?? Promise.resolve();\r\n const result = previous.then(fn, fn);\r\n const tail = result.then(\r\n () => undefined,\r\n () => undefined\r\n );\r\n this.tails.set(key, tail);\r\n\r\n try {\r\n return await result;\r\n } finally {\r\n // Drop the entry once we are the last waiter, so the map does not grow\r\n // one permanent entry per user id ever seen.\r\n if (this.tails.get(key) === tail) {\r\n this.tails.delete(key);\r\n }\r\n }\r\n }\r\n}\r\n","import type {\r\n PortableJournalEntry,\r\n PortableReservation,\r\n PortableTransaction,\r\n PortableUsageLog,\r\n PortableUserCredits,\r\n} from \"../../core/types.js\";\r\nimport { KeyedMutex } from \"./mutex.js\";\r\n\r\n/**\r\n * Backing state for {@link InMemoryCreditRepository}.\r\n *\r\n * Split out so the V2 transition logic can operate on the same state without\r\n * the repository class having to expose its internals publicly.\r\n */\r\nexport class MemoryStore {\r\n readonly users = new Map<string, PortableUserCredits>();\r\n readonly reservations = new Map<string, Map<string, PortableReservation>>();\r\n readonly transactions = new Map<string, PortableTransaction[]>();\r\n usageLogs: PortableUsageLog[] = [];\r\n readonly journalEntries = new Map<string, PortableJournalEntry[]>();\r\n\r\n /** Stands in for the reservations table's partial unique index. */\r\n readonly reservationKeys = new Map<string, string>();\r\n /** Stands in for the journal table's partial unique index. */\r\n readonly journalKeys = new Map<string, string>();\r\n\r\n /** Stands in for the database's per-user row locks. */\r\n readonly locks = new KeyedMutex();\r\n\r\n /**\r\n * Test-only yield point, awaited between reading state and mutating it.\r\n *\r\n * Unset in production, so the critical sections stay synchronous and this\r\n * costs nothing. Tests set it to a real macrotask yield, which forces\r\n * concurrent callers to interleave at exactly the spot a database would have\r\n * them contend — turning the lock above from decoration into the thing the\r\n * concurrency tests actually depend on.\r\n */\r\n schedulingHook?: () => Promise<void>;\r\n\r\n clear(): void {\r\n this.users.clear();\r\n this.reservations.clear();\r\n this.transactions.clear();\r\n this.usageLogs = [];\r\n this.journalEntries.clear();\r\n this.reservationKeys.clear();\r\n this.journalKeys.clear();\r\n }\r\n}\r\n\r\n/**\r\n * Composite map key standing in for a two-column unique index.\r\n *\r\n * The separator is a newline because it cannot appear in a user id and is\r\n * vanishingly unlikely in a caller-supplied idempotency key, so two distinct\r\n * pairs cannot collide into one entry.\r\n */\r\nexport function scopedKey(userId: string, key: string): string {\r\n return `${userId}\\n${key}`;\r\n}\r\n","/**\r\n * Deterministic journal writes for the in-memory V2 transitions.\r\n *\r\n * The SQL adapter gets this from a partial unique index plus an\r\n * `ON CONFLICT DO NOTHING` that re-reads and compares the losing row. There is\r\n * no index here, so the same contract has to be built by hand — and it has to\r\n * be the *same* contract, or the shared reservation tests prove nothing about\r\n * the adapter anyone actually ships.\r\n *\r\n * Two properties matter. A key that already exists must be accepted only when\r\n * the stored row describes the identical event, so a wrong row pre-seeded\r\n * through the public `createJournalEntry` cannot make a transition report a\r\n * charge the ledger does not record. And the check has to happen *before* the\r\n * balances move: this store has no transaction to roll back, so a throw after\r\n * mutation would leave the ledger half-transitioned.\r\n */\r\n\r\nimport { assertRepresentableFields, sameAmount } from \"../../core/amount.js\";\r\nimport { CreditError, CreditErrorCode } from \"../../core/errors.js\";\r\nimport type { CreditSource, PortableJournalEntry } from \"../../core/types.js\";\r\nimport { generateId } from \"../utils.js\";\r\nimport { MemoryStore, scopedKey } from \"./store.js\";\r\n\r\nexport interface JournalWriteInput {\r\n userId: string;\r\n /** The transition writing this entry, so a refusal names what was refused. */\r\n operation: string;\r\n entryType: \"debit\" | \"credit\";\r\n amount: number;\r\n balanceBefore: number;\r\n balanceAfter: number;\r\n source: CreditSource;\r\n reservationId: string;\r\n description: string;\r\n metadata?: Record<string, unknown>;\r\n idempotencyKey: string;\r\n}\r\n\r\n/** Name the first field an existing entry disagrees on, or `null` if it matches. */\r\nfunction describeJournalMismatch(\r\n row: PortableJournalEntry,\r\n input: JournalWriteInput\r\n): string | null {\r\n if (row.userId !== input.userId) return \"user_id\";\r\n if (row.entryType !== input.entryType) return \"entry_type\";\r\n if (!sameAmount(row.amount, input.amount)) return \"amount\";\r\n if (!sameAmount(row.balanceAfter, input.balanceAfter)) return \"balance_after\";\r\n if (row.source !== input.source) return \"source\";\r\n if (row.referenceId !== input.reservationId) return \"reference_id\";\r\n if (row.referenceType !== \"reservation\") return \"reference_type\";\r\n\r\n // Only the fields this adapter sets deterministically. Caller-supplied extras\r\n // are free-form and may legitimately differ between a request and its retry,\r\n // so they are not part of the identity of the event.\r\n const existingMeta = (row.metadata ?? {}) as Record<string, unknown>;\r\n const expectedMeta = (input.metadata ?? {}) as Record<string, unknown>;\r\n if (existingMeta.operationType !== expectedMeta.operationType) {\r\n return \"metadata.operationType\";\r\n }\r\n const hadAmount = existingMeta.amount !== undefined;\r\n const wantsAmount = expectedMeta.amount !== undefined;\r\n if (hadAmount !== wantsAmount) return \"metadata.amount\";\r\n if (wantsAmount && !sameAmount(existingMeta.amount, expectedMeta.amount)) {\r\n return \"metadata.amount\";\r\n }\r\n return null;\r\n}\r\n\r\nfunction findEntry(\r\n store: MemoryStore,\r\n userId: string,\r\n entryId: string\r\n): PortableJournalEntry | undefined {\r\n return store.journalEntries.get(userId)?.find((entry) => entry.id === entryId);\r\n}\r\n\r\n/**\r\n * Decide what the journal write will do — without doing it.\r\n *\r\n * Returns the id of an existing, exactly matching entry to reuse, or\r\n * `undefined` when a fresh row is needed. Throws when the key is taken by a\r\n * different event, and it throws *here*, before the caller has touched a single\r\n * balance.\r\n */\r\nexport function planTransitionJournal(\r\n store: MemoryStore,\r\n input: JournalWriteInput\r\n): string | undefined {\r\n // `balanceAfter` is a derived total, and every transition journal write\r\n // passes through this preflight before the ledger moves. Validating it here\r\n // rather than at each call site means a transition added later cannot forget\r\n // it, and a total the numeric column could not hold refuses while the\r\n // balances are still untouched.\r\n assertRepresentableFields(\r\n { amount: input.amount, \"journal balanceAfter\": input.balanceAfter },\r\n { userId: input.userId, reservationId: input.reservationId, operation: input.operation }\r\n );\r\n\r\n const existingId = store.journalKeys.get(scopedKey(input.userId, input.idempotencyKey));\r\n if (!existingId) return undefined;\r\n\r\n const row = findEntry(store, input.userId, existingId);\r\n const mismatch = row\r\n ? describeJournalMismatch(row, input)\r\n : \"the key is registered but its entry is missing\";\r\n if (mismatch) {\r\n throw new CreditError(\r\n `Journal key ${input.idempotencyKey} is already used by a different entry ` +\r\n `(${mismatch}). The transition was refused rather than reported as applied.`,\r\n CreditErrorCode.DATABASE_ERROR,\r\n {\r\n idempotencyKey: input.idempotencyKey,\r\n reservationId: input.reservationId,\r\n mismatch,\r\n }\r\n );\r\n }\r\n return existingId;\r\n}\r\n\r\n/**\r\n * Perform the write the preflight authorised.\r\n *\r\n * `existingId` is whatever {@link planTransitionJournal} returned for this same\r\n * input; passing it through keeps the decision and the write from drifting\r\n * apart, which is the whole point of splitting them.\r\n */\r\nexport function commitTransitionJournal(\r\n store: MemoryStore,\r\n input: JournalWriteInput,\r\n existingId: string | undefined\r\n): string {\r\n if (existingId) return existingId;\r\n\r\n const entry: PortableJournalEntry = {\r\n id: generateId(),\r\n userId: input.userId,\r\n entryType: input.entryType,\r\n amount: input.amount,\r\n balanceBefore: input.balanceBefore,\r\n balanceAfter: input.balanceAfter,\r\n source: input.source,\r\n referenceId: input.reservationId,\r\n referenceType: \"reservation\",\r\n description: input.description,\r\n metadata: input.metadata,\r\n idempotencyKey: input.idempotencyKey,\r\n createdAt: new Date().toISOString(),\r\n };\r\n\r\n if (!store.journalEntries.has(input.userId)) {\r\n store.journalEntries.set(input.userId, []);\r\n }\r\n store.journalEntries.get(input.userId)!.push(entry);\r\n store.journalKeys.set(scopedKey(input.userId, input.idempotencyKey), entry.id);\r\n return entry.id;\r\n}\r\n","/**\r\n * V2 reservation transitions for the in-memory repository.\r\n *\r\n * These mirror the SQL adapter's guarantees rather than its mechanics: every\r\n * transition runs inside the user's lock (standing in for the row locks),\r\n * re-reads state *after* acquiring it, compares-and-sets the status, and\r\n * writes at most one journal entry keyed so a retry cannot duplicate it.\r\n *\r\n * Modelling that here is what makes the shared contract tests meaningful — a\r\n * naive implementation would pass them by accident because nothing interleaves.\r\n */\r\n\r\nimport {\r\n assertValidCreditAmount,\r\n assertValidStoredAmount,\r\n allocateExpiredBacking,\r\n expiredBackingPatch,\r\n heldExpiredBacking,\r\n holdExpiredBacking,\r\n holdPlacedAtAfter,\r\n planHoldCommit,\r\n type ExpiredBackingAllocation,\r\n settleExpiredHoldBacking,\r\n sumAmounts,\r\n} from \"../../core/amount.js\";\r\nimport { assertValidIdempotencyKey } from \"../../core/idempotency.js\";\r\nimport {\r\n assertHoldPlaced,\r\n assertKnownReservationStatus,\r\n terminalStatusOf as narrowTerminalStatus,\r\n} from \"../../core/reservation-integrity.js\";\r\nimport { copyRecord } from \"./snapshot.js\";\r\nimport { assertRepresentableFields } from \"../../core/amount.js\";\r\nimport { CreditError, CreditErrorCode } from \"../../core/errors.js\";\r\nimport type {\r\n CommitOutcome,\r\n ExpireOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n TerminalReservationStatus,\r\n} from \"../../core/outcomes.js\";\r\nimport type {\r\n CreditSource,\r\n PortableReservation,\r\n PortableUserCredits,\r\n} from \"../../core/types.js\";\r\nimport { getOperationLabel } from \"../../config/index.js\";\r\nimport { generateId } from \"../utils.js\";\r\nimport {\r\n commitTransitionJournal,\r\n planTransitionJournal,\r\n type JournalWriteInput,\r\n} from \"./transition-journal.js\";\r\nimport type {\r\n ExpireReservationV2Options,\r\n ReservationTransitionOptions,\r\n ReserveCreditsV2Input,\r\n} from \"../v2-types.js\";\r\nimport { reservationJournalKey } from \"../v2-types.js\";\r\nimport { MemoryStore, scopedKey } from \"./store.js\";\r\n\r\n/** Yield at the read/write seam so tests can force real interleaving. */\r\nasync function yieldPoint(store: MemoryStore): Promise<void> {\r\n if (store.schedulingHook) await store.schedulingHook();\r\n}\r\n\r\n/**\r\n * Refuse a transition whose hold is no longer covered by `reserved`.\r\n *\r\n * The SQL adapter enforces this as `reserved >= amount` in the UPDATE's WHERE.\r\n * Clamping with `Math.max(reserved - amount, 0)` instead would look harmless\r\n * and quietly consume the coverage of *other* live holds, so those commits\r\n * would later fail or overdraw. Failing here keeps the damage contained and\r\n * visible, and — because the caller sees `DATABASE_ERROR` rather than\r\n * `INSUFFICIENT_CREDITS` — correctly reads as corruption, not as a user who\r\n * ran out of money.\r\n */\r\nfunction assertReservedCoversHold(\r\n credits: PortableUserCredits,\r\n userId: string,\r\n reservationId: string,\r\n amount: number\r\n): void {\r\n if (credits.reserved >= amount) return;\r\n throw new CreditError(\r\n `Credit balance invariant violated for user ${userId} while processing ` +\r\n `reservation ${reservationId}: reserved (${credits.reserved}) is less ` +\r\n `than the hold (${amount}). The transition was rolled back.`,\r\n CreditErrorCode.DATABASE_ERROR,\r\n { userId, reservationId, amount, reserved: credits.reserved }\r\n );\r\n}\r\n\r\n/**\r\n * Re-validate a persisted amount before the transition touches anything.\r\n *\r\n * Mirrors the SQL adapter: the reservation row was written once and is read\r\n * back on every transition, and a value that is negative, non-finite or off the\r\n * cent grid makes every downstream invariant meaningless — `reserved >= -10` is\r\n * trivially true, and subtracting a negative *adds* credits.\r\n */\r\nfunction assertLockedAmount(reservation: PortableReservation, transition: string): void {\r\n assertValidStoredAmount(reservation.amount, {\r\n userId: reservation.userId,\r\n reservationId: reservation.id,\r\n transition,\r\n });\r\n}\r\n\r\n/**\r\n * Narrow a non-`reserved` status, validating instead of casting.\r\n *\r\n * The cast this replaces reported `already_terminal` — a *success* outcome —\r\n * over a row holding any string at all, so a corrupt status read to the caller\r\n * as \"this reservation is resolved\". See `core/reservation-integrity.ts`.\r\n */\r\nfunction terminalStatusOf(\r\n reservation: PortableReservation,\r\n transition: string\r\n): TerminalReservationStatus {\r\n return narrowTerminalStatus(reservation, transition);\r\n}\r\n\r\n/**\r\n * Every check a persisted reservation must pass before a transition touches it.\r\n *\r\n * Ordered ahead of the `already_terminal` and `not_due` early exits on purpose:\r\n * both are success outcomes, and reporting one over a row that is corrupt or\r\n * unbacked tells the caller the reservation is fine.\r\n */\r\nfunction assertTrustworthy(reservation: PortableReservation, transition: string): void {\r\n assertKnownReservationStatus(reservation, transition);\r\n assertLockedAmount(reservation, transition);\r\n assertHoldPlaced(reservation, transition);\r\n}\r\n\r\nfunction openHolds(store: MemoryStore, userId: string): PortableReservation[] {\r\n return [...(store.reservations.get(userId)?.values() ?? [])].filter(\r\n (reservation) => reservation.status === \"reserved\"\r\n );\r\n}\r\n\r\n/**\r\n * The marked credits in force. A total stored without a frontier (written by 2.2.0, or\r\n * never allocated) is allocated across the open holds now, ahead of whatever is about\r\n * to happen, so a hold placed or closed next cannot take what belongs to an older one.\r\n */\r\nfunction expiredBackingInForce(\r\n store: MemoryStore,\r\n credits: PortableUserCredits\r\n): ExpiredBackingAllocation {\r\n const total = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);\r\n if (total > 0 && (!credits.expiredBackingFrontier || credits.expiredHoldBacking !== total)) {\r\n return allocateExpiredBacking(openHolds(store, credits.userId), total);\r\n }\r\n return { total, frontier: credits.expiredBackingFrontier ?? null };\r\n}\r\n\r\n/** Write the stored shares an allocation changed. Empty on all but a 2.2.0 account. */\r\nfunction writeRecordedShares(\r\n store: MemoryStore,\r\n userId: string,\r\n allocation: ExpiredBackingAllocation\r\n): void {\r\n for (const { id, expiredBacking } of allocation.rewrites ?? []) {\r\n const hold = getReservation(store, userId, id);\r\n if (hold) hold.expiredBacking = expiredBacking;\r\n }\r\n}\r\n\r\n/**\r\n * Set the marked credits on an account, which also repairs a stored value that was never\r\n * within the balance. A hold's share follows from the frontier, or from its own record. An\r\n * account that never had any, or whose total does not change, reads no holds.\r\n */\r\nexport function writeExpiredHoldBacking(\r\n store: MemoryStore,\r\n credits: PortableUserCredits,\r\n next: number\r\n): void {\r\n if (next <= 0) {\r\n Object.assign(credits, expiredBackingPatch({ total: 0 }, credits));\r\n return;\r\n }\r\n if (next === credits.expiredHoldBacking) return;\r\n const allocation = allocateExpiredBacking(openHolds(store, credits.userId), next);\r\n writeRecordedShares(store, credits.userId, allocation);\r\n Object.assign(credits, expiredBackingPatch(allocation, credits));\r\n}\r\n\r\nfunction getReservation(\r\n store: MemoryStore,\r\n userId: string,\r\n reservationId: string\r\n): PortableReservation | undefined {\r\n return store.reservations.get(userId)?.get(reservationId);\r\n}\r\n\r\n/**\r\n * The *live* stored record, deliberately — the transitions in this module write\r\n * through it. Everything that crosses the repository's public surface goes\r\n * through `copyRecord` instead; see `./snapshot.js`.\r\n */\r\nfunction requireUser(store: MemoryStore, userId: string): PortableUserCredits {\r\n const credits = store.users.get(userId);\r\n if (!credits) {\r\n throw new CreditError(\r\n `User credits not found for user ${userId}`,\r\n CreditErrorCode.USER_NOT_FOUND,\r\n { userId }\r\n );\r\n }\r\n return credits;\r\n}\r\n\r\nexport async function reserveCreditsV2(\r\n store: MemoryStore,\r\n input: ReserveCreditsV2Input\r\n): Promise<ReserveOutcome> {\r\n assertValidCreditAmount(input.amount, { userId: input.userId });\r\n assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });\r\n\r\n return store.locks.run(input.userId, async () => {\r\n if (input.idempotencyKey) {\r\n const existingId = store.reservationKeys.get(\r\n scopedKey(input.userId, input.idempotencyKey)\r\n );\r\n if (existingId) {\r\n const existing = getReservation(store, input.userId, existingId);\r\n if (existing) {\r\n // Before the payload comparison, and before any outcome is chosen: a\r\n // row is only adoptable as a replay if *its own* reserve placed the\r\n // hold. Adopting an unbacked row would report `replayed` over credits\r\n // nothing ever held. Mirrors the SQL adapter.\r\n assertHoldPlaced(existing, \"reserve replay\");\r\n // `expiresAt` is excluded on purpose: a retry legitimately computes a\r\n // later deadline and that must not read as a conflict.\r\n const samePayload =\r\n existing.amount === input.amount &&\r\n existing.operationType === input.operationType;\r\n return samePayload\r\n ? { outcome: \"replayed\", reservation: copyRecord(existing) }\r\n : {\r\n outcome: \"idempotency_conflict\",\r\n idempotencyKey: input.idempotencyKey,\r\n existing: copyRecord(existing),\r\n };\r\n }\r\n }\r\n }\r\n\r\n const credits = requireUser(store, input.userId);\r\n const available = sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved);\r\n await yieldPoint(store);\r\n if (available < input.amount) {\r\n return {\r\n outcome: \"insufficient\",\r\n available,\r\n required: input.amount,\r\n shortfall: sumAmounts(input.amount, -available),\r\n };\r\n }\r\n\r\n const now = new Date().toISOString();\r\n const backing = expiredBackingInForce(store, credits);\r\n const reservation: PortableReservation = {\r\n id: generateId(),\r\n userId: input.userId,\r\n amount: input.amount,\r\n operationType: input.operationType,\r\n status: \"reserved\",\r\n createdAt: now,\r\n expiresAt: input.expiresAt.toISOString(),\r\n idempotencyKey: input.idempotencyKey,\r\n // The hold-origin fact. Written here and nowhere else, in the same\r\n // critical section that raises `reserved` below, so the row and the hold\r\n // become visible together. `createReservation` cannot set it.\r\n holdPlacedAt: holdPlacedAtAfter(now, backing.total > 0 ? backing.frontier : null),\r\n };\r\n\r\n if (!store.reservations.has(input.userId)) {\r\n store.reservations.set(input.userId, new Map());\r\n }\r\n // The derived hold is checked before the reservation row exists, not after:\r\n // a refusal here must leave no reservation, no key registration and no\r\n // counter change behind.\r\n const nextReserved = sumAmounts(credits.reserved, input.amount);\r\n assertRepresentableFields(\r\n { reserved: nextReserved },\r\n { userId: input.userId, operation: \"reserveCredits\" }\r\n );\r\n\r\n store.reservations.get(input.userId)!.set(reservation.id, reservation);\r\n if (input.idempotencyKey) {\r\n store.reservationKeys.set(\r\n scopedKey(input.userId, input.idempotencyKey),\r\n reservation.id\r\n );\r\n }\r\n\r\n credits.reserved = nextReserved;\r\n writeRecordedShares(store, input.userId, backing);\r\n Object.assign(credits, expiredBackingPatch(backing, credits));\r\n credits.updatedAt = now;\r\n\r\n return { outcome: \"created\", reservation: copyRecord(reservation) };\r\n });\r\n}\r\n\r\nexport async function commitReservationV2(\r\n store: MemoryStore,\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n): Promise<CommitOutcome> {\r\n return store.locks.run(userId, async () => {\r\n const reservation = getReservation(store, userId, reservationId);\r\n if (!reservation) return { outcome: \"not_found\", reservationId };\r\n // Ahead of the early exits: `already_terminal` and `not_due` are success\r\n // outcomes, and reporting one over a row that is unusable tells the caller\r\n // the reservation is fine. Mirrors the SQL adapter.\r\n assertTrustworthy(reservation, \"commit\");\r\n\r\n if (reservation.status !== \"reserved\") {\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation: copyRecord(reservation),\r\n terminalStatus: terminalStatusOf(reservation, \"commit\"),\r\n };\r\n }\r\n\r\n const credits = requireUser(store, userId);\r\n const amount = reservation.amount;\r\n await yieldPoint(store);\r\n assertReservedCoversHold(credits, userId, reservationId, amount);\r\n const backing = expiredBackingInForce(store, credits);\r\n const paid = planHoldCommit(\r\n credits.balance,\r\n credits.bonusCredits,\r\n backing.total,\r\n holdExpiredBacking(reservation, backing.frontier, backing.rewrites),\r\n amount\r\n );\r\n if (!paid.covered) {\r\n throw new CreditError(\r\n `Insufficient credits to commit reservation ${reservationId}`,\r\n CreditErrorCode.INSUFFICIENT_CREDITS,\r\n { userId, reservationId, required: amount }\r\n );\r\n }\r\n\r\n // Project the post-transition balances before applying them. The store has\r\n // no transaction to roll back, so everything that can refuse — the journal\r\n // key check below — has to refuse while the ledger is still untouched. The\r\n // projected values are then assigned verbatim, so the number the journal\r\n // records and the number the balance ends up holding cannot drift apart.\r\n const nextBalance = paid.balance;\r\n const nextBonusCredits = paid.bonusCredits;\r\n const balanceAfter = sumAmounts(nextBalance, nextBonusCredits);\r\n\r\n const journal: JournalWriteInput = {\r\n userId,\r\n operation: \"commitReservation\",\r\n entryType: \"debit\",\r\n amount,\r\n balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),\r\n balanceAfter,\r\n source: \"operation_commit\",\r\n reservationId,\r\n description:\r\n options?.description ??\r\n `Committed ${amount} credits for ${getOperationLabel(reservation.operationType)}`,\r\n // Caller metadata first: the deterministic fields carry the identity of\r\n // the event that the collision check compares on.\r\n metadata: { ...options?.metadata, operationType: reservation.operationType, amount },\r\n idempotencyKey: reservationJournalKey(reservationId, \"commit\"),\r\n };\r\n const nextReserved = sumAmounts(credits.reserved, -amount);\r\n const nextMonthlyUsed = sumAmounts(credits.monthlyUsed, amount);\r\n assertRepresentableFields(\r\n {\r\n balance: nextBalance,\r\n bonusCredits: nextBonusCredits,\r\n reserved: nextReserved,\r\n monthlyUsed: nextMonthlyUsed,\r\n },\r\n { userId, operation: \"commitReservation\" }\r\n );\r\n\r\n const existingEntryId = planTransitionJournal(store, journal);\r\n\r\n reservation.status = \"committed\";\r\n reservation.completedAt = new Date().toISOString();\r\n\r\n // Drain `balance` before `bonusCredits`, matching the SQL adapter.\r\n credits.balance = nextBalance;\r\n credits.bonusCredits = nextBonusCredits;\r\n credits.reserved = nextReserved;\r\n credits.monthlyUsed = nextMonthlyUsed;\r\n writeRecordedShares(store, userId, backing);\r\n Object.assign(\r\n credits,\r\n expiredBackingPatch({ total: paid.expiredHoldBacking, frontier: backing.frontier }, credits)\r\n );\r\n credits.updatedAt = new Date().toISOString();\r\n\r\n const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);\r\n\r\n return {\r\n outcome: \"committed\",\r\n reservation: copyRecord(reservation),\r\n amount,\r\n balanceAfter,\r\n journalEntryId,\r\n };\r\n });\r\n}\r\n\r\nexport async function releaseReservationV2(\r\n store: MemoryStore,\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n): Promise<ReleaseOutcome> {\r\n return store.locks.run(userId, async () => {\r\n const reservation = getReservation(store, userId, reservationId);\r\n if (!reservation) return { outcome: \"not_found\", reservationId };\r\n // Ahead of the early exits: `already_terminal` and `not_due` are success\r\n // outcomes, and reporting one over a row that is unusable tells the caller\r\n // the reservation is fine. Mirrors the SQL adapter.\r\n assertTrustworthy(reservation, \"release\");\r\n\r\n if (reservation.status !== \"reserved\") {\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation: copyRecord(reservation),\r\n terminalStatus: terminalStatusOf(reservation, \"release\"),\r\n };\r\n }\r\n\r\n await yieldPoint(store);\r\n const journalEntryId = finishUnspent(store, reservation, {\r\n status: \"released\",\r\n source: \"operation_release\",\r\n transition: \"release\",\r\n verb: \"Released\",\r\n options,\r\n });\r\n\r\n return {\r\n outcome: \"released\",\r\n reservation: copyRecord(reservation),\r\n amount: reservation.amount,\r\n journalEntryId,\r\n };\r\n });\r\n}\r\n\r\nexport async function expireReservationV2(\r\n store: MemoryStore,\r\n userId: string,\r\n reservationId: string,\r\n options?: ExpireReservationV2Options\r\n): Promise<ExpireOutcome> {\r\n const asOf = options?.asOf ?? new Date();\r\n return store.locks.run(userId, async () => {\r\n const reservation = getReservation(store, userId, reservationId);\r\n if (!reservation) return { outcome: \"not_found\", reservationId };\r\n // Ahead of the early exits: `already_terminal` and `not_due` are success\r\n // outcomes, and reporting one over a row that is unusable tells the caller\r\n // the reservation is fine. Mirrors the SQL adapter.\r\n assertTrustworthy(reservation, \"expire\");\r\n\r\n if (reservation.status !== \"reserved\") {\r\n return {\r\n outcome: \"already_terminal\",\r\n reservation: copyRecord(reservation),\r\n terminalStatus: terminalStatusOf(reservation, \"expire\"),\r\n };\r\n }\r\n if (new Date(reservation.expiresAt).getTime() > asOf.getTime()) {\r\n return { outcome: \"not_due\", reservation: copyRecord(reservation) };\r\n }\r\n\r\n await yieldPoint(store);\r\n const journalEntryId = finishUnspent(store, reservation, {\r\n status: \"expired\",\r\n source: \"reservation_expired\",\r\n transition: \"expire\",\r\n verb: \"Expired\",\r\n options,\r\n });\r\n\r\n return {\r\n outcome: \"expired\",\r\n reservation: copyRecord(reservation),\r\n amount: reservation.amount,\r\n journalEntryId,\r\n };\r\n });\r\n}\r\n\r\ninterface UnspentTransition {\r\n status: \"released\" | \"expired\";\r\n source: CreditSource;\r\n transition: \"release\" | \"expire\";\r\n verb: string;\r\n options?: ReservationTransitionOptions;\r\n}\r\n\r\n/**\r\n * Hand a hold back unspent and journal it.\r\n *\r\n * `amount` is 0 because no credits changed hands — the hold only moves back\r\n * into \"available\". The hold size stays in metadata, matching the SQL adapter.\r\n */\r\nfunction finishUnspent(\r\n store: MemoryStore,\r\n reservation: PortableReservation,\r\n transition: UnspentTransition\r\n): string {\r\n const credits = requireUser(store, reservation.userId);\r\n assertReservedCoversHold(\r\n credits,\r\n reservation.userId,\r\n reservation.id,\r\n reservation.amount\r\n );\r\n\r\n // Handing a hold back removes only this hold's share of the credits kept solely to\r\n // back holds. The journal check still runs before the mutation, so a key collision\r\n // leaves `reserved`, the balance, the status and `completedAt` untouched.\r\n const inForce = expiredBackingInForce(store, credits);\r\n const backing = settleExpiredHoldBacking(\r\n credits.balance,\r\n inForce.total,\r\n holdExpiredBacking(reservation, inForce.frontier, inForce.rewrites)\r\n );\r\n const nextBalance = sumAmounts(credits.balance, -backing.used);\r\n const journal: JournalWriteInput = {\r\n userId: reservation.userId,\r\n operation: `${transition.transition}Reservation`,\r\n entryType: \"credit\",\r\n amount: 0,\r\n balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),\r\n balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),\r\n source: transition.source,\r\n reservationId: reservation.id,\r\n description:\r\n transition.options?.description ??\r\n `${transition.verb} ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,\r\n metadata: {\r\n ...transition.options?.metadata,\r\n operationType: reservation.operationType,\r\n amount: reservation.amount,\r\n ...(backing.used > 0 ? { expiredCreditsRemoved: backing.used } : {}),\r\n },\r\n idempotencyKey: reservationJournalKey(reservation.id, transition.transition),\r\n };\r\n const nextReserved = sumAmounts(credits.reserved, -reservation.amount);\r\n assertRepresentableFields(\r\n { reserved: nextReserved },\r\n { userId: reservation.userId, operation: `${transition.transition}Reservation` }\r\n );\r\n\r\n const existingEntryId = planTransitionJournal(store, journal);\r\n\r\n reservation.status = transition.status;\r\n reservation.completedAt = new Date().toISOString();\r\n credits.reserved = nextReserved;\r\n credits.balance = nextBalance;\r\n writeRecordedShares(store, reservation.userId, inForce);\r\n Object.assign(\r\n credits,\r\n expiredBackingPatch({ total: backing.remaining, frontier: inForce.frontier }, credits)\r\n );\r\n credits.updatedAt = new Date().toISOString();\r\n\r\n return commitTransitionJournal(store, journal, existingEntryId);\r\n}\r\n","/**\r\n * In-Memory Credit Repository Implementation\r\n *\r\n * A database-agnostic implementation of ICreditRepository for testing and prototyping.\r\n * All data is stored in memory and lost when the process restarts.\r\n *\r\n * Usage:\r\n * - Unit tests without database dependency\r\n * - Local development and prototyping\r\n * - Reference implementation for custom repositories\r\n */\r\n\r\nimport type {\r\n PortableUserCredits,\r\n PortableReservation,\r\n PortableTransaction,\r\n PortableJournalEntry,\r\n PortableUsageLog,\r\n SubscriptionTier,\r\n ReservationStatus,\r\n MonthlyResetResult,\r\n SubscriptionExpiryResult,\r\n} from \"../../core/types.js\";\r\nimport type {\r\n ICreditRepository,\r\n CreateReservationInput,\r\n CreateTransactionInput,\r\n CreateUsageLogInput,\r\n CreateJournalEntryInput,\r\n UsageLogQuery,\r\n JournalEntryQuery,\r\n CreditBalanceUpdate,\r\n TierUpdateInput,\r\n AddCreditsAtomicOptions,\r\n CreditPolicyOptions,\r\n DeductJournalInput,\r\n} from \"../types.js\";\r\nimport type {\r\n AddCreditsOutcome,\r\n CommitOutcome,\r\n ExpireOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n} from \"../../core/outcomes.js\";\r\nimport type {\r\n AddCreditsV2Input,\r\n ExpireReservationV2Options,\r\n ReservationTransitionOptions,\r\n ReserveCreditsV2Input,\r\n} from \"../v2-types.js\";\r\nimport {\r\n assertUnreferencedDirectTransaction,\r\n createPaymentRefConflictError,\r\n describePaymentMismatch,\r\n normalizePaymentRef,\r\n type PaymentEventPayload,\r\n} from \"../../core/payment-ref.js\";\r\nimport {\r\n createInsufficientCreditsError,\r\n createReservationAlreadyProcessedError,\r\n createReservationNotFoundError,\r\n} from \"../../core/errors.js\";\r\nimport { assertValidBalanceUpdate, generateId, toDate, getNextMonthlyReset } from \"../utils.js\";\r\nimport { copyRecord, copyRecords } from \"./snapshot.js\";\r\nimport {\r\n assertRepresentableAmount,\r\n assertRepresentableFields,\r\n assertRepresentableTierAmount,\r\n assertValidBalanceTarget,\r\n assertValidTierBalanceInput,\r\n heldExpiredBacking,\r\n planBalanceCap,\r\n planBalanceTarget,\r\n spendableBalance,\r\n storedMonthlyLimit,\r\n assertValidCreditAmount,\r\n sumAmounts,\r\n} from \"../../core/amount.js\";\r\nimport {\r\n assertPublicJournalKey,\r\n assertValidIdempotencyKey,\r\n} from \"../../core/idempotency.js\";\r\nimport {\r\n assertDirectStatusWriteAllowed,\r\n assertUnkeyedDirectReservation,\r\n} from \"../../core/reservation-integrity.js\";\r\nimport { CreditError, CreditErrorCode } from \"../../core/errors.js\";\r\nimport { MemoryStore, scopedKey } from \"./store.js\";\r\nimport {\r\n commitReservationV2,\r\n expireReservationV2,\r\n releaseReservationV2,\r\n reserveCreditsV2,\r\n writeExpiredHoldBacking,\r\n} from \"./v2.js\";\r\nimport {\r\n getConfigMonthlyLimit,\r\n getConfigTierConfig,\r\n getDefaultTier,\r\n isFreeTier,\r\n monthlyResetBalance,\r\n} from \"../../config/index.js\";\r\n\r\n/**\r\n * In-Memory implementation of ICreditRepository\r\n *\r\n * Implements all repository methods using Map-based storage.\r\n * Useful for testing and as a reference implementation.\r\n */\r\n/** Apply an increment, or report `undefined` when the caller sent none. */\r\n/** Accept either half of the `Date | string` timestamp inputs the API takes. */\r\nfunction isoOf(value: Date | string): string {\r\n return value instanceof Date ? value.toISOString() : value;\r\n}\r\n\r\nexport class InMemoryCreditRepository implements ICreditRepository {\r\n private readonly store = new MemoryStore();\r\n private readonly options: CreditPolicyOptions;\r\n\r\n constructor(options: CreditPolicyOptions = {}) {\r\n this.options = options;\r\n }\r\n\r\n // ==================== User Credits ====================\r\n\r\n /**\r\n * A copy, never the live record.\r\n *\r\n * Handing out the stored object made this adapter behave unlike any real\r\n * one: a caller holding a \"before\" snapshot saw it mutate under them. The\r\n * service's monthly reset compared `resetResult.credits.balance` against the\r\n * balance it had read moments earlier, got zero because both were the same\r\n * object, and silently skipped the reset's journal entry — while the SQL\r\n * adapter, which maps each row into a fresh object, wrote it.\r\n */\r\n async getUserCredits(userId: string): Promise<PortableUserCredits | null> {\r\n const credits = this.store.users.get(userId);\r\n return copyRecord(credits) ?? null;\r\n }\r\n\r\n async initializeUserCredits(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n initialBalance: number\r\n ): Promise<PortableUserCredits> {\r\n // Create only: a late second call must not wipe an account already in use.\r\n const existing = this.store.users.get(userId);\r\n return copyRecord(existing ?? this.seedUser(userId, tier, initialBalance));\r\n }\r\n\r\n /**\r\n * The synchronous body of `initializeUserCredits`.\r\n *\r\n * Kept `await`-free on purpose: `addCreditsV2` seeds a missing user *inside*\r\n * its critical section, and an `await` between the payment-reference check\r\n * and the transaction write opens a gap another caller can run through — two\r\n * simultaneous first deliveries of the same reference would then both credit.\r\n */\r\n private seedUser(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n initialBalance: number\r\n ): PortableUserCredits {\r\n assertValidBalanceTarget(initialBalance, \"initialBalance\", { userId });\r\n // Derived from tier configuration rather than supplied by the caller, and\r\n // therefore just as capable of being unrepresentable: a tier configured\r\n // with `monthlyCredits: 1.005` writes a number the column cannot hold.\r\n const configured = getConfigMonthlyLimit(tier);\r\n assertRepresentableTierAmount(configured, \"monthlyLimit\", { userId, tier });\r\n // Stored through the shared contract so both adapters hold the same number\r\n // for an unlimited tier. See `storedMonthlyLimit`.\r\n const monthlyLimit = storedMonthlyLimit(configured);\r\n\r\n const now = new Date().toISOString();\r\n const credits: PortableUserCredits = {\r\n userId,\r\n balance: initialBalance,\r\n bonusCredits: 0,\r\n reserved: 0,\r\n tier,\r\n monthlyLimit,\r\n monthlyUsed: 0,\r\n monthlyResetAt: getNextMonthlyReset().toISOString(),\r\n subscriptionExpiresAt: null,\r\n createdAt: now,\r\n updatedAt: now,\r\n };\r\n this.store.users.set(userId, credits);\r\n return credits;\r\n }\r\n\r\n async updateUserCredits(userId: string, updates: CreditBalanceUpdate): Promise<void> {\r\n assertRepresentableFields(\r\n {\r\n balance: updates.balance,\r\n bonusCredits: updates.bonusCredits,\r\n reserved: updates.reserved,\r\n monthlyLimit: updates.monthlyLimit,\r\n monthlyUsed: updates.monthlyUsed,\r\n balanceIncrement: updates.balanceIncrement,\r\n bonusCreditsIncrement: updates.bonusCreditsIncrement,\r\n reservedIncrement: updates.reservedIncrement,\r\n monthlyUsedIncrement: updates.monthlyUsedIncrement,\r\n },\r\n { userId, operation: \"updateUserCredits\" }\r\n );\r\n\r\n const credits = this.store.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n assertValidBalanceUpdate(credits, updates);\r\n\r\n // The entire record is projected onto a candidate before anything is\r\n // written back. Assigning the absolute fields first and validating the\r\n // increments afterwards would leave a refused update half-applied — an\r\n // explicit `monthlyUsed: 5` would stick even though the derived `balance`\r\n // it was sent with was rejected — and this store has no transaction to\r\n // undo it. Every numeric field is checked on the candidate, so the record\r\n // either takes the whole update or none of it.\r\n const next: PortableUserCredits = { ...credits };\r\n\r\n if (updates.balance !== undefined) {\r\n next.balance = updates.balance;\r\n }\r\n if (updates.bonusCredits !== undefined) next.bonusCredits = updates.bonusCredits;\r\n if (updates.reserved !== undefined) next.reserved = updates.reserved;\r\n if (updates.tier !== undefined) next.tier = updates.tier;\r\n if (updates.monthlyLimit !== undefined) next.monthlyLimit = updates.monthlyLimit;\r\n if (updates.monthlyUsed !== undefined) next.monthlyUsed = updates.monthlyUsed;\r\n if (updates.monthlyResetAt !== undefined) {\r\n next.monthlyResetAt = isoOf(updates.monthlyResetAt);\r\n }\r\n if (updates.subscriptionExpiresAt !== undefined) {\r\n next.subscriptionExpiresAt =\r\n updates.subscriptionExpiresAt === null\r\n ? null\r\n : isoOf(updates.subscriptionExpiresAt);\r\n }\r\n\r\n // Increments are checked as *results*, not just as arguments: adding a\r\n // legal 0.01 to a legal 9999999999.99 produces a number the column cannot\r\n // hold. They apply on top of whatever the absolute fields just set; the SQL\r\n // adapter is built to match, and the SQL package's\r\n // `__tests__/integration/adapter-parity.test.ts` pins the two together.\r\n //\r\n // `sumAmounts`, not `+`: a float sum can land off the cent grid the ledger\r\n // validates against, and rejecting a legal increment as `INVALID_AMOUNT` is\r\n // a worse failure than the overflow this check exists for.\r\n if (updates.balanceIncrement !== undefined) {\r\n next.balance = sumAmounts(next.balance, updates.balanceIncrement);\r\n }\r\n if (updates.bonusCreditsIncrement !== undefined) {\r\n next.bonusCredits = sumAmounts(next.bonusCredits, updates.bonusCreditsIncrement);\r\n }\r\n if (updates.reservedIncrement !== undefined) {\r\n next.reserved = sumAmounts(next.reserved, updates.reservedIncrement);\r\n }\r\n if (updates.monthlyUsedIncrement !== undefined) {\r\n next.monthlyUsed = sumAmounts(next.monthlyUsed, updates.monthlyUsedIncrement);\r\n }\r\n\r\n assertRepresentableFields(\r\n {\r\n balance: next.balance,\r\n bonusCredits: next.bonusCredits,\r\n reserved: next.reserved,\r\n monthlyUsed: next.monthlyUsed,\r\n },\r\n { userId, operation: \"updateUserCredits\" }\r\n );\r\n assertRepresentableTierAmount(next.monthlyLimit, \"monthlyLimit\", {\r\n userId,\r\n operation: \"updateUserCredits\",\r\n });\r\n\r\n // An absolute balance replaces what the marked credits were part of. An increment\r\n // cannot reach them, because the validator refused it, but a stored value that was\r\n // never within the balance stops here rather than growing with a top-up.\r\n writeExpiredHoldBacking(\r\n this.store,\r\n next,\r\n updates.balance !== undefined\r\n ? 0\r\n : Math.min(heldExpiredBacking(credits.balance, credits.expiredHoldBacking), next.balance)\r\n );\r\n\r\n next.updatedAt = new Date().toISOString();\r\n this.store.users.set(userId, next);\r\n }\r\n\r\n async updateUserTier(userId: string, input: TierUpdateInput): Promise<void> {\r\n assertRepresentableFields(\r\n { monthlyLimit: input.monthlyLimit, balance: input.balance, monthlyUsed: input.monthlyUsed },\r\n { userId, operation: \"updateUserTier\" }\r\n );\r\n assertValidTierBalanceInput(input, { userId, operation: \"updateUserTier\" });\r\n\r\n const credits = this.store.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n // Decided from the tier the account holds now, before this write replaces it.\r\n if (input.balanceCap !== undefined && !isFreeTier(credits.tier)) {\r\n const capped = planBalanceCap(\r\n credits.balance,\r\n input.balanceCap,\r\n credits.reserved,\r\n credits.bonusCredits,\r\n credits.expiredHoldBacking\r\n );\r\n credits.balance = capped.balance;\r\n writeExpiredHoldBacking(this.store, credits, capped.expiredHoldBacking);\r\n }\r\n\r\n credits.tier = input.tier;\r\n credits.monthlyLimit = input.monthlyLimit;\r\n // A tier write may lower the balance, but never below what still backs the\r\n // outstanding holds — see `backedBalanceFloor`. Same clamp as the SQL\r\n // adapter, so a downgrade cannot strand a live reservation.\r\n if (input.balance !== undefined) {\r\n const planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);\r\n credits.balance = planned.balance;\r\n writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);\r\n }\r\n // A write with no balance change still leaves the marks within the balance.\r\n writeExpiredHoldBacking(\r\n this.store,\r\n credits,\r\n heldExpiredBacking(credits.balance, credits.expiredHoldBacking)\r\n );\r\n if (input.monthlyUsed !== undefined) credits.monthlyUsed = input.monthlyUsed;\r\n if (input.subscriptionExpiresAt !== undefined) {\r\n if (input.subscriptionExpiresAt === null) {\r\n credits.subscriptionExpiresAt = null;\r\n } else {\r\n credits.subscriptionExpiresAt = input.subscriptionExpiresAt instanceof Date\r\n ? input.subscriptionExpiresAt.toISOString()\r\n : input.subscriptionExpiresAt;\r\n }\r\n }\r\n credits.updatedAt = new Date().toISOString();\r\n\r\n this.store.users.set(userId, credits);\r\n }\r\n\r\n // ==================== Reservations ====================\r\n\r\n async createReservation(input: CreateReservationInput): Promise<PortableReservation> {\r\n // Same refusal as the SQL adapter: a row written with an amount the ledger\r\n // cannot honour has no repair path once it exists, and every transition\r\n // that later locks it would have to refuse anyway.\r\n assertValidCreditAmount(input.amount, { userId: input.userId, operation: \"createReservation\" });\r\n assertUnkeyedDirectReservation(input);\r\n\r\n const now = new Date().toISOString();\r\n const reservation: PortableReservation = {\r\n id: generateId(),\r\n userId: input.userId,\r\n amount: input.amount,\r\n operationType: input.operationType,\r\n status: \"reserved\",\r\n createdAt: now,\r\n expiresAt: input.expiresAt.toISOString(),\r\n };\r\n\r\n if (!this.store.reservations.has(input.userId)) {\r\n this.store.reservations.set(input.userId, new Map());\r\n }\r\n this.store.reservations.get(input.userId)!.set(reservation.id, reservation);\r\n\r\n return copyRecord(reservation);\r\n }\r\n\r\n async getReservation(\r\n userId: string,\r\n reservationId: string\r\n ): Promise<PortableReservation | null> {\r\n const userReservations = this.store.reservations.get(userId);\r\n if (!userReservations) return null;\r\n return copyRecord(userReservations.get(reservationId)) ?? null;\r\n }\r\n\r\n async updateReservationStatus(\r\n userId: string,\r\n reservationId: string,\r\n status: ReservationStatus,\r\n completedAt?: Date\r\n ): Promise<void> {\r\n const userReservations = this.store.reservations.get(userId);\r\n if (!userReservations) {\r\n throw new Error(`No reservations found for user ${userId}`);\r\n }\r\n\r\n const reservation = userReservations.get(reservationId);\r\n if (!reservation) {\r\n throw new Error(`Reservation ${reservationId} not found`);\r\n }\r\n\r\n // A backed hold's status is owned by the transition that settles it, and no\r\n // row may be reopened: this method assigns a status and nothing else, so on\r\n // a V2 row it would change the status without the ledger movement the\r\n // status stands for. See `assertDirectStatusWriteAllowed`.\r\n assertDirectStatusWriteAllowed(reservation, status);\r\n\r\n reservation.status = status;\r\n if (completedAt) reservation.completedAt = completedAt.toISOString();\r\n\r\n userReservations.set(reservationId, reservation);\r\n }\r\n\r\n // ==================== Atomic Operations ====================\r\n\r\n // ==================== V2 boundary ====================\r\n //\r\n // As in the Drizzle adapter, V2 is the real implementation and the legacy\r\n // `*Atomic` methods are adapters over it, so both APIs share one set of\r\n // guarantees.\r\n\r\n async reserveCreditsV2(input: ReserveCreditsV2Input): Promise<ReserveOutcome> {\r\n return reserveCreditsV2(this.store, input);\r\n }\r\n\r\n async commitReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<CommitOutcome> {\r\n return commitReservationV2(this.store, userId, reservationId, options);\r\n }\r\n\r\n async releaseReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<ReleaseOutcome> {\r\n return releaseReservationV2(this.store, userId, reservationId, options);\r\n }\r\n\r\n async expireReservationV2(\r\n userId: string,\r\n reservationId: string,\r\n options?: ExpireReservationV2Options\r\n ): Promise<ExpireOutcome> {\r\n return expireReservationV2(this.store, userId, reservationId, options);\r\n }\r\n\r\n // ==================== Legacy atomic operations ====================\r\n\r\n async reserveCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n operationType: string,\r\n expiresAt: Date\r\n ): Promise<PortableReservation> {\r\n const outcome = await this.reserveCreditsV2({ userId, amount, operationType, expiresAt });\r\n if (outcome.outcome === \"created\" || outcome.outcome === \"replayed\") {\r\n return outcome.reservation;\r\n }\r\n if (outcome.outcome === \"insufficient\") {\r\n throw createInsufficientCreditsError(outcome.required, outcome.available);\r\n }\r\n throw createReservationAlreadyProcessedError(outcome.existing.id, outcome.existing.status);\r\n }\r\n\r\n async commitReservationAtomic(userId: string, reservationId: string): Promise<void> {\r\n const outcome = await this.commitReservationV2(userId, reservationId);\r\n if (outcome.outcome === \"committed\") return;\r\n if (outcome.outcome === \"not_found\") throw createReservationNotFoundError(reservationId);\r\n // Re-delivering a commit for an already-committed reservation stays a no-op.\r\n if (outcome.terminalStatus === \"committed\") return;\r\n throw createReservationAlreadyProcessedError(reservationId, outcome.terminalStatus);\r\n }\r\n\r\n async releaseReservationAtomic(userId: string, reservationId: string): Promise<void> {\r\n const outcome = await this.releaseReservationV2(userId, reservationId);\r\n if (outcome.outcome === \"not_found\") throw createReservationNotFoundError(reservationId);\r\n // Releasing an already-terminal reservation is a no-op, as before.\r\n }\r\n\r\n /**\r\n * The stored transaction carrying this reference, in *any* user's ledger.\r\n *\r\n * Deliberately not scoped to one user. The SQL adapter's unique index covers\r\n * `payment_ref` alone, so searching only the crediting user's own history made\r\n * the same reference credit two different accounts here while the SQL adapter\r\n * treated the second delivery as a duplicate. See `core/payment-ref.ts`.\r\n */\r\n private findByPaymentRef(paymentRef: string): PortableTransaction | undefined {\r\n for (const recorded of this.store.transactions.values()) {\r\n const hit = recorded.find((transaction) => transaction.paymentRef === paymentRef);\r\n if (hit) return hit;\r\n }\r\n return undefined;\r\n }\r\n\r\n /** The journal entry written alongside a stored purchase, if there is one. */\r\n private journalForTransaction(\r\n transaction: PortableTransaction\r\n ): PortableJournalEntry | undefined {\r\n return this.store.journalEntries\r\n .get(transaction.userId)\r\n ?.find((entry) => entry.referenceId === transaction.id);\r\n }\r\n\r\n /**\r\n * Credit an account, resolving `paymentRef` to created / replayed / conflict.\r\n *\r\n * The reference is checked and the transaction is recorded with no `await`\r\n * between them. `createTransaction` is `async` but its body is synchronous, so\r\n * under JavaScript's run-to-completion semantics no other caller can observe\r\n * the gap — which is what stands in for the SQL adapter's unique index.\r\n *\r\n * A `replayed` or `conflict` outcome writes nothing at all: the check runs\r\n * before the balance moves, not after.\r\n */\r\n async addCreditsV2(input: AddCreditsV2Input): Promise<AddCreditsOutcome> {\r\n const { userId, amount, description, options } = input;\r\n assertValidCreditAmount(amount, { userId, operation: \"addCredits\" });\r\n\r\n // Empty and whitespace-only strings are not references. Normalised in one\r\n // place so this adapter and the SQL one agree on which calls carry one.\r\n const paymentRef = normalizePaymentRef(input.paymentRef);\r\n const payload: PaymentEventPayload = {\r\n userId,\r\n amount,\r\n type: \"purchase\",\r\n source: options?.source ?? \"purchase\",\r\n referenceType: options?.referenceType ?? \"transaction\",\r\n };\r\n\r\n if (paymentRef) {\r\n const existing = this.findByPaymentRef(paymentRef);\r\n if (existing) {\r\n const journal = this.journalForTransaction(existing);\r\n // Presence is not enough. A reference that arrives again with a\r\n // different user, amount or source is a *different* credit event, and\r\n // reporting it as a replay would credit the first amount and call the\r\n // second one done.\r\n const mismatch = describePaymentMismatch(\r\n {\r\n userId: existing.userId,\r\n amount: existing.amount,\r\n type: existing.type,\r\n source: journal?.source,\r\n referenceType: journal?.referenceType,\r\n },\r\n payload\r\n );\r\n return mismatch === null\r\n ? { outcome: \"replayed\", paymentRef, transaction: copyRecord(existing) }\r\n : { outcome: \"conflict\", paymentRef, existing: copyRecord(existing), mismatch };\r\n }\r\n }\r\n\r\n // After the reference check, never before it: a replay or a conflict must\r\n // write nothing, including the account row this creates. An absent user is\r\n // created at tier defaults, matching the SQL adapter's `ensureUserCredits`\r\n // — the first credit for a not-yet-seeded user (a webhook that outran\r\n // provisioning) lands instead of throwing in one adapter and landing in the\r\n // other.\r\n if (!this.store.users.has(userId)) {\r\n const tier = getDefaultTier();\r\n // `seedUser`, not `await initializeUserCredits(...)`: an `await` here sat\r\n // between the reference check above and the transaction write below, and\r\n // two simultaneous first deliveries of one reference both ran through\r\n // that gap and both credited. The seed is synchronous so the critical\r\n // section stays unbroken under run-to-completion.\r\n this.seedUser(userId, tier, storedMonthlyLimit(getConfigMonthlyLimit(tier)));\r\n }\r\n const credits = this.store.users.get(userId)!;\r\n\r\n // Project first, mutate second. The transaction write below validates the\r\n // derived `newBalance`, and if that throws after the balance has already\r\n // moved the store is left holding half of an operation that reported\r\n // failure. There is no rollback here, so nothing may change until every\r\n // derived value is known to be storable.\r\n const previousTotal = sumAmounts(credits.balance, credits.bonusCredits);\r\n const nextBonusCredits = sumAmounts(credits.bonusCredits, amount);\r\n const newTotal = sumAmounts(credits.balance, nextBonusCredits);\r\n assertRepresentableFields(\r\n { previousBalance: previousTotal, bonusCredits: nextBonusCredits, newBalance: newTotal },\r\n { userId, operation: \"addCredits\" }\r\n );\r\n\r\n credits.bonusCredits = nextBonusCredits;\r\n // A credit does not change the marks, and leaves them within the balance.\r\n writeExpiredHoldBacking(\r\n this.store,\r\n credits,\r\n heldExpiredBacking(credits.balance, credits.expiredHoldBacking)\r\n );\r\n credits.updatedAt = new Date().toISOString();\r\n this.store.users.set(userId, credits);\r\n\r\n // Recorded through the internal writer: the public `createTransaction`\r\n // refuses a `paymentRef` precisely so that only this path — which moved the\r\n // balance above — can claim one.\r\n const transaction = this.recordTransaction({\r\n userId,\r\n type: \"purchase\",\r\n amount,\r\n description,\r\n paymentRef,\r\n previousBalance: previousTotal,\r\n newBalance: newTotal,\r\n });\r\n\r\n // Journal entry (parity with the Drizzle adapter): a single journal can then\r\n // serve as the app's credit ledger, including revenue-attribution metadata.\r\n const journalMetadata = {\r\n ...(paymentRef ? { paymentRef } : {}),\r\n ...(options?.metadata ?? {}),\r\n };\r\n const entry = await this.createJournalEntry({\r\n userId,\r\n entryType: \"credit\",\r\n amount,\r\n balanceBefore: previousTotal,\r\n balanceAfter: newTotal,\r\n source: payload.source,\r\n referenceId: transaction.id ?? paymentRef ?? \"unknown\",\r\n referenceType: payload.referenceType,\r\n description,\r\n metadata: Object.keys(journalMetadata).length > 0 ? journalMetadata : undefined,\r\n });\r\n\r\n return paymentRef\r\n ? { outcome: \"created\", paymentRef, transaction, journalEntryId: entry.id }\r\n : { outcome: \"created\", transaction, journalEntryId: entry.id };\r\n }\r\n\r\n /**\r\n * The legacy signature, on top of {@link addCreditsV2}.\r\n *\r\n * `Promise<void>` has nowhere to report a conflict, and silently returning is\r\n * indistinguishable from \"credited\" — the exact failure this round is closing\r\n * — so a conflict throws. A genuine replay still returns quietly, because\r\n * that *is* the idempotent no-op the caller asked for.\r\n */\r\n async addCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n description: string,\r\n paymentRef?: string,\r\n options?: AddCreditsAtomicOptions\r\n ): Promise<void> {\r\n const outcome = await this.addCreditsV2({\r\n userId,\r\n amount,\r\n description,\r\n paymentRef,\r\n options,\r\n });\r\n if (outcome.outcome === \"conflict\") {\r\n throw createPaymentRefConflictError(outcome.paymentRef, {\r\n userId,\r\n mismatch: outcome.mismatch,\r\n existingUserId: outcome.existing.userId,\r\n });\r\n }\r\n }\r\n\r\n async deductCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n journal?: DeductJournalInput\r\n ): Promise<{ previousBalance: number; newBalance: number; journalEntryId?: string }> {\r\n // `amount > 0` alone let 1.005, Infinity and out-of-range values through to\r\n // arithmetic the `numeric(12, 2)` adapter could never have stored.\r\n assertValidCreditAmount(amount, { userId, operation: \"deductCredits\" });\r\n\r\n const credits = this.store.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User credits not found for userId: ${userId}`);\r\n }\r\n\r\n const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);\r\n const available = Math.min(\r\n sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved),\r\n sumAmounts(spendable, credits.bonusCredits)\r\n );\r\n if (available < amount) {\r\n throw new Error(\r\n `Insufficient credits. Available: ${available}, requested: ${amount}`\r\n );\r\n }\r\n\r\n // Drain balance first, then bonusCredits — same policy as commit. Credits kept\r\n // only for holds have expired: a deduct cannot land on them.\r\n const balanceDeduction = Math.min(spendable, amount);\r\n const bonusDeduction = sumAmounts(amount, -balanceDeduction);\r\n\r\n // Projected and checked before anything moves, for the same reason as\r\n // `addCreditsAtomic`: there is no rollback in this store.\r\n const previousBalance = sumAmounts(credits.balance, credits.bonusCredits);\r\n const nextBalance = sumAmounts(credits.balance, -balanceDeduction);\r\n const nextBonusCredits = sumAmounts(credits.bonusCredits, -bonusDeduction);\r\n const newBalance = sumAmounts(previousBalance, -amount);\r\n assertRepresentableFields(\r\n { previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },\r\n { userId, operation: \"deductCredits\" }\r\n );\r\n\r\n const held = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);\r\n credits.balance = nextBalance;\r\n credits.bonusCredits = nextBonusCredits;\r\n writeExpiredHoldBacking(this.store, credits, held);\r\n credits.updatedAt = new Date().toISOString();\r\n this.store.users.set(userId, credits);\r\n\r\n if (!journal) return { previousBalance, newBalance };\r\n const entry = this.recordJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount,\r\n balanceBefore: previousBalance,\r\n balanceAfter: newBalance,\r\n source: journal.source ?? \"operation_commit\",\r\n referenceId: journal.referenceId,\r\n referenceType: journal.referenceType ?? \"adjustment\",\r\n description: journal.description,\r\n metadata: journal.metadata,\r\n });\r\n return { previousBalance, newBalance, journalEntryId: entry.id };\r\n }\r\n\r\n // ==================== Transactions ====================\r\n\r\n async createTransaction(input: CreateTransactionInput): Promise<PortableTransaction> {\r\n // A record with a `paymentRef` would occupy the global payment boundary\r\n // without crediting anyone — a later addCredits with the same reference\r\n // then reports `replayed` and credits nothing. Referenced payments go\r\n // through addCredits; see `assertUnreferencedDirectTransaction`.\r\n assertUnreferencedDirectTransaction(input);\r\n // Always absent after the guard; spelled as the normalised value so a blank\r\n // string is stored as \"no reference\", matching the SQL adapter.\r\n return this.recordTransaction({\r\n ...input,\r\n paymentRef: normalizePaymentRef(input.paymentRef),\r\n });\r\n }\r\n\r\n /** The unguarded writer, for `addCreditsV2` — which has moved the balance. */\r\n private recordTransaction(input: CreateTransactionInput): PortableTransaction {\r\n // Ledger records, not movements: a correction may be negative and a balance\r\n // may be below zero, so these check what `numeric(12, 2)` can hold.\r\n assertRepresentableAmount(input.amount, \"transaction amount\", { userId: input.userId });\r\n assertRepresentableAmount(input.previousBalance, \"transaction previousBalance\", {\r\n userId: input.userId,\r\n });\r\n assertRepresentableAmount(input.newBalance, \"transaction newBalance\", {\r\n userId: input.userId,\r\n });\r\n\r\n const transaction: PortableTransaction = {\r\n id: generateId(),\r\n userId: input.userId,\r\n type: input.type,\r\n amount: input.amount,\r\n description: input.description,\r\n paymentRef: input.paymentRef,\r\n previousBalance: input.previousBalance,\r\n newBalance: input.newBalance,\r\n createdAt: new Date().toISOString(),\r\n };\r\n\r\n if (!this.store.transactions.has(input.userId)) {\r\n this.store.transactions.set(input.userId, []);\r\n }\r\n this.store.transactions.get(input.userId)!.push(transaction);\r\n\r\n return copyRecord(transaction);\r\n }\r\n\r\n async getTransactions(\r\n userId: string,\r\n limit = 50,\r\n offset = 0\r\n ): Promise<PortableTransaction[]> {\r\n const userTransactions = this.store.transactions.get(userId) ?? [];\r\n // Sort by createdAt descending (most recent first)\r\n const sorted = [...userTransactions].sort((a, b) => {\r\n const aDate = toDate(a.createdAt).getTime();\r\n const bDate = toDate(b.createdAt).getTime();\r\n return bDate - aDate;\r\n });\r\n return copyRecords(sorted.slice(offset, offset + limit));\r\n }\r\n\r\n // ==================== Usage Logs ====================\r\n\r\n async logUsage(input: CreateUsageLogInput): Promise<PortableUsageLog> {\r\n assertRepresentableAmount(input.creditsUsed, \"creditsUsed\", { userId: input.userId });\r\n\r\n const log: PortableUsageLog = {\r\n id: generateId(),\r\n userId: input.userId,\r\n operationType: input.operationType,\r\n provider: input.provider,\r\n creditsUsed: input.creditsUsed,\r\n success: input.success,\r\n errorMessage: input.errorMessage,\r\n resourceId: input.resourceId,\r\n resourceType: input.resourceType,\r\n requestId: input.requestId,\r\n metadata: input.metadata ? { ...input.metadata } : input.metadata,\r\n createdAt: new Date().toISOString(),\r\n };\r\n\r\n this.store.usageLogs.push(log);\r\n return copyRecord(log);\r\n }\r\n\r\n async getUsageLogs(query: UsageLogQuery): Promise<PortableUsageLog[]> {\r\n let results = [...this.store.usageLogs];\r\n\r\n // Apply filters\r\n if (query.userId) {\r\n results = results.filter((log) => log.userId === query.userId);\r\n }\r\n if (query.operationType) {\r\n results = results.filter((log) => log.operationType === query.operationType);\r\n }\r\n if (query.success !== undefined) {\r\n results = results.filter((log) => log.success === query.success);\r\n }\r\n if (query.startDate) {\r\n const startTime = query.startDate.getTime();\r\n results = results.filter(\r\n (log) => toDate(log.createdAt).getTime() >= startTime\r\n );\r\n }\r\n if (query.endDate) {\r\n const endTime = query.endDate.getTime();\r\n results = results.filter(\r\n (log) => toDate(log.createdAt).getTime() <= endTime\r\n );\r\n }\r\n\r\n // Sort by createdAt descending\r\n results.sort((a, b) => {\r\n return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();\r\n });\r\n\r\n // Apply pagination\r\n const offset = query.offset ?? 0;\r\n const limit = query.limit ?? 50;\r\n return copyRecords(results.slice(offset, offset + limit));\r\n }\r\n\r\n async getUsageLogsCount(\r\n query: Omit<UsageLogQuery, \"limit\" | \"offset\">\r\n ): Promise<number> {\r\n const results = await this.getUsageLogs({ ...query, limit: Infinity, offset: 0 });\r\n return results.length;\r\n }\r\n\r\n // ==================== Journal Entries ====================\r\n\r\n async createJournalEntry(input: CreateJournalEntryInput): Promise<PortableJournalEntry> {\r\n return this.recordJournalEntry(input);\r\n }\r\n\r\n /**\r\n * The synchronous body of `createJournalEntry`, so `atomicMonthlyReset` can\r\n * write its journal line inside the same run-to-completion step as the\r\n * balance write — an `await` between the two is exactly the gap that loses\r\n * the line when the second half fails.\r\n */\r\n private recordJournalEntry(input: CreateJournalEntryInput): PortableJournalEntry {\r\n assertRepresentableAmount(input.amount, \"journal amount\", { userId: input.userId });\r\n assertRepresentableAmount(input.balanceAfter, \"journal balanceAfter\", {\r\n userId: input.userId,\r\n });\r\n if (input.balanceBefore !== undefined) {\r\n assertRepresentableAmount(input.balanceBefore, \"journal balanceBefore\", {\r\n userId: input.userId,\r\n });\r\n }\r\n assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });\r\n assertPublicJournalKey(input.idempotencyKey, input.userId);\r\n\r\n // The SQL adapter has a partial unique index on `(user_id, idempotency_key)`.\r\n // Without the equivalent here, a key written through this public method was\r\n // invisible to the V2 transitions, which would then happily write a *second*\r\n // row under the same deterministic key — the ledger would record one event\r\n // twice and the two adapters would disagree about what happened.\r\n const key =\r\n input.idempotencyKey !== undefined\r\n ? scopedKey(input.userId, input.idempotencyKey)\r\n : undefined;\r\n if (key !== undefined && this.store.journalKeys.has(key)) {\r\n throw new CreditError(\r\n `Journal idempotency key ${input.idempotencyKey} is already used for user ` +\r\n `${input.userId}. The unique constraint refuses a second entry.`,\r\n CreditErrorCode.DATABASE_ERROR,\r\n { userId: input.userId, idempotencyKey: input.idempotencyKey }\r\n );\r\n }\r\n\r\n const entry: PortableJournalEntry = {\r\n id: generateId(),\r\n userId: input.userId,\r\n entryType: input.entryType,\r\n amount: input.amount,\r\n ...(input.balanceBefore !== undefined ? { balanceBefore: input.balanceBefore } : {}),\r\n balanceAfter: input.balanceAfter,\r\n source: input.source,\r\n referenceId: input.referenceId,\r\n referenceType: input.referenceType,\r\n description: input.description,\r\n metadata: input.metadata ? { ...input.metadata } : input.metadata,\r\n idempotencyKey: input.idempotencyKey,\r\n createdAt: new Date().toISOString(),\r\n };\r\n\r\n if (!this.store.journalEntries.has(input.userId)) {\r\n this.store.journalEntries.set(input.userId, []);\r\n }\r\n this.store.journalEntries.get(input.userId)!.push(entry);\r\n if (key !== undefined) this.store.journalKeys.set(key, entry.id);\r\n\r\n return copyRecord(entry);\r\n }\r\n\r\n async getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]> {\r\n let results = this.store.journalEntries.get(query.userId) ?? [];\r\n\r\n // Apply filters\r\n if (query.source) {\r\n results = results.filter((entry) => entry.source === query.source);\r\n }\r\n if (query.referenceType) {\r\n results = results.filter((entry) => entry.referenceType === query.referenceType);\r\n }\r\n if (query.startDate) {\r\n const startTime = query.startDate.getTime();\r\n results = results.filter(\r\n (entry) => toDate(entry.createdAt).getTime() >= startTime\r\n );\r\n }\r\n if (query.endDate) {\r\n const endTime = query.endDate.getTime();\r\n results = results.filter(\r\n (entry) => toDate(entry.createdAt).getTime() <= endTime\r\n );\r\n }\r\n\r\n // Sort by createdAt descending\r\n results = [...results].sort((a, b) => {\r\n return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();\r\n });\r\n\r\n // Apply pagination\r\n const offset = query.offset ?? 0;\r\n const limit = query.limit ?? 50;\r\n return copyRecords(results.slice(offset, offset + limit));\r\n }\r\n\r\n async getJournalEntriesCount(\r\n query: Omit<JournalEntryQuery, \"limit\" | \"offset\">\r\n ): Promise<number> {\r\n const results = await this.getJournalEntries({\r\n ...query,\r\n limit: Infinity,\r\n offset: 0,\r\n });\r\n return results.length;\r\n }\r\n\r\n // ==================== Cleanup Operations ====================\r\n\r\n /**\r\n * Expire every hold whose deadline has passed.\r\n *\r\n * Each candidate goes through the guarded {@link expireReservationV2}, which\r\n * re-checks the status and the deadline under the user's lock. The previous\r\n * implementation released the hold and then overwrote the status to\r\n * `expired`, so a commit landing in between got its credits handed back and\r\n * was then relabelled.\r\n */\r\n async findAndExpireReservations(\r\n _batchSize = 100,\r\n _maxIterations = 100\r\n ): Promise<{\r\n expiredCount: number;\r\n creditsReleased: number;\r\n errors: string[];\r\n }> {\r\n const asOf = new Date();\r\n let expiredCount = 0;\r\n let creditsReleased = 0;\r\n const errors: string[] = [];\r\n\r\n // Snapshot the candidates first: expiring mutates the maps being iterated.\r\n const candidates: Array<{ userId: string; reservationId: string }> = [];\r\n for (const [userId, userReservations] of this.store.reservations) {\r\n for (const [reservationId, reservation] of userReservations) {\r\n if (\r\n reservation.status === \"reserved\" &&\r\n toDate(reservation.expiresAt).getTime() < asOf.getTime()\r\n ) {\r\n candidates.push({ userId, reservationId });\r\n }\r\n }\r\n }\r\n\r\n for (const candidate of candidates) {\r\n try {\r\n const outcome = await this.expireReservationV2(\r\n candidate.userId,\r\n candidate.reservationId,\r\n { asOf }\r\n );\r\n if (outcome.outcome === \"expired\") {\r\n expiredCount++;\r\n creditsReleased = sumAmounts(creditsReleased, outcome.amount);\r\n }\r\n } catch (error) {\r\n errors.push(\r\n `Failed to expire reservation ${candidate.reservationId}: ${error}`\r\n );\r\n }\r\n }\r\n\r\n return { expiredCount, creditsReleased, errors };\r\n }\r\n\r\n // ==================== Atomic Monthly Reset ====================\r\n\r\n /**\r\n * @param _tier - Not trusted. The caller read it earlier and the account may\r\n * have changed tier since, so the reset uses the tier stored on the account.\r\n */\r\n async atomicMonthlyReset(\r\n userId: string,\r\n _tier: SubscriptionTier,\r\n expectedResetAt: Date | string\r\n ): Promise<MonthlyResetResult> {\r\n const credits = this.store.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n // Optimistic locking: check if expectedResetAt matches current value\r\n const currentResetAt = toDate(credits.monthlyResetAt).getTime();\r\n const expected = toDate(expectedResetAt).getTime();\r\n\r\n if (currentResetAt !== expected) {\r\n // Another request already performed the reset\r\n return { wasReset: false, credits: { ...credits } };\r\n }\r\n\r\n const tier = credits.tier;\r\n // The resolver decides on credits the account may spend, not on those kept\r\n // only for holds; otherwise \"keep the balance\" would unmark them.\r\n const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);\r\n const decision = this.options.resolveMonthlyReset?.({ tier, balance: spendable });\r\n if (decision === null) {\r\n return { wasReset: false, credits: { ...credits } };\r\n }\r\n if (decision) {\r\n assertValidBalanceTarget(decision.balance, \"resolveMonthlyReset balance\", { userId, tier });\r\n if (decision.monthlyLimit !== undefined) {\r\n assertValidBalanceTarget(decision.monthlyLimit, \"resolveMonthlyReset monthlyLimit\", {\r\n userId,\r\n tier,\r\n });\r\n }\r\n } else {\r\n assertRepresentableTierAmount(getConfigMonthlyLimit(tier), \"monthlyLimit\", { userId, tier });\r\n }\r\n\r\n // Perform the reset\r\n const nextReset = getNextMonthlyReset();\r\n\r\n // The unlimited contract, from the one place that defines it. This used to\r\n // read `newBalance === Infinity ? credits.balance : newBalance` — \"leave an\r\n // unlimited balance alone\" — which never restored an unlimited account whose\r\n // balance had been driven to zero. See `monthlyResetBalance`.\r\n // Floored at what still backs the outstanding holds: a reset that cut\r\n // `balance + bonusCredits` below `reserved` would strand every live\r\n // reservation at commit time with INSUFFICIENT_CREDITS. See\r\n // `backedBalanceFloor`.\r\n const previousBalance = credits.balance;\r\n let target: number;\r\n if (decision) {\r\n target = decision.balance;\r\n } else {\r\n const configured = monthlyResetBalance(tier);\r\n target =\r\n configured.kind === \"atLeast\" ? Math.max(spendable, configured.value) : configured.value;\r\n }\r\n const planned = planBalanceTarget(target, credits.reserved, credits.bonusCredits);\r\n const newBalance = planned.balance;\r\n\r\n // The journal line is part of the reset, not a follow-up. It is validated\r\n // before anything mutates and written synchronously after, so no failure or\r\n // interleaving can consume the CAS and lose the line — which is exactly\r\n // what happened when the service wrote it as a separate call: the retry saw\r\n // `wasReset: false` and the reset entry was gone for good.\r\n const change = sumAmounts(newBalance, -previousBalance);\r\n const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);\r\n assertRepresentableAmount(Math.abs(change), \"journal amount\", { userId });\r\n assertRepresentableAmount(balanceAfter, \"journal balanceAfter\", { userId });\r\n\r\n credits.balance = newBalance;\r\n writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);\r\n if (decision?.monthlyLimit !== undefined) credits.monthlyLimit = decision.monthlyLimit;\r\n credits.monthlyUsed = 0;\r\n credits.monthlyResetAt = nextReset.toISOString();\r\n credits.updatedAt = new Date().toISOString();\r\n this.store.users.set(userId, credits);\r\n\r\n if (change !== 0) {\r\n this.recordJournalEntry({\r\n userId,\r\n entryType: change > 0 ? \"credit\" : \"debit\",\r\n amount: Math.abs(change),\r\n balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),\r\n balanceAfter,\r\n source: \"monthly_reset\",\r\n referenceId: `reset-${Date.now()}`,\r\n referenceType: \"reset\",\r\n description: `Monthly credit reset for ${tier} tier.`,\r\n metadata: { tier, previousBalance, newBalance },\r\n });\r\n }\r\n\r\n return { wasReset: true, credits: { ...credits }, journaled: true };\r\n }\r\n\r\n // ==================== Subscription Expiry ====================\r\n\r\n async checkAndHandleSubscriptionExpiry(\r\n userId: string,\r\n gracePeriodDays = 3\r\n ): Promise<SubscriptionExpiryResult> {\r\n const credits = this.store.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n // Free tier doesn't expire\r\n if (isFreeTier(credits.tier) || !credits.subscriptionExpiresAt) {\r\n return {\r\n wasDowngraded: false,\r\n inGracePeriod: false,\r\n graceDaysRemaining: 0,\r\n credits: { ...credits },\r\n };\r\n }\r\n\r\n const now = new Date();\r\n const expiresAt = toDate(credits.subscriptionExpiresAt);\r\n const daysSinceExpiry =\r\n (now.getTime() - expiresAt.getTime()) / (1000 * 60 * 60 * 24);\r\n\r\n if (daysSinceExpiry <= 0) {\r\n // Not expired yet\r\n return {\r\n wasDowngraded: false,\r\n inGracePeriod: false,\r\n graceDaysRemaining: 0,\r\n credits: { ...credits },\r\n };\r\n }\r\n\r\n if (daysSinceExpiry <= gracePeriodDays) {\r\n // In grace period\r\n return {\r\n wasDowngraded: false,\r\n inGracePeriod: true,\r\n graceDaysRemaining: Math.ceil(gracePeriodDays - daysSinceExpiry),\r\n credits: { ...credits },\r\n };\r\n }\r\n\r\n // Grace period expired - downgrade to the configured default tier\r\n const defaultTier = getDefaultTier();\r\n let limit: number;\r\n if (this.options.getDowngradeBalanceCap) {\r\n limit = this.options.getDowngradeBalanceCap();\r\n assertValidBalanceTarget(limit, \"getDowngradeBalanceCap\", { userId, tier: defaultTier });\r\n } else {\r\n // `getConfigMonthlyLimit`, not the raw `monthlyCredits`: in tier *config*\r\n // zero means unlimited, so reading the field directly would downgrade onto\r\n // an unlimited default tier with a limit and a balance of zero.\r\n const configured = getConfigMonthlyLimit(defaultTier);\r\n assertRepresentableTierAmount(configured, \"monthlyLimit\", {\r\n userId,\r\n tier: defaultTier,\r\n });\r\n limit = storedMonthlyLimit(configured);\r\n }\r\n\r\n // The downgrade may cut the balance to the new tier's limit, but never\r\n // below what still backs the outstanding holds — see `backedBalanceFloor`.\r\n const previousTier = credits.tier;\r\n const previousBalance = credits.balance;\r\n const planned = planBalanceCap(\r\n credits.balance,\r\n limit,\r\n credits.reserved,\r\n credits.bonusCredits,\r\n credits.expiredHoldBacking\r\n );\r\n const newBalance = planned.balance;\r\n\r\n // The journal line is part of the downgrade, not a follow-up: validated\r\n // before anything mutates, written synchronously after. Written separately\r\n // by the service, a failure landed after the tier write, and no retry ever\r\n // fired again — the row was no longer eligible, the audit line gone.\r\n const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);\r\n assertRepresentableAmount(balanceAfter, \"journal balanceAfter\", { userId });\r\n\r\n credits.tier = defaultTier;\r\n credits.monthlyLimit = limit;\r\n credits.balance = newBalance;\r\n writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);\r\n credits.subscriptionExpiresAt = null;\r\n credits.updatedAt = new Date().toISOString();\r\n this.store.users.set(userId, credits);\r\n\r\n this.recordJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount: 0, // No credits deducted, just tier change\r\n balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),\r\n balanceAfter,\r\n source: \"subscription_downgrade\",\r\n referenceId: `downgrade-${Date.now()}`,\r\n referenceType: \"subscription\",\r\n description: `Subscription expired. Downgraded from ${previousTier} to ${defaultTier} tier.`,\r\n metadata: { previousTier, previousBalance, newBalance },\r\n });\r\n\r\n return {\r\n wasDowngraded: true,\r\n inGracePeriod: false,\r\n graceDaysRemaining: 0,\r\n credits: { ...credits },\r\n journaled: true,\r\n };\r\n }\r\n\r\n // ==================== Testing Utilities ====================\r\n\r\n /**\r\n * Clear all data (useful for testing)\r\n */\r\n clear(): void {\r\n this.store.clear();\r\n }\r\n\r\n /**\r\n * Install a yield point inside every V2 critical section (testing only).\r\n *\r\n * Unset, the critical sections run start-to-finish synchronously, so\r\n * concurrent callers never interleave and a concurrency test would pass even\r\n * with the locking removed. Setting this to a real yield (a macrotask or a\r\n * barrier) makes callers genuinely overlap, so the tests exercise the lock\r\n * instead of the event loop. Never call this in production code.\r\n */\r\n setSchedulingHook(hook: (() => Promise<void>) | undefined): void {\r\n this.store.schedulingHook = hook;\r\n }\r\n\r\n /**\r\n * Get all users (useful for testing/debugging)\r\n */\r\n getAllUsers(): PortableUserCredits[] {\r\n return copyRecords(Array.from(this.store.users.values()));\r\n }\r\n\r\n /**\r\n * Get all reservations for a user (useful for testing)\r\n */\r\n getAllReservations(userId: string): PortableReservation[] {\r\n const userReservations = this.store.reservations.get(userId);\r\n if (!userReservations) return [];\r\n return copyRecords(Array.from(userReservations.values()));\r\n }\r\n}\r\n\r\n/**\r\n * Create a new in-memory repository instance\r\n * Each call creates a fresh, isolated instance\r\n */\r\nexport function createInMemoryCreditRepository(\r\n options: CreditPolicyOptions = {}\r\n): InMemoryCreditRepository {\r\n return new InMemoryCreditRepository(options);\r\n}\r\n","import type { ICreditsAuthProvider, CreditsUser } from \"./types.js\";\r\n\r\n/**\r\n * Timing-safe string comparison to prevent timing attacks\r\n */\r\nfunction timingSafeEqual(a: string, b: string): boolean {\r\n if (!a || !b) {\r\n return false;\r\n }\r\n\r\n // Pad strings to same length to prevent length-based timing attacks\r\n const maxLength = Math.max(a.length, b.length);\r\n const paddedA = a.padEnd(maxLength);\r\n const paddedB = b.padEnd(maxLength);\r\n\r\n let result = 0;\r\n for (let i = 0; i < maxLength; i++) {\r\n result |= paddedA.charCodeAt(i) ^ paddedB.charCodeAt(i);\r\n }\r\n\r\n // Also check that lengths were originally equal\r\n return result === 0 && a.length === b.length;\r\n}\r\n\r\n/**\r\n * Verify a Bearer token from an Authorization header\r\n *\r\n * @param header - The Authorization header value (e.g., \"Bearer secret123\")\r\n * @param secret - The expected secret value\r\n * @returns True if token is valid\r\n */\r\nexport function verifyBearerToken(header: string | null, secret: string): boolean {\r\n if (!header || !secret) {\r\n return false;\r\n }\r\n\r\n // Extract token from \"Bearer {token}\" format\r\n const token = header.replace(/^Bearer\\s+/i, \"\");\r\n\r\n // Use timing-safe comparison\r\n return timingSafeEqual(token, secret);\r\n}\r\n\r\n/**\r\n * API Key auth provider for admin routes\r\n *\r\n * Uses Bearer token authentication for external API access\r\n * The API key is stored in CREDITS_ADMIN_API_KEY environment variable\r\n */\r\nexport class ApiKeyCreditsProvider implements ICreditsAuthProvider {\r\n private readonly apiKey: string | undefined;\r\n private isValidated = false;\r\n\r\n constructor(authHeader?: string | null, apiKey?: string) {\r\n this.apiKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;\r\n\r\n // Validate the provided auth header using timing-safe comparison\r\n if (authHeader && this.apiKey) {\r\n this.isValidated = verifyBearerToken(authHeader, this.apiKey);\r\n }\r\n }\r\n\r\n async getCurrentUser(): Promise<CreditsUser | null> {\r\n if (!this.isValidated) {\r\n return null;\r\n }\r\n\r\n // API key auth returns a special \"admin\" user\r\n return {\r\n id: \"api-admin\",\r\n email: \"api@admin.internal\",\r\n name: \"API Admin\",\r\n };\r\n }\r\n\r\n async verifyAdminAccess(): Promise<boolean> {\r\n return this.isValidated;\r\n }\r\n}\r\n\r\n/**\r\n * Verify an API key from a request header\r\n * @param authHeader - Authorization header value\r\n * @param apiKey - Optional API key to use (defaults to CREDITS_ADMIN_API_KEY env var)\r\n * @returns True if API key is valid\r\n */\r\nexport function verifyAdminApiKey(authHeader: string | null, apiKey?: string): boolean {\r\n if (!authHeader) {\r\n return false;\r\n }\r\n\r\n const expectedKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;\r\n if (!expectedKey) {\r\n console.warn(\"CREDITS_ADMIN_API_KEY not configured\");\r\n return false;\r\n }\r\n\r\n // Use timing-safe comparison to prevent timing attacks\r\n return verifyBearerToken(authHeader, expectedKey);\r\n}\r\n\r\n/**\r\n * Create an API key auth provider from a request\r\n * @param request - Request object with headers\r\n * @param apiKey - Optional API key to use\r\n * @returns API key auth provider\r\n */\r\nexport function createApiKeyProvider(request: Request, apiKey?: string): ApiKeyCreditsProvider {\r\n const authHeader = request.headers.get(\"Authorization\");\r\n return new ApiKeyCreditsProvider(authHeader, apiKey);\r\n}\r\n","import type {\r\n PortableUserCredits,\r\n PortableReservation,\r\n CreditCheckResult,\r\n CreditOperationType,\r\n SubscriptionTier,\r\n PortableUsageLog,\r\n PortableJournalEntry,\r\n UsageHistoryEntry,\r\n UsageHistoryResponse,\r\n DeductCreditsResult,\r\n} from \"../core/types.js\";\r\nimport { toDate } from \"../core/types.js\";\r\nimport type {\r\n AddCreditsOutcome,\r\n CommitOutcome,\r\n ReleaseOutcome,\r\n ReserveOutcome,\r\n} from \"../core/outcomes.js\";\r\nimport {\r\n CreditErrorCode,\r\n createIdempotencyConflictError,\r\n createInsufficientCreditsError,\r\n createReservationAlreadyProcessedError,\r\n createReservationNotFoundError,\r\n isCreditError,\r\n} from \"../core/errors.js\";\r\nimport {\r\n assertValidCreditAmount,\r\n storedMonthlyLimit,\r\n sumAmounts,\r\n} from \"../core/amount.js\";\r\nimport type { ReservationTransitionOptions } from \"../repository/v2-types.js\";\r\nimport {\r\n addCreditsThroughRepository,\r\n commitThroughRepository,\r\n releaseThroughRepository,\r\n reserveThroughRepository,\r\n} from \"../repository/flow.js\";\r\nimport type {\r\n ICreditRepository,\r\n CreateUsageLogInput,\r\n JournalEntryQuery,\r\n AddCreditsAtomicOptions,\r\n} from \"../repository/types.js\";\r\nimport { toClientUserCredits } from \"../repository/types.js\";\r\nimport {\r\n getConfig,\r\n getConfigMonthlyLimit,\r\n getOperationLabel,\r\n isFreeTier,\r\n isUnlimitedTier,\r\n getDefaultTier,\r\n getUnlimitedSentinelBalance,\r\n} from \"../config/index.js\";\r\n\r\n/**\r\n * Check if a date is past the monthly reset date\r\n */\r\nfunction isPastMonthlyReset(resetAt: unknown): boolean {\r\n if (!resetAt) return false;\r\n const resetDate = toDate(resetAt);\r\n return new Date() >= resetDate;\r\n}\r\n\r\n/**\r\n * Notification callback type for low balance notifications\r\n */\r\nexport type LowBalanceNotificationCallback = (userId: string, balance: number) => Promise<void>;\r\n\r\n/**\r\n * Notification callback type for subscription expired notifications\r\n */\r\nexport type SubscriptionExpiredNotificationCallback = (userId: string, wasDowngraded: boolean) => Promise<void>;\r\n\r\n/**\r\n * Options for reserving credits.\r\n */\r\nexport interface ReserveCreditsOptions {\r\n /**\r\n * Time-to-live for the reservation, in milliseconds, before the cleanup\r\n * sweep may expire it. Defaults to `config.reservationExpiryMs` (suitable\r\n * for synchronous, in-request operations). Long-running async jobs\r\n * (e.g. background media generation) should pass a TTL matching their\r\n * own lifecycle so the reservation is not expired while the job is still\r\n * legitimately in flight.\r\n */\r\n ttlMs?: number;\r\n /**\r\n * Caller-supplied idempotency key, unique per user.\r\n *\r\n * Retrying a reserve with the same key and the same amount/operation returns\r\n * the original reservation instead of placing a second hold — the fix for a\r\n * webhook or job runner delivering the same request twice. Reusing a key\r\n * with a *different* amount or operation is a\r\n * `CreditErrorCode.IDEMPOTENCY_CONFLICT`.\r\n *\r\n * Requires a repository implementing the V2 boundary (see\r\n * `supportsCreditsV2`). A legacy adapter has no unique index to enforce the\r\n * key, so it is ignored there rather than silently promised.\r\n */\r\n idempotencyKey?: string;\r\n}\r\n\r\n/**\r\n * Options for a one-shot atomic credit deduction via `deductCredits`.\r\n */\r\nexport interface DeductCreditsOptions {\r\n /**\r\n * Operation type for cost tracking / journal + usage-log labeling.\r\n * Falls back to a generic \"adjustment\" description when omitted.\r\n */\r\n operationType?: CreditOperationType;\r\n /** Resource ID to attach to the usage log (e.g. the generated asset's id) */\r\n resourceId?: string;\r\n /** Resource type to attach to the usage log */\r\n resourceType?: string;\r\n /** Request ID to attach to the usage log, for tracing */\r\n requestId?: string;\r\n /** Additional metadata recorded on the journal entry */\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\n/**\r\n * Credits service with dependency injection for repository\r\n *\r\n * Provides business logic for credit operations, delegating\r\n * database operations to the injected repository.\r\n */\r\nexport class CreditsService {\r\n private lowBalanceCallback?: LowBalanceNotificationCallback;\r\n private subscriptionExpiredCallback?: SubscriptionExpiredNotificationCallback;\r\n\r\n constructor(private readonly repository: ICreditRepository) {}\r\n\r\n /**\r\n * Set callback for low balance notifications\r\n */\r\n setLowBalanceCallback(callback: LowBalanceNotificationCallback): void {\r\n this.lowBalanceCallback = callback;\r\n }\r\n\r\n /**\r\n * Set callback for subscription expired notifications\r\n */\r\n setSubscriptionExpiredCallback(callback: SubscriptionExpiredNotificationCallback): void {\r\n this.subscriptionExpiredCallback = callback;\r\n }\r\n\r\n /**\r\n * Get user credits, performing monthly reset and subscription expiry checks if needed\r\n *\r\n * This method uses atomic operations to prevent race conditions:\r\n * 1. Checks subscription expiry with grace period\r\n * 2. Atomically performs monthly reset if needed (with optimistic locking)\r\n *\r\n * @param userId - User ID\r\n * @returns User credits or null if not found\r\n */\r\n async getUserCredits(userId: string): Promise<PortableUserCredits | null> {\r\n let data = await this.repository.getUserCredits(userId);\r\n\r\n if (!data) {\r\n return null;\r\n }\r\n\r\n // Step 1: Check subscription expiry (for non-free tiers)\r\n if (!isFreeTier(data.tier) && data.subscriptionExpiresAt) {\r\n const expiryResult = await this.repository.checkAndHandleSubscriptionExpiry(\r\n userId,\r\n getConfig().subscriptionGracePeriodDays\r\n );\r\n\r\n if (expiryResult.wasDowngraded && !expiryResult.journaled) {\r\n // Legacy path for repositories that do not journal the downgrade\r\n // themselves. This write sits outside the downgrade's atomic step: if\r\n // it fails after the tier write committed, the account is downgraded\r\n // with no audit line and no retry ever fires (the row is no longer\r\n // eligible). Repositories that can journal atomically return\r\n // `journaled: true` and this block is skipped.\r\n await this.repository.createJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount: 0, // No credits deducted, just tier change\r\n balanceBefore: sumAmounts(data.balance, data.bonusCredits),\r\n // The ledger balance, matching every other journal writer: the V2\r\n // transitions and add/deduct all record `balance + bonusCredits`, so\r\n // recording `balance` alone made the audit trail disagree with itself\r\n // for any user holding bonus credits.\r\n balanceAfter: sumAmounts(\r\n expiryResult.credits.balance,\r\n expiryResult.credits.bonusCredits\r\n ),\r\n source: \"subscription_downgrade\",\r\n referenceId: `downgrade-${Date.now()}`,\r\n referenceType: \"subscription\",\r\n // The tier actually landed on, not a hard-coded \"free\": the downgrade\r\n // target is configurable, so naming `free` here could describe a\r\n // transition that never happened.\r\n description: `Subscription expired. Downgraded from ${data.tier} to ${expiryResult.credits.tier} tier.`,\r\n metadata: {\r\n previousTier: data.tier,\r\n previousBalance: data.balance,\r\n newBalance: expiryResult.credits.balance,\r\n },\r\n });\r\n }\r\n\r\n // Trigger subscription expired notification (non-blocking) — on every\r\n // downgrade, whichever side wrote the journal.\r\n if (expiryResult.wasDowngraded && this.subscriptionExpiredCallback) {\r\n this.subscriptionExpiredCallback(userId, true).catch((error) => {\r\n console.error(\"[Credits] Failed to send subscription expired notification:\", error);\r\n });\r\n }\r\n\r\n // Use the potentially updated credits\r\n data = expiryResult.credits;\r\n }\r\n\r\n // Step 2: Check if monthly reset is needed (use atomic operation)\r\n if (isPastMonthlyReset(data.monthlyResetAt)) {\r\n // Convert monthlyResetAt to a compatible type (Date or string)\r\n const expectedResetAt = toDate(data.monthlyResetAt);\r\n const resetResult = await this.repository.atomicMonthlyReset(\r\n userId,\r\n data.tier,\r\n expectedResetAt\r\n );\r\n\r\n if (resetResult.wasReset && !resetResult.journaled) {\r\n // Legacy path for repositories that do not journal the reset\r\n // themselves. This write sits outside the reset's atomic step: if it\r\n // fails after the CAS was consumed, the journal line is lost for good.\r\n // Repositories that can journal atomically return `journaled: true`\r\n // and this block is skipped.\r\n const balanceChange = sumAmounts(resetResult.credits.balance, -data.balance);\r\n if (balanceChange !== 0) {\r\n await this.repository.createJournalEntry({\r\n userId,\r\n entryType: balanceChange > 0 ? \"credit\" : \"debit\",\r\n amount: Math.abs(balanceChange),\r\n balanceBefore: sumAmounts(data.balance, data.bonusCredits),\r\n balanceAfter: sumAmounts(\r\n resetResult.credits.balance,\r\n resetResult.credits.bonusCredits\r\n ),\r\n source: \"monthly_reset\",\r\n referenceId: `reset-${Date.now()}`,\r\n referenceType: \"reset\",\r\n // The tier the reset ran on, which a concurrent downgrade may have\r\n // changed since `data` was read.\r\n description: `Monthly credit reset for ${resetResult.credits.tier} tier.`,\r\n metadata: {\r\n tier: resetResult.credits.tier,\r\n previousBalance: data.balance,\r\n newBalance: resetResult.credits.balance,\r\n },\r\n });\r\n }\r\n }\r\n\r\n // Use the potentially updated credits\r\n data = resetResult.credits;\r\n }\r\n\r\n return toClientUserCredits(data);\r\n }\r\n\r\n /**\r\n * Initialize credits for a new user with free tier\r\n * @param userId - User ID\r\n * @returns Initialized user credits\r\n */\r\n async initializeUserCredits(userId: string): Promise<PortableUserCredits> {\r\n const credits = await this.repository.initializeUserCredits(\r\n userId,\r\n getDefaultTier(),\r\n getConfig().defaultFreeCredits\r\n );\r\n return toClientUserCredits(credits);\r\n }\r\n\r\n /**\r\n * Get or create user credits\r\n * Initializes with free tier if not exists\r\n * @param userId - User ID\r\n * @returns User credits\r\n */\r\n async getOrCreateUserCredits(userId: string): Promise<PortableUserCredits> {\r\n const existing = await this.getUserCredits(userId);\r\n if (existing) {\r\n return existing;\r\n }\r\n return this.initializeUserCredits(userId);\r\n }\r\n\r\n /**\r\n * Check if user has sufficient credits for an operation\r\n * @param userId - User ID\r\n * @param requiredCredits - Credits required\r\n * @returns Credit check result\r\n */\r\n async checkCredits(userId: string, requiredCredits: number): Promise<CreditCheckResult> {\r\n const credits = await this.getOrCreateUserCredits(userId);\r\n\r\n // Available = balance + bonusCredits - reserved\r\n const totalBalance = sumAmounts(credits.balance, credits.bonusCredits);\r\n const available = sumAmounts(totalBalance, -credits.reserved);\r\n const hasCredits = available >= requiredCredits;\r\n\r\n return {\r\n hasCredits,\r\n balance: totalBalance,\r\n required: requiredCredits,\r\n shortfall: hasCredits ? 0 : sumAmounts(requiredCredits, -available),\r\n };\r\n }\r\n\r\n /**\r\n * Reserve credits for an operation (phase 1 of two-phase commit)\r\n * Creates a reservation and locks the credits\r\n * @param userId - User ID\r\n * @param amount - Credits to reserve\r\n * @param operationType - Operation type for tracking\r\n * @param options - Optional settings (e.g. a custom `ttlMs` for long-running async jobs)\r\n * @returns Reservation object\r\n * @throws Error if insufficient credits\r\n */\r\n async reserveCredits(\r\n userId: string,\r\n amount: number,\r\n operationType: CreditOperationType,\r\n options?: ReserveCreditsOptions\r\n ): Promise<PortableReservation> {\r\n const outcome = await this.reserveCreditsDetailed(userId, amount, operationType, options);\r\n if (outcome.outcome === \"created\" || outcome.outcome === \"replayed\") {\r\n return outcome.reservation;\r\n }\r\n if (outcome.outcome === \"insufficient\") {\r\n throw createInsufficientCreditsError(outcome.required, outcome.available);\r\n }\r\n throw createIdempotencyConflictError(outcome.idempotencyKey, {\r\n userId,\r\n requested: { amount, operationType },\r\n existing: {\r\n amount: outcome.existing.amount,\r\n operationType: outcome.existing.operationType,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Reserve credits and get the typed outcome instead of an exception.\r\n *\r\n * Prefer this over {@link reserveCredits} in async callers: it distinguishes\r\n * a fresh hold from an idempotent replay, and reports insufficient funds as\r\n * a value rather than as control flow.\r\n */\r\n async reserveCreditsDetailed(\r\n userId: string,\r\n amount: number,\r\n operationType: CreditOperationType,\r\n options?: ReserveCreditsOptions\r\n ): Promise<ReserveOutcome> {\r\n const ttlMs = options?.ttlMs ?? getConfig().reservationExpiryMs;\r\n const expiresAt = new Date(Date.now() + ttlMs);\r\n return reserveThroughRepository(this.repository, {\r\n userId,\r\n amount,\r\n operationType,\r\n expiresAt,\r\n idempotencyKey: options?.idempotencyKey,\r\n });\r\n }\r\n\r\n /**\r\n * Commit a reservation (phase 2 of two-phase commit - success).\r\n *\r\n * Idempotent: re-delivering a commit for an already-committed reservation is\r\n * a no-op. Committing one that was released or expired throws, because the\r\n * credits are no longer held and pretending otherwise would hide a real bug.\r\n */\r\n async commitCredits(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<void> {\r\n const outcome = await this.commitCreditsDetailed(userId, reservationId, options);\r\n if (outcome.outcome === \"not_found\") {\r\n throw createReservationNotFoundError(reservationId);\r\n }\r\n if (outcome.outcome === \"already_terminal\" && outcome.terminalStatus !== \"committed\") {\r\n throw createReservationAlreadyProcessedError(reservationId, outcome.terminalStatus);\r\n }\r\n }\r\n\r\n /**\r\n * Commit a reservation and get the typed outcome.\r\n *\r\n * Exactly one concurrent caller sees `committed`; that call moved the balance\r\n * and wrote the single journal entry. On a V2 repository the journal is\r\n * written inside the same transaction, so this method adds none of its own —\r\n * the duplicate entry the previous implementation wrote is gone.\r\n *\r\n * The low-balance notification fires only for the winner, and only after the\r\n * transaction has committed, so no callback runs inside a database\r\n * transaction.\r\n */\r\n async commitCreditsDetailed(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<CommitOutcome> {\r\n const outcome = await commitThroughRepository(\r\n this.repository,\r\n userId,\r\n reservationId,\r\n options\r\n );\r\n\r\n if (outcome.outcome === \"committed\" && this.lowBalanceCallback) {\r\n this.lowBalanceCallback(userId, outcome.balanceAfter).catch((error) => {\r\n console.error(\"[Credits] Failed to send low balance notification:\", error);\r\n });\r\n }\r\n\r\n return outcome;\r\n }\r\n\r\n /**\r\n * Release a reservation (phase 2 of two-phase commit - failure).\r\n *\r\n * Throws `RESERVATION_NOT_FOUND` for an unknown reservation, matching the\r\n * pre-V2 behaviour — a release naming a reservation that does not exist is a\r\n * caller bug, and swallowing it hides the bug while the credits stay held.\r\n *\r\n * Releasing one that is already terminal — `released`, `expired`, or\r\n * `committed` — is a no-op, which is the behaviour this method has always\r\n * had. A release that loses a race to a concurrent commit is the common\r\n * case in a retry-heavy caller, and callers written against the pre-V2\r\n * contract handle it by not handling it.\r\n *\r\n * That does mean this method cannot tell you that the credits were spent\r\n * rather than returned. Use {@link releaseCreditsDetailed}, which reports\r\n * `already_terminal` with `terminalStatus: 'committed'`, when you need to\r\n * branch on it.\r\n */\r\n async releaseCredits(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<void> {\r\n const outcome = await this.releaseCreditsDetailed(userId, reservationId, options);\r\n if (outcome.outcome === \"not_found\") {\r\n throw createReservationNotFoundError(reservationId);\r\n }\r\n }\r\n\r\n /** Release a reservation and get the typed outcome. */\r\n async releaseCreditsDetailed(\r\n userId: string,\r\n reservationId: string,\r\n options?: ReservationTransitionOptions\r\n ): Promise<ReleaseOutcome> {\r\n return releaseThroughRepository(this.repository, userId, reservationId, options);\r\n }\r\n\r\n /**\r\n * Log usage for audit trail\r\n * @param log - Usage log data\r\n */\r\n async logUsage(log: Omit<PortableUsageLog, \"id\" | \"createdAt\">): Promise<void> {\r\n await this.repository.logUsage(log as CreateUsageLogInput);\r\n }\r\n\r\n /**\r\n * Add credits to user account (for purchases, bonuses, etc.)\r\n * @param userId - User ID\r\n * @param amount - Credits to add\r\n * @param description - Transaction description\r\n * @param paymentRef - Optional payment reference\r\n * @param options - Optional journal source / reference type / extra metadata\r\n * for the credit journal entry (e.g. revenue-attribution fields).\r\n * @returns Whether this delivery credited the account (`created`), repeated an\r\n * earlier one (`replayed`), or reused the reference for a different credit\r\n * event (`conflict`, which credited nothing). On a repository without\r\n * `addCreditsV2` the result is always `created` and carries no\r\n * deduplication guarantee — see {@link addCreditsThroughRepository}.\r\n */\r\n async addCredits(\r\n userId: string,\r\n amount: number,\r\n description: string,\r\n paymentRef?: string,\r\n options?: AddCreditsAtomicOptions\r\n ): Promise<AddCreditsOutcome> {\r\n return addCreditsThroughRepository(this.repository, {\r\n userId,\r\n amount,\r\n description,\r\n paymentRef,\r\n options,\r\n });\r\n }\r\n\r\n /**\r\n * One-shot atomic \"deduct-if-sufficient\" credit charge.\r\n *\r\n * Unlike the reserve/commit/release two-phase flow, this performs a single\r\n * atomic check-and-deduct against the repository — suited for synchronous,\r\n * in-request operations that don't need a hold across an async job. Exactly\r\n * one call is made to `repository.deductCreditsAtomic`, which enforces\r\n * atomicity so concurrent callers can never both succeed against the same\r\n * limited balance.\r\n *\r\n * On success, creates a journal entry (same audit trail as `commitCredits`)\r\n * and, if `options.operationType` is provided, a usage log entry via the\r\n * same `logUsage` path.\r\n *\r\n * @param userId - User ID\r\n * @param amount - Credits to deduct (must be > 0)\r\n * @param options - Optional usage-log / journal metadata\r\n * @returns Typed result: `{ success: true, newBalance }` or\r\n * `{ success: false, reason: 'insufficient', available, required, shortfall }`\r\n * @throws Error if amount is not a positive number\r\n */\r\n async deductCredits(\r\n userId: string,\r\n amount: number,\r\n options?: DeductCreditsOptions\r\n ): Promise<DeductCreditsResult> {\r\n // `amount > 0` accepted 1.005, Infinity and values past what a\r\n // `numeric(12, 2)` column can hold; each of those reached the repository's\r\n // raw arithmetic. Validate here, *before* the try, so a rejected amount can\r\n // never be mistaken for a shortfall by the catch below.\r\n assertValidCreditAmount(amount, { userId, operation: \"deductCredits\" });\r\n\r\n const operationType = options?.operationType;\r\n const journal = {\r\n referenceId: options?.requestId ?? `deduct-${Date.now()}`,\r\n description: operationType\r\n ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}`\r\n : `Deducted ${amount} credits`,\r\n metadata: {\r\n ...options?.metadata,\r\n ...(operationType && { operationType }),\r\n },\r\n };\r\n\r\n let deduction: { previousBalance: number; newBalance: number; journalEntryId?: string };\r\n try {\r\n deduction = await this.repository.deductCreditsAtomic(userId, amount, journal);\r\n } catch (error) {\r\n // A rejected amount is a caller bug, not a balance problem, and must not\r\n // be reshaped into `{ success: false, reason: 'insufficient' }`.\r\n if (isCreditError(error) && error.code === CreditErrorCode.INVALID_AMOUNT) throw error;\r\n\r\n // The repository layer throws on insufficient funds (and on missing\r\n // user docs, which we surface as unavailable rather than a shortfall).\r\n const credits = await this.repository.getUserCredits(userId);\r\n const available = credits\r\n ? sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved)\r\n : 0;\r\n\r\n if (available >= amount) {\r\n // Not an insufficient-credits condition (e.g. missing user record) —\r\n // this is unexpected, so let the original error propagate.\r\n throw error;\r\n }\r\n\r\n return {\r\n success: false,\r\n reason: \"insufficient\",\r\n available,\r\n required: amount,\r\n shortfall: sumAmounts(amount, -available),\r\n };\r\n }\r\n\r\n // A repository that journals in the deduct's own step returns the entry's id. For\r\n // one that does not, this write sits outside its atomicity, as the reset's does.\r\n if (deduction.journalEntryId === undefined) {\r\n await this.repository.createJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount,\r\n balanceBefore: deduction.previousBalance,\r\n balanceAfter: deduction.newBalance,\r\n source: \"operation_commit\",\r\n referenceType: \"adjustment\",\r\n ...journal,\r\n });\r\n }\r\n\r\n if (operationType) {\r\n await this.logUsage({\r\n userId,\r\n operationType,\r\n provider: \"gemini\",\r\n creditsUsed: amount,\r\n success: true,\r\n resourceId: options?.resourceId,\r\n resourceType: options?.resourceType,\r\n requestId: options?.requestId,\r\n metadata: options?.metadata,\r\n });\r\n }\r\n\r\n // Trigger low balance notification (non-blocking) — same hook commitCredits uses.\r\n if (this.lowBalanceCallback) {\r\n this.lowBalanceCallback(userId, deduction.newBalance).catch((error) => {\r\n console.error(\"[Credits] Failed to send low balance notification:\", error);\r\n });\r\n }\r\n\r\n return { success: true, newBalance: deduction.newBalance };\r\n }\r\n\r\n /**\r\n * Update user subscription tier\r\n * @param userId - User ID\r\n * @param tier - New subscription tier\r\n * @param expiresAt - Subscription expiry date (optional)\r\n */\r\n async updateTier(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n expiresAt?: Date\r\n ): Promise<void> {\r\n const monthlyLimit = getConfigMonthlyLimit(tier);\r\n const unlimited = isUnlimitedTier(tier);\r\n const free = isFreeTier(tier);\r\n\r\n await this.repository.updateUserTier(userId, {\r\n tier,\r\n // Through the shared contract, like every path that resolves the tier's\r\n // limit from configuration. (The repository's `updateUserCredits` and\r\n // `updateUserTier` still store a caller-supplied `monthlyLimit` verbatim;\r\n // they are given a value, not a tier, so there is no sentinel to map.)\r\n // This used to store 0 for an unlimited tier under a \"0 means unlimited\"\r\n // convention that nothing actually implemented — no read path anywhere in\r\n // the library branches on it — so upgrading a user to unlimited gave them\r\n // an allowance of zero.\r\n monthlyLimit: storedMonthlyLimit(monthlyLimit),\r\n // Reset balance to new tier limit if upgrading\r\n balance: !free\r\n ? (unlimited ? getUnlimitedSentinelBalance() : monthlyLimit)\r\n : undefined,\r\n monthlyUsed: !free ? 0 : undefined,\r\n subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null,\r\n });\r\n }\r\n\r\n /**\r\n * Get usage logs with optional filtering\r\n * @param userId - Optional user ID filter\r\n * @param limit - Max results\r\n * @param offset - Skip results\r\n * @returns List of usage logs\r\n */\r\n async getUsageLogs(\r\n userId?: string,\r\n limit = 50,\r\n offset = 0\r\n ): Promise<PortableUsageLog[]> {\r\n return this.repository.getUsageLogs({ userId, limit, offset });\r\n }\r\n\r\n /**\r\n * Get user-friendly usage history\r\n * Combines journal entries into a user-facing format\r\n *\r\n * @param userId - User ID\r\n * @param limit - Max results per page\r\n * @param offset - Skip results for pagination\r\n * @returns Paginated usage history response\r\n */\r\n async getUsageHistory(\r\n userId: string,\r\n limit = 20,\r\n offset = 0\r\n ): Promise<UsageHistoryResponse> {\r\n // Get journal entries\r\n const [entries, total] = await Promise.all([\r\n this.repository.getJournalEntries({ userId, limit, offset }),\r\n this.repository.getJournalEntriesCount({ userId }),\r\n ]);\r\n\r\n // Convert journal entries to user-friendly format\r\n const historyEntries: UsageHistoryEntry[] = entries.map((entry) => {\r\n const type = this.mapSourceToHistoryType(entry.source);\r\n const creditsChange = entry.entryType === \"credit\" ? entry.amount : -entry.amount;\r\n\r\n return {\r\n id: entry.id,\r\n type,\r\n creditsChange,\r\n balanceAfter: entry.balanceAfter,\r\n description: entry.description,\r\n createdAt: typeof entry.createdAt === \"string\"\r\n ? entry.createdAt\r\n : toDate(entry.createdAt).toISOString(),\r\n };\r\n });\r\n\r\n return {\r\n entries: historyEntries,\r\n pagination: {\r\n total,\r\n limit,\r\n offset,\r\n hasMore: offset + entries.length < total,\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * Map journal entry source to user-friendly history type\r\n */\r\n private mapSourceToHistoryType(\r\n source: PortableJournalEntry[\"source\"]\r\n ): UsageHistoryEntry[\"type\"] {\r\n switch (source) {\r\n case \"operation_commit\":\r\n case \"reservation_expired\":\r\n return \"usage\";\r\n case \"purchase\":\r\n return \"purchase\";\r\n case \"subscription_grant\":\r\n case \"subscription_upgrade\":\r\n case \"bonus\":\r\n return \"bonus\";\r\n case \"monthly_reset\":\r\n return \"reset\";\r\n case \"refund\":\r\n case \"operation_release\":\r\n return \"refund\";\r\n case \"admin_adjustment\":\r\n case \"subscription_downgrade\":\r\n case \"expiry\":\r\n default:\r\n return \"adjustment\";\r\n }\r\n }\r\n\r\n /**\r\n * Get journal entries directly (for admin or debugging)\r\n * @param query - Journal entry query parameters\r\n * @returns List of journal entries\r\n */\r\n async getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]> {\r\n return this.repository.getJournalEntries(query);\r\n }\r\n\r\n /**\r\n * Get the underlying repository (for advanced use cases)\r\n */\r\n getRepository(): ICreditRepository {\r\n return this.repository;\r\n }\r\n}\r\n\r\n/**\r\n * Create a credits service with a repository\r\n */\r\nexport function createCreditsService(repository: ICreditRepository): CreditsService {\r\n return new CreditsService(repository);\r\n}\r\n","/**\r\n * Generic credits adapter - framework agnostic\r\n *\r\n * This adapter uses standard JavaScript APIs and works in any\r\n * JavaScript environment (Node.js, browser, etc.).\r\n */\r\n\r\nimport type { PortableReservation, WithCreditsOptions } from \"../core/types.js\";\r\nimport type { ActionResult, CreditsAdapter, CreditsAdapterConfig, CreditActionHandler } from \"./types.js\";\r\nimport {\r\n commitReservationWithJournal,\r\n releaseReservationWithJournal,\r\n reserveCreditsForOperation,\r\n} from \"../core/operations.js\";\r\n\r\n/**\r\n * Generate a unique request ID\r\n */\r\nfunction generateRequestId(): string {\r\n const timestamp = Date.now();\r\n const randomPart = Math.random().toString(36).substring(2, 14);\r\n return `req_${timestamp}_${randomPart}`;\r\n}\r\n\r\n/**\r\n * Create a generic credits adapter\r\n *\r\n * This adapter:\r\n * - Authenticates users via the provided auth provider\r\n * - Reserves credits before action execution\r\n * - Commits credits on success, releases on failure\r\n * - Runs an operation configured with a cost of 0 without touching the ledger\r\n * - Logs usage asynchronously via the deferred executor\r\n */\r\nexport function createGenericAdapter(config: CreditsAdapterConfig): CreditsAdapter {\r\n const { repository, authProvider, deferred, operationCosts } = config;\r\n\r\n function getOperationCost(operationType: string): number {\r\n const cost = operationCosts[operationType];\r\n if (cost === undefined) {\r\n throw new Error(\r\n `Unknown operation type: \"${operationType}\". ` +\r\n `Configured types: ${Object.keys(operationCosts).join(\", \")}`\r\n );\r\n }\r\n return cost;\r\n }\r\n\r\n function logUsage(params: {\r\n userId: string;\r\n operationType: string;\r\n creditsUsed: number;\r\n success: boolean;\r\n errorMessage?: string;\r\n resourceId?: string;\r\n resourceType?: string;\r\n requestId: string;\r\n }): void {\r\n deferred.defer(async () => {\r\n await repository.logUsage({\r\n userId: params.userId,\r\n operationType: params.operationType,\r\n provider: \"gemini\",\r\n creditsUsed: params.creditsUsed,\r\n success: params.success,\r\n errorMessage: params.errorMessage,\r\n resourceId: params.resourceId,\r\n resourceType: params.resourceType,\r\n requestId: params.requestId,\r\n });\r\n });\r\n }\r\n\r\n return {\r\n withCredits<TInput, TOutput>(\r\n options: WithCreditsOptions,\r\n handler: CreditActionHandler<TInput, TOutput>\r\n ): (data: TInput) => Promise<ActionResult<TOutput>> {\r\n return async (data: TInput): Promise<ActionResult<TOutput>> => {\r\n const requestId = generateRequestId();\r\n\r\n // Authenticate\r\n const user = await authProvider.getCurrentUser();\r\n if (!user?.id) {\r\n return { success: false, error: \"Authentication required\" };\r\n }\r\n\r\n // Handle preview mode - skip credit handling entirely\r\n if (isPreviewMode(data)) {\r\n const dummyReservation = createDummyReservation(user.id, options.operationType);\r\n return handler(user, data, dummyReservation);\r\n }\r\n\r\n // Calculate cost and reserve credits\r\n let reservation: PortableReservation;\r\n let isFree = false;\r\n try {\r\n const cost = options.customCost ?? getOperationCost(options.operationType);\r\n // A reservation row must hold a positive amount, so a free operation\r\n // has no hold to place, commit or release.\r\n isFree = options.customCost === undefined && cost === 0;\r\n reservation = isFree\r\n ? { ...createDummyReservation(user.id, options.operationType), id: \"free-operation\" }\r\n : await reserveCreditsForOperation(\r\n repository,\r\n user.id,\r\n cost,\r\n options.operationType\r\n );\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"Failed to reserve credits\";\r\n logUsage({\r\n userId: user.id,\r\n operationType: options.operationType,\r\n creditsUsed: 0,\r\n success: false,\r\n errorMessage: message,\r\n resourceId: options.resourceId,\r\n resourceType: options.resourceType,\r\n requestId,\r\n });\r\n return { success: false, error: message };\r\n }\r\n\r\n // Execute action\r\n try {\r\n const result = await handler(user, data, reservation);\r\n\r\n if (result.success) {\r\n if (!isFree) await commitReservationWithJournal(repository, user.id, reservation.id);\r\n logUsage({\r\n userId: user.id,\r\n operationType: options.operationType,\r\n creditsUsed: reservation.amount,\r\n success: true,\r\n resourceId: options.resourceId,\r\n resourceType: options.resourceType,\r\n requestId,\r\n });\r\n } else {\r\n if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);\r\n logUsage({\r\n userId: user.id,\r\n operationType: options.operationType,\r\n creditsUsed: 0,\r\n success: false,\r\n errorMessage: result.error,\r\n resourceId: options.resourceId,\r\n resourceType: options.resourceType,\r\n requestId,\r\n });\r\n }\r\n\r\n return result;\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : \"An unexpected error occurred\";\r\n if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);\r\n logUsage({\r\n userId: user.id,\r\n operationType: options.operationType,\r\n creditsUsed: 0,\r\n success: false,\r\n errorMessage: message,\r\n resourceId: options.resourceId,\r\n resourceType: options.resourceType,\r\n requestId,\r\n });\r\n console.error(\"Action error:\", error);\r\n return { success: false, error: message };\r\n }\r\n };\r\n },\r\n };\r\n}\r\n\r\nfunction isPreviewMode(data: unknown): boolean {\r\n return (\r\n typeof data === \"object\" &&\r\n data !== null &&\r\n \"preview\" in data &&\r\n (data as Record<string, unknown>).preview === true\r\n );\r\n}\r\n\r\nfunction createDummyReservation(userId: string, operationType: string): PortableReservation {\r\n return {\r\n id: \"preview-mode\",\r\n userId,\r\n amount: 0,\r\n operationType,\r\n status: \"released\",\r\n createdAt: new Date().toISOString(),\r\n expiresAt: new Date().toISOString(),\r\n };\r\n}\r\n","import { getConfig } from \"../config/index.js\";\r\n\r\n/**\r\n * Notification event types\r\n */\r\nexport type CreditNotificationEvent =\r\n | {\r\n type: \"low_balance\";\r\n userId: string;\r\n balance: number;\r\n threshold: number;\r\n }\r\n | {\r\n type: \"balance_depleted\";\r\n userId: string;\r\n }\r\n | {\r\n type: \"subscription_expiring\";\r\n userId: string;\r\n expiresAt: Date;\r\n daysRemaining: number;\r\n }\r\n | {\r\n type: \"subscription_expired\";\r\n userId: string;\r\n wasDowngraded: boolean;\r\n };\r\n\r\n/**\r\n * Notification handler interface\r\n * Implementations can send emails, push notifications, etc.\r\n */\r\nexport interface ICreditNotificationHandler {\r\n /**\r\n * Handle a notification event\r\n * @param event - The notification event\r\n */\r\n handleNotification(event: CreditNotificationEvent): Promise<void>;\r\n}\r\n\r\n/**\r\n * Notification cooldown tracker\r\n * Prevents spam by tracking last notification time per user per event type\r\n */\r\ninterface NotificationCooldownState {\r\n lastLowBalanceNotification?: Date;\r\n lastDepletedNotification?: Date;\r\n lastExpiringNotification?: Date;\r\n}\r\n\r\n/**\r\n * In-memory cooldown tracker\r\n * In production, this should be stored in a persistent store\r\n */\r\nconst cooldownState = new Map<string, NotificationCooldownState>();\r\n\r\n/**\r\n * Check if a notification is allowed (not in cooldown)\r\n */\r\nfunction isNotificationAllowed(\r\n userId: string,\r\n eventType: \"lowBalance\" | \"depleted\" | \"expiring\"\r\n): boolean {\r\n const state = cooldownState.get(userId);\r\n if (!state) return true;\r\n\r\n const now = Date.now();\r\n let lastNotification: Date | undefined;\r\n\r\n switch (eventType) {\r\n case \"lowBalance\":\r\n lastNotification = state.lastLowBalanceNotification;\r\n break;\r\n case \"depleted\":\r\n lastNotification = state.lastDepletedNotification;\r\n break;\r\n case \"expiring\":\r\n lastNotification = state.lastExpiringNotification;\r\n break;\r\n }\r\n\r\n if (!lastNotification) return true;\r\n return now - lastNotification.getTime() > getConfig().lowBalanceNotificationCooldownHours * 60 * 60 * 1000;\r\n}\r\n\r\n/**\r\n * Record that a notification was sent\r\n */\r\nfunction recordNotification(\r\n userId: string,\r\n eventType: \"lowBalance\" | \"depleted\" | \"expiring\"\r\n): void {\r\n const state = cooldownState.get(userId) || {};\r\n const now = new Date();\r\n\r\n switch (eventType) {\r\n case \"lowBalance\":\r\n state.lastLowBalanceNotification = now;\r\n break;\r\n case \"depleted\":\r\n state.lastDepletedNotification = now;\r\n break;\r\n case \"expiring\":\r\n state.lastExpiringNotification = now;\r\n break;\r\n }\r\n\r\n cooldownState.set(userId, state);\r\n}\r\n\r\n/**\r\n * Default notification handlers registry\r\n * Handlers can be registered at application startup\r\n */\r\nconst notificationHandlers: ICreditNotificationHandler[] = [];\r\n\r\n/**\r\n * Register a notification handler\r\n */\r\nexport function registerNotificationHandler(\r\n handler: ICreditNotificationHandler\r\n): void {\r\n notificationHandlers.push(handler);\r\n}\r\n\r\n/**\r\n * Clear all notification handlers (for testing)\r\n */\r\nexport function clearNotificationHandlers(): void {\r\n notificationHandlers.length = 0;\r\n}\r\n\r\n/**\r\n * Clear cooldown state (for testing)\r\n */\r\nexport function clearCooldownState(): void {\r\n cooldownState.clear();\r\n}\r\n\r\n/**\r\n * Dispatch a notification event to all handlers\r\n * Runs asynchronously without blocking\r\n */\r\nasync function dispatchNotification(\r\n event: CreditNotificationEvent\r\n): Promise<void> {\r\n if (notificationHandlers.length === 0) {\r\n // No handlers registered - log for debugging\r\n console.debug(`[Credits] No notification handlers registered for: ${event.type}`);\r\n return;\r\n }\r\n\r\n // Run handlers in parallel\r\n await Promise.allSettled(\r\n notificationHandlers.map((handler) =>\r\n handler.handleNotification(event).catch((error) => {\r\n console.error(`[Credits] Notification handler error:`, error);\r\n })\r\n )\r\n );\r\n}\r\n\r\n/**\r\n * Check and trigger low balance notification if needed\r\n * Call this after commit operations\r\n *\r\n * @param userId - User ID\r\n * @param newBalance - Balance after the operation\r\n * @param threshold - Custom threshold (defaults to 10)\r\n */\r\nexport async function checkAndNotifyLowBalance(\r\n userId: string,\r\n newBalance: number,\r\n threshold = getConfig().lowBalanceThreshold\r\n): Promise<void> {\r\n // Check if balance is depleted\r\n if (newBalance <= 0 && isNotificationAllowed(userId, \"depleted\")) {\r\n recordNotification(userId, \"depleted\");\r\n await dispatchNotification({\r\n type: \"balance_depleted\",\r\n userId,\r\n });\r\n return; // Don't also send low balance notification\r\n }\r\n\r\n // Check if balance is below threshold\r\n if (newBalance > 0 && newBalance <= threshold && isNotificationAllowed(userId, \"lowBalance\")) {\r\n recordNotification(userId, \"lowBalance\");\r\n await dispatchNotification({\r\n type: \"low_balance\",\r\n userId,\r\n balance: newBalance,\r\n threshold,\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * Trigger subscription expiring notification\r\n *\r\n * @param userId - User ID\r\n * @param expiresAt - When subscription expires\r\n * @param daysRemaining - Days until expiry\r\n */\r\nexport async function notifySubscriptionExpiring(\r\n userId: string,\r\n expiresAt: Date,\r\n daysRemaining: number\r\n): Promise<void> {\r\n if (!isNotificationAllowed(userId, \"expiring\")) {\r\n return;\r\n }\r\n\r\n recordNotification(userId, \"expiring\");\r\n await dispatchNotification({\r\n type: \"subscription_expiring\",\r\n userId,\r\n expiresAt,\r\n daysRemaining,\r\n });\r\n}\r\n\r\n/**\r\n * Trigger subscription expired notification\r\n *\r\n * @param userId - User ID\r\n * @param wasDowngraded - Whether the user was downgraded to free tier\r\n */\r\nexport async function notifySubscriptionExpired(\r\n userId: string,\r\n wasDowngraded: boolean\r\n): Promise<void> {\r\n await dispatchNotification({\r\n type: \"subscription_expired\",\r\n userId,\r\n wasDowngraded,\r\n });\r\n}\r\n\r\n/**\r\n * Console notification handler for development/testing\r\n * Logs notifications to console\r\n */\r\nexport class ConsoleNotificationHandler implements ICreditNotificationHandler {\r\n async handleNotification(event: CreditNotificationEvent): Promise<void> {\r\n switch (event.type) {\r\n case \"low_balance\":\r\n console.log(\r\n `[Credits Notification] Low balance alert for user ${event.userId}: ` +\r\n `${event.balance} credits remaining (threshold: ${event.threshold})`\r\n );\r\n break;\r\n case \"balance_depleted\":\r\n console.log(\r\n `[Credits Notification] Balance depleted for user ${event.userId}`\r\n );\r\n break;\r\n case \"subscription_expiring\":\r\n console.log(\r\n `[Credits Notification] Subscription expiring for user ${event.userId}: ` +\r\n `${event.daysRemaining} days remaining`\r\n );\r\n break;\r\n case \"subscription_expired\":\r\n console.log(\r\n `[Credits Notification] Subscription expired for user ${event.userId}: ` +\r\n `downgraded=${event.wasDowngraded}`\r\n );\r\n break;\r\n }\r\n }\r\n}\r\n","/**\r\n * SDK types for the credits system\r\n *\r\n * These types are used by the client SDKs for REST API consumption.\r\n */\r\n\r\n// Re-export core types for SDK consumers\r\nexport type {\r\n PortableUserCredits as UserCredits,\r\n PortableReservation as CreditReservation,\r\n CreditCheckResult,\r\n SubscriptionTier,\r\n} from \"../core/types.js\";\r\n\r\n/**\r\n * Configuration for CreditsClient\r\n */\r\nexport interface CreditsClientConfig {\r\n /** Base URL of the credits API (e.g., \"https://api.example.com\") */\r\n baseUrl: string;\r\n /** Function to get the auth token for session-based auth */\r\n getAuthToken?: () => Promise<string>;\r\n /** Optional custom fetch function for testing */\r\n fetch?: typeof fetch;\r\n}\r\n\r\n/**\r\n * Configuration for AdminCreditsClient\r\n */\r\nexport interface AdminCreditsClientConfig {\r\n /** Base URL of the credits API */\r\n baseUrl: string;\r\n /** Admin API key */\r\n apiKey: string;\r\n /** Optional custom fetch function for testing */\r\n fetch?: typeof fetch;\r\n}\r\n\r\n/**\r\n * Result of a credit reservation\r\n */\r\nexport interface ReservationResult {\r\n /** The reservation ID */\r\n reservationId: string;\r\n /** Amount of credits reserved */\r\n amount: number;\r\n /** When the reservation expires (ISO 8601) */\r\n expiresAt: string;\r\n}\r\n\r\n/**\r\n * Pagination options for API requests\r\n */\r\nexport interface PaginationOptions {\r\n /** Page number (1-indexed) */\r\n page?: number;\r\n /** Number of items per page */\r\n limit?: number;\r\n}\r\n\r\n/**\r\n * Usage history entry\r\n */\r\nexport interface UsageHistoryEntry {\r\n /** Unique ID of the usage log */\r\n id: string;\r\n /** Type of operation */\r\n operationType: string;\r\n /** AI provider used */\r\n provider: string;\r\n /** Credits consumed */\r\n creditsUsed: number;\r\n /** Whether the operation was successful */\r\n success: boolean;\r\n /** Error message if unsuccessful */\r\n errorMessage?: string;\r\n /** ID of the related resource */\r\n resourceId?: string;\r\n /** Type of the related resource */\r\n resourceType?: string;\r\n /** When the operation occurred (ISO 8601) */\r\n createdAt: string;\r\n}\r\n\r\n/**\r\n * Response from usage history endpoint\r\n */\r\nexport interface UsageHistoryResponse {\r\n /** List of usage entries */\r\n entries: UsageHistoryEntry[];\r\n /** Whether there are more results */\r\n hasMore: boolean;\r\n /** Total count (if available) */\r\n total?: number;\r\n}\r\n\r\n/**\r\n * Generic API response wrapper\r\n */\r\nexport interface ApiResponse<T> {\r\n success: boolean;\r\n data?: T;\r\n error?: {\r\n code: string;\r\n message: string;\r\n details?: unknown;\r\n };\r\n}\r\n\r\n/**\r\n * SDK error codes\r\n */\r\nexport const SDKErrorCode = {\r\n NETWORK_ERROR: \"NETWORK_ERROR\",\r\n AUTHENTICATION_ERROR: \"AUTHENTICATION_ERROR\",\r\n AUTHORIZATION_ERROR: \"AUTHORIZATION_ERROR\",\r\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\r\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\r\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\r\n VALIDATION_ERROR: \"VALIDATION_ERROR\",\r\n SERVER_ERROR: \"SERVER_ERROR\",\r\n UNKNOWN_ERROR: \"UNKNOWN_ERROR\",\r\n} as const;\r\n\r\nexport type SDKErrorCodeType = (typeof SDKErrorCode)[keyof typeof SDKErrorCode];\r\n","/**\r\n * SDK error classes\r\n *\r\n * Provides typed error handling for SDK operations.\r\n */\r\n\r\nimport { SDKErrorCode, type SDKErrorCodeType } from \"./types.js\";\r\n\r\n/**\r\n * Base error class for SDK errors\r\n */\r\nexport class CreditsSDKError extends Error {\r\n public readonly code: SDKErrorCodeType;\r\n public readonly details?: Record<string, unknown>;\r\n public readonly statusCode?: number;\r\n\r\n constructor(\r\n message: string,\r\n code: SDKErrorCodeType,\r\n options?: {\r\n details?: Record<string, unknown>;\r\n statusCode?: number;\r\n cause?: Error;\r\n }\r\n ) {\r\n super(message, { cause: options?.cause });\r\n this.name = \"CreditsSDKError\";\r\n this.code = code;\r\n this.details = options?.details;\r\n this.statusCode = options?.statusCode;\r\n }\r\n\r\n /**\r\n * Check if this is a specific error type\r\n */\r\n is(code: SDKErrorCodeType): boolean {\r\n return this.code === code;\r\n }\r\n\r\n /**\r\n * Create a JSON representation for logging\r\n */\r\n toJSON(): Record<string, unknown> {\r\n return {\r\n name: this.name,\r\n message: this.message,\r\n code: this.code,\r\n details: this.details,\r\n statusCode: this.statusCode,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when a network request fails\r\n */\r\nexport class NetworkError extends CreditsSDKError {\r\n constructor(message: string, cause?: Error) {\r\n super(message, SDKErrorCode.NETWORK_ERROR, { cause });\r\n this.name = \"NetworkError\";\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when authentication fails\r\n */\r\nexport class AuthenticationError extends CreditsSDKError {\r\n constructor(message: string = \"Authentication required\") {\r\n super(message, SDKErrorCode.AUTHENTICATION_ERROR, { statusCode: 401 });\r\n this.name = \"AuthenticationError\";\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when authorization fails\r\n */\r\nexport class AuthorizationError extends CreditsSDKError {\r\n constructor(message: string = \"Access denied\") {\r\n super(message, SDKErrorCode.AUTHORIZATION_ERROR, { statusCode: 403 });\r\n this.name = \"AuthorizationError\";\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when credits are insufficient\r\n */\r\nexport class InsufficientCreditsError extends CreditsSDKError {\r\n public readonly available: number;\r\n public readonly required: number;\r\n\r\n constructor(available: number, required: number) {\r\n super(\r\n `Insufficient credits: available ${available}, required ${required}`,\r\n SDKErrorCode.INSUFFICIENT_CREDITS,\r\n {\r\n statusCode: 402,\r\n details: { available, required },\r\n }\r\n );\r\n this.name = \"InsufficientCreditsError\";\r\n this.available = available;\r\n this.required = required;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when a reservation is not found\r\n */\r\nexport class ReservationNotFoundError extends CreditsSDKError {\r\n public readonly reservationId: string;\r\n\r\n constructor(reservationId: string) {\r\n super(\r\n `Reservation not found: ${reservationId}`,\r\n SDKErrorCode.RESERVATION_NOT_FOUND,\r\n {\r\n statusCode: 404,\r\n details: { reservationId },\r\n }\r\n );\r\n this.name = \"ReservationNotFoundError\";\r\n this.reservationId = reservationId;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown when a reservation has expired\r\n */\r\nexport class ReservationExpiredError extends CreditsSDKError {\r\n public readonly reservationId: string;\r\n\r\n constructor(reservationId: string) {\r\n super(\r\n `Reservation expired: ${reservationId}`,\r\n SDKErrorCode.RESERVATION_EXPIRED,\r\n {\r\n statusCode: 410,\r\n details: { reservationId },\r\n }\r\n );\r\n this.name = \"ReservationExpiredError\";\r\n this.reservationId = reservationId;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown for validation failures\r\n */\r\nexport class ValidationError extends CreditsSDKError {\r\n public readonly field?: string;\r\n\r\n constructor(message: string, field?: string) {\r\n super(message, SDKErrorCode.VALIDATION_ERROR, {\r\n statusCode: 400,\r\n details: field ? { field } : undefined,\r\n });\r\n this.name = \"ValidationError\";\r\n this.field = field;\r\n }\r\n}\r\n\r\n/**\r\n * Error thrown for server errors\r\n */\r\nexport class ServerError extends CreditsSDKError {\r\n constructor(message: string = \"Internal server error\") {\r\n super(message, SDKErrorCode.SERVER_ERROR, { statusCode: 500 });\r\n this.name = \"ServerError\";\r\n }\r\n}\r\n\r\n/**\r\n * Parse an API error response into the appropriate SDK error\r\n */\r\nexport function parseApiError(\r\n statusCode: number,\r\n body: { error?: { code?: string; message?: string; details?: unknown } }\r\n): CreditsSDKError {\r\n const errorCode = body.error?.code || \"\";\r\n const errorMessage = body.error?.message || \"An error occurred\";\r\n const details = body.error?.details as Record<string, unknown> | undefined;\r\n\r\n switch (statusCode) {\r\n case 401:\r\n return new AuthenticationError(errorMessage);\r\n case 403:\r\n return new AuthorizationError(errorMessage);\r\n case 402:\r\n // Parse insufficient credits details\r\n if (details?.available !== undefined && details?.required !== undefined) {\r\n return new InsufficientCreditsError(\r\n details.available as number,\r\n details.required as number\r\n );\r\n }\r\n return new CreditsSDKError(errorMessage, SDKErrorCode.INSUFFICIENT_CREDITS, {\r\n statusCode,\r\n details,\r\n });\r\n case 404:\r\n if (errorCode === \"RESERVATION_NOT_FOUND\" && details?.reservationId) {\r\n return new ReservationNotFoundError(details.reservationId as string);\r\n }\r\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_NOT_FOUND, {\r\n statusCode,\r\n details,\r\n });\r\n case 410:\r\n if (errorCode === \"RESERVATION_EXPIRED\" && details?.reservationId) {\r\n return new ReservationExpiredError(details.reservationId as string);\r\n }\r\n return new CreditsSDKError(errorMessage, SDKErrorCode.RESERVATION_EXPIRED, {\r\n statusCode,\r\n details,\r\n });\r\n case 400:\r\n case 422:\r\n return new ValidationError(errorMessage, details?.field as string | undefined);\r\n case 500:\r\n default:\r\n if (statusCode >= 500) {\r\n return new ServerError(errorMessage);\r\n }\r\n return new CreditsSDKError(errorMessage, SDKErrorCode.UNKNOWN_ERROR, {\r\n statusCode,\r\n details,\r\n });\r\n }\r\n}\r\n","/**\r\n * CreditsClient - REST API client for the credits system\r\n *\r\n * Provides a type-safe interface for consuming credits API endpoints\r\n * from external services or other applications.\r\n *\r\n * @example\r\n * ```typescript\r\n * const client = new CreditsClient({\r\n * baseUrl: process.env.CREDITS_API_URL,\r\n * getAuthToken: async () => session.accessToken,\r\n * });\r\n *\r\n * // Check if user has sufficient credits\r\n * const check = await client.checkAvailability(\"story_generation\");\r\n * if (!check.hasCredits) {\r\n * console.log(\"Insufficient credits\");\r\n * return;\r\n * }\r\n *\r\n * // Two-phase commit pattern\r\n * const reservation = await client.reserve(\"story_generation\");\r\n * try {\r\n * const result = await doExpensiveWork();\r\n * await client.commit(reservation.reservationId);\r\n * return result;\r\n * } catch (error) {\r\n * await client.release(reservation.reservationId);\r\n * throw error;\r\n * }\r\n * ```\r\n */\r\n\r\nimport type {\r\n CreditsClientConfig,\r\n UserCredits,\r\n CreditCheckResult,\r\n ReservationResult,\r\n UsageHistoryResponse,\r\n PaginationOptions,\r\n} from \"./types.js\";\r\nimport { parseApiError, NetworkError } from \"./errors.js\";\r\nimport { sumAmounts } from \"../core/amount.js\";\r\n\r\nexport class CreditsClient {\r\n private readonly baseUrl: string;\r\n private readonly getAuthToken?: () => Promise<string>;\r\n private readonly fetchFn: typeof fetch;\r\n\r\n constructor(config: CreditsClientConfig) {\r\n // Ensure baseUrl doesn't have trailing slash\r\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\r\n this.getAuthToken = config.getAuthToken;\r\n this.fetchFn = config.fetch ?? fetch;\r\n }\r\n\r\n /**\r\n * Make an authenticated request to the API\r\n */\r\n private async request<T>(\r\n path: string,\r\n options: {\r\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\r\n body?: unknown;\r\n params?: Record<string, string | number | undefined>;\r\n } = {}\r\n ): Promise<T> {\r\n const { method = \"GET\", body, params } = options;\r\n\r\n // Build URL with query params\r\n let url = `${this.baseUrl}${path}`;\r\n if (params) {\r\n const searchParams = new URLSearchParams();\r\n for (const [key, value] of Object.entries(params)) {\r\n if (value !== undefined) {\r\n searchParams.set(key, String(value));\r\n }\r\n }\r\n const queryString = searchParams.toString();\r\n if (queryString) {\r\n url += `?${queryString}`;\r\n }\r\n }\r\n\r\n // Build headers\r\n const headers: Record<string, string> = {\r\n \"Content-Type\": \"application/json\",\r\n };\r\n\r\n // Add auth token if available\r\n if (this.getAuthToken) {\r\n try {\r\n const token = await this.getAuthToken();\r\n if (token) {\r\n headers[\"Authorization\"] = `Bearer ${token}`;\r\n }\r\n } catch {\r\n // Auth token not available, proceed without it\r\n }\r\n }\r\n\r\n // Make the request\r\n let response: Response;\r\n try {\r\n response = await this.fetchFn(url, {\r\n method,\r\n headers,\r\n body: body ? JSON.stringify(body) : undefined,\r\n });\r\n } catch (error) {\r\n throw new NetworkError(\r\n error instanceof Error ? error.message : \"Network request failed\",\r\n error instanceof Error ? error : undefined\r\n );\r\n }\r\n\r\n // Parse response\r\n const responseBody = (await response.json()) as {\r\n data?: T;\r\n error?: { code?: string; message?: string; details?: unknown };\r\n };\r\n\r\n // Handle errors\r\n if (!response.ok) {\r\n throw parseApiError(response.status, responseBody);\r\n }\r\n\r\n // Return data\r\n return responseBody.data as T;\r\n }\r\n\r\n /**\r\n * Get the current user's credit balance\r\n *\r\n * @returns User's credit information\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async getBalance(): Promise<UserCredits> {\r\n return this.request<UserCredits>(\"/api/v1/credits/balance\");\r\n }\r\n\r\n /**\r\n * Check if the user has sufficient credits for an operation\r\n *\r\n * @param operationType - Type of operation to check\r\n * @param customCost - Optional custom cost to check against\r\n * @returns Credit availability status\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async checkAvailability(\r\n operationType: string,\r\n customCost?: number\r\n ): Promise<CreditCheckResult> {\r\n return this.request<CreditCheckResult>(\"/api/v1/credits/availability\", {\r\n params: {\r\n operationType,\r\n customCost: customCost?.toString(),\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Reserve credits for an operation\r\n *\r\n * This is the first step in the two-phase commit pattern.\r\n * Reserved credits are held for 5 minutes before expiring.\r\n *\r\n * @param operationType - Type of operation\r\n * @param options - Optional reservation options\r\n * @returns Reservation details including ID and expiry\r\n * @throws InsufficientCreditsError if not enough credits\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async reserve(\r\n operationType: string,\r\n options?: {\r\n customCost?: number;\r\n resourceId?: string;\r\n resourceType?: string;\r\n }\r\n ): Promise<ReservationResult> {\r\n return this.request<ReservationResult>(\"/api/v1/credits/reserve\", {\r\n method: \"POST\",\r\n body: {\r\n operationType,\r\n ...options,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Commit a reservation (deduct credits)\r\n *\r\n * This is the second step in the two-phase commit pattern.\r\n * Call this after your operation succeeds.\r\n *\r\n * @param reservationId - ID of the reservation to commit\r\n * @throws ReservationNotFoundError if reservation doesn't exist\r\n * @throws ReservationExpiredError if reservation has expired\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async commit(reservationId: string): Promise<void> {\r\n await this.request<{ message: string }>(\"/api/v1/credits/commit\", {\r\n method: \"POST\",\r\n body: { reservationId },\r\n });\r\n }\r\n\r\n /**\r\n * Release a reservation (refund credits)\r\n *\r\n * Call this if your operation fails after reserving credits.\r\n *\r\n * @param reservationId - ID of the reservation to release\r\n * @throws ReservationNotFoundError if reservation doesn't exist\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async release(reservationId: string): Promise<void> {\r\n await this.request<{ message: string }>(\"/api/v1/credits/release\", {\r\n method: \"POST\",\r\n body: { reservationId },\r\n });\r\n }\r\n\r\n /**\r\n * Get usage history for the current user\r\n *\r\n * @param options - Optional pagination options\r\n * @returns Paginated list of usage entries\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async getHistory(options?: PaginationOptions): Promise<UsageHistoryResponse> {\r\n return this.request<UsageHistoryResponse>(\"/api/v1/credits/history\", {\r\n params: {\r\n page: options?.page,\r\n limit: options?.limit,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Get the available credits (balance + bonus - reserved)\r\n *\r\n * Convenience method that calculates available credits from balance.\r\n *\r\n * @returns Number of available credits\r\n * @throws AuthenticationError if not authenticated\r\n */\r\n async getAvailableCredits(): Promise<number> {\r\n const balance = await this.getBalance();\r\n return sumAmounts(balance.balance, balance.bonusCredits, -balance.reserved);\r\n }\r\n\r\n /**\r\n * Execute an operation with automatic credit handling\r\n *\r\n * This is a convenience method that handles the full two-phase commit\r\n * pattern automatically.\r\n *\r\n * @param operationType - Type of operation\r\n * @param operation - The operation to execute\r\n * @param options - Optional reservation options\r\n * @returns Result of the operation\r\n * @throws InsufficientCreditsError if not enough credits\r\n * @throws AuthenticationError if not authenticated\r\n *\r\n * @example\r\n * ```typescript\r\n * const result = await client.withCredits(\r\n * \"story_generation\",\r\n * async () => {\r\n * return generateStory(data);\r\n * }\r\n * );\r\n * ```\r\n */\r\n async withCredits<T>(\r\n operationType: string,\r\n operation: () => Promise<T>,\r\n options?: {\r\n customCost?: number;\r\n resourceId?: string;\r\n resourceType?: string;\r\n }\r\n ): Promise<T> {\r\n const reservation = await this.reserve(operationType, options);\r\n\r\n try {\r\n const result = await operation();\r\n await this.commit(reservation.reservationId);\r\n return result;\r\n } catch (error) {\r\n // Always attempt to release on failure\r\n try {\r\n await this.release(reservation.reservationId);\r\n } catch {\r\n // Ignore release errors, original error is more important\r\n }\r\n throw error;\r\n }\r\n }\r\n}\r\n","/**\r\n * AdminCreditsClient - Admin REST API client for the credits system\r\n *\r\n * Provides administrative access to the credits system for managing\r\n * user credits, configurations, and subscriptions.\r\n *\r\n * @example\r\n * ```typescript\r\n * const adminClient = new AdminCreditsClient({\r\n * baseUrl: process.env.CREDITS_API_URL,\r\n * apiKey: process.env.CREDITS_ADMIN_API_KEY,\r\n * });\r\n *\r\n * // Add credits to a user\r\n * await adminClient.addCredits(\"user-123\", 100, \"Support credit grant\");\r\n *\r\n * // Update subscription tier\r\n * await adminClient.updateSubscription(\"user-123\", \"premium\", \"2027-01-01T00:00:00Z\");\r\n *\r\n * // Update operation costs\r\n * await adminClient.updateCosts({\r\n * story_generation: 8,\r\n * image_generation: 12,\r\n * });\r\n * ```\r\n */\r\n\r\nimport type { AdminCreditsClientConfig, UserCredits } from \"./types.js\";\r\nimport { parseApiError, NetworkError } from \"./errors.js\";\r\n\r\n/**\r\n * Credits configuration returned by the API\r\n */\r\nexport interface CreditsConfig {\r\n operationCosts: Record<string, number>;\r\n tierConfigs: Record<\r\n string,\r\n {\r\n monthlyLimit: number;\r\n features: string[];\r\n }\r\n >;\r\n}\r\n\r\n/**\r\n * Response from listing users\r\n */\r\nexport interface ListUsersResponse {\r\n users: Array<{\r\n userId: string;\r\n balance: number;\r\n bonusCredits?: number;\r\n tier: string;\r\n monthlyUsed?: number;\r\n }>;\r\n hasMore: boolean;\r\n total?: number;\r\n}\r\n\r\n/**\r\n * Options for listing users\r\n */\r\nexport interface ListUsersOptions {\r\n page?: number;\r\n limit?: number;\r\n tier?: string;\r\n search?: string;\r\n}\r\n\r\nexport class AdminCreditsClient {\r\n private readonly baseUrl: string;\r\n private readonly apiKey: string;\r\n private readonly fetchFn: typeof fetch;\r\n\r\n constructor(config: AdminCreditsClientConfig) {\r\n // Ensure baseUrl doesn't have trailing slash\r\n this.baseUrl = config.baseUrl.replace(/\\/$/, \"\");\r\n this.apiKey = config.apiKey;\r\n this.fetchFn = config.fetch ?? fetch;\r\n }\r\n\r\n /**\r\n * Make an authenticated admin request to the API\r\n */\r\n private async request<T>(\r\n path: string,\r\n options: {\r\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\";\r\n body?: unknown;\r\n params?: Record<string, string | number | undefined>;\r\n } = {}\r\n ): Promise<T> {\r\n const { method = \"GET\", body, params } = options;\r\n\r\n // Build URL with query params\r\n let url = `${this.baseUrl}${path}`;\r\n if (params) {\r\n const searchParams = new URLSearchParams();\r\n for (const [key, value] of Object.entries(params)) {\r\n if (value !== undefined) {\r\n searchParams.set(key, String(value));\r\n }\r\n }\r\n const queryString = searchParams.toString();\r\n if (queryString) {\r\n url += `?${queryString}`;\r\n }\r\n }\r\n\r\n // Build headers with admin API key\r\n const headers: Record<string, string> = {\r\n \"Content-Type\": \"application/json\",\r\n \"x-api-key\": this.apiKey,\r\n };\r\n\r\n // Make the request\r\n let response: Response;\r\n try {\r\n response = await this.fetchFn(url, {\r\n method,\r\n headers,\r\n body: body ? JSON.stringify(body) : undefined,\r\n });\r\n } catch (error) {\r\n throw new NetworkError(\r\n error instanceof Error ? error.message : \"Network request failed\",\r\n error instanceof Error ? error : undefined\r\n );\r\n }\r\n\r\n // Parse response\r\n const responseBody = (await response.json()) as {\r\n data?: T;\r\n error?: { code?: string; message?: string; details?: unknown };\r\n };\r\n\r\n // Handle errors\r\n if (!response.ok) {\r\n throw parseApiError(response.status, responseBody);\r\n }\r\n\r\n // Return data\r\n return responseBody.data as T;\r\n }\r\n\r\n /**\r\n * Get a specific user's credits\r\n *\r\n * @param userId - ID of the user\r\n * @returns User's credit information\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async getUserCredits(userId: string): Promise<UserCredits> {\r\n return this.request<UserCredits>(`/api/admin/credits/users/${userId}`);\r\n }\r\n\r\n /**\r\n * Add credits to a user's account\r\n *\r\n * Credits are added to bonusCredits, which don't expire on monthly reset.\r\n *\r\n * @param userId - ID of the user\r\n * @param amount - Amount of credits to add\r\n * @param description - Reason for adding credits\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async addCredits(\r\n userId: string,\r\n amount: number,\r\n description: string\r\n ): Promise<void> {\r\n await this.request<{ message: string; newBalance: number }>(\r\n `/api/admin/credits/users/${userId}/add`,\r\n {\r\n method: \"POST\",\r\n body: { amount, description },\r\n }\r\n );\r\n }\r\n\r\n /**\r\n * Update a user's subscription tier\r\n *\r\n * @param userId - ID of the user\r\n * @param tier - New subscription tier\r\n * @param expiresAt - Optional expiration date (ISO 8601)\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async updateSubscription(\r\n userId: string,\r\n tier: string,\r\n expiresAt?: string\r\n ): Promise<void> {\r\n const body: { tier: string; expiresAt?: string } = { tier };\r\n if (expiresAt) {\r\n body.expiresAt = expiresAt;\r\n }\r\n\r\n await this.request<{ message: string }>(\r\n `/api/admin/credits/users/${userId}/subscription`,\r\n {\r\n method: \"PUT\",\r\n body,\r\n }\r\n );\r\n }\r\n\r\n /**\r\n * Get the current credits configuration\r\n *\r\n * @returns Credits system configuration\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async getConfig(): Promise<CreditsConfig> {\r\n return this.request<CreditsConfig>(\"/api/admin/credits/config\");\r\n }\r\n\r\n /**\r\n * Update operation costs\r\n *\r\n * @param costs - New operation costs (operation type -> cost)\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async updateCosts(costs: Record<string, number>): Promise<void> {\r\n await this.request<{ message: string }>(\"/api/admin/credits/config/costs\", {\r\n method: \"PUT\",\r\n body: { operationCosts: costs },\r\n });\r\n }\r\n\r\n /**\r\n * List all users with their credits\r\n *\r\n * @param options - Pagination and filtering options\r\n * @returns Paginated list of users\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async listUsers(options?: ListUsersOptions): Promise<ListUsersResponse> {\r\n return this.request<ListUsersResponse>(\"/api/admin/credits/users\", {\r\n params: {\r\n page: options?.page,\r\n limit: options?.limit,\r\n tier: options?.tier,\r\n search: options?.search,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Deduct credits from a user's account\r\n *\r\n * @param userId - ID of the user\r\n * @param amount - Amount of credits to deduct\r\n * @param description - Reason for deducting credits\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async deductCredits(\r\n userId: string,\r\n amount: number,\r\n description: string\r\n ): Promise<void> {\r\n await this.request<{ message: string; newBalance: number }>(\r\n `/api/admin/credits/users/${userId}/deduct`,\r\n {\r\n method: \"POST\",\r\n body: { amount, description },\r\n }\r\n );\r\n }\r\n\r\n /**\r\n * Get usage statistics for a user\r\n *\r\n * @param userId - ID of the user\r\n * @param options - Optional date range\r\n * @returns Usage statistics\r\n * @throws AuthorizationError if API key is invalid\r\n */\r\n async getUserStats(\r\n userId: string,\r\n options?: { startDate?: string; endDate?: string }\r\n ): Promise<{\r\n totalCreditsUsed: number;\r\n operationBreakdown: Record<string, number>;\r\n successRate: number;\r\n }> {\r\n return this.request<{\r\n totalCreditsUsed: number;\r\n operationBreakdown: Record<string, number>;\r\n successRate: number;\r\n }>(`/api/admin/credits/users/${userId}/stats`, {\r\n params: {\r\n startDate: options?.startDate,\r\n endDate: options?.endDate,\r\n },\r\n });\r\n }\r\n}\r\n","import { sumAmounts } from \"../core/amount.js\";\r\n/**\r\n * Credit error utilities\r\n *\r\n * Provides functions to parse and identify credit-related errors\r\n * for consistent error handling across the application.\r\n */\r\n\r\n/**\r\n * Parsed information from a credit error message\r\n */\r\nexport interface CreditErrorInfo {\r\n /** Credits currently available */\r\n available: number;\r\n /** Credits required for the operation */\r\n required: number;\r\n /** Difference between required and available */\r\n shortfall: number;\r\n /** Original error message */\r\n rawMessage: string;\r\n}\r\n\r\n/**\r\n * Parse a credit error message to extract available/required credits\r\n *\r\n * @param error - Error message string\r\n * @returns Parsed credit info or null if not a valid credit error format\r\n *\r\n * @example\r\n * ```ts\r\n * const error = \"Insufficient credits. Available: 3, Required: 10\";\r\n * const info = parseCreditError(error);\r\n * // { available: 3, required: 10, shortfall: 7, rawMessage: \"...\" }\r\n * ```\r\n */\r\nexport function parseCreditError(error: string): CreditErrorInfo | null {\r\n // Pattern: Available: X, Required: Y (case insensitive, flexible whitespace)\r\n const match = error.match(/Available:\\s*(\\d+)\\s*,\\s*Required:\\s*(\\d+)/i);\r\n\r\n if (!match) {\r\n return null;\r\n }\r\n\r\n const available = parseInt(match[1], 10);\r\n const required = parseInt(match[2], 10);\r\n\r\n // Validate parsed numbers\r\n if (isNaN(available) || isNaN(required)) {\r\n return null;\r\n }\r\n\r\n return {\r\n available,\r\n required,\r\n shortfall: sumAmounts(required, -available),\r\n rawMessage: error,\r\n };\r\n}\r\n\r\n/**\r\n * Check if an error message is a credit-related error\r\n *\r\n * @param error - Error message string\r\n * @returns true if the error is credit-related\r\n *\r\n * @example\r\n * ```ts\r\n * if (isCreditError(error)) {\r\n * showCreditErrorDialog(parseCreditError(error));\r\n * }\r\n * ```\r\n */\r\nexport function isCreditErrorMessage(error: string): boolean {\r\n const lowerError = error.toLowerCase();\r\n return (\r\n lowerError.includes(\"insufficient credits\") ||\r\n lowerError.includes(\"not enough credits\")\r\n );\r\n}\r\n","/**\r\n * Credit utility functions\r\n *\r\n * Common helper functions for credit calculations\r\n */\r\n\r\nimport type { PortableReservation } from \"../core/index.js\";\r\nimport { sumAmounts } from \"../core/amount.js\";\r\n\r\nexport {\r\n parseCreditError,\r\n isCreditErrorMessage,\r\n type CreditErrorInfo,\r\n} from \"./error-utils.js\";\r\n\r\n/**\r\n * Calculate total available credits from balance and bonus credits\r\n *\r\n * @param credits - Object containing balance and bonusCredits properties\r\n * @returns Total credits (balance + bonusCredits)\r\n *\r\n * @example\r\n * ```ts\r\n * const total = getTotalCredits({ balance: 100, bonusCredits: 50 });\r\n * // Returns 150\r\n * ```\r\n */\r\nexport function getTotalCredits(credits: {\r\n balance: number;\r\n bonusCredits: number;\r\n}): number {\r\n return sumAmounts(credits.balance, credits.bonusCredits);\r\n}\r\n\r\n/**\r\n * Generate a unique request ID\r\n *\r\n * Format: req_{timestamp}_{randomPart}\r\n * Example: req_1706234567890_a1b2c3d4e5f6\r\n *\r\n * @returns URL-safe request ID string\r\n */\r\nexport function generateRequestId(): string {\r\n const timestamp = Date.now();\r\n const randomPart = Math.random().toString(36).substring(2, 14);\r\n return `req_${timestamp}_${randomPart}`;\r\n}\r\n\r\n/**\r\n * Check if the input data indicates preview mode\r\n *\r\n * Preview mode skips actual credit deduction - used for dry runs\r\n * and UI previews where the user wants to see results without committing.\r\n *\r\n * @param data - The input data to check\r\n * @returns true if data has preview: true\r\n */\r\nexport function isPreviewMode(data: unknown): boolean {\r\n return (\r\n typeof data === \"object\" &&\r\n data !== null &&\r\n \"preview\" in data &&\r\n (data as Record<string, unknown>).preview === true\r\n );\r\n}\r\n\r\n/**\r\n * Create a dummy reservation for preview mode\r\n *\r\n * Used when `isPreviewMode(data)` returns true to provide\r\n * a valid reservation object without actual credit operations.\r\n *\r\n * @param userId - The user ID\r\n * @param operationType - The operation type\r\n * @returns A dummy PortableReservation with id \"preview-mode\"\r\n */\r\nexport function createDummyReservation(\r\n userId: string,\r\n operationType: string\r\n): PortableReservation {\r\n const now = new Date().toISOString();\r\n return {\r\n id: \"preview-mode\",\r\n userId,\r\n amount: 0,\r\n operationType,\r\n status: \"released\",\r\n createdAt: now,\r\n expiresAt: now,\r\n };\r\n}\r\n"],"mappings":"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","names":["cents","toDate","terminalStatusOf","toDate","getOperationCost","generateRequestId","isPreviewMode","createDummyReservation"]}
|