@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.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +153 -153
  3. package/dist/adapters/index.cjs +13 -3
  4. package/dist/adapters/index.cjs.map +1 -1
  5. package/dist/adapters/index.d.cts +2 -2
  6. package/dist/adapters/index.d.ts +2 -2
  7. package/dist/adapters/index.js +13 -3
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/auth/index.cjs.map +1 -1
  10. package/dist/auth/index.js.map +1 -1
  11. package/dist/core/index.cjs +13 -3
  12. package/dist/core/index.cjs.map +1 -1
  13. package/dist/core/index.d.cts +3 -3
  14. package/dist/core/index.d.ts +3 -3
  15. package/dist/core/index.js +13 -3
  16. package/dist/core/index.js.map +1 -1
  17. package/dist/index.cjs +88 -19
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +74 -15
  20. package/dist/index.d.ts +74 -15
  21. package/dist/index.js +79 -19
  22. package/dist/index.js.map +1 -1
  23. package/dist/repository/index.cjs +50 -11
  24. package/dist/repository/index.cjs.map +1 -1
  25. package/dist/repository/index.d.cts +3 -3
  26. package/dist/repository/index.d.ts +3 -3
  27. package/dist/repository/index.js +50 -11
  28. package/dist/repository/index.js.map +1 -1
  29. package/dist/sdk/index.cjs.map +1 -1
  30. package/dist/sdk/index.d.cts +2 -2
  31. package/dist/sdk/index.d.ts +2 -2
  32. package/dist/sdk/index.js.map +1 -1
  33. package/dist/service/index.cjs +60 -12
  34. package/dist/service/index.cjs.map +1 -1
  35. package/dist/service/index.d.cts +18 -3
  36. package/dist/service/index.d.ts +18 -3
  37. package/dist/service/index.js +60 -12
  38. package/dist/service/index.js.map +1 -1
  39. package/dist/{types-BBxIWHK2.d.cts → types-DgQtamEe.d.cts} +16 -3
  40. package/dist/{types-BBxIWHK2.d.ts → types-DgQtamEe.d.ts} +16 -3
  41. package/dist/{types-D1vdeBcA.d.cts → types-KgNdBuEF.d.cts} +1 -1
  42. package/dist/{types-aJsyv8df.d.ts → types-fW2JNmmr.d.ts} +1 -1
  43. package/package.json +1 -1
@@ -86,10 +86,13 @@ function getConfigProvider() {
86
86
 
87
87
  // src/config/index.ts
88
88
  var tierConfigSchema = import_zod.z.object({
89
- tier: import_zod.z.enum(["free", "basic", "premium", "unlimited"]),
89
+ tier: import_zod.z.string().min(1),
90
90
  monthlyCredits: import_zod.z.number().min(0),
91
91
  priceUsd: import_zod.z.number().min(0),
92
- features: import_zod.z.array(import_zod.z.string())
92
+ features: import_zod.z.array(import_zod.z.string()),
93
+ isFree: import_zod.z.boolean().optional(),
94
+ unlimited: import_zod.z.boolean().optional(),
95
+ isDefault: import_zod.z.boolean().optional()
93
96
  });
94
97
  var creditSystemConfigSchema = import_zod.z.object({
95
98
  /**
@@ -113,7 +116,7 @@ var creditSystemConfigSchema = import_zod.z.object({
113
116
  * Tier configurations
114
117
  */
115
118
  tierConfigs: import_zod.z.record(
116
- import_zod.z.enum(["free", "basic", "premium", "unlimited"]),
119
+ import_zod.z.string().min(1),
117
120
  tierConfigSchema
118
121
  ),
119
122
  /**
@@ -169,6 +172,8 @@ var DEFAULT_CONFIG = {
169
172
  tier: "free",
170
173
  monthlyCredits: 25,
171
174
  priceUsd: 0,
175
+ isFree: true,
176
+ isDefault: true,
172
177
  features: [
173
178
  "25 credits per month",
174
179
  "Basic story generation",
@@ -202,6 +207,7 @@ var DEFAULT_CONFIG = {
202
207
  tier: "unlimited",
203
208
  monthlyCredits: 0,
204
209
  // 0 = unlimited
210
+ unlimited: true,
205
211
  priceUsd: 49.99,
206
212
  features: [
207
213
  "Unlimited credits",
@@ -314,10 +320,40 @@ function getConfig() {
314
320
  }
315
321
  return currentConfig;
316
322
  }
323
+ function getValidTiers() {
324
+ return Object.keys(getConfig().tierConfigs);
325
+ }
326
+ function isValidTier(tier) {
327
+ return tier in getConfig().tierConfigs;
328
+ }
329
+ function isFreeTier(tier) {
330
+ const c = getConfig().tierConfigs[tier];
331
+ if (!c) return false;
332
+ return c.isFree ?? c.priceUsd === 0;
333
+ }
334
+ function isUnlimitedTier(tier) {
335
+ const c = getConfig().tierConfigs[tier];
336
+ if (!c) return false;
337
+ return c.unlimited ?? c.monthlyCredits === 0;
338
+ }
339
+ function getDefaultTier() {
340
+ const entries = Object.entries(getConfig().tierConfigs);
341
+ const def = entries.find(([, c]) => c.isDefault)?.[0] ?? entries.find(([, c]) => c.isFree ?? c.priceUsd === 0)?.[0];
342
+ return def ?? "free";
343
+ }
344
+ function getConfigTierConfig(tier) {
345
+ const cfg = getConfig().tierConfigs[tier];
346
+ if (cfg) return cfg;
347
+ const fallback = getDefaultTier();
348
+ console.warn(
349
+ `[Credits Config] Unknown tier "${tier}". Falling back to "${fallback}". Valid: ${getValidTiers().join(", ")}`
350
+ );
351
+ return getConfig().tierConfigs[fallback];
352
+ }
317
353
  function getConfigMonthlyLimit(tier) {
318
- const config = getConfig().tierConfigs[tier];
319
- return config.monthlyCredits === 0 ? Infinity : config.monthlyCredits;
354
+ return isUnlimitedTier(tier) ? Infinity : getConfigTierConfig(tier).monthlyCredits;
320
355
  }
356
+ var tierSchema = import_zod.z.string().refine(isValidTier, { message: "Unknown subscription tier" });
321
357
  initializeConfig();
322
358
 
323
359
  // src/repository/memory/index.ts
@@ -473,6 +509,9 @@ var InMemoryCreditRepository = class {
473
509
  if (!reservation) {
474
510
  throw new Error(`Reservation ${reservationId} not found`);
475
511
  }
512
+ if (reservation.status === "committed") {
513
+ return;
514
+ }
476
515
  if (reservation.status !== "reserved") {
477
516
  throw new Error(
478
517
  `Cannot commit reservation in ${reservation.status} state`
@@ -733,7 +772,7 @@ var InMemoryCreditRepository = class {
733
772
  if (!credits) {
734
773
  throw new Error(`User ${userId} not found`);
735
774
  }
736
- if (credits.tier === "free" || !credits.subscriptionExpiresAt) {
775
+ if (isFreeTier(credits.tier) || !credits.subscriptionExpiresAt) {
737
776
  return {
738
777
  wasDowngraded: false,
739
778
  inGracePeriod: false,
@@ -760,11 +799,11 @@ var InMemoryCreditRepository = class {
760
799
  credits
761
800
  };
762
801
  }
763
- const config = getConfig();
764
- const freeTierConfig = config.tierConfigs.free;
765
- credits.tier = "free";
766
- credits.monthlyLimit = freeTierConfig.monthlyCredits;
767
- credits.balance = Math.min(credits.balance, freeTierConfig.monthlyCredits);
802
+ const defaultTier = getDefaultTier();
803
+ const defaultTierConfig = getConfigTierConfig(defaultTier);
804
+ credits.tier = defaultTier;
805
+ credits.monthlyLimit = defaultTierConfig.monthlyCredits;
806
+ credits.balance = Math.min(credits.balance, defaultTierConfig.monthlyCredits);
768
807
  credits.subscriptionExpiresAt = null;
769
808
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
770
809
  this.users.set(userId, credits);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/repository/index.ts","../../src/repository/types.ts","../../src/repository/utils.ts","../../src/config/index.ts","../../src/config/provider.ts","../../src/repository/memory/index.ts"],"sourcesContent":["/**\n * Credits repository - barrel exports\n *\n * Provides database-agnostic interface for credits storage\n */\n\n// Types and interfaces\nexport type {\n ICreditRepository,\n CreditRepositoryFactory,\n CreateReservationInput,\n CreateTransactionInput,\n CreateUsageLogInput,\n CreateJournalEntryInput,\n UsageLogQuery,\n JournalEntryQuery,\n CreditBalanceUpdate,\n TierUpdateInput,\n} from \"./types.js\";\n\nexport { toClientUserCredits } from \"./types.js\";\n\n// Shared utilities\nexport { generateId, toDate, getNextMonthlyReset } from \"./utils.js\";\n\n// In-memory implementation (for testing and prototyping)\nexport {\n InMemoryCreditRepository,\n createInMemoryCreditRepository,\n} from \"./memory/index.js\";\n","import type {\n PortableUserCredits,\n PortableReservation,\n PortableTransaction,\n PortableUsageLog,\n PortableJournalEntry,\n CreditOperationType,\n SubscriptionTier,\n ReservationStatus,\n AIProviderType,\n MonthlyResetResult,\n SubscriptionExpiryResult,\n CreditSource,\n JournalReferenceType,\n} from \"../core/types.js\";\n\n/**\n * Input for creating a credit reservation\n */\nexport interface CreateReservationInput {\n userId: string;\n amount: number;\n operationType: CreditOperationType;\n expiresAt: Date;\n}\n\n/**\n * Input for creating a credit transaction\n */\nexport interface CreateTransactionInput {\n userId: string;\n type: PortableTransaction[\"type\"];\n amount: number;\n description: string;\n paymentRef?: string;\n previousBalance: number;\n newBalance: number;\n}\n\n/**\n * Input for logging usage\n */\nexport interface CreateUsageLogInput {\n userId: string;\n operationType: CreditOperationType;\n provider: AIProviderType;\n creditsUsed: number;\n success: boolean;\n errorMessage?: string;\n resourceId?: string;\n resourceType?: string;\n requestId?: string;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Query options for usage logs\n */\nexport interface UsageLogQuery {\n userId?: string;\n operationType?: CreditOperationType;\n success?: boolean;\n limit?: number;\n offset?: number;\n startDate?: Date;\n endDate?: Date;\n}\n\n/**\n * Input for creating a journal entry\n */\nexport interface CreateJournalEntryInput {\n userId: string;\n entryType: \"debit\" | \"credit\";\n amount: number;\n balanceAfter: number;\n source: CreditSource;\n referenceId: string;\n referenceType: JournalReferenceType;\n description: string;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Query options for journal entries\n */\nexport interface JournalEntryQuery {\n userId: string;\n source?: CreditSource;\n referenceType?: JournalReferenceType;\n limit?: number;\n offset?: number;\n startDate?: Date;\n endDate?: Date;\n}\n\n/**\n * Partial update for user credits balance\n */\nexport interface CreditBalanceUpdate {\n balance?: number;\n bonusCredits?: number;\n reserved?: number;\n tier?: SubscriptionTier;\n monthlyLimit?: number;\n monthlyUsed?: number;\n monthlyResetAt?: Date | string;\n subscriptionExpiresAt?: Date | string | null;\n /** Increment balance by this amount (alternative to absolute value) */\n balanceIncrement?: number;\n /** Increment bonusCredits by this amount (alternative to absolute value) */\n bonusCreditsIncrement?: number;\n /** Increment reserved by this amount (alternative to absolute value) */\n reservedIncrement?: number;\n /** Increment monthlyUsed by this amount */\n monthlyUsedIncrement?: number;\n}\n\n/**\n * Input for tier update\n */\nexport interface TierUpdateInput {\n tier: SubscriptionTier;\n monthlyLimit: number;\n balance?: number;\n monthlyUsed?: number;\n subscriptionExpiresAt?: Date | string | null;\n}\n\n/**\n * Repository interface for credits database operations\n *\n * Implementations can use any database (Firestore, PostgreSQL, etc.)\n * All methods should handle their own error handling and transactions\n */\nexport interface ICreditRepository {\n // ==================== User Credits ====================\n\n /**\n * Get user credits balance\n * @param userId - User ID\n * @returns User credits or null if not found\n */\n getUserCredits(userId: string): Promise<PortableUserCredits | null>;\n\n /**\n * Initialize credits for a new user\n * @param userId - User ID\n * @param tier - Initial subscription tier\n * @param initialBalance - Initial credit balance\n * @returns Initialized user credits\n */\n initializeUserCredits(\n userId: string,\n tier: SubscriptionTier,\n initialBalance: number\n ): Promise<PortableUserCredits>;\n\n /**\n * Update user credits balance\n * @param userId - User ID\n * @param updates - Partial updates to apply\n */\n updateUserCredits(userId: string, updates: CreditBalanceUpdate): Promise<void>;\n\n /**\n * Update user subscription tier\n * @param userId - User ID\n * @param input - Tier update data\n */\n updateUserTier(userId: string, input: TierUpdateInput): Promise<void>;\n\n // ==================== Reservations ====================\n\n /**\n * Create a credit reservation (phase 1 of two-phase commit)\n * @param input - Reservation data\n * @returns Created reservation\n */\n createReservation(input: CreateReservationInput): Promise<PortableReservation>;\n\n /**\n * Get a reservation by ID\n * @param userId - User ID\n * @param reservationId - Reservation ID\n * @returns Reservation or null\n */\n getReservation(userId: string, reservationId: string): Promise<PortableReservation | null>;\n\n /**\n * Update reservation status\n * @param userId - User ID\n * @param reservationId - Reservation ID\n * @param status - New status\n * @param completedAt - Completion timestamp\n */\n updateReservationStatus(\n userId: string,\n reservationId: string,\n status: ReservationStatus,\n completedAt?: Date\n ): Promise<void>;\n\n // ==================== Atomic Operations ====================\n\n /**\n * Reserve credits atomically (creates reservation + updates balance in transaction)\n * @param userId - User ID\n * @param amount - Credits to reserve\n * @param operationType - Operation type for tracking\n * @param expiresAt - Reservation expiry time\n * @returns Created reservation\n * @throws Error if insufficient credits\n */\n reserveCreditsAtomic(\n userId: string,\n amount: number,\n operationType: CreditOperationType,\n expiresAt: Date\n ): Promise<PortableReservation>;\n\n /**\n * Commit a reservation atomically (deducts credits + marks reservation committed)\n * @param userId - User ID\n * @param reservationId - Reservation ID\n * @throws Error if reservation not found or not in reserved state\n */\n commitReservationAtomic(userId: string, reservationId: string): Promise<void>;\n\n /**\n * Release a reservation atomically (releases reserved credits + marks reservation released)\n * @param userId - User ID\n * @param reservationId - Reservation ID\n */\n releaseReservationAtomic(userId: string, reservationId: string): Promise<void>;\n\n /**\n * Add credits atomically (creates transaction + updates balance)\n * @param userId - User ID\n * @param amount - Credits to add\n * @param description - Transaction description\n * @param paymentRef - Optional payment reference\n */\n addCreditsAtomic(\n userId: string,\n amount: number,\n description: string,\n paymentRef?: string\n ): Promise<void>;\n\n /**\n * Deduct credits from a user's balance atomically.\n *\n * Splits the deduction across `balance` and `bonusCredits`, draining\n * `balance` (monthly, resets each cycle) first so persistent bonus\n * credits survive longer. Runs in a single atomic transaction so\n * concurrent deducts cannot drive either field negative.\n *\n * Callers are responsible for writing any audit record (journal /\n * transaction) — this method only moves credits.\n *\n * @param userId - User ID\n * @param amount - Credits to deduct (positive)\n * @returns Combined totals (balance + bonusCredits) before and after.\n * @throws If user has no credits doc or insufficient credits\n */\n deductCreditsAtomic(\n userId: string,\n amount: number\n ): Promise<{ previousBalance: number; newBalance: number }>;\n\n // ==================== Transactions ====================\n\n /**\n * Create a credit transaction record\n * @param input - Transaction data\n * @returns Created transaction\n */\n createTransaction(input: CreateTransactionInput): Promise<PortableTransaction>;\n\n /**\n * Get user's transaction history\n * @param userId - User ID\n * @param limit - Max results\n * @param offset - Skip results\n * @returns List of transactions\n */\n getTransactions(\n userId: string,\n limit?: number,\n offset?: number\n ): Promise<PortableTransaction[]>;\n\n // ==================== Usage Logs ====================\n\n /**\n * Log a usage event\n * @param input - Usage log data\n * @returns Created usage log\n */\n logUsage(input: CreateUsageLogInput): Promise<PortableUsageLog>;\n\n /**\n * Query usage logs\n * @param query - Query parameters\n * @returns List of usage logs\n */\n getUsageLogs(query: UsageLogQuery): Promise<PortableUsageLog[]>;\n\n /**\n * Get usage log count (for pagination)\n * @param query - Query parameters (without limit/offset)\n * @returns Count of matching logs\n */\n getUsageLogsCount(query: Omit<UsageLogQuery, \"limit\" | \"offset\">): Promise<number>;\n\n // ==================== Cleanup Operations ====================\n\n /**\n * Find and expire reservations past their expiration time\n * Used by cron job to clean up stale reservations\n * @param batchSize - Maximum number of reservations to process per batch (default: 100)\n * @param maxIterations - Maximum number of pagination iterations to prevent infinite loops (default: 100)\n * @returns Cleanup results with counts and errors\n */\n findAndExpireReservations(batchSize?: number, maxIterations?: number): Promise<{\n expiredCount: number;\n creditsReleased: number;\n errors: string[];\n }>;\n\n // ==================== Atomic Monthly Reset ====================\n\n /**\n * Atomically perform monthly reset if needed\n * Uses optimistic locking to prevent race conditions\n *\n * @param userId - User ID\n * @param tier - User's current subscription tier (for determining new balance)\n * @param expectedResetAt - The expected monthlyResetAt value (for optimistic locking)\n * @returns Result indicating whether reset was performed and updated credits\n */\n atomicMonthlyReset(\n userId: string,\n tier: SubscriptionTier,\n expectedResetAt: Date | string\n ): Promise<MonthlyResetResult>;\n\n // ==================== Subscription Expiry ====================\n\n /**\n * Check and handle subscription expiry with grace period\n * Auto-downgrades expired subscriptions after grace period\n *\n * @param userId - User ID\n * @param gracePeriodDays - Days to allow after expiry before downgrade (default: 3)\n * @returns Result indicating whether downgrade occurred\n */\n checkAndHandleSubscriptionExpiry(\n userId: string,\n gracePeriodDays?: number\n ): Promise<SubscriptionExpiryResult>;\n\n // ==================== Journal Entries ====================\n\n /**\n * Create a journal entry for audit trail\n * @param input - Journal entry data\n * @returns Created journal entry\n */\n createJournalEntry(input: CreateJournalEntryInput): Promise<PortableJournalEntry>;\n\n /**\n * Get journal entries for a user\n * @param query - Query parameters\n * @returns List of journal entries\n */\n getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]>;\n\n /**\n * Get journal entry count for pagination\n * @param query - Query parameters (without limit/offset)\n * @returns Count of matching entries\n */\n getJournalEntriesCount(query: Omit<JournalEntryQuery, \"limit\" | \"offset\">): Promise<number>;\n}\n\n/**\n * Factory type for creating repository instances\n */\nexport type CreditRepositoryFactory = () => ICreditRepository;\n\n/**\n * Convert PortableUserCredits to client-safe format\n * Utility function that implementations can use\n */\nexport function toClientUserCredits(credits: PortableUserCredits): PortableUserCredits {\n // Already in portable format, just ensure all timestamps are ISO strings\n return {\n userId: credits.userId,\n balance: credits.balance,\n bonusCredits: credits.bonusCredits ?? 0,\n reserved: credits.reserved,\n tier: credits.tier,\n monthlyLimit: credits.monthlyLimit,\n monthlyUsed: credits.monthlyUsed,\n monthlyResetAt: toISOString(credits.monthlyResetAt),\n subscriptionExpiresAt: credits.subscriptionExpiresAt\n ? toISOString(credits.subscriptionExpiresAt)\n : null,\n createdAt: toISOString(credits.createdAt),\n updatedAt: toISOString(credits.updatedAt),\n };\n}\n\n/**\n * Convert any timestamp-like value to ISO string\n */\nfunction toISOString(value: unknown): string {\n if (!value) return new Date().toISOString();\n if (typeof value === \"string\") return value;\n if (value instanceof Date) return value.toISOString();\n // Handle Firestore Timestamp-like objects\n if (typeof value === \"object\" && \"toDate\" in value && typeof (value as { toDate: () => Date }).toDate === \"function\") {\n return (value as { toDate: () => Date }).toDate().toISOString();\n }\n return new Date().toISOString();\n}\n","/**\n * Shared utilities for credit repository implementations\n *\n * These are pure functions with no server-only dependencies,\n * making them safe to use in any repository implementation.\n */\n\n/**\n * Generate a unique ID\n * Uses timestamp + random string for uniqueness\n */\nexport function generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\n}\n\n/**\n * Convert date-like value to Date object\n * Handles: Date, string, Firestore Timestamp\n */\nexport function toDate(value: Date | string | unknown): Date {\n if (value instanceof Date) return value;\n if (typeof value === \"string\") return new Date(value);\n // Handle Firestore Timestamp\n if (typeof value === \"object\" && value !== null && \"toDate\" in value) {\n return (value as { toDate: () => Date }).toDate();\n }\n return new Date();\n}\n\n/**\n * Get the first day of next month\n * Used for calculating monthly reset dates\n */\nexport function getNextMonthlyReset(from: Date = new Date()): Date {\n const next = new Date(from);\n next.setMonth(next.getMonth() + 1);\n next.setDate(1);\n next.setHours(0, 0, 0, 0);\n return next;\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 * In-Memory Credit Repository Implementation\n *\n * A database-agnostic implementation of ICreditRepository for testing and prototyping.\n * All data is stored in memory and lost when the process restarts.\n *\n * Usage:\n * - Unit tests without database dependency\n * - Local development and prototyping\n * - Reference implementation for custom repositories\n */\n\nimport type {\n PortableUserCredits,\n PortableReservation,\n PortableTransaction,\n PortableJournalEntry,\n PortableUsageLog,\n SubscriptionTier,\n ReservationStatus,\n MonthlyResetResult,\n SubscriptionExpiryResult,\n} from \"../../core/types.js\";\nimport type {\n ICreditRepository,\n CreateReservationInput,\n CreateTransactionInput,\n CreateUsageLogInput,\n CreateJournalEntryInput,\n UsageLogQuery,\n JournalEntryQuery,\n CreditBalanceUpdate,\n TierUpdateInput,\n} from \"../types.js\";\nimport { generateId, toDate, getNextMonthlyReset } from \"../utils.js\";\nimport { getConfig, getConfigMonthlyLimit } from \"../../config/index.js\";\n\n/**\n * In-Memory implementation of ICreditRepository\n *\n * Implements all repository methods using Map-based storage.\n * Useful for testing and as a reference implementation.\n */\nexport class InMemoryCreditRepository implements ICreditRepository {\n private users = new Map<string, PortableUserCredits>();\n private reservations = new Map<string, Map<string, PortableReservation>>();\n private transactions = new Map<string, PortableTransaction[]>();\n private usageLogs: PortableUsageLog[] = [];\n private journalEntries = new Map<string, PortableJournalEntry[]>();\n\n // ==================== User Credits ====================\n\n async getUserCredits(userId: string): Promise<PortableUserCredits | null> {\n return this.users.get(userId) ?? null;\n }\n\n async initializeUserCredits(\n userId: string,\n tier: SubscriptionTier,\n initialBalance: number\n ): Promise<PortableUserCredits> {\n const now = new Date().toISOString();\n const credits: PortableUserCredits = {\n userId,\n balance: initialBalance,\n bonusCredits: 0,\n reserved: 0,\n tier,\n monthlyLimit: getConfigMonthlyLimit(tier),\n monthlyUsed: 0,\n monthlyResetAt: getNextMonthlyReset().toISOString(),\n subscriptionExpiresAt: null,\n createdAt: now,\n updatedAt: now,\n };\n this.users.set(userId, credits);\n return credits;\n }\n\n async updateUserCredits(userId: string, updates: CreditBalanceUpdate): Promise<void> {\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n const now = new Date().toISOString();\n\n // Apply absolute updates\n if (updates.balance !== undefined) credits.balance = updates.balance;\n if (updates.bonusCredits !== undefined) credits.bonusCredits = updates.bonusCredits;\n if (updates.reserved !== undefined) credits.reserved = updates.reserved;\n if (updates.tier !== undefined) credits.tier = updates.tier;\n if (updates.monthlyLimit !== undefined) credits.monthlyLimit = updates.monthlyLimit;\n if (updates.monthlyUsed !== undefined) credits.monthlyUsed = updates.monthlyUsed;\n if (updates.monthlyResetAt !== undefined) {\n credits.monthlyResetAt = updates.monthlyResetAt instanceof Date\n ? updates.monthlyResetAt.toISOString()\n : updates.monthlyResetAt;\n }\n if (updates.subscriptionExpiresAt !== undefined) {\n if (updates.subscriptionExpiresAt === null) {\n credits.subscriptionExpiresAt = null;\n } else {\n credits.subscriptionExpiresAt = updates.subscriptionExpiresAt instanceof Date\n ? updates.subscriptionExpiresAt.toISOString()\n : updates.subscriptionExpiresAt;\n }\n }\n\n // Apply increments\n if (updates.balanceIncrement !== undefined) {\n credits.balance += updates.balanceIncrement;\n }\n if (updates.bonusCreditsIncrement !== undefined) {\n credits.bonusCredits += updates.bonusCreditsIncrement;\n }\n if (updates.reservedIncrement !== undefined) {\n credits.reserved += updates.reservedIncrement;\n }\n if (updates.monthlyUsedIncrement !== undefined) {\n credits.monthlyUsed += updates.monthlyUsedIncrement;\n }\n\n credits.updatedAt = now;\n this.users.set(userId, credits);\n }\n\n async updateUserTier(userId: string, input: TierUpdateInput): Promise<void> {\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n credits.tier = input.tier;\n credits.monthlyLimit = input.monthlyLimit;\n if (input.balance !== undefined) credits.balance = input.balance;\n if (input.monthlyUsed !== undefined) credits.monthlyUsed = input.monthlyUsed;\n if (input.subscriptionExpiresAt !== undefined) {\n if (input.subscriptionExpiresAt === null) {\n credits.subscriptionExpiresAt = null;\n } else {\n credits.subscriptionExpiresAt = input.subscriptionExpiresAt instanceof Date\n ? input.subscriptionExpiresAt.toISOString()\n : input.subscriptionExpiresAt;\n }\n }\n credits.updatedAt = new Date().toISOString();\n\n this.users.set(userId, credits);\n }\n\n // ==================== Reservations ====================\n\n async createReservation(input: CreateReservationInput): Promise<PortableReservation> {\n const now = new Date().toISOString();\n const reservation: PortableReservation = {\n id: generateId(),\n userId: input.userId,\n amount: input.amount,\n operationType: input.operationType,\n status: \"reserved\",\n createdAt: now,\n expiresAt: input.expiresAt.toISOString(),\n };\n\n if (!this.reservations.has(input.userId)) {\n this.reservations.set(input.userId, new Map());\n }\n this.reservations.get(input.userId)!.set(reservation.id, reservation);\n\n return reservation;\n }\n\n async getReservation(\n userId: string,\n reservationId: string\n ): Promise<PortableReservation | null> {\n const userReservations = this.reservations.get(userId);\n if (!userReservations) return null;\n return userReservations.get(reservationId) ?? null;\n }\n\n async updateReservationStatus(\n userId: string,\n reservationId: string,\n status: ReservationStatus,\n completedAt?: Date\n ): Promise<void> {\n const userReservations = this.reservations.get(userId);\n if (!userReservations) {\n throw new Error(`No reservations found for user ${userId}`);\n }\n\n const reservation = userReservations.get(reservationId);\n if (!reservation) {\n throw new Error(`Reservation ${reservationId} not found`);\n }\n\n reservation.status = status;\n if (completedAt) reservation.completedAt = completedAt.toISOString();\n\n userReservations.set(reservationId, reservation);\n }\n\n // ==================== Atomic Operations ====================\n\n async reserveCreditsAtomic(\n userId: string,\n amount: number,\n operationType: string,\n expiresAt: Date\n ): Promise<PortableReservation> {\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n // Calculate available credits (balance + bonusCredits - reserved)\n const available = credits.balance + credits.bonusCredits - credits.reserved;\n if (available < amount) {\n throw new Error(\n `Insufficient credits. Available: ${available}, Required: ${amount}`\n );\n }\n\n // Create reservation\n const reservation = await this.createReservation({\n userId,\n amount,\n operationType,\n expiresAt,\n });\n\n // Update reserved amount\n credits.reserved += amount;\n credits.updatedAt = new Date().toISOString();\n this.users.set(userId, credits);\n\n return reservation;\n }\n\n async commitReservationAtomic(userId: string, reservationId: string): Promise<void> {\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n const reservation = await this.getReservation(userId, reservationId);\n if (!reservation) {\n throw new Error(`Reservation ${reservationId} not found`);\n }\n\n if (reservation.status !== \"reserved\") {\n throw new Error(\n `Cannot commit reservation in ${reservation.status} state`\n );\n }\n\n const amount = reservation.amount;\n\n // Deduct from balance (bonus credits first logic can be added if needed)\n credits.balance -= amount;\n credits.reserved -= amount;\n credits.monthlyUsed += amount;\n credits.updatedAt = new Date().toISOString();\n this.users.set(userId, credits);\n\n // Update reservation status\n await this.updateReservationStatus(userId, reservationId, \"committed\", new Date());\n }\n\n async releaseReservationAtomic(userId: string, reservationId: string): Promise<void> {\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n const reservation = await this.getReservation(userId, reservationId);\n if (!reservation) {\n throw new Error(`Reservation ${reservationId} not found`);\n }\n\n if (reservation.status !== \"reserved\") {\n // Already processed, no-op\n return;\n }\n\n // Release reserved credits\n credits.reserved -= reservation.amount;\n credits.updatedAt = new Date().toISOString();\n this.users.set(userId, credits);\n\n // Update reservation status\n await this.updateReservationStatus(userId, reservationId, \"released\", new Date());\n }\n\n async addCreditsAtomic(\n userId: string,\n amount: number,\n description: string,\n paymentRef?: string\n ): Promise<void> {\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n const previousBalance = credits.bonusCredits;\n credits.bonusCredits += amount;\n credits.updatedAt = new Date().toISOString();\n this.users.set(userId, credits);\n\n // Create transaction\n await this.createTransaction({\n userId,\n type: \"purchase\",\n amount,\n description,\n paymentRef,\n previousBalance,\n newBalance: credits.bonusCredits,\n });\n }\n\n async deductCreditsAtomic(\n userId: string,\n amount: number\n ): Promise<{ previousBalance: number; newBalance: number }> {\n if (amount <= 0) {\n throw new Error(`deductCreditsAtomic amount must be positive (got ${amount})`);\n }\n\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User credits not found for userId: ${userId}`);\n }\n\n const available = credits.balance + credits.bonusCredits - credits.reserved;\n if (available < amount) {\n throw new Error(\n `Insufficient credits. Available: ${available}, requested: ${amount}`\n );\n }\n\n // Drain balance first, then bonusCredits — same policy as commit.\n const balanceDeduction = Math.min(credits.balance, amount);\n const bonusDeduction = amount - balanceDeduction;\n\n const previousBalance = credits.balance + credits.bonusCredits;\n credits.balance -= balanceDeduction;\n credits.bonusCredits -= bonusDeduction;\n credits.updatedAt = new Date().toISOString();\n this.users.set(userId, credits);\n\n return { previousBalance, newBalance: previousBalance - amount };\n }\n\n // ==================== Transactions ====================\n\n async createTransaction(input: CreateTransactionInput): Promise<PortableTransaction> {\n const transaction: PortableTransaction = {\n id: generateId(),\n userId: input.userId,\n type: input.type,\n amount: input.amount,\n description: input.description,\n paymentRef: input.paymentRef,\n previousBalance: input.previousBalance,\n newBalance: input.newBalance,\n createdAt: new Date().toISOString(),\n };\n\n if (!this.transactions.has(input.userId)) {\n this.transactions.set(input.userId, []);\n }\n this.transactions.get(input.userId)!.push(transaction);\n\n return transaction;\n }\n\n async getTransactions(\n userId: string,\n limit = 50,\n offset = 0\n ): Promise<PortableTransaction[]> {\n const userTransactions = this.transactions.get(userId) ?? [];\n // Sort by createdAt descending (most recent first)\n const sorted = [...userTransactions].sort((a, b) => {\n const aDate = toDate(a.createdAt).getTime();\n const bDate = toDate(b.createdAt).getTime();\n return bDate - aDate;\n });\n return sorted.slice(offset, offset + limit);\n }\n\n // ==================== Usage Logs ====================\n\n async logUsage(input: CreateUsageLogInput): Promise<PortableUsageLog> {\n const log: PortableUsageLog = {\n id: generateId(),\n userId: input.userId,\n operationType: input.operationType,\n provider: input.provider,\n creditsUsed: input.creditsUsed,\n success: input.success,\n errorMessage: input.errorMessage,\n resourceId: input.resourceId,\n resourceType: input.resourceType,\n requestId: input.requestId,\n metadata: input.metadata,\n createdAt: new Date().toISOString(),\n };\n\n this.usageLogs.push(log);\n return log;\n }\n\n async getUsageLogs(query: UsageLogQuery): Promise<PortableUsageLog[]> {\n let results = [...this.usageLogs];\n\n // Apply filters\n if (query.userId) {\n results = results.filter((log) => log.userId === query.userId);\n }\n if (query.operationType) {\n results = results.filter((log) => log.operationType === query.operationType);\n }\n if (query.success !== undefined) {\n results = results.filter((log) => log.success === query.success);\n }\n if (query.startDate) {\n const startTime = query.startDate.getTime();\n results = results.filter(\n (log) => toDate(log.createdAt).getTime() >= startTime\n );\n }\n if (query.endDate) {\n const endTime = query.endDate.getTime();\n results = results.filter(\n (log) => toDate(log.createdAt).getTime() <= endTime\n );\n }\n\n // Sort by createdAt descending\n results.sort((a, b) => {\n return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();\n });\n\n // Apply pagination\n const offset = query.offset ?? 0;\n const limit = query.limit ?? 50;\n return results.slice(offset, offset + limit);\n }\n\n async getUsageLogsCount(\n query: Omit<UsageLogQuery, \"limit\" | \"offset\">\n ): Promise<number> {\n const results = await this.getUsageLogs({ ...query, limit: Infinity, offset: 0 });\n return results.length;\n }\n\n // ==================== Journal Entries ====================\n\n async createJournalEntry(input: CreateJournalEntryInput): Promise<PortableJournalEntry> {\n const entry: PortableJournalEntry = {\n id: generateId(),\n userId: input.userId,\n entryType: input.entryType,\n amount: input.amount,\n balanceAfter: input.balanceAfter,\n source: input.source,\n referenceId: input.referenceId,\n referenceType: input.referenceType,\n description: input.description,\n metadata: input.metadata,\n createdAt: new Date().toISOString(),\n };\n\n if (!this.journalEntries.has(input.userId)) {\n this.journalEntries.set(input.userId, []);\n }\n this.journalEntries.get(input.userId)!.push(entry);\n\n return entry;\n }\n\n async getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]> {\n let results = this.journalEntries.get(query.userId) ?? [];\n\n // Apply filters\n if (query.source) {\n results = results.filter((entry) => entry.source === query.source);\n }\n if (query.referenceType) {\n results = results.filter((entry) => entry.referenceType === query.referenceType);\n }\n if (query.startDate) {\n const startTime = query.startDate.getTime();\n results = results.filter(\n (entry) => toDate(entry.createdAt).getTime() >= startTime\n );\n }\n if (query.endDate) {\n const endTime = query.endDate.getTime();\n results = results.filter(\n (entry) => toDate(entry.createdAt).getTime() <= endTime\n );\n }\n\n // Sort by createdAt descending\n results = [...results].sort((a, b) => {\n return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();\n });\n\n // Apply pagination\n const offset = query.offset ?? 0;\n const limit = query.limit ?? 50;\n return results.slice(offset, offset + limit);\n }\n\n async getJournalEntriesCount(\n query: Omit<JournalEntryQuery, \"limit\" | \"offset\">\n ): Promise<number> {\n const results = await this.getJournalEntries({\n ...query,\n limit: Infinity,\n offset: 0,\n });\n return results.length;\n }\n\n // ==================== Cleanup Operations ====================\n\n async findAndExpireReservations(\n _batchSize = 100,\n _maxIterations = 100\n ): Promise<{\n expiredCount: number;\n creditsReleased: number;\n errors: string[];\n }> {\n const now = new Date();\n let expiredCount = 0;\n let creditsReleased = 0;\n const errors: string[] = [];\n\n for (const [userId, userReservations] of this.reservations) {\n for (const [reservationId, reservation] of userReservations) {\n if (\n reservation.status === \"reserved\" &&\n toDate(reservation.expiresAt).getTime() < now.getTime()\n ) {\n try {\n // Release the reservation\n await this.releaseReservationAtomic(userId, reservationId);\n // Mark as expired instead of released\n reservation.status = \"expired\";\n userReservations.set(reservationId, reservation);\n\n expiredCount++;\n creditsReleased += reservation.amount;\n } catch (error) {\n errors.push(\n `Failed to expire reservation ${reservationId}: ${error}`\n );\n }\n }\n }\n }\n\n return { expiredCount, creditsReleased, errors };\n }\n\n // ==================== Atomic Monthly Reset ====================\n\n async atomicMonthlyReset(\n userId: string,\n tier: SubscriptionTier,\n expectedResetAt: Date | string\n ): Promise<MonthlyResetResult> {\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n // Optimistic locking: check if expectedResetAt matches current value\n const currentResetAt = toDate(credits.monthlyResetAt).getTime();\n const expected = toDate(expectedResetAt).getTime();\n\n if (currentResetAt !== expected) {\n // Another request already performed the reset\n return { wasReset: false, credits };\n }\n\n // Perform the reset\n const newBalance = getConfigMonthlyLimit(tier);\n const nextReset = getNextMonthlyReset();\n\n credits.balance = newBalance === Infinity ? credits.balance : newBalance;\n credits.monthlyUsed = 0;\n credits.monthlyResetAt = nextReset.toISOString();\n credits.updatedAt = new Date().toISOString();\n\n this.users.set(userId, credits);\n\n return { wasReset: true, credits };\n }\n\n // ==================== Subscription Expiry ====================\n\n async checkAndHandleSubscriptionExpiry(\n userId: string,\n gracePeriodDays = 3\n ): Promise<SubscriptionExpiryResult> {\n const credits = this.users.get(userId);\n if (!credits) {\n throw new Error(`User ${userId} not found`);\n }\n\n // Free tier doesn't expire\n if (credits.tier === \"free\" || !credits.subscriptionExpiresAt) {\n return {\n wasDowngraded: false,\n inGracePeriod: false,\n graceDaysRemaining: 0,\n credits,\n };\n }\n\n const now = new Date();\n const expiresAt = toDate(credits.subscriptionExpiresAt);\n const daysSinceExpiry =\n (now.getTime() - expiresAt.getTime()) / (1000 * 60 * 60 * 24);\n\n if (daysSinceExpiry <= 0) {\n // Not expired yet\n return {\n wasDowngraded: false,\n inGracePeriod: false,\n graceDaysRemaining: 0,\n credits,\n };\n }\n\n if (daysSinceExpiry <= gracePeriodDays) {\n // In grace period\n return {\n wasDowngraded: false,\n inGracePeriod: true,\n graceDaysRemaining: Math.ceil(gracePeriodDays - daysSinceExpiry),\n credits,\n };\n }\n\n // Grace period expired - downgrade to free\n const config = getConfig();\n const freeTierConfig = config.tierConfigs.free!;\n\n credits.tier = \"free\";\n credits.monthlyLimit = freeTierConfig.monthlyCredits;\n credits.balance = Math.min(credits.balance, freeTierConfig.monthlyCredits);\n credits.subscriptionExpiresAt = null;\n credits.updatedAt = new Date().toISOString();\n\n this.users.set(userId, credits);\n\n return {\n wasDowngraded: true,\n inGracePeriod: false,\n graceDaysRemaining: 0,\n credits,\n };\n }\n\n // ==================== Testing Utilities ====================\n\n /**\n * Clear all data (useful for testing)\n */\n clear(): void {\n this.users.clear();\n this.reservations.clear();\n this.transactions.clear();\n this.usageLogs = [];\n this.journalEntries.clear();\n }\n\n /**\n * Get all users (useful for testing/debugging)\n */\n getAllUsers(): PortableUserCredits[] {\n return Array.from(this.users.values());\n }\n\n /**\n * Get all reservations for a user (useful for testing)\n */\n getAllReservations(userId: string): PortableReservation[] {\n const userReservations = this.reservations.get(userId);\n if (!userReservations) return [];\n return Array.from(userReservations.values());\n }\n}\n\n/**\n * Create a new in-memory repository instance\n * Each call creates a fresh, isolated instance\n */\nexport function createInMemoryCreditRepository(): InMemoryCreditRepository {\n return new InMemoryCreditRepository();\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/repository/index.ts","../../src/repository/types.ts","../../src/repository/utils.ts","../../src/config/index.ts","../../src/config/provider.ts","../../src/repository/memory/index.ts"],"sourcesContent":["/**\r\n * Credits repository - barrel exports\r\n *\r\n * Provides database-agnostic interface for credits storage\r\n */\r\n\r\n// Types and interfaces\r\nexport type {\r\n ICreditRepository,\r\n CreditRepositoryFactory,\r\n CreateReservationInput,\r\n CreateTransactionInput,\r\n CreateUsageLogInput,\r\n CreateJournalEntryInput,\r\n UsageLogQuery,\r\n JournalEntryQuery,\r\n CreditBalanceUpdate,\r\n TierUpdateInput,\r\n} from \"./types.js\";\r\n\r\nexport { toClientUserCredits } from \"./types.js\";\r\n\r\n// Shared utilities\r\nexport { generateId, toDate, getNextMonthlyReset } from \"./utils.js\";\r\n\r\n// In-memory implementation (for testing and prototyping)\r\nexport {\r\n InMemoryCreditRepository,\r\n createInMemoryCreditRepository,\r\n} from \"./memory/index.js\";\r\n","import 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\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}\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 * 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 // ==================== 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 credit reservation (phase 1 of two-phase commit)\r\n * @param input - Reservation data\r\n * @returns Created reservation\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 * Update reservation status\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 */\r\n addCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n description: string,\r\n paymentRef?: string\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\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 * 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 * Shared utilities for credit repository implementations\r\n *\r\n * These are pure functions with no server-only dependencies,\r\n * making them safe to use in any repository implementation.\r\n */\r\n\r\n/**\r\n * Generate a unique ID\r\n * Uses timestamp + random string for uniqueness\r\n */\r\nexport function generateId(): string {\r\n return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\r\n}\r\n\r\n/**\r\n * Convert date-like value to Date object\r\n * Handles: Date, string, Firestore Timestamp\r\n */\r\nexport function toDate(value: Date | string | unknown): Date {\r\n if (value instanceof Date) return value;\r\n if (typeof value === \"string\") return new Date(value);\r\n // Handle Firestore Timestamp\r\n if (typeof value === \"object\" && value !== null && \"toDate\" in value) {\r\n return (value as { toDate: () => Date }).toDate();\r\n }\r\n return new Date();\r\n}\r\n\r\n/**\r\n * Get the first day of next month\r\n * Used for calculating monthly reset dates\r\n */\r\nexport function getNextMonthlyReset(from: Date = new Date()): Date {\r\n const next = new Date(from);\r\n next.setMonth(next.getMonth() + 1);\r\n next.setDate(1);\r\n next.setHours(0, 0, 0, 0);\r\n return next;\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 * In-Memory Credit Repository Implementation\r\n *\r\n * A database-agnostic implementation of ICreditRepository for testing and prototyping.\r\n * All data is stored in memory and lost when the process restarts.\r\n *\r\n * Usage:\r\n * - Unit tests without database dependency\r\n * - Local development and prototyping\r\n * - Reference implementation for custom repositories\r\n */\r\n\r\nimport type {\r\n PortableUserCredits,\r\n PortableReservation,\r\n PortableTransaction,\r\n PortableJournalEntry,\r\n PortableUsageLog,\r\n SubscriptionTier,\r\n ReservationStatus,\r\n MonthlyResetResult,\r\n SubscriptionExpiryResult,\r\n} from \"../../core/types.js\";\r\nimport type {\r\n ICreditRepository,\r\n CreateReservationInput,\r\n CreateTransactionInput,\r\n CreateUsageLogInput,\r\n CreateJournalEntryInput,\r\n UsageLogQuery,\r\n JournalEntryQuery,\r\n CreditBalanceUpdate,\r\n TierUpdateInput,\r\n} from \"../types.js\";\r\nimport { generateId, toDate, getNextMonthlyReset } from \"../utils.js\";\r\nimport {\r\n getConfigMonthlyLimit,\r\n getConfigTierConfig,\r\n getDefaultTier,\r\n isFreeTier,\r\n} from \"../../config/index.js\";\r\n\r\n/**\r\n * In-Memory implementation of ICreditRepository\r\n *\r\n * Implements all repository methods using Map-based storage.\r\n * Useful for testing and as a reference implementation.\r\n */\r\nexport class InMemoryCreditRepository implements ICreditRepository {\r\n private users = new Map<string, PortableUserCredits>();\r\n private reservations = new Map<string, Map<string, PortableReservation>>();\r\n private transactions = new Map<string, PortableTransaction[]>();\r\n private usageLogs: PortableUsageLog[] = [];\r\n private journalEntries = new Map<string, PortableJournalEntry[]>();\r\n\r\n // ==================== User Credits ====================\r\n\r\n async getUserCredits(userId: string): Promise<PortableUserCredits | null> {\r\n return this.users.get(userId) ?? null;\r\n }\r\n\r\n async initializeUserCredits(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n initialBalance: number\r\n ): Promise<PortableUserCredits> {\r\n const now = new Date().toISOString();\r\n const credits: PortableUserCredits = {\r\n userId,\r\n balance: initialBalance,\r\n bonusCredits: 0,\r\n reserved: 0,\r\n tier,\r\n monthlyLimit: getConfigMonthlyLimit(tier),\r\n monthlyUsed: 0,\r\n monthlyResetAt: getNextMonthlyReset().toISOString(),\r\n subscriptionExpiresAt: null,\r\n createdAt: now,\r\n updatedAt: now,\r\n };\r\n this.users.set(userId, credits);\r\n return credits;\r\n }\r\n\r\n async updateUserCredits(userId: string, updates: CreditBalanceUpdate): Promise<void> {\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n const now = new Date().toISOString();\r\n\r\n // Apply absolute updates\r\n if (updates.balance !== undefined) credits.balance = updates.balance;\r\n if (updates.bonusCredits !== undefined) credits.bonusCredits = updates.bonusCredits;\r\n if (updates.reserved !== undefined) credits.reserved = updates.reserved;\r\n if (updates.tier !== undefined) credits.tier = updates.tier;\r\n if (updates.monthlyLimit !== undefined) credits.monthlyLimit = updates.monthlyLimit;\r\n if (updates.monthlyUsed !== undefined) credits.monthlyUsed = updates.monthlyUsed;\r\n if (updates.monthlyResetAt !== undefined) {\r\n credits.monthlyResetAt = updates.monthlyResetAt instanceof Date\r\n ? updates.monthlyResetAt.toISOString()\r\n : updates.monthlyResetAt;\r\n }\r\n if (updates.subscriptionExpiresAt !== undefined) {\r\n if (updates.subscriptionExpiresAt === null) {\r\n credits.subscriptionExpiresAt = null;\r\n } else {\r\n credits.subscriptionExpiresAt = updates.subscriptionExpiresAt instanceof Date\r\n ? updates.subscriptionExpiresAt.toISOString()\r\n : updates.subscriptionExpiresAt;\r\n }\r\n }\r\n\r\n // Apply increments\r\n if (updates.balanceIncrement !== undefined) {\r\n credits.balance += updates.balanceIncrement;\r\n }\r\n if (updates.bonusCreditsIncrement !== undefined) {\r\n credits.bonusCredits += updates.bonusCreditsIncrement;\r\n }\r\n if (updates.reservedIncrement !== undefined) {\r\n credits.reserved += updates.reservedIncrement;\r\n }\r\n if (updates.monthlyUsedIncrement !== undefined) {\r\n credits.monthlyUsed += updates.monthlyUsedIncrement;\r\n }\r\n\r\n credits.updatedAt = now;\r\n this.users.set(userId, credits);\r\n }\r\n\r\n async updateUserTier(userId: string, input: TierUpdateInput): Promise<void> {\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n credits.tier = input.tier;\r\n credits.monthlyLimit = input.monthlyLimit;\r\n if (input.balance !== undefined) credits.balance = input.balance;\r\n if (input.monthlyUsed !== undefined) credits.monthlyUsed = input.monthlyUsed;\r\n if (input.subscriptionExpiresAt !== undefined) {\r\n if (input.subscriptionExpiresAt === null) {\r\n credits.subscriptionExpiresAt = null;\r\n } else {\r\n credits.subscriptionExpiresAt = input.subscriptionExpiresAt instanceof Date\r\n ? input.subscriptionExpiresAt.toISOString()\r\n : input.subscriptionExpiresAt;\r\n }\r\n }\r\n credits.updatedAt = new Date().toISOString();\r\n\r\n this.users.set(userId, credits);\r\n }\r\n\r\n // ==================== Reservations ====================\r\n\r\n async createReservation(input: CreateReservationInput): Promise<PortableReservation> {\r\n const now = new Date().toISOString();\r\n const reservation: PortableReservation = {\r\n id: generateId(),\r\n userId: input.userId,\r\n amount: input.amount,\r\n operationType: input.operationType,\r\n status: \"reserved\",\r\n createdAt: now,\r\n expiresAt: input.expiresAt.toISOString(),\r\n };\r\n\r\n if (!this.reservations.has(input.userId)) {\r\n this.reservations.set(input.userId, new Map());\r\n }\r\n this.reservations.get(input.userId)!.set(reservation.id, reservation);\r\n\r\n return reservation;\r\n }\r\n\r\n async getReservation(\r\n userId: string,\r\n reservationId: string\r\n ): Promise<PortableReservation | null> {\r\n const userReservations = this.reservations.get(userId);\r\n if (!userReservations) return null;\r\n return userReservations.get(reservationId) ?? null;\r\n }\r\n\r\n async updateReservationStatus(\r\n userId: string,\r\n reservationId: string,\r\n status: ReservationStatus,\r\n completedAt?: Date\r\n ): Promise<void> {\r\n const userReservations = this.reservations.get(userId);\r\n if (!userReservations) {\r\n throw new Error(`No reservations found for user ${userId}`);\r\n }\r\n\r\n const reservation = userReservations.get(reservationId);\r\n if (!reservation) {\r\n throw new Error(`Reservation ${reservationId} not found`);\r\n }\r\n\r\n reservation.status = status;\r\n if (completedAt) reservation.completedAt = completedAt.toISOString();\r\n\r\n userReservations.set(reservationId, reservation);\r\n }\r\n\r\n // ==================== Atomic Operations ====================\r\n\r\n async reserveCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n operationType: string,\r\n expiresAt: Date\r\n ): Promise<PortableReservation> {\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n // Calculate available credits (balance + bonusCredits - reserved)\r\n const available = credits.balance + credits.bonusCredits - credits.reserved;\r\n if (available < amount) {\r\n throw new Error(\r\n `Insufficient credits. Available: ${available}, Required: ${amount}`\r\n );\r\n }\r\n\r\n // Create reservation\r\n const reservation = await this.createReservation({\r\n userId,\r\n amount,\r\n operationType,\r\n expiresAt,\r\n });\r\n\r\n // Update reserved amount\r\n credits.reserved += amount;\r\n credits.updatedAt = new Date().toISOString();\r\n this.users.set(userId, credits);\r\n\r\n return reservation;\r\n }\r\n\r\n async commitReservationAtomic(userId: string, reservationId: string): Promise<void> {\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n const reservation = await this.getReservation(userId, reservationId);\r\n if (!reservation) {\r\n throw new Error(`Reservation ${reservationId} not found`);\r\n }\r\n\r\n // Idempotent: re-committing an already-committed reservation is a no-op\r\n // (retry-safe). Committing a released/expired one is a genuine conflict.\r\n if (reservation.status === \"committed\") {\r\n return;\r\n }\r\n\r\n if (reservation.status !== \"reserved\") {\r\n throw new Error(\r\n `Cannot commit reservation in ${reservation.status} state`\r\n );\r\n }\r\n\r\n const amount = reservation.amount;\r\n\r\n // Deduct from balance (bonus credits first logic can be added if needed)\r\n credits.balance -= amount;\r\n credits.reserved -= amount;\r\n credits.monthlyUsed += amount;\r\n credits.updatedAt = new Date().toISOString();\r\n this.users.set(userId, credits);\r\n\r\n // Update reservation status\r\n await this.updateReservationStatus(userId, reservationId, \"committed\", new Date());\r\n }\r\n\r\n async releaseReservationAtomic(userId: string, reservationId: string): Promise<void> {\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n const reservation = await this.getReservation(userId, reservationId);\r\n if (!reservation) {\r\n throw new Error(`Reservation ${reservationId} not found`);\r\n }\r\n\r\n if (reservation.status !== \"reserved\") {\r\n // Already processed, no-op\r\n return;\r\n }\r\n\r\n // Release reserved credits\r\n credits.reserved -= reservation.amount;\r\n credits.updatedAt = new Date().toISOString();\r\n this.users.set(userId, credits);\r\n\r\n // Update reservation status\r\n await this.updateReservationStatus(userId, reservationId, \"released\", new Date());\r\n }\r\n\r\n async addCreditsAtomic(\r\n userId: string,\r\n amount: number,\r\n description: string,\r\n paymentRef?: string\r\n ): Promise<void> {\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n const previousBalance = credits.bonusCredits;\r\n credits.bonusCredits += amount;\r\n credits.updatedAt = new Date().toISOString();\r\n this.users.set(userId, credits);\r\n\r\n // Create transaction\r\n await this.createTransaction({\r\n userId,\r\n type: \"purchase\",\r\n amount,\r\n description,\r\n paymentRef,\r\n previousBalance,\r\n newBalance: credits.bonusCredits,\r\n });\r\n }\r\n\r\n async deductCreditsAtomic(\r\n userId: string,\r\n amount: number\r\n ): Promise<{ previousBalance: number; newBalance: number }> {\r\n if (amount <= 0) {\r\n throw new Error(`deductCreditsAtomic amount must be positive (got ${amount})`);\r\n }\r\n\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User credits not found for userId: ${userId}`);\r\n }\r\n\r\n const available = credits.balance + credits.bonusCredits - credits.reserved;\r\n if (available < amount) {\r\n throw new Error(\r\n `Insufficient credits. Available: ${available}, requested: ${amount}`\r\n );\r\n }\r\n\r\n // Drain balance first, then bonusCredits — same policy as commit.\r\n const balanceDeduction = Math.min(credits.balance, amount);\r\n const bonusDeduction = amount - balanceDeduction;\r\n\r\n const previousBalance = credits.balance + credits.bonusCredits;\r\n credits.balance -= balanceDeduction;\r\n credits.bonusCredits -= bonusDeduction;\r\n credits.updatedAt = new Date().toISOString();\r\n this.users.set(userId, credits);\r\n\r\n return { previousBalance, newBalance: previousBalance - amount };\r\n }\r\n\r\n // ==================== Transactions ====================\r\n\r\n async createTransaction(input: CreateTransactionInput): Promise<PortableTransaction> {\r\n const transaction: PortableTransaction = {\r\n id: generateId(),\r\n userId: input.userId,\r\n type: input.type,\r\n amount: input.amount,\r\n description: input.description,\r\n paymentRef: input.paymentRef,\r\n previousBalance: input.previousBalance,\r\n newBalance: input.newBalance,\r\n createdAt: new Date().toISOString(),\r\n };\r\n\r\n if (!this.transactions.has(input.userId)) {\r\n this.transactions.set(input.userId, []);\r\n }\r\n this.transactions.get(input.userId)!.push(transaction);\r\n\r\n return transaction;\r\n }\r\n\r\n async getTransactions(\r\n userId: string,\r\n limit = 50,\r\n offset = 0\r\n ): Promise<PortableTransaction[]> {\r\n const userTransactions = this.transactions.get(userId) ?? [];\r\n // Sort by createdAt descending (most recent first)\r\n const sorted = [...userTransactions].sort((a, b) => {\r\n const aDate = toDate(a.createdAt).getTime();\r\n const bDate = toDate(b.createdAt).getTime();\r\n return bDate - aDate;\r\n });\r\n return sorted.slice(offset, offset + limit);\r\n }\r\n\r\n // ==================== Usage Logs ====================\r\n\r\n async logUsage(input: CreateUsageLogInput): Promise<PortableUsageLog> {\r\n const log: PortableUsageLog = {\r\n id: generateId(),\r\n userId: input.userId,\r\n operationType: input.operationType,\r\n provider: input.provider,\r\n creditsUsed: input.creditsUsed,\r\n success: input.success,\r\n errorMessage: input.errorMessage,\r\n resourceId: input.resourceId,\r\n resourceType: input.resourceType,\r\n requestId: input.requestId,\r\n metadata: input.metadata,\r\n createdAt: new Date().toISOString(),\r\n };\r\n\r\n this.usageLogs.push(log);\r\n return log;\r\n }\r\n\r\n async getUsageLogs(query: UsageLogQuery): Promise<PortableUsageLog[]> {\r\n let results = [...this.usageLogs];\r\n\r\n // Apply filters\r\n if (query.userId) {\r\n results = results.filter((log) => log.userId === query.userId);\r\n }\r\n if (query.operationType) {\r\n results = results.filter((log) => log.operationType === query.operationType);\r\n }\r\n if (query.success !== undefined) {\r\n results = results.filter((log) => log.success === query.success);\r\n }\r\n if (query.startDate) {\r\n const startTime = query.startDate.getTime();\r\n results = results.filter(\r\n (log) => toDate(log.createdAt).getTime() >= startTime\r\n );\r\n }\r\n if (query.endDate) {\r\n const endTime = query.endDate.getTime();\r\n results = results.filter(\r\n (log) => toDate(log.createdAt).getTime() <= endTime\r\n );\r\n }\r\n\r\n // Sort by createdAt descending\r\n results.sort((a, b) => {\r\n return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();\r\n });\r\n\r\n // Apply pagination\r\n const offset = query.offset ?? 0;\r\n const limit = query.limit ?? 50;\r\n return results.slice(offset, offset + limit);\r\n }\r\n\r\n async getUsageLogsCount(\r\n query: Omit<UsageLogQuery, \"limit\" | \"offset\">\r\n ): Promise<number> {\r\n const results = await this.getUsageLogs({ ...query, limit: Infinity, offset: 0 });\r\n return results.length;\r\n }\r\n\r\n // ==================== Journal Entries ====================\r\n\r\n async createJournalEntry(input: CreateJournalEntryInput): Promise<PortableJournalEntry> {\r\n const entry: PortableJournalEntry = {\r\n id: generateId(),\r\n userId: input.userId,\r\n entryType: input.entryType,\r\n amount: input.amount,\r\n balanceAfter: input.balanceAfter,\r\n source: input.source,\r\n referenceId: input.referenceId,\r\n referenceType: input.referenceType,\r\n description: input.description,\r\n metadata: input.metadata,\r\n createdAt: new Date().toISOString(),\r\n };\r\n\r\n if (!this.journalEntries.has(input.userId)) {\r\n this.journalEntries.set(input.userId, []);\r\n }\r\n this.journalEntries.get(input.userId)!.push(entry);\r\n\r\n return entry;\r\n }\r\n\r\n async getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]> {\r\n let results = this.journalEntries.get(query.userId) ?? [];\r\n\r\n // Apply filters\r\n if (query.source) {\r\n results = results.filter((entry) => entry.source === query.source);\r\n }\r\n if (query.referenceType) {\r\n results = results.filter((entry) => entry.referenceType === query.referenceType);\r\n }\r\n if (query.startDate) {\r\n const startTime = query.startDate.getTime();\r\n results = results.filter(\r\n (entry) => toDate(entry.createdAt).getTime() >= startTime\r\n );\r\n }\r\n if (query.endDate) {\r\n const endTime = query.endDate.getTime();\r\n results = results.filter(\r\n (entry) => toDate(entry.createdAt).getTime() <= endTime\r\n );\r\n }\r\n\r\n // Sort by createdAt descending\r\n results = [...results].sort((a, b) => {\r\n return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();\r\n });\r\n\r\n // Apply pagination\r\n const offset = query.offset ?? 0;\r\n const limit = query.limit ?? 50;\r\n return results.slice(offset, offset + limit);\r\n }\r\n\r\n async getJournalEntriesCount(\r\n query: Omit<JournalEntryQuery, \"limit\" | \"offset\">\r\n ): Promise<number> {\r\n const results = await this.getJournalEntries({\r\n ...query,\r\n limit: Infinity,\r\n offset: 0,\r\n });\r\n return results.length;\r\n }\r\n\r\n // ==================== Cleanup Operations ====================\r\n\r\n async findAndExpireReservations(\r\n _batchSize = 100,\r\n _maxIterations = 100\r\n ): Promise<{\r\n expiredCount: number;\r\n creditsReleased: number;\r\n errors: string[];\r\n }> {\r\n const now = new Date();\r\n let expiredCount = 0;\r\n let creditsReleased = 0;\r\n const errors: string[] = [];\r\n\r\n for (const [userId, userReservations] of this.reservations) {\r\n for (const [reservationId, reservation] of userReservations) {\r\n if (\r\n reservation.status === \"reserved\" &&\r\n toDate(reservation.expiresAt).getTime() < now.getTime()\r\n ) {\r\n try {\r\n // Release the reservation\r\n await this.releaseReservationAtomic(userId, reservationId);\r\n // Mark as expired instead of released\r\n reservation.status = \"expired\";\r\n userReservations.set(reservationId, reservation);\r\n\r\n expiredCount++;\r\n creditsReleased += reservation.amount;\r\n } catch (error) {\r\n errors.push(\r\n `Failed to expire reservation ${reservationId}: ${error}`\r\n );\r\n }\r\n }\r\n }\r\n }\r\n\r\n return { expiredCount, creditsReleased, errors };\r\n }\r\n\r\n // ==================== Atomic Monthly Reset ====================\r\n\r\n async atomicMonthlyReset(\r\n userId: string,\r\n tier: SubscriptionTier,\r\n expectedResetAt: Date | string\r\n ): Promise<MonthlyResetResult> {\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n // Optimistic locking: check if expectedResetAt matches current value\r\n const currentResetAt = toDate(credits.monthlyResetAt).getTime();\r\n const expected = toDate(expectedResetAt).getTime();\r\n\r\n if (currentResetAt !== expected) {\r\n // Another request already performed the reset\r\n return { wasReset: false, credits };\r\n }\r\n\r\n // Perform the reset\r\n const newBalance = getConfigMonthlyLimit(tier);\r\n const nextReset = getNextMonthlyReset();\r\n\r\n credits.balance = newBalance === Infinity ? credits.balance : newBalance;\r\n credits.monthlyUsed = 0;\r\n credits.monthlyResetAt = nextReset.toISOString();\r\n credits.updatedAt = new Date().toISOString();\r\n\r\n this.users.set(userId, credits);\r\n\r\n return { wasReset: true, credits };\r\n }\r\n\r\n // ==================== Subscription Expiry ====================\r\n\r\n async checkAndHandleSubscriptionExpiry(\r\n userId: string,\r\n gracePeriodDays = 3\r\n ): Promise<SubscriptionExpiryResult> {\r\n const credits = this.users.get(userId);\r\n if (!credits) {\r\n throw new Error(`User ${userId} not found`);\r\n }\r\n\r\n // Free tier doesn't expire\r\n if (isFreeTier(credits.tier) || !credits.subscriptionExpiresAt) {\r\n return {\r\n wasDowngraded: false,\r\n inGracePeriod: false,\r\n graceDaysRemaining: 0,\r\n credits,\r\n };\r\n }\r\n\r\n const now = new Date();\r\n const expiresAt = toDate(credits.subscriptionExpiresAt);\r\n const daysSinceExpiry =\r\n (now.getTime() - expiresAt.getTime()) / (1000 * 60 * 60 * 24);\r\n\r\n if (daysSinceExpiry <= 0) {\r\n // Not expired yet\r\n return {\r\n wasDowngraded: false,\r\n inGracePeriod: false,\r\n graceDaysRemaining: 0,\r\n credits,\r\n };\r\n }\r\n\r\n if (daysSinceExpiry <= gracePeriodDays) {\r\n // In grace period\r\n return {\r\n wasDowngraded: false,\r\n inGracePeriod: true,\r\n graceDaysRemaining: Math.ceil(gracePeriodDays - daysSinceExpiry),\r\n credits,\r\n };\r\n }\r\n\r\n // Grace period expired - downgrade to the default (free) tier\r\n const defaultTier = getDefaultTier();\r\n const defaultTierConfig = getConfigTierConfig(defaultTier);\r\n\r\n credits.tier = defaultTier;\r\n credits.monthlyLimit = defaultTierConfig.monthlyCredits;\r\n credits.balance = Math.min(credits.balance, defaultTierConfig.monthlyCredits);\r\n credits.subscriptionExpiresAt = null;\r\n credits.updatedAt = new Date().toISOString();\r\n\r\n this.users.set(userId, credits);\r\n\r\n return {\r\n wasDowngraded: true,\r\n inGracePeriod: false,\r\n graceDaysRemaining: 0,\r\n credits,\r\n };\r\n }\r\n\r\n // ==================== Testing Utilities ====================\r\n\r\n /**\r\n * Clear all data (useful for testing)\r\n */\r\n clear(): void {\r\n this.users.clear();\r\n this.reservations.clear();\r\n this.transactions.clear();\r\n this.usageLogs = [];\r\n this.journalEntries.clear();\r\n }\r\n\r\n /**\r\n * Get all users (useful for testing/debugging)\r\n */\r\n getAllUsers(): PortableUserCredits[] {\r\n return Array.from(this.users.values());\r\n }\r\n\r\n /**\r\n * Get all reservations for a user (useful for testing)\r\n */\r\n getAllReservations(userId: string): PortableReservation[] {\r\n const userReservations = this.reservations.get(userId);\r\n if (!userReservations) return [];\r\n return Array.from(userReservations.values());\r\n }\r\n}\r\n\r\n/**\r\n * Create a new in-memory repository instance\r\n * Each call creates a fresh, isolated instance\r\n */\r\nexport function createInMemoryCreditRepository(): InMemoryCreditRepository {\r\n return new InMemoryCreditRepository();\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
- import { I as ICreditRepository, d as CreditBalanceUpdate, T as TierUpdateInput, a as CreateReservationInput, b as CreateTransactionInput, c as CreateUsageLogInput, U as UsageLogQuery, C as CreateJournalEntryInput, J as JournalEntryQuery } from '../types-D1vdeBcA.cjs';
2
- export { e as CreditRepositoryFactory, t as toClientUserCredits } from '../types-D1vdeBcA.cjs';
3
- import { b as PortableUserCredits, S as SubscriptionTier, c as PortableReservation, R as ReservationStatus, P as PortableTransaction, d as PortableUsageLog, f as PortableJournalEntry, M as MonthlyResetResult, e as SubscriptionExpiryResult } from '../types-BBxIWHK2.cjs';
1
+ import { I as ICreditRepository, d as CreditBalanceUpdate, T as TierUpdateInput, a as CreateReservationInput, b as CreateTransactionInput, c as CreateUsageLogInput, U as UsageLogQuery, C as CreateJournalEntryInput, J as JournalEntryQuery } from '../types-KgNdBuEF.cjs';
2
+ export { e as CreditRepositoryFactory, t as toClientUserCredits } from '../types-KgNdBuEF.cjs';
3
+ import { b as PortableUserCredits, S as SubscriptionTier, c as PortableReservation, R as ReservationStatus, P as PortableTransaction, d as PortableUsageLog, f as PortableJournalEntry, M as MonthlyResetResult, e as SubscriptionExpiryResult } from '../types-DgQtamEe.cjs';
4
4
 
5
5
  /**
6
6
  * Shared utilities for credit repository implementations
@@ -1,6 +1,6 @@
1
- import { I as ICreditRepository, d as CreditBalanceUpdate, T as TierUpdateInput, a as CreateReservationInput, b as CreateTransactionInput, c as CreateUsageLogInput, U as UsageLogQuery, C as CreateJournalEntryInput, J as JournalEntryQuery } from '../types-aJsyv8df.js';
2
- export { e as CreditRepositoryFactory, t as toClientUserCredits } from '../types-aJsyv8df.js';
3
- import { b as PortableUserCredits, S as SubscriptionTier, c as PortableReservation, R as ReservationStatus, P as PortableTransaction, d as PortableUsageLog, f as PortableJournalEntry, M as MonthlyResetResult, e as SubscriptionExpiryResult } from '../types-BBxIWHK2.js';
1
+ import { I as ICreditRepository, d as CreditBalanceUpdate, T as TierUpdateInput, a as CreateReservationInput, b as CreateTransactionInput, c as CreateUsageLogInput, U as UsageLogQuery, C as CreateJournalEntryInput, J as JournalEntryQuery } from '../types-fW2JNmmr.js';
2
+ export { e as CreditRepositoryFactory, t as toClientUserCredits } from '../types-fW2JNmmr.js';
3
+ import { b as PortableUserCredits, S as SubscriptionTier, c as PortableReservation, R as ReservationStatus, P as PortableTransaction, d as PortableUsageLog, f as PortableJournalEntry, M as MonthlyResetResult, e as SubscriptionExpiryResult } from '../types-DgQtamEe.js';
4
4
 
5
5
  /**
6
6
  * Shared utilities for credit repository implementations
@@ -55,10 +55,13 @@ function getConfigProvider() {
55
55
 
56
56
  // src/config/index.ts
57
57
  var tierConfigSchema = z.object({
58
- tier: z.enum(["free", "basic", "premium", "unlimited"]),
58
+ tier: z.string().min(1),
59
59
  monthlyCredits: z.number().min(0),
60
60
  priceUsd: z.number().min(0),
61
- features: z.array(z.string())
61
+ features: z.array(z.string()),
62
+ isFree: z.boolean().optional(),
63
+ unlimited: z.boolean().optional(),
64
+ isDefault: z.boolean().optional()
62
65
  });
63
66
  var creditSystemConfigSchema = z.object({
64
67
  /**
@@ -82,7 +85,7 @@ var creditSystemConfigSchema = z.object({
82
85
  * Tier configurations
83
86
  */
84
87
  tierConfigs: z.record(
85
- z.enum(["free", "basic", "premium", "unlimited"]),
88
+ z.string().min(1),
86
89
  tierConfigSchema
87
90
  ),
88
91
  /**
@@ -138,6 +141,8 @@ var DEFAULT_CONFIG = {
138
141
  tier: "free",
139
142
  monthlyCredits: 25,
140
143
  priceUsd: 0,
144
+ isFree: true,
145
+ isDefault: true,
141
146
  features: [
142
147
  "25 credits per month",
143
148
  "Basic story generation",
@@ -171,6 +176,7 @@ var DEFAULT_CONFIG = {
171
176
  tier: "unlimited",
172
177
  monthlyCredits: 0,
173
178
  // 0 = unlimited
179
+ unlimited: true,
174
180
  priceUsd: 49.99,
175
181
  features: [
176
182
  "Unlimited credits",
@@ -283,10 +289,40 @@ function getConfig() {
283
289
  }
284
290
  return currentConfig;
285
291
  }
292
+ function getValidTiers() {
293
+ return Object.keys(getConfig().tierConfigs);
294
+ }
295
+ function isValidTier(tier) {
296
+ return tier in getConfig().tierConfigs;
297
+ }
298
+ function isFreeTier(tier) {
299
+ const c = getConfig().tierConfigs[tier];
300
+ if (!c) return false;
301
+ return c.isFree ?? c.priceUsd === 0;
302
+ }
303
+ function isUnlimitedTier(tier) {
304
+ const c = getConfig().tierConfigs[tier];
305
+ if (!c) return false;
306
+ return c.unlimited ?? c.monthlyCredits === 0;
307
+ }
308
+ function getDefaultTier() {
309
+ const entries = Object.entries(getConfig().tierConfigs);
310
+ const def = entries.find(([, c]) => c.isDefault)?.[0] ?? entries.find(([, c]) => c.isFree ?? c.priceUsd === 0)?.[0];
311
+ return def ?? "free";
312
+ }
313
+ function getConfigTierConfig(tier) {
314
+ const cfg = getConfig().tierConfigs[tier];
315
+ if (cfg) return cfg;
316
+ const fallback = getDefaultTier();
317
+ console.warn(
318
+ `[Credits Config] Unknown tier "${tier}". Falling back to "${fallback}". Valid: ${getValidTiers().join(", ")}`
319
+ );
320
+ return getConfig().tierConfigs[fallback];
321
+ }
286
322
  function getConfigMonthlyLimit(tier) {
287
- const config = getConfig().tierConfigs[tier];
288
- return config.monthlyCredits === 0 ? Infinity : config.monthlyCredits;
323
+ return isUnlimitedTier(tier) ? Infinity : getConfigTierConfig(tier).monthlyCredits;
289
324
  }
325
+ var tierSchema = z.string().refine(isValidTier, { message: "Unknown subscription tier" });
290
326
  initializeConfig();
291
327
 
292
328
  // src/repository/memory/index.ts
@@ -442,6 +478,9 @@ var InMemoryCreditRepository = class {
442
478
  if (!reservation) {
443
479
  throw new Error(`Reservation ${reservationId} not found`);
444
480
  }
481
+ if (reservation.status === "committed") {
482
+ return;
483
+ }
445
484
  if (reservation.status !== "reserved") {
446
485
  throw new Error(
447
486
  `Cannot commit reservation in ${reservation.status} state`
@@ -702,7 +741,7 @@ var InMemoryCreditRepository = class {
702
741
  if (!credits) {
703
742
  throw new Error(`User ${userId} not found`);
704
743
  }
705
- if (credits.tier === "free" || !credits.subscriptionExpiresAt) {
744
+ if (isFreeTier(credits.tier) || !credits.subscriptionExpiresAt) {
706
745
  return {
707
746
  wasDowngraded: false,
708
747
  inGracePeriod: false,
@@ -729,11 +768,11 @@ var InMemoryCreditRepository = class {
729
768
  credits
730
769
  };
731
770
  }
732
- const config = getConfig();
733
- const freeTierConfig = config.tierConfigs.free;
734
- credits.tier = "free";
735
- credits.monthlyLimit = freeTierConfig.monthlyCredits;
736
- credits.balance = Math.min(credits.balance, freeTierConfig.monthlyCredits);
771
+ const defaultTier = getDefaultTier();
772
+ const defaultTierConfig = getConfigTierConfig(defaultTier);
773
+ credits.tier = defaultTier;
774
+ credits.monthlyLimit = defaultTierConfig.monthlyCredits;
775
+ credits.balance = Math.min(credits.balance, defaultTierConfig.monthlyCredits);
737
776
  credits.subscriptionExpiresAt = null;
738
777
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
739
778
  this.users.set(userId, credits);