@nehorai/credits 2.1.0 → 2.2.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/LICENSE +21 -0
- package/dist/adapters/index.cjs +2 -2
- 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 +2 -2
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +102 -2
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +92 -5
- package/dist/core/index.d.ts +92 -5
- package/dist/core/index.js +92 -2
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +272 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +260 -39
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +247 -39
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +34 -6
- package/dist/repository/index.d.ts +34 -6
- package/dist/repository/index.js +244 -38
- package/dist/repository/index.js.map +1 -1
- 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.map +1 -1
- package/dist/service/index.cjs +8 -4
- 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 +8 -4
- package/dist/service/index.js.map +1 -1
- package/dist/{types-BhCdep_t.d.ts → types-3c-IAwZI.d.ts} +38 -3
- package/dist/{types-Dzc443vy.d.cts → types-MaViK5tD.d.cts} +18 -0
- package/dist/{types-Dzc443vy.d.ts → types-MaViK5tD.d.ts} +18 -0
- package/dist/{types-wtksMa4n.d.cts → types-XSjNXO5l.d.cts} +38 -3
- package/package.json +70 -71
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Nehorai Hadad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/adapters/index.cjs
CHANGED
|
@@ -75,9 +75,9 @@ var creditSystemConfigSchema = import_zod.z.object({
|
|
|
75
75
|
*/
|
|
76
76
|
reservationExpiryMs: import_zod.z.number().positive().default(5 * 60 * 1e3),
|
|
77
77
|
/**
|
|
78
|
-
* Default credits for new users (free tier)
|
|
78
|
+
* Default credits for new users (free tier). 0 means no signup grant.
|
|
79
79
|
*/
|
|
80
|
-
defaultFreeCredits: import_zod.z.number().
|
|
80
|
+
defaultFreeCredits: import_zod.z.number().min(0).default(25),
|
|
81
81
|
/**
|
|
82
82
|
* Grace period in days after subscription expires before downgrade
|
|
83
83
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/index.ts","../../src/config/index.ts","../../src/config/provider.ts","../../src/core/errors.ts","../../src/core/amount.ts","../../src/repository/types.ts","../../src/repository/flow.ts","../../src/core/operations.ts","../../src/adapters/generic.ts"],"sourcesContent":["export type {\r\n ActionResult,\r\n CreditActionHandler,\r\n CreditsAdapterConfig,\r\n CreditsAdapter,\r\n} from \"./types.js\";\r\n\r\nexport { createGenericAdapter } from \"./generic.js\";\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)\r\n */\r\n defaultFreeCredits: z.number().positive().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 { 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\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/** 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 * 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 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 monthlyUsed?: number;\r\n subscriptionExpiresAt?: Date | string | null;\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 - User's current subscription tier (for determining new balance)\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 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","/**\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"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/index.ts","../../src/config/index.ts","../../src/config/provider.ts","../../src/core/errors.ts","../../src/core/amount.ts","../../src/repository/types.ts","../../src/repository/flow.ts","../../src/core/operations.ts","../../src/adapters/generic.ts"],"sourcesContent":["export type {\r\n ActionResult,\r\n CreditActionHandler,\r\n CreditsAdapterConfig,\r\n CreditsAdapter,\r\n} from \"./types.js\";\r\n\r\nexport { createGenericAdapter } from \"./generic.js\";\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 { 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 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","/**\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"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as PortableReservation, W as WithCreditsOptions } from '../types-
|
|
1
|
+
import { a as PortableReservation, W as WithCreditsOptions } from '../types-MaViK5tD.cjs';
|
|
2
2
|
import { a as CreditsUser, I as ICreditsAuthProvider } from '../types-OZU3I5OM.cjs';
|
|
3
|
-
import { I as ICreditRepository } from '../types-
|
|
3
|
+
import { I as ICreditRepository } from '../types-XSjNXO5l.cjs';
|
|
4
4
|
import { D as DeferredExecutor } from '../deferred-C7rrRfkm.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as PortableReservation, W as WithCreditsOptions } from '../types-
|
|
1
|
+
import { a as PortableReservation, W as WithCreditsOptions } from '../types-MaViK5tD.js';
|
|
2
2
|
import { a as CreditsUser, I as ICreditsAuthProvider } from '../types-OZU3I5OM.js';
|
|
3
|
-
import { I as ICreditRepository } from '../types-
|
|
3
|
+
import { I as ICreditRepository } from '../types-3c-IAwZI.js';
|
|
4
4
|
import { D as DeferredExecutor } from '../deferred-C7rrRfkm.js';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/adapters/index.js
CHANGED
|
@@ -49,9 +49,9 @@ var creditSystemConfigSchema = z.object({
|
|
|
49
49
|
*/
|
|
50
50
|
reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
|
|
51
51
|
/**
|
|
52
|
-
* Default credits for new users (free tier)
|
|
52
|
+
* Default credits for new users (free tier). 0 means no signup grant.
|
|
53
53
|
*/
|
|
54
|
-
defaultFreeCredits: z.number().
|
|
54
|
+
defaultFreeCredits: z.number().min(0).default(25),
|
|
55
55
|
/**
|
|
56
56
|
* Grace period in days after subscription expires before downgrade
|
|
57
57
|
*/
|