@nehorai/credits 1.3.0 → 1.5.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 -21
- package/README.md +153 -153
- package/dist/adapters/index.cjs +13 -3
- 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 +13 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.cjs +13 -3
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +13 -3
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +88 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -15
- package/dist/index.d.ts +74 -15
- package/dist/index.js +79 -19
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +50 -11
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +3 -3
- package/dist/repository/index.d.ts +3 -3
- package/dist/repository/index.js +50 -11
- 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 +60 -12
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +18 -3
- package/dist/service/index.d.ts +18 -3
- package/dist/service/index.js +60 -12
- package/dist/service/index.js.map +1 -1
- package/dist/{types-BBxIWHK2.d.cts → types-DgQtamEe.d.cts} +16 -3
- package/dist/{types-BBxIWHK2.d.ts → types-DgQtamEe.d.ts} +16 -3
- package/dist/{types-D1vdeBcA.d.cts → types-KgNdBuEF.d.cts} +1 -1
- package/dist/{types-aJsyv8df.d.ts → types-fW2JNmmr.d.ts} +1 -1
- package/package.json +1 -1
package/dist/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/types.ts","../../src/core/errors.ts","../../src/core/deferred.ts","../../src/config/index.ts","../../src/config/provider.ts","../../src/core/operations.ts"],"sourcesContent":["/**\n * Portable credits types - framework and database agnostic\n *\n * These types use standard JavaScript types (string, Date) instead of\n * Firebase-specific types (Timestamp). This makes the credits system\n * portable to other environments without Firebase dependencies.\n *\n * Note: Firestore Timestamp is handled by the credits-firestore adapter.\n */\n\n// ==================== Utility Functions ====================\n\n/**\n * Calculate available credits from balance, bonus, and reserved amounts.\n *\n * Available = balance + bonusCredits - reserved\n *\n * @param balance - Regular monthly credits\n * @param bonusCredits - Purchased/admin credits\n * @param reserved - Credits locked for in-flight operations\n * @returns Available credits for new operations\n */\nexport function calculateAvailableCredits(\n balance: number,\n bonusCredits: number,\n reserved: number\n): number {\n return balance + bonusCredits - reserved;\n}\n\n/**\n * Convert various timestamp formats to ISO string.\n *\n * Handles:\n * - Date objects\n * - ISO string (returned as-is)\n * - Timestamp-like objects with toDate() method (Firebase)\n * - undefined/null (returns current time)\n *\n * @param value - Value to convert\n * @returns ISO 8601 string\n */\nexport function toPortableTimestamp(value: unknown): string {\n if (!value) {\n return new Date().toISOString();\n }\n\n if (typeof value === \"string\") {\n return value;\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (\n typeof value === \"object\" &&\n value !== null &&\n \"toDate\" in value &&\n typeof (value as { toDate: () => Date }).toDate === \"function\"\n ) {\n return (value as { toDate: () => Date }).toDate().toISOString();\n }\n\n return new Date().toISOString();\n}\n\n/**\n * Convert various timestamp formats to Date object.\n *\n * Handles:\n * - Date objects (returned as-is)\n * - ISO string\n * - Timestamp-like objects with toDate() method (Firebase)\n * - Invalid values (returns current date)\n *\n * @param value - Value to convert\n * @returns Date object\n */\nexport function toDate(value: unknown): Date {\n if (value instanceof Date) {\n return value;\n }\n\n if (typeof value === \"string\") {\n return new Date(value);\n }\n\n if (\n typeof value === \"object\" &&\n value !== null &&\n \"toDate\" in value &&\n typeof (value as { toDate: () => Date }).toDate === \"function\"\n ) {\n return (value as { toDate: () => Date }).toDate();\n }\n\n return new Date();\n}\n\n// ==================== Subscription Types ====================\n\n/**\n * Subscription tiers for the credits system\n */\nexport type SubscriptionTier = \"free\" | \"basic\" | \"premium\" | \"unlimited\";\n\n// ==================== Operation Types ====================\n\n/**\n * Operation types for cost tracking (provider-agnostic)\n *\n * Dynamic string type - any operation configured in the system is valid.\n */\nexport type CreditOperationType = string;\n\n/**\n * AI providers\n */\nexport type AIProviderType = \"gemini\";\n\n/**\n * Resource type for usage logging\n */\nexport type ResourceType = string;\n\n// ==================== User Credits ====================\n\n/**\n * Portable user credits balance\n *\n * Uses ISO string timestamps instead of Firebase Timestamp.\n * This type is safe for JSON serialization and works across frameworks.\n */\nexport interface PortableUserCredits {\n userId: string;\n /** Current available balance (monthly credits, reset each month) */\n balance: number;\n /** Bonus credits from purchases/admin (never reset, persist until used) */\n bonusCredits: number;\n /** Credits currently reserved for in-flight operations */\n reserved: number;\n /** User's subscription tier */\n tier: SubscriptionTier;\n /** Monthly credit limit based on tier (0 = unlimited) */\n monthlyLimit: number;\n /** Credits used this month (resets monthly) */\n monthlyUsed: number;\n /** When monthly credits reset (start of next month) - ISO 8601 */\n monthlyResetAt: string;\n /** Subscription expiry (null/undefined for free tier) - ISO 8601 or null */\n subscriptionExpiresAt?: string | null;\n /** Creation timestamp - ISO 8601 */\n createdAt: string;\n /** Last update timestamp - ISO 8601 */\n updatedAt: string;\n}\n\n// ==================== Reservation Types ====================\n\n/**\n * Credit reservation status for two-phase commit\n */\nexport type ReservationStatus = \"reserved\" | \"committed\" | \"released\" | \"expired\";\n\n/**\n * Portable credit reservation\n *\n * Used for two-phase commit to prevent double-spending.\n */\nexport interface PortableReservation {\n id: string;\n userId: string;\n /** Amount of credits reserved */\n amount: number;\n /** Operation type for tracking */\n operationType: CreditOperationType;\n /** Current status of reservation */\n status: ReservationStatus;\n /** When reservation was created - ISO 8601 */\n createdAt: string;\n /** When reservation expires (for cleanup) - ISO 8601 */\n expiresAt: string;\n /** When reservation was committed/released - ISO 8601 or undefined */\n completedAt?: string;\n}\n\n// ==================== Result Types ====================\n\n/**\n * Result of a credit check operation\n */\nexport interface CreditCheckResult {\n /** Whether user has sufficient credits */\n hasCredits: boolean;\n /** Current balance (balance + bonusCredits) */\n balance: number;\n /** Required credits for operation */\n required: number;\n /** Shortfall if insufficient */\n shortfall: number;\n}\n\n/**\n * Result of monthly reset operation\n */\nexport interface MonthlyResetResult {\n /** Whether the reset was performed */\n wasReset: boolean;\n /** The user credits after the operation */\n credits: PortableUserCredits;\n}\n\n/**\n * Result of subscription expiry check\n */\nexport interface SubscriptionExpiryResult {\n /** Whether the subscription was downgraded */\n wasDowngraded: boolean;\n /** Whether user is in grace period */\n inGracePeriod: boolean;\n /** Days remaining in grace period (0 if not in grace) */\n graceDaysRemaining: number;\n /** The user credits after the operation */\n credits: PortableUserCredits;\n}\n\n// ==================== Transaction Types ====================\n\n/**\n * Credit transaction type\n */\nexport type TransactionType = \"purchase\" | \"subscription\" | \"bonus\" | \"refund\" | \"adjustment\";\n\n/**\n * Portable credit transaction\n */\nexport interface PortableTransaction {\n id: string;\n userId: string;\n type: TransactionType;\n /** Amount of credits (positive for additions, negative for deductions) */\n amount: number;\n description: string;\n /** Reference to payment provider */\n paymentRef?: string;\n previousBalance: number;\n newBalance: number;\n createdAt: string;\n}\n\n// ==================== Journal Types ====================\n\n/**\n * Source of a credit journal entry\n */\nexport type CreditSource =\n | \"operation_commit\"\n | \"operation_release\"\n | \"purchase\"\n | \"subscription_grant\"\n | \"subscription_upgrade\"\n | \"subscription_downgrade\"\n | \"monthly_reset\"\n | \"bonus\"\n | \"refund\"\n | \"admin_adjustment\"\n | \"expiry\"\n | \"reservation_expired\";\n\n/**\n * Type of reference for a journal entry\n */\nexport type JournalReferenceType =\n | \"reservation\"\n | \"transaction\"\n | \"adjustment\"\n | \"reset\"\n | \"subscription\";\n\n/**\n * Portable credit journal entry for audit trail\n */\nexport interface PortableJournalEntry {\n id: string;\n userId: string;\n /** Type of entry - debit decreases balance, credit increases balance */\n entryType: \"debit\" | \"credit\";\n /** Amount of credits involved */\n amount: number;\n /** Balance after this entry */\n balanceAfter: number;\n /** Source of the credit change */\n source: CreditSource;\n /** Reference ID (e.g., reservationId, transactionId) */\n referenceId: string;\n /** Type of reference */\n referenceType: JournalReferenceType;\n /** Human-readable description */\n description: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n /** When this entry was created - ISO 8601 */\n createdAt: string;\n}\n\n// ==================== Usage Types ====================\n\n/**\n * Portable usage log entry\n */\nexport interface PortableUsageLog {\n id: string;\n userId: string;\n operationType: CreditOperationType;\n provider: AIProviderType;\n creditsUsed: number;\n success: boolean;\n errorMessage?: string;\n resourceId?: string;\n resourceType?: ResourceType;\n requestId?: string;\n metadata?: Record<string, unknown>;\n createdAt: string;\n}\n\n/**\n * User-friendly usage history entry\n */\nexport interface UsageHistoryEntry {\n id: string;\n /** Type of entry for display */\n type: \"usage\" | \"purchase\" | \"bonus\" | \"reset\" | \"refund\" | \"adjustment\";\n /** Positive = earned, Negative = spent */\n creditsChange: number;\n /** Balance after this entry */\n balanceAfter: number;\n /** Human-readable description */\n description: string;\n /** When this occurred - ISO 8601 */\n createdAt: string;\n}\n\n/**\n * Paginated usage history response\n */\nexport interface UsageHistoryResponse {\n entries: UsageHistoryEntry[];\n pagination: {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n };\n}\n\n// ==================== Tier Configuration ====================\n\n/**\n * Tier configuration for credit limits and pricing\n */\nexport interface TierConfig {\n tier: SubscriptionTier;\n monthlyCredits: number;\n priceUsd: number;\n features: string[];\n}\n\n// ==================== Options Types ====================\n\n/**\n * Options for withCredits HOF\n */\nexport interface WithCreditsOptions {\n /** Operation type for cost lookup */\n operationType: CreditOperationType;\n /** AI provider (defaults to \"gemini\") */\n provider?: AIProviderType;\n /** Custom cost override (use with caution) */\n customCost?: number;\n /** Resource ID for usage logging */\n resourceId?: string;\n /** Resource type for usage logging */\n resourceType?: ResourceType;\n}\n\n// ==================== Constants ====================\n","/**\n * Credit system error classes - framework agnostic\n *\n * Provides typed error handling for credit operations.\n * These errors can be used across different environments.\n */\n\n/**\n * Error codes for credit operations\n */\nexport const CreditErrorCode = {\n INSUFFICIENT_CREDITS: \"INSUFFICIENT_CREDITS\",\n RESERVATION_NOT_FOUND: \"RESERVATION_NOT_FOUND\",\n RESERVATION_EXPIRED: \"RESERVATION_EXPIRED\",\n RESERVATION_ALREADY_PROCESSED: \"RESERVATION_ALREADY_PROCESSED\",\n USER_NOT_FOUND: \"USER_NOT_FOUND\",\n INVALID_OPERATION_TYPE: \"INVALID_OPERATION_TYPE\",\n CONFIGURATION_ERROR: \"CONFIGURATION_ERROR\",\n DATABASE_ERROR: \"DATABASE_ERROR\",\n} as const;\n\nexport type CreditErrorCodeType = (typeof CreditErrorCode)[keyof typeof CreditErrorCode];\n\n/**\n * Custom error class for credit operations\n *\n * Provides structured error information including:\n * - Error code for programmatic handling\n * - Human-readable message\n * - Optional details for debugging\n */\nexport class CreditError extends Error {\n public readonly code: CreditErrorCodeType;\n public readonly details?: Record<string, unknown>;\n\n constructor(\n message: string,\n code: CreditErrorCodeType,\n details?: Record<string, unknown>\n ) {\n super(message);\n this.name = \"CreditError\";\n this.code = code;\n this.details = details;\n\n // Maintains proper stack trace for where error was thrown\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, CreditError);\n }\n }\n\n /**\n * Convert to JSON-serializable object\n */\n toJSON(): { name: string; code: string; message: string; details?: Record<string, unknown> } {\n return {\n name: this.name,\n code: this.code,\n message: this.message,\n ...(this.details && { details: this.details }),\n };\n }\n}\n\n/**\n * Type guard to check if an error is a CreditError\n */\nexport function isCreditError(error: unknown): error is CreditError {\n return error instanceof CreditError;\n}\n\n/**\n * Check if an error represents insufficient credits\n *\n * Works with both CreditError and regular Error instances.\n * Useful for handling errors from different sources.\n */\nexport function isInsufficientCreditsError(error: unknown): boolean {\n if (isCreditError(error)) {\n return error.code === CreditErrorCode.INSUFFICIENT_CREDITS;\n }\n\n if (error instanceof Error) {\n return error.message.toLowerCase().includes(\"insufficient credits\");\n }\n\n return false;\n}\n\n/**\n * Create an insufficient credits error\n */\nexport function createInsufficientCreditsError(\n required: number,\n available: number\n): CreditError {\n return new CreditError(\n `Insufficient credits: available ${available}, required ${required}`,\n CreditErrorCode.INSUFFICIENT_CREDITS,\n { required, available, shortfall: required - available }\n );\n}\n\n/**\n * Create a reservation not found error\n */\nexport function createReservationNotFoundError(reservationId: string): CreditError {\n return new CreditError(\n `Reservation ${reservationId} not found`,\n CreditErrorCode.RESERVATION_NOT_FOUND,\n { reservationId }\n );\n}\n\n/**\n * Create a reservation expired error\n */\nexport function createReservationExpiredError(reservationId: string): CreditError {\n return new CreditError(\n `Reservation ${reservationId} has expired`,\n CreditErrorCode.RESERVATION_EXPIRED,\n { reservationId }\n );\n}\n\n/**\n * Create a reservation already processed error\n */\nexport function createReservationAlreadyProcessedError(\n reservationId: string,\n status: string\n): CreditError {\n return new CreditError(\n `Reservation ${reservationId} has already been ${status}`,\n CreditErrorCode.RESERVATION_ALREADY_PROCESSED,\n { reservationId, status }\n );\n}\n\n/**\n * Create a user not found error\n */\nexport function createUserNotFoundError(userId: string): CreditError {\n return new CreditError(\n `User ${userId} not found`,\n CreditErrorCode.USER_NOT_FOUND,\n { userId }\n );\n}\n\n/**\n * Create an invalid operation type error\n */\nexport function createInvalidOperationTypeError(\n operationType: string,\n validTypes: string[]\n): CreditError {\n return new CreditError(\n `Invalid operation type: ${operationType}. Valid types: ${validTypes.join(\", \")}`,\n CreditErrorCode.INVALID_OPERATION_TYPE,\n { operationType, validTypes }\n );\n}\n","/**\n * Deferred execution abstraction - framework agnostic\n *\n * Provides a way to schedule work to run after the current operation completes.\n * Different frameworks can provide their own implementations:\n * - Next.js: uses `after()` from `next/server`\n * - Generic: uses `setImmediate()` or `setTimeout()`\n * - Testing: can use synchronous execution\n */\n\n/**\n * Interface for deferred execution\n *\n * Implementations should schedule the function to run asynchronously\n * without blocking the current operation.\n */\nexport interface DeferredExecutor {\n /**\n * Schedule a function to run asynchronously\n *\n * The function should be executed after the current operation completes.\n * Errors should be caught and logged, not propagated.\n *\n * @param fn - Async function to execute\n */\n defer(fn: () => Promise<void>): void;\n}\n\n/**\n * Generic deferred executor using setImmediate/setTimeout\n *\n * Works in Node.js and browser environments.\n * Suitable for non-framework environments or testing.\n */\nexport const genericDeferred: DeferredExecutor = {\n defer(fn: () => Promise<void>): void {\n // Use setImmediate in Node.js, setTimeout as fallback\n const scheduler = typeof setImmediate !== \"undefined\" ? setImmediate : setTimeout;\n scheduler(() => {\n fn().catch((error) => {\n console.error(\"[Credits] Deferred task error:\", error);\n });\n });\n },\n};\n\n/**\n * Synchronous deferred executor\n *\n * Executes the function immediately (fire-and-forget).\n * Useful for testing or when deferred execution isn't needed.\n */\nexport const synchronousDeferred: DeferredExecutor = {\n defer(fn: () => Promise<void>): void {\n fn().catch((error) => {\n console.error(\"[Credits] Deferred task error:\", error);\n });\n },\n};\n\n/**\n * Create a custom deferred executor\n *\n * Allows frameworks to provide their own scheduling mechanism.\n *\n * @example\n * ```typescript\n * // Next.js adapter\n * import { after } from 'next/server';\n *\n * const nextJsDeferred = createDeferredExecutor((fn) => after(fn));\n * ```\n *\n * @param schedule - Function that schedules async work\n * @returns DeferredExecutor instance\n */\nexport function createDeferredExecutor(\n schedule: (fn: () => Promise<void>) => void\n): DeferredExecutor {\n return {\n defer: schedule,\n };\n}\n\n/**\n * No-op deferred executor\n *\n * Discards deferred work. Useful when you want to disable\n * background tasks (e.g., in certain test scenarios).\n */\nexport const noopDeferred: DeferredExecutor = {\n defer(_fn: () => Promise<void>): void {\n // Intentionally do nothing\n },\n};\n","import { z } from \"zod\";\nimport type { SubscriptionTier, TierConfig } from \"../core/types.js\";\nimport { getConfigProvider } from \"./provider.js\";\n\n/**\n * Schema for tier configuration\n */\nconst tierConfigSchema = z.object({\n tier: z.enum([\"free\", \"basic\", \"premium\", \"unlimited\"]),\n monthlyCredits: z.number().min(0),\n priceUsd: z.number().min(0),\n features: z.array(z.string()),\n});\n\n/**\n * Schema for credit system configuration\n */\nconst creditSystemConfigSchema = z.object({\n /**\n * Operation costs in credits\n * Keys are operation type names (any non-empty string)\n * Values are positive numbers representing credit cost\n */\n operationCosts: z.record(\n z.string().min(1),\n z.number().positive()\n ),\n\n /**\n * Display labels for operation types (English canonical labels).\n * Falls back to auto-generating from snake_case keys if not provided.\n */\n operationLabels: z.record(\n z.string().min(1),\n z.string().min(1)\n ).optional().default({}),\n\n /**\n * Tier configurations\n */\n tierConfigs: z.record(\n z.enum([\"free\", \"basic\", \"premium\", \"unlimited\"]),\n tierConfigSchema\n ),\n\n /**\n * Reservation expiry time in milliseconds\n */\n reservationExpiryMs: z.number().positive().default(5 * 60 * 1000),\n\n /**\n * Default credits for new users (free tier)\n */\n defaultFreeCredits: z.number().positive().default(25),\n\n /**\n * Grace period in days after subscription expires before downgrade\n */\n subscriptionGracePeriodDays: z.number().min(0).default(3),\n\n /**\n * Low balance notification threshold\n */\n lowBalanceThreshold: z.number().min(0).default(10),\n\n /**\n * Cooldown in hours between low balance notifications\n */\n lowBalanceNotificationCooldownHours: z.number().positive().default(24),\n\n /**\n * Feature flags for optional features\n */\n features: z.object({\n journalEntries: z.boolean().default(true),\n notifications: z.boolean().default(true),\n subscriptionExpiry: z.boolean().default(true),\n usageHistory: z.boolean().default(true),\n }).default({\n journalEntries: true,\n notifications: true,\n subscriptionExpiry: true,\n usageHistory: true,\n }),\n});\n\n/**\n * Credit system configuration type\n */\nexport type CreditSystemConfig = z.infer<typeof creditSystemConfigSchema>;\n\n/**\n * Default configuration\n */\nconst DEFAULT_CONFIG: CreditSystemConfig = {\n operationCosts: {\n story_generation: 5,\n conversation: 2,\n image_generation: 10,\n template_generation: 10,\n },\n operationLabels: {\n story_generation: \"Story generation\",\n conversation: \"Conversation\",\n image_generation: \"Image generation\",\n template_generation: \"Template generation\",\n },\n tierConfigs: {\n free: {\n tier: \"free\",\n monthlyCredits: 25,\n priceUsd: 0,\n features: [\n \"25 credits per month\",\n \"Basic story generation\",\n \"Standard image quality\",\n ],\n },\n basic: {\n tier: \"basic\",\n monthlyCredits: 200,\n priceUsd: 9.99,\n features: [\n \"200 credits per month\",\n \"All story features\",\n \"High quality images\",\n \"Priority support\",\n ],\n },\n premium: {\n tier: \"premium\",\n monthlyCredits: 500,\n priceUsd: 19.99,\n features: [\n \"500 credits per month\",\n \"All features\",\n \"Highest quality images\",\n \"Early access to new features\",\n \"Priority support\",\n ],\n },\n unlimited: {\n tier: \"unlimited\",\n monthlyCredits: 0, // 0 = unlimited\n priceUsd: 49.99,\n features: [\n \"Unlimited credits\",\n \"All features\",\n \"Highest quality images\",\n \"Early access to new features\",\n \"Dedicated support\",\n ],\n },\n },\n reservationExpiryMs: 5 * 60 * 1000, // 5 minutes\n defaultFreeCredits: 25,\n subscriptionGracePeriodDays: 3,\n lowBalanceThreshold: 10,\n lowBalanceNotificationCooldownHours: 24,\n features: {\n journalEntries: true,\n notifications: true,\n subscriptionExpiry: true,\n usageHistory: true,\n },\n};\n\n/**\n * Current configuration (can be overridden)\n */\nlet currentConfig: CreditSystemConfig = DEFAULT_CONFIG;\n\n/**\n * Load configuration from environment variables\n * Can be extended to load from other sources (file, remote config, etc.)\n *\n * Operation costs are loaded from CREDITS_OPERATION_COSTS as JSON:\n * CREDITS_OPERATION_COSTS={\"story_generation\":5,\"image_generation\":10}\n */\nexport function loadConfigFromEnv(): Partial<CreditSystemConfig> {\n const envConfig: Partial<CreditSystemConfig> = {};\n\n // Load operation costs from env as JSON\n const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;\n if (operationCostsEnv) {\n try {\n const parsed = JSON.parse(operationCostsEnv);\n if (typeof parsed === \"object\" && parsed !== null) {\n envConfig.operationCosts = {\n ...DEFAULT_CONFIG.operationCosts,\n ...parsed,\n };\n }\n } catch {\n console.warn(\"[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON\");\n }\n }\n\n // Load other settings from env\n const reservationExpiry = process.env.CREDITS_RESERVATION_EXPIRY_MS;\n if (reservationExpiry) {\n envConfig.reservationExpiryMs = parseInt(reservationExpiry, 10);\n }\n\n const defaultCredits = process.env.CREDITS_DEFAULT_FREE;\n if (defaultCredits) {\n envConfig.defaultFreeCredits = parseInt(defaultCredits, 10);\n }\n\n const gracePeriod = process.env.CREDITS_GRACE_PERIOD_DAYS;\n if (gracePeriod) {\n envConfig.subscriptionGracePeriodDays = parseInt(gracePeriod, 10);\n }\n\n const lowBalanceThreshold = process.env.CREDITS_LOW_BALANCE_THRESHOLD;\n if (lowBalanceThreshold) {\n envConfig.lowBalanceThreshold = parseInt(lowBalanceThreshold, 10);\n }\n\n // Load feature flags\n const featuresEnv = process.env.CREDITS_FEATURES;\n if (featuresEnv) {\n try {\n const features = JSON.parse(featuresEnv);\n envConfig.features = {\n ...DEFAULT_CONFIG.features,\n ...features,\n };\n } catch {\n console.warn(\"[Credits Config] Failed to parse CREDITS_FEATURES env var\");\n }\n }\n\n return envConfig;\n}\n\n/**\n * Initialize configuration\n * Merges default config with environment overrides\n */\nexport function initializeConfig(overrides?: Partial<CreditSystemConfig>): CreditSystemConfig {\n const envConfig = loadConfigFromEnv();\n\n const mergedConfig = {\n ...DEFAULT_CONFIG,\n ...envConfig,\n ...overrides,\n operationCosts: {\n ...DEFAULT_CONFIG.operationCosts,\n ...envConfig.operationCosts,\n ...overrides?.operationCosts,\n },\n operationLabels: {\n ...DEFAULT_CONFIG.operationLabels,\n ...envConfig.operationLabels,\n ...overrides?.operationLabels,\n },\n tierConfigs: {\n ...DEFAULT_CONFIG.tierConfigs,\n ...envConfig.tierConfigs,\n ...overrides?.tierConfigs,\n },\n features: {\n ...DEFAULT_CONFIG.features,\n ...envConfig.features,\n ...overrides?.features,\n },\n };\n\n // Validate configuration\n const result = creditSystemConfigSchema.safeParse(mergedConfig);\n if (!result.success) {\n console.error(\"[Credits Config] Invalid configuration:\", result.error.issues);\n throw new Error(\"Invalid credit system configuration\");\n }\n\n currentConfig = result.data;\n return currentConfig;\n}\n\n/**\n * Get the current configuration.\n * If an external provider is registered, delegates to it.\n * Otherwise falls back to the local currentConfig.\n */\nexport function getConfig(): CreditSystemConfig {\n const provider = getConfigProvider();\n if (provider) {\n return provider.getConfig();\n }\n return currentConfig;\n}\n\n/**\n * Get all valid operation types from configuration\n * Returns the keys of the operationCosts object\n */\nexport function getValidOperationTypes(): string[] {\n return Object.keys(getConfig().operationCosts);\n}\n\n/**\n * Check if an operation type is valid (configured in the system)\n * @param type - The operation type to check\n * @returns true if the operation type is configured\n */\nexport function isValidOperationType(type: string): boolean {\n return type in getConfig().operationCosts;\n}\n\n/**\n * Get operation cost from configuration\n * @throws Error if the operation type is not configured\n */\nexport function getConfigOperationCost(operationType: string): number {\n const cost = getConfig().operationCosts[operationType];\n if (cost === undefined) {\n throw new Error(\n `Unknown operation type: ${operationType}. Valid types: ${getValidOperationTypes().join(\", \")}`\n );\n }\n return cost;\n}\n\n/**\n * Get tier configuration from configuration\n */\nexport function getConfigTierConfig(tier: SubscriptionTier): TierConfig {\n return getConfig().tierConfigs[tier]!;\n}\n\n/**\n * Get monthly limit for a tier from configuration\n * Returns Infinity for unlimited tier\n */\nexport function getConfigMonthlyLimit(tier: SubscriptionTier): number {\n const config = getConfig().tierConfigs[tier]!;\n return config.monthlyCredits === 0 ? Infinity : config.monthlyCredits;\n}\n\n/**\n * Check if a feature is enabled\n */\nexport function isFeatureEnabled(feature: keyof CreditSystemConfig[\"features\"]): boolean {\n return getConfig().features[feature];\n}\n\n/**\n * Convert a snake_case string to Title Case.\n * E.g., \"story_generation\" -> \"Story Generation\"\n */\nfunction snakeCaseToTitleCase(str: string): string {\n return str\n .replace(/_/g, \" \")\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Get the display label for an operation type.\n * Falls back to converting snake_case to Title Case if no label is configured.\n */\nexport function getOperationLabel(operationType: string): string {\n const labels = getConfig().operationLabels;\n if (labels && operationType in labels) {\n return labels[operationType];\n }\n return snakeCaseToTitleCase(operationType);\n}\n\n/**\n * Get all operation labels as a record.\n * For any operation in operationCosts that lacks a label, generates one from the key.\n */\nexport function getOperationLabels(): Record<string, string> {\n const config = getConfig();\n const result: Record<string, string> = {};\n for (const key of Object.keys(config.operationCosts)) {\n result[key] = config.operationLabels?.[key]\n ?? snakeCaseToTitleCase(key);\n }\n return result;\n}\n\n/**\n * Reset configuration to defaults (for testing)\n */\nexport function resetConfig(): void {\n currentConfig = DEFAULT_CONFIG;\n}\n\n// Initialize configuration on module load\ninitializeConfig();\n","import type { CreditSystemConfig } from \"./index.js\";\n\n/** Interface for external config providers (e.g. Firestore-backed) */\nexport interface ICreditConfigProvider {\n getConfig(): CreditSystemConfig;\n}\n\nlet configProvider: ICreditConfigProvider | null = null;\n\n/** Register an external config provider. Called once at app startup. */\nexport function registerConfigProvider(provider: ICreditConfigProvider): void {\n configProvider = provider;\n}\n\n/** Get the registered provider, or null if none registered. */\nexport function getConfigProvider(): ICreditConfigProvider | null {\n return configProvider;\n}\n\n/** Clear registered provider (for testing). */\nexport function clearConfigProvider(): void {\n configProvider = null;\n}\n","/**\n * Core credit operations - framework agnostic\n *\n * Contains the business logic for credit operations that can be\n * used by any adapter or service implementation.\n */\n\nimport type { ICreditRepository } from \"../repository/types.js\";\nimport type { PortableReservation } from \"./types.js\";\nimport { getOperationLabel } from \"../config/index.js\";\n\n/**\n * Commit a reservation with journal entry\n *\n * @param repository - The credit repository\n * @param userId - User ID\n * @param reservationId - Reservation to commit\n */\nexport async function commitReservationWithJournal(\n repository: ICreditRepository,\n userId: string,\n reservationId: string\n): Promise<void> {\n // Get the reservation to know the amount\n const reservation = await repository.getReservation(userId, reservationId);\n if (!reservation) {\n throw new Error(`Reservation ${reservationId} not found`);\n }\n\n // Commit the reservation atomically\n await repository.commitReservationAtomic(userId, reservationId);\n\n // Create journal entry\n const credits = await repository.getUserCredits(userId);\n if (credits) {\n await repository.createJournalEntry({\n userId,\n entryType: \"debit\",\n amount: reservation.amount,\n balanceAfter: credits.balance,\n source: \"operation_commit\",\n referenceId: reservationId,\n referenceType: \"reservation\",\n description: `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,\n metadata: {\n operationType: reservation.operationType,\n },\n });\n }\n}\n\n/**\n * Release a reservation with journal entry\n *\n * @param repository - The credit repository\n * @param userId - User ID\n * @param reservationId - Reservation to release\n */\nexport async function releaseReservationWithJournal(\n repository: ICreditRepository,\n userId: string,\n reservationId: string\n): Promise<void> {\n // Get the reservation to check its state\n const reservation = await repository.getReservation(userId, reservationId);\n\n // Release the reservation atomically\n await repository.releaseReservationAtomic(userId, reservationId);\n\n // Create journal entry only if reservation was in reserved state\n if (reservation?.status === \"reserved\") {\n const credits = await repository.getUserCredits(userId);\n if (credits) {\n await repository.createJournalEntry({\n userId,\n entryType: \"credit\",\n amount: 0, // No actual credits returned (they were reserved, not spent)\n balanceAfter: credits.balance,\n source: \"operation_release\",\n referenceId: reservationId,\n referenceType: \"reservation\",\n description: `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,\n metadata: {\n operationType: reservation.operationType,\n amount: reservation.amount,\n },\n });\n }\n }\n}\n\n/**\n * Reserve credits for an operation\n *\n * @param repository - The credit repository\n * @param userId - User ID\n * @param amount - Credits to reserve\n * @param operationType - Type of operation\n * @param expiryMs - Reservation expiry time in milliseconds\n * @returns The reservation\n */\nexport async function reserveCreditsForOperation(\n repository: ICreditRepository,\n userId: string,\n amount: number,\n operationType: string,\n expiryMs: number = 5 * 60 * 1000\n): Promise<PortableReservation> {\n const expiresAt = new Date(Date.now() + expiryMs);\n return repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/types.ts","../../src/core/errors.ts","../../src/core/deferred.ts","../../src/config/index.ts","../../src/config/provider.ts","../../src/core/operations.ts"],"sourcesContent":["/**\r\n * Portable credits types - framework and database agnostic\r\n *\r\n * These types use standard JavaScript types (string, Date) instead of\r\n * Firebase-specific types (Timestamp). This makes the credits system\r\n * portable to other environments without Firebase dependencies.\r\n *\r\n * Note: Firestore Timestamp is handled by the credits-firestore adapter.\r\n */\r\n\r\n// ==================== Utility Functions ====================\r\n\r\n/**\r\n * Calculate available credits from balance, bonus, and reserved amounts.\r\n *\r\n * Available = balance + bonusCredits - reserved\r\n *\r\n * @param balance - Regular monthly credits\r\n * @param bonusCredits - Purchased/admin credits\r\n * @param reserved - Credits locked for in-flight operations\r\n * @returns Available credits for new operations\r\n */\r\nexport function calculateAvailableCredits(\r\n balance: number,\r\n bonusCredits: number,\r\n reserved: number\r\n): number {\r\n return balance + bonusCredits - reserved;\r\n}\r\n\r\n/**\r\n * Convert various timestamp formats to ISO string.\r\n *\r\n * Handles:\r\n * - Date objects\r\n * - ISO string (returned as-is)\r\n * - Timestamp-like objects with toDate() method (Firebase)\r\n * - undefined/null (returns current time)\r\n *\r\n * @param value - Value to convert\r\n * @returns ISO 8601 string\r\n */\r\nexport function toPortableTimestamp(value: unknown): string {\r\n if (!value) {\r\n return new Date().toISOString();\r\n }\r\n\r\n if (typeof value === \"string\") {\r\n return value;\r\n }\r\n\r\n if (value instanceof Date) {\r\n return value.toISOString();\r\n }\r\n\r\n if (\r\n typeof value === \"object\" &&\r\n value !== null &&\r\n \"toDate\" in value &&\r\n typeof (value as { toDate: () => Date }).toDate === \"function\"\r\n ) {\r\n return (value as { toDate: () => Date }).toDate().toISOString();\r\n }\r\n\r\n return new Date().toISOString();\r\n}\r\n\r\n/**\r\n * Convert various timestamp formats to Date object.\r\n *\r\n * Handles:\r\n * - Date objects (returned as-is)\r\n * - ISO string\r\n * - Timestamp-like objects with toDate() method (Firebase)\r\n * - Invalid values (returns current date)\r\n *\r\n * @param value - Value to convert\r\n * @returns Date object\r\n */\r\nexport function toDate(value: unknown): Date {\r\n if (value instanceof Date) {\r\n return value;\r\n }\r\n\r\n if (typeof value === \"string\") {\r\n return new Date(value);\r\n }\r\n\r\n if (\r\n typeof value === \"object\" &&\r\n value !== null &&\r\n \"toDate\" in value &&\r\n typeof (value as { toDate: () => Date }).toDate === \"function\"\r\n ) {\r\n return (value as { toDate: () => Date }).toDate();\r\n }\r\n\r\n return new Date();\r\n}\r\n\r\n// ==================== Subscription Types ====================\r\n\r\n/**\r\n * Built-in tier ids (autocomplete) — apps may add more via config.\r\n */\r\nexport type BuiltinTier = \"free\" | \"basic\" | \"premium\" | \"unlimited\";\r\n\r\n/**\r\n * Subscription tiers for the credits system.\r\n *\r\n * Any built-in OR any config-defined tier id. The `(string & {})` keeps builtin\r\n * autocomplete while accepting arbitrary configured tiers.\r\n */\r\nexport type SubscriptionTier = BuiltinTier | (string & {});\r\n\r\n// ==================== Operation Types ====================\r\n\r\n/**\r\n * Operation types for cost tracking (provider-agnostic)\r\n *\r\n * Dynamic string type - any operation configured in the system is valid.\r\n */\r\nexport type CreditOperationType = string;\r\n\r\n/**\r\n * AI providers\r\n */\r\nexport type AIProviderType = \"gemini\";\r\n\r\n/**\r\n * Resource type for usage logging\r\n */\r\nexport type ResourceType = string;\r\n\r\n// ==================== User Credits ====================\r\n\r\n/**\r\n * Portable user credits balance\r\n *\r\n * Uses ISO string timestamps instead of Firebase Timestamp.\r\n * This type is safe for JSON serialization and works across frameworks.\r\n */\r\nexport interface PortableUserCredits {\r\n userId: string;\r\n /** Current available balance (monthly credits, reset each month) */\r\n balance: number;\r\n /** Bonus credits from purchases/admin (never reset, persist until used) */\r\n bonusCredits: number;\r\n /** Credits currently reserved for in-flight operations */\r\n reserved: number;\r\n /** User's subscription tier */\r\n tier: SubscriptionTier;\r\n /** Monthly credit limit based on tier (0 = unlimited) */\r\n monthlyLimit: number;\r\n /** Credits used this month (resets monthly) */\r\n monthlyUsed: number;\r\n /** When monthly credits reset (start of next month) - ISO 8601 */\r\n monthlyResetAt: string;\r\n /** Subscription expiry (null/undefined for free tier) - ISO 8601 or null */\r\n subscriptionExpiresAt?: string | null;\r\n /** Creation timestamp - ISO 8601 */\r\n createdAt: string;\r\n /** Last update timestamp - ISO 8601 */\r\n updatedAt: string;\r\n}\r\n\r\n// ==================== Reservation Types ====================\r\n\r\n/**\r\n * Credit reservation status for two-phase commit\r\n */\r\nexport type ReservationStatus = \"reserved\" | \"committed\" | \"released\" | \"expired\";\r\n\r\n/**\r\n * Portable credit reservation\r\n *\r\n * Used for two-phase commit to prevent double-spending.\r\n */\r\nexport interface PortableReservation {\r\n id: string;\r\n userId: string;\r\n /** Amount of credits reserved */\r\n amount: number;\r\n /** Operation type for tracking */\r\n operationType: CreditOperationType;\r\n /** Current status of reservation */\r\n status: ReservationStatus;\r\n /** When reservation was created - ISO 8601 */\r\n createdAt: string;\r\n /** When reservation expires (for cleanup) - ISO 8601 */\r\n expiresAt: string;\r\n /** When reservation was committed/released - ISO 8601 or undefined */\r\n completedAt?: string;\r\n}\r\n\r\n// ==================== Result Types ====================\r\n\r\n/**\r\n * Result of a credit check operation\r\n */\r\nexport interface CreditCheckResult {\r\n /** Whether user has sufficient credits */\r\n hasCredits: boolean;\r\n /** Current balance (balance + bonusCredits) */\r\n balance: number;\r\n /** Required credits for operation */\r\n required: number;\r\n /** Shortfall if insufficient */\r\n shortfall: number;\r\n}\r\n\r\n/**\r\n * Result of monthly reset operation\r\n */\r\nexport interface MonthlyResetResult {\r\n /** Whether the reset was performed */\r\n wasReset: boolean;\r\n /** The user credits after the operation */\r\n credits: PortableUserCredits;\r\n}\r\n\r\n/**\r\n * Result of subscription expiry check\r\n */\r\nexport interface SubscriptionExpiryResult {\r\n /** Whether the subscription was downgraded */\r\n wasDowngraded: boolean;\r\n /** Whether user is in grace period */\r\n inGracePeriod: boolean;\r\n /** Days remaining in grace period (0 if not in grace) */\r\n graceDaysRemaining: number;\r\n /** The user credits after the operation */\r\n credits: PortableUserCredits;\r\n}\r\n\r\n// ==================== Transaction Types ====================\r\n\r\n/**\r\n * Credit transaction type\r\n */\r\nexport type TransactionType = \"purchase\" | \"subscription\" | \"bonus\" | \"refund\" | \"adjustment\";\r\n\r\n/**\r\n * Portable credit transaction\r\n */\r\nexport interface PortableTransaction {\r\n id: string;\r\n userId: string;\r\n type: TransactionType;\r\n /** Amount of credits (positive for additions, negative for deductions) */\r\n amount: number;\r\n description: string;\r\n /** Reference to payment provider */\r\n paymentRef?: string;\r\n previousBalance: number;\r\n newBalance: number;\r\n createdAt: string;\r\n}\r\n\r\n// ==================== Journal Types ====================\r\n\r\n/**\r\n * Source of a credit journal entry\r\n */\r\nexport type CreditSource =\r\n | \"operation_commit\"\r\n | \"operation_release\"\r\n | \"purchase\"\r\n | \"subscription_grant\"\r\n | \"subscription_upgrade\"\r\n | \"subscription_downgrade\"\r\n | \"monthly_reset\"\r\n | \"bonus\"\r\n | \"refund\"\r\n | \"admin_adjustment\"\r\n | \"expiry\"\r\n | \"reservation_expired\";\r\n\r\n/**\r\n * Type of reference for a journal entry\r\n */\r\nexport type JournalReferenceType =\r\n | \"reservation\"\r\n | \"transaction\"\r\n | \"adjustment\"\r\n | \"reset\"\r\n | \"subscription\";\r\n\r\n/**\r\n * Portable credit journal entry for audit trail\r\n */\r\nexport interface PortableJournalEntry {\r\n id: string;\r\n userId: string;\r\n /** Type of entry - debit decreases balance, credit increases balance */\r\n entryType: \"debit\" | \"credit\";\r\n /** Amount of credits involved */\r\n amount: number;\r\n /** Balance after this entry */\r\n balanceAfter: number;\r\n /** Source of the credit change */\r\n source: CreditSource;\r\n /** Reference ID (e.g., reservationId, transactionId) */\r\n referenceId: string;\r\n /** Type of reference */\r\n referenceType: JournalReferenceType;\r\n /** Human-readable description */\r\n description: string;\r\n /** Additional metadata */\r\n metadata?: Record<string, unknown>;\r\n /** When this entry was created - ISO 8601 */\r\n createdAt: string;\r\n}\r\n\r\n// ==================== Usage Types ====================\r\n\r\n/**\r\n * Portable usage log entry\r\n */\r\nexport interface PortableUsageLog {\r\n id: string;\r\n userId: string;\r\n operationType: CreditOperationType;\r\n provider: AIProviderType;\r\n creditsUsed: number;\r\n success: boolean;\r\n errorMessage?: string;\r\n resourceId?: string;\r\n resourceType?: ResourceType;\r\n requestId?: string;\r\n metadata?: Record<string, unknown>;\r\n createdAt: string;\r\n}\r\n\r\n/**\r\n * User-friendly usage history entry\r\n */\r\nexport interface UsageHistoryEntry {\r\n id: string;\r\n /** Type of entry for display */\r\n type: \"usage\" | \"purchase\" | \"bonus\" | \"reset\" | \"refund\" | \"adjustment\";\r\n /** Positive = earned, Negative = spent */\r\n creditsChange: number;\r\n /** Balance after this entry */\r\n balanceAfter: number;\r\n /** Human-readable description */\r\n description: string;\r\n /** When this occurred - ISO 8601 */\r\n createdAt: string;\r\n}\r\n\r\n/**\r\n * Paginated usage history response\r\n */\r\nexport interface UsageHistoryResponse {\r\n entries: UsageHistoryEntry[];\r\n pagination: {\r\n total: number;\r\n limit: number;\r\n offset: number;\r\n hasMore: boolean;\r\n };\r\n}\r\n\r\n// ==================== Tier Configuration ====================\r\n\r\n/**\r\n * Tier configuration for credit limits and pricing\r\n */\r\nexport interface TierConfig {\r\n tier: SubscriptionTier;\r\n monthlyCredits: number;\r\n priceUsd: number;\r\n features: string[];\r\n /** Free/default tier marker — no subscription expiry, balance untouched on tier change. Defaults to priceUsd === 0. */\r\n isFree?: boolean;\r\n /** Unlimited tier marker. Defaults to monthlyCredits === 0. */\r\n unlimited?: boolean;\r\n /** Tier assigned to brand-new users. Defaults to the tier flagged isFree. */\r\n isDefault?: boolean;\r\n}\r\n\r\n// ==================== Options Types ====================\r\n\r\n/**\r\n * Options for withCredits HOF\r\n */\r\nexport interface WithCreditsOptions {\r\n /** Operation type for cost lookup */\r\n operationType: CreditOperationType;\r\n /** AI provider (defaults to \"gemini\") */\r\n provider?: AIProviderType;\r\n /** Custom cost override (use with caution) */\r\n customCost?: number;\r\n /** Resource ID for usage logging */\r\n resourceId?: string;\r\n /** Resource type for usage logging */\r\n resourceType?: ResourceType;\r\n}\r\n\r\n// ==================== Constants ====================\r\n","/**\r\n * 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} 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: 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 * Deferred execution abstraction - framework agnostic\r\n *\r\n * Provides a way to schedule work to run after the current operation completes.\r\n * Different frameworks can provide their own implementations:\r\n * - Next.js: uses `after()` from `next/server`\r\n * - Generic: uses `setImmediate()` or `setTimeout()`\r\n * - Testing: can use synchronous execution\r\n */\r\n\r\n/**\r\n * Interface for deferred execution\r\n *\r\n * Implementations should schedule the function to run asynchronously\r\n * without blocking the current operation.\r\n */\r\nexport interface DeferredExecutor {\r\n /**\r\n * Schedule a function to run asynchronously\r\n *\r\n * The function should be executed after the current operation completes.\r\n * Errors should be caught and logged, not propagated.\r\n *\r\n * @param fn - Async function to execute\r\n */\r\n defer(fn: () => Promise<void>): void;\r\n}\r\n\r\n/**\r\n * Generic deferred executor using setImmediate/setTimeout\r\n *\r\n * Works in Node.js and browser environments.\r\n * Suitable for non-framework environments or testing.\r\n */\r\nexport const genericDeferred: DeferredExecutor = {\r\n defer(fn: () => Promise<void>): void {\r\n // Use setImmediate in Node.js, setTimeout as fallback\r\n const scheduler = typeof setImmediate !== \"undefined\" ? setImmediate : setTimeout;\r\n scheduler(() => {\r\n fn().catch((error) => {\r\n console.error(\"[Credits] Deferred task error:\", error);\r\n });\r\n });\r\n },\r\n};\r\n\r\n/**\r\n * Synchronous deferred executor\r\n *\r\n * Executes the function immediately (fire-and-forget).\r\n * Useful for testing or when deferred execution isn't needed.\r\n */\r\nexport const synchronousDeferred: DeferredExecutor = {\r\n defer(fn: () => Promise<void>): void {\r\n fn().catch((error) => {\r\n console.error(\"[Credits] Deferred task error:\", error);\r\n });\r\n },\r\n};\r\n\r\n/**\r\n * Create a custom deferred executor\r\n *\r\n * Allows frameworks to provide their own scheduling mechanism.\r\n *\r\n * @example\r\n * ```typescript\r\n * // Next.js adapter\r\n * import { after } from 'next/server';\r\n *\r\n * const nextJsDeferred = createDeferredExecutor((fn) => after(fn));\r\n * ```\r\n *\r\n * @param schedule - Function that schedules async work\r\n * @returns DeferredExecutor instance\r\n */\r\nexport function createDeferredExecutor(\r\n schedule: (fn: () => Promise<void>) => void\r\n): DeferredExecutor {\r\n return {\r\n defer: schedule,\r\n };\r\n}\r\n\r\n/**\r\n * No-op deferred executor\r\n *\r\n * Discards deferred work. Useful when you want to disable\r\n * background tasks (e.g., in certain test scenarios).\r\n */\r\nexport const noopDeferred: DeferredExecutor = {\r\n defer(_fn: () => Promise<void>): void {\r\n // Intentionally do nothing\r\n },\r\n};\r\n","import { z } from \"zod\";\r\nimport type { SubscriptionTier, TierConfig } from \"../core/types.js\";\r\nimport { getConfigProvider } from \"./provider.js\";\r\n\r\n/**\r\n * Schema for tier configuration\r\n */\r\nconst tierConfigSchema = z.object({\r\n tier: z.string().min(1),\r\n monthlyCredits: z.number().min(0),\r\n priceUsd: z.number().min(0),\r\n features: z.array(z.string()),\r\n isFree: z.boolean().optional(),\r\n unlimited: z.boolean().optional(),\r\n isDefault: z.boolean().optional(),\r\n});\r\n\r\n/**\r\n * Schema for credit system configuration\r\n */\r\nconst creditSystemConfigSchema = z.object({\r\n /**\r\n * Operation costs in credits\r\n * Keys are operation type names (any non-empty string)\r\n * Values are positive numbers representing credit cost\r\n */\r\n operationCosts: z.record(\r\n z.string().min(1),\r\n z.number().positive()\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 * 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","/**\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 { getOperationLabel } from \"../config/index.js\";\r\n\r\n/**\r\n * Commit 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 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 // Get the reservation to know the amount\r\n const reservation = await repository.getReservation(userId, reservationId);\r\n if (!reservation) {\r\n throw new Error(`Reservation ${reservationId} not found`);\r\n }\r\n\r\n // Commit the reservation atomically\r\n await repository.commitReservationAtomic(userId, reservationId);\r\n\r\n // Create journal entry\r\n const credits = await repository.getUserCredits(userId);\r\n if (credits) {\r\n await repository.createJournalEntry({\r\n userId,\r\n entryType: \"debit\",\r\n amount: reservation.amount,\r\n balanceAfter: credits.balance,\r\n source: \"operation_commit\",\r\n referenceId: reservationId,\r\n referenceType: \"reservation\",\r\n description: `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,\r\n metadata: {\r\n operationType: reservation.operationType,\r\n },\r\n });\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 // Get the reservation to check its state\r\n const reservation = await repository.getReservation(userId, reservationId);\r\n\r\n // Release the reservation atomically\r\n await repository.releaseReservationAtomic(userId, reservationId);\r\n\r\n // Create journal entry only if reservation was in reserved state\r\n if (reservation?.status === \"reserved\") {\r\n const credits = await repository.getUserCredits(userId);\r\n if (credits) {\r\n await repository.createJournalEntry({\r\n userId,\r\n entryType: \"credit\",\r\n amount: 0, // No actual credits returned (they were reserved, not spent)\r\n balanceAfter: credits.balance,\r\n source: \"operation_release\",\r\n referenceId: reservationId,\r\n referenceType: \"reservation\",\r\n description: `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,\r\n metadata: {\r\n operationType: reservation.operationType,\r\n amount: reservation.amount,\r\n },\r\n });\r\n }\r\n }\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 * @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): Promise<PortableReservation> {\r\n const expiresAt = new Date(Date.now() + expiryMs);\r\n return repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);\r\n}\r\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(src_exports, {
|
|
|
37
37
|
SDKReservationExpiredError: () => ReservationExpiredError,
|
|
38
38
|
SDKReservationNotFoundError: () => ReservationNotFoundError,
|
|
39
39
|
ServerError: () => ServerError,
|
|
40
|
+
UNLIMITED_BALANCE_SENTINEL: () => UNLIMITED_BALANCE_SENTINEL,
|
|
40
41
|
ValidationError: () => ValidationError,
|
|
41
42
|
calculateAvailableCredits: () => calculateAvailableCredits,
|
|
42
43
|
checkAndNotifyLowBalance: () => checkAndNotifyLowBalance,
|
|
@@ -65,6 +66,7 @@ __export(src_exports, {
|
|
|
65
66
|
getConfigProvider: () => getConfigProvider,
|
|
66
67
|
getConfigTierConfig: () => getConfigTierConfig,
|
|
67
68
|
getDefaultFreeCredits: () => getDefaultFreeCredits,
|
|
69
|
+
getDefaultTier: () => getDefaultTier,
|
|
68
70
|
getMonthlyLimit: () => getMonthlyLimit,
|
|
69
71
|
getNextMonthlyReset: () => getNextMonthlyReset,
|
|
70
72
|
getOperationCost: () => getOperationCost,
|
|
@@ -75,26 +77,33 @@ __export(src_exports, {
|
|
|
75
77
|
getReservationExpiryMs: () => getReservationExpiryMs,
|
|
76
78
|
getTierConfig: () => getTierConfig,
|
|
77
79
|
getTotalCredits: () => getTotalCredits,
|
|
80
|
+
getUnlimitedSentinelBalance: () => getUnlimitedSentinelBalance,
|
|
78
81
|
getValidOperationTypes: () => getValidOperationTypes,
|
|
82
|
+
getValidTiers: () => getValidTiers,
|
|
79
83
|
initializeConfig: () => initializeConfig,
|
|
80
84
|
isCreditError: () => isCreditError,
|
|
81
85
|
isCreditErrorMessage: () => isCreditErrorMessage,
|
|
82
86
|
isFeatureEnabled: () => isFeatureEnabled,
|
|
87
|
+
isFreeTier: () => isFreeTier,
|
|
83
88
|
isInsufficientCreditsError: () => isInsufficientCreditsError,
|
|
84
89
|
isPreviewMode: () => isPreviewMode2,
|
|
90
|
+
isUnlimitedTier: () => isUnlimitedTier,
|
|
85
91
|
isValidOperationType: () => isValidOperationType,
|
|
92
|
+
isValidTier: () => isValidTier,
|
|
86
93
|
loadConfigFromEnv: () => loadConfigFromEnv,
|
|
87
94
|
noopDeferred: () => noopDeferred,
|
|
88
95
|
notifySubscriptionExpired: () => notifySubscriptionExpired,
|
|
89
96
|
notifySubscriptionExpiring: () => notifySubscriptionExpiring,
|
|
90
97
|
parseApiError: () => parseApiError,
|
|
91
98
|
parseCreditError: () => parseCreditError,
|
|
99
|
+
parseTier: () => parseTier,
|
|
92
100
|
registerConfigProvider: () => registerConfigProvider,
|
|
93
101
|
registerNotificationHandler: () => registerNotificationHandler,
|
|
94
102
|
releaseReservationWithJournal: () => releaseReservationWithJournal,
|
|
95
103
|
reserveCreditsForOperation: () => reserveCreditsForOperation,
|
|
96
104
|
resetConfig: () => resetConfig,
|
|
97
105
|
synchronousDeferred: () => synchronousDeferred,
|
|
106
|
+
tierSchema: () => tierSchema,
|
|
98
107
|
toClientUserCredits: () => toClientUserCredits,
|
|
99
108
|
toDate: () => toDate,
|
|
100
109
|
toPortableTimestamp: () => toPortableTimestamp,
|
|
@@ -270,10 +279,13 @@ function clearConfigProvider() {
|
|
|
270
279
|
|
|
271
280
|
// src/config/index.ts
|
|
272
281
|
var tierConfigSchema = import_zod.z.object({
|
|
273
|
-
tier: import_zod.z.
|
|
282
|
+
tier: import_zod.z.string().min(1),
|
|
274
283
|
monthlyCredits: import_zod.z.number().min(0),
|
|
275
284
|
priceUsd: import_zod.z.number().min(0),
|
|
276
|
-
features: import_zod.z.array(import_zod.z.string())
|
|
285
|
+
features: import_zod.z.array(import_zod.z.string()),
|
|
286
|
+
isFree: import_zod.z.boolean().optional(),
|
|
287
|
+
unlimited: import_zod.z.boolean().optional(),
|
|
288
|
+
isDefault: import_zod.z.boolean().optional()
|
|
277
289
|
});
|
|
278
290
|
var creditSystemConfigSchema = import_zod.z.object({
|
|
279
291
|
/**
|
|
@@ -297,7 +309,7 @@ var creditSystemConfigSchema = import_zod.z.object({
|
|
|
297
309
|
* Tier configurations
|
|
298
310
|
*/
|
|
299
311
|
tierConfigs: import_zod.z.record(
|
|
300
|
-
import_zod.z.
|
|
312
|
+
import_zod.z.string().min(1),
|
|
301
313
|
tierConfigSchema
|
|
302
314
|
),
|
|
303
315
|
/**
|
|
@@ -353,6 +365,8 @@ var DEFAULT_CONFIG = {
|
|
|
353
365
|
tier: "free",
|
|
354
366
|
monthlyCredits: 25,
|
|
355
367
|
priceUsd: 0,
|
|
368
|
+
isFree: true,
|
|
369
|
+
isDefault: true,
|
|
356
370
|
features: [
|
|
357
371
|
"25 credits per month",
|
|
358
372
|
"Basic story generation",
|
|
@@ -386,6 +400,7 @@ var DEFAULT_CONFIG = {
|
|
|
386
400
|
tier: "unlimited",
|
|
387
401
|
monthlyCredits: 0,
|
|
388
402
|
// 0 = unlimited
|
|
403
|
+
unlimited: true,
|
|
389
404
|
priceUsd: 49.99,
|
|
390
405
|
features: [
|
|
391
406
|
"Unlimited credits",
|
|
@@ -513,12 +528,46 @@ function getConfigOperationCost(operationType) {
|
|
|
513
528
|
}
|
|
514
529
|
return cost;
|
|
515
530
|
}
|
|
531
|
+
function getValidTiers() {
|
|
532
|
+
return Object.keys(getConfig().tierConfigs);
|
|
533
|
+
}
|
|
534
|
+
function isValidTier(tier) {
|
|
535
|
+
return tier in getConfig().tierConfigs;
|
|
536
|
+
}
|
|
537
|
+
function isFreeTier(tier) {
|
|
538
|
+
const c = getConfig().tierConfigs[tier];
|
|
539
|
+
if (!c) return false;
|
|
540
|
+
return c.isFree ?? c.priceUsd === 0;
|
|
541
|
+
}
|
|
542
|
+
function isUnlimitedTier(tier) {
|
|
543
|
+
const c = getConfig().tierConfigs[tier];
|
|
544
|
+
if (!c) return false;
|
|
545
|
+
return c.unlimited ?? c.monthlyCredits === 0;
|
|
546
|
+
}
|
|
547
|
+
function getDefaultTier() {
|
|
548
|
+
const entries = Object.entries(getConfig().tierConfigs);
|
|
549
|
+
const def = entries.find(([, c]) => c.isDefault)?.[0] ?? entries.find(([, c]) => c.isFree ?? c.priceUsd === 0)?.[0];
|
|
550
|
+
return def ?? "free";
|
|
551
|
+
}
|
|
552
|
+
var UNLIMITED_BALANCE_SENTINEL = 999999;
|
|
553
|
+
function getUnlimitedSentinelBalance() {
|
|
554
|
+
return UNLIMITED_BALANCE_SENTINEL;
|
|
555
|
+
}
|
|
516
556
|
function getConfigTierConfig(tier) {
|
|
517
|
-
|
|
557
|
+
const cfg = getConfig().tierConfigs[tier];
|
|
558
|
+
if (cfg) return cfg;
|
|
559
|
+
const fallback = getDefaultTier();
|
|
560
|
+
console.warn(
|
|
561
|
+
`[Credits Config] Unknown tier "${tier}". Falling back to "${fallback}". Valid: ${getValidTiers().join(", ")}`
|
|
562
|
+
);
|
|
563
|
+
return getConfig().tierConfigs[fallback];
|
|
518
564
|
}
|
|
519
565
|
function getConfigMonthlyLimit(tier) {
|
|
520
|
-
|
|
521
|
-
|
|
566
|
+
return isUnlimitedTier(tier) ? Infinity : getConfigTierConfig(tier).monthlyCredits;
|
|
567
|
+
}
|
|
568
|
+
var tierSchema = import_zod.z.string().refine(isValidTier, { message: "Unknown subscription tier" });
|
|
569
|
+
function parseTier(value) {
|
|
570
|
+
return tierSchema.parse(value);
|
|
522
571
|
}
|
|
523
572
|
function isFeatureEnabled(feature) {
|
|
524
573
|
return getConfig().features[feature];
|
|
@@ -830,6 +879,9 @@ var InMemoryCreditRepository = class {
|
|
|
830
879
|
if (!reservation) {
|
|
831
880
|
throw new Error(`Reservation ${reservationId} not found`);
|
|
832
881
|
}
|
|
882
|
+
if (reservation.status === "committed") {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
833
885
|
if (reservation.status !== "reserved") {
|
|
834
886
|
throw new Error(
|
|
835
887
|
`Cannot commit reservation in ${reservation.status} state`
|
|
@@ -1090,7 +1142,7 @@ var InMemoryCreditRepository = class {
|
|
|
1090
1142
|
if (!credits) {
|
|
1091
1143
|
throw new Error(`User ${userId} not found`);
|
|
1092
1144
|
}
|
|
1093
|
-
if (credits.tier
|
|
1145
|
+
if (isFreeTier(credits.tier) || !credits.subscriptionExpiresAt) {
|
|
1094
1146
|
return {
|
|
1095
1147
|
wasDowngraded: false,
|
|
1096
1148
|
inGracePeriod: false,
|
|
@@ -1117,11 +1169,11 @@ var InMemoryCreditRepository = class {
|
|
|
1117
1169
|
credits
|
|
1118
1170
|
};
|
|
1119
1171
|
}
|
|
1120
|
-
const
|
|
1121
|
-
const
|
|
1122
|
-
credits.tier =
|
|
1123
|
-
credits.monthlyLimit =
|
|
1124
|
-
credits.balance = Math.min(credits.balance,
|
|
1172
|
+
const defaultTier = getDefaultTier();
|
|
1173
|
+
const defaultTierConfig = getConfigTierConfig(defaultTier);
|
|
1174
|
+
credits.tier = defaultTier;
|
|
1175
|
+
credits.monthlyLimit = defaultTierConfig.monthlyCredits;
|
|
1176
|
+
credits.balance = Math.min(credits.balance, defaultTierConfig.monthlyCredits);
|
|
1125
1177
|
credits.subscriptionExpiresAt = null;
|
|
1126
1178
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1127
1179
|
this.users.set(userId, credits);
|
|
@@ -1261,7 +1313,7 @@ var CreditsService = class {
|
|
|
1261
1313
|
if (!data) {
|
|
1262
1314
|
return null;
|
|
1263
1315
|
}
|
|
1264
|
-
if (data.tier
|
|
1316
|
+
if (!isFreeTier(data.tier) && data.subscriptionExpiresAt) {
|
|
1265
1317
|
const expiryResult = await this.repository.checkAndHandleSubscriptionExpiry(
|
|
1266
1318
|
userId,
|
|
1267
1319
|
getConfig().subscriptionGracePeriodDays
|
|
@@ -1330,7 +1382,7 @@ var CreditsService = class {
|
|
|
1330
1382
|
async initializeUserCredits(userId) {
|
|
1331
1383
|
const credits = await this.repository.initializeUserCredits(
|
|
1332
1384
|
userId,
|
|
1333
|
-
|
|
1385
|
+
getDefaultTier(),
|
|
1334
1386
|
getConfig().defaultFreeCredits
|
|
1335
1387
|
);
|
|
1336
1388
|
return toClientUserCredits(credits);
|
|
@@ -1372,11 +1424,13 @@ var CreditsService = class {
|
|
|
1372
1424
|
* @param userId - User ID
|
|
1373
1425
|
* @param amount - Credits to reserve
|
|
1374
1426
|
* @param operationType - Operation type for tracking
|
|
1427
|
+
* @param options - Optional settings (e.g. a custom `ttlMs` for long-running async jobs)
|
|
1375
1428
|
* @returns Reservation object
|
|
1376
1429
|
* @throws Error if insufficient credits
|
|
1377
1430
|
*/
|
|
1378
|
-
async reserveCredits(userId, amount, operationType) {
|
|
1379
|
-
const
|
|
1431
|
+
async reserveCredits(userId, amount, operationType, options) {
|
|
1432
|
+
const ttlMs = options?.ttlMs ?? getConfig().reservationExpiryMs;
|
|
1433
|
+
const expiresAt = new Date(Date.now() + ttlMs);
|
|
1380
1434
|
return this.repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);
|
|
1381
1435
|
}
|
|
1382
1436
|
/**
|
|
@@ -1389,6 +1443,9 @@ var CreditsService = class {
|
|
|
1389
1443
|
if (!reservation) {
|
|
1390
1444
|
throw new Error(`Reservation ${reservationId} not found`);
|
|
1391
1445
|
}
|
|
1446
|
+
if (reservation.status === "committed") {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1392
1449
|
await this.repository.commitReservationAtomic(userId, reservationId);
|
|
1393
1450
|
const credits = await this.repository.getUserCredits(userId);
|
|
1394
1451
|
if (credits) {
|
|
@@ -1465,12 +1522,15 @@ var CreditsService = class {
|
|
|
1465
1522
|
*/
|
|
1466
1523
|
async updateTier(userId, tier, expiresAt) {
|
|
1467
1524
|
const monthlyLimit = getConfigMonthlyLimit(tier);
|
|
1525
|
+
const unlimited = isUnlimitedTier(tier);
|
|
1526
|
+
const free = isFreeTier(tier);
|
|
1468
1527
|
await this.repository.updateUserTier(userId, {
|
|
1469
1528
|
tier,
|
|
1470
|
-
|
|
1529
|
+
// Stored monthlyLimit uses the 0-means-unlimited convention.
|
|
1530
|
+
monthlyLimit: unlimited ? 0 : monthlyLimit,
|
|
1471
1531
|
// Reset balance to new tier limit if upgrading
|
|
1472
|
-
balance:
|
|
1473
|
-
monthlyUsed:
|
|
1532
|
+
balance: !free ? unlimited ? getUnlimitedSentinelBalance() : monthlyLimit : void 0,
|
|
1533
|
+
monthlyUsed: !free ? 0 : void 0,
|
|
1474
1534
|
subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null
|
|
1475
1535
|
});
|
|
1476
1536
|
}
|
|
@@ -2439,6 +2499,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
2439
2499
|
SDKReservationExpiredError,
|
|
2440
2500
|
SDKReservationNotFoundError,
|
|
2441
2501
|
ServerError,
|
|
2502
|
+
UNLIMITED_BALANCE_SENTINEL,
|
|
2442
2503
|
ValidationError,
|
|
2443
2504
|
calculateAvailableCredits,
|
|
2444
2505
|
checkAndNotifyLowBalance,
|
|
@@ -2467,6 +2528,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
2467
2528
|
getConfigProvider,
|
|
2468
2529
|
getConfigTierConfig,
|
|
2469
2530
|
getDefaultFreeCredits,
|
|
2531
|
+
getDefaultTier,
|
|
2470
2532
|
getMonthlyLimit,
|
|
2471
2533
|
getNextMonthlyReset,
|
|
2472
2534
|
getOperationCost,
|
|
@@ -2477,26 +2539,33 @@ function createDummyReservation2(userId, operationType) {
|
|
|
2477
2539
|
getReservationExpiryMs,
|
|
2478
2540
|
getTierConfig,
|
|
2479
2541
|
getTotalCredits,
|
|
2542
|
+
getUnlimitedSentinelBalance,
|
|
2480
2543
|
getValidOperationTypes,
|
|
2544
|
+
getValidTiers,
|
|
2481
2545
|
initializeConfig,
|
|
2482
2546
|
isCreditError,
|
|
2483
2547
|
isCreditErrorMessage,
|
|
2484
2548
|
isFeatureEnabled,
|
|
2549
|
+
isFreeTier,
|
|
2485
2550
|
isInsufficientCreditsError,
|
|
2486
2551
|
isPreviewMode,
|
|
2552
|
+
isUnlimitedTier,
|
|
2487
2553
|
isValidOperationType,
|
|
2554
|
+
isValidTier,
|
|
2488
2555
|
loadConfigFromEnv,
|
|
2489
2556
|
noopDeferred,
|
|
2490
2557
|
notifySubscriptionExpired,
|
|
2491
2558
|
notifySubscriptionExpiring,
|
|
2492
2559
|
parseApiError,
|
|
2493
2560
|
parseCreditError,
|
|
2561
|
+
parseTier,
|
|
2494
2562
|
registerConfigProvider,
|
|
2495
2563
|
registerNotificationHandler,
|
|
2496
2564
|
releaseReservationWithJournal,
|
|
2497
2565
|
reserveCreditsForOperation,
|
|
2498
2566
|
resetConfig,
|
|
2499
2567
|
synchronousDeferred,
|
|
2568
|
+
tierSchema,
|
|
2500
2569
|
toClientUserCredits,
|
|
2501
2570
|
toDate,
|
|
2502
2571
|
toPortableTimestamp,
|