@nehorai/credits 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/adapters/index.cjs +463 -0
- package/dist/adapters/index.cjs.map +1 -0
- package/dist/adapters/index.d.cts +66 -0
- package/dist/adapters/index.d.ts +66 -3
- package/dist/adapters/index.js +435 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/auth/index.cjs +96 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/{api-key.d.ts → index.d.cts} +9 -6
- package/dist/auth/index.d.ts +37 -5
- package/dist/auth/index.js +65 -7
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.cjs +522 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +119 -0
- package/dist/core/index.d.ts +115 -25
- package/dist/core/index.js +475 -27
- package/dist/core/index.js.map +1 -1
- package/dist/{core/deferred.d.ts → deferred-C7rrRfkm.d.cts} +7 -6
- package/dist/{core/deferred.js → deferred-C7rrRfkm.d.ts} +23 -29
- package/dist/index.cjs +2506 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +437 -0
- package/dist/index.d.ts +429 -50
- package/dist/index.js +2399 -63
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +816 -0
- package/dist/repository/index.cjs.map +1 -0
- package/dist/repository/{memory/index.d.ts → index.d.cts} +35 -5
- package/dist/repository/index.d.ts +97 -7
- package/dist/repository/index.js +783 -10
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.cjs +608 -0
- package/dist/sdk/index.cjs.map +1 -0
- package/dist/sdk/index.d.cts +495 -0
- package/dist/sdk/index.d.ts +491 -9
- package/dist/sdk/index.js +568 -12
- package/dist/sdk/index.js.map +1 -1
- package/dist/service/index.cjs +670 -0
- package/dist/service/index.cjs.map +1 -0
- package/dist/service/{credits-service.d.ts → index.d.cts} +9 -7
- package/dist/service/index.d.ts +141 -3
- package/dist/service/index.js +641 -1
- package/dist/service/index.js.map +1 -1
- package/dist/{core/types.d.ts → types-BBxIWHK2.d.cts} +25 -24
- package/dist/types-BBxIWHK2.d.ts +277 -0
- package/dist/types-D1vdeBcA.d.cts +305 -0
- package/dist/{auth/types.d.ts → types-OZU3I5OM.d.cts} +5 -4
- package/dist/types-OZU3I5OM.d.ts +32 -0
- package/dist/{repository/types.d.ts → types-aJsyv8df.d.ts} +35 -13
- package/package.json +21 -19
- package/dist/adapters/generic.d.ts +0 -18
- package/dist/adapters/generic.d.ts.map +0 -1
- package/dist/adapters/generic.js +0 -151
- package/dist/adapters/generic.js.map +0 -1
- package/dist/adapters/index.d.ts.map +0 -1
- package/dist/adapters/types.d.ts +0 -45
- package/dist/adapters/types.d.ts.map +0 -1
- package/dist/adapters/types.js +0 -8
- package/dist/adapters/types.js.map +0 -1
- package/dist/auth/api-key.d.ts.map +0 -1
- package/dist/auth/api-key.js +0 -94
- package/dist/auth/api-key.js.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/auth/types.js +0 -2
- package/dist/auth/types.js.map +0 -1
- package/dist/config/costs.d.ts +0 -56
- package/dist/config/costs.d.ts.map +0 -1
- package/dist/config/costs.js +0 -71
- package/dist/config/costs.js.map +0 -1
- package/dist/config/index.d.ts +0 -184
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -342
- package/dist/config/index.js.map +0 -1
- package/dist/config/provider.d.ts +0 -12
- package/dist/config/provider.d.ts.map +0 -1
- package/dist/config/provider.js +0 -14
- package/dist/config/provider.js.map +0 -1
- package/dist/core/deferred.d.ts.map +0 -1
- package/dist/core/deferred.js.map +0 -1
- package/dist/core/errors.d.ts +0 -78
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/errors.js +0 -110
- package/dist/core/errors.js.map +0 -1
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/operations.d.ts +0 -36
- package/dist/core/operations.d.ts.map +0 -1
- package/dist/core/operations.js +0 -88
- package/dist/core/operations.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js +0 -82
- package/dist/core/types.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/notifications/index.d.ts +0 -76
- package/dist/notifications/index.d.ts.map +0 -1
- package/dist/notifications/index.js +0 -174
- package/dist/notifications/index.js.map +0 -1
- package/dist/repository/index.d.ts.map +0 -1
- package/dist/repository/memory/index.d.ts.map +0 -1
- package/dist/repository/memory/index.js +0 -518
- package/dist/repository/memory/index.js.map +0 -1
- package/dist/repository/types.d.ts.map +0 -1
- package/dist/repository/types.js +0 -39
- package/dist/repository/types.js.map +0 -1
- package/dist/repository/utils.d.ts +0 -22
- package/dist/repository/utils.d.ts.map +0 -1
- package/dist/repository/utils.js +0 -40
- package/dist/repository/utils.js.map +0 -1
- package/dist/sdk/admin-client.d.ts +0 -146
- package/dist/sdk/admin-client.d.ts.map +0 -1
- package/dist/sdk/admin-client.js +0 -196
- package/dist/sdk/admin-client.js.map +0 -1
- package/dist/sdk/client.d.ts +0 -144
- package/dist/sdk/client.d.ts.map +0 -1
- package/dist/sdk/client.js +0 -247
- package/dist/sdk/client.js.map +0 -1
- package/dist/sdk/errors.d.ts +0 -91
- package/dist/sdk/errors.d.ts.map +0 -1
- package/dist/sdk/errors.js +0 -185
- package/dist/sdk/errors.js.map +0 -1
- package/dist/sdk/index.d.ts.map +0 -1
- package/dist/sdk/types.d.ts +0 -110
- package/dist/sdk/types.d.ts.map +0 -1
- package/dist/sdk/types.js +0 -20
- package/dist/sdk/types.js.map +0 -1
- package/dist/service/credits-service.d.ts.map +0 -1
- package/dist/service/credits-service.js +0 -368
- package/dist/service/credits-service.js.map +0 -1
- package/dist/service/index.d.ts.map +0 -1
- package/dist/utils/error-utils.d.ts +0 -48
- package/dist/utils/error-utils.d.ts.map +0 -1
- package/dist/utils/error-utils.js +0 -57
- package/dist/utils/error-utils.js.map +0 -1
- package/dist/utils/index.d.ts +0 -54
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -72
- package/dist/utils/index.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/service/index.ts","../../src/core/types.ts","../../src/repository/types.ts","../../src/config/index.ts","../../src/config/provider.ts","../../src/service/credits-service.ts"],"sourcesContent":["export {\n CreditsService,\n createCreditsService,\n} from \"./credits-service.js\";\n\nexport type {\n LowBalanceNotificationCallback,\n SubscriptionExpiredNotificationCallback,\n} from \"./credits-service.js\";\n","/**\n * Portable credits types - framework and database agnostic\n *\n * These types use standard JavaScript types (string, Date) instead of\n * Firebase-specific types (Timestamp). This makes the credits system\n * portable to other environments without Firebase dependencies.\n *\n * Note: Firestore Timestamp is handled by the credits-firestore adapter.\n */\n\n// ==================== Utility Functions ====================\n\n/**\n * Calculate available credits from balance, bonus, and reserved amounts.\n *\n * Available = balance + bonusCredits - reserved\n *\n * @param balance - Regular monthly credits\n * @param bonusCredits - Purchased/admin credits\n * @param reserved - Credits locked for in-flight operations\n * @returns Available credits for new operations\n */\nexport function calculateAvailableCredits(\n balance: number,\n bonusCredits: number,\n reserved: number\n): number {\n return balance + bonusCredits - reserved;\n}\n\n/**\n * Convert various timestamp formats to ISO string.\n *\n * Handles:\n * - Date objects\n * - ISO string (returned as-is)\n * - Timestamp-like objects with toDate() method (Firebase)\n * - undefined/null (returns current time)\n *\n * @param value - Value to convert\n * @returns ISO 8601 string\n */\nexport function toPortableTimestamp(value: unknown): string {\n if (!value) {\n return new Date().toISOString();\n }\n\n if (typeof value === \"string\") {\n return value;\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (\n typeof value === \"object\" &&\n value !== null &&\n \"toDate\" in value &&\n typeof (value as { toDate: () => Date }).toDate === \"function\"\n ) {\n return (value as { toDate: () => Date }).toDate().toISOString();\n }\n\n return new Date().toISOString();\n}\n\n/**\n * Convert various timestamp formats to Date object.\n *\n * Handles:\n * - Date objects (returned as-is)\n * - ISO string\n * - Timestamp-like objects with toDate() method (Firebase)\n * - Invalid values (returns current date)\n *\n * @param value - Value to convert\n * @returns Date object\n */\nexport function toDate(value: unknown): Date {\n if (value instanceof Date) {\n return value;\n }\n\n if (typeof value === \"string\") {\n return new Date(value);\n }\n\n if (\n typeof value === \"object\" &&\n value !== null &&\n \"toDate\" in value &&\n typeof (value as { toDate: () => Date }).toDate === \"function\"\n ) {\n return (value as { toDate: () => Date }).toDate();\n }\n\n return new Date();\n}\n\n// ==================== Subscription Types ====================\n\n/**\n * Subscription tiers for the credits system\n */\nexport type SubscriptionTier = \"free\" | \"basic\" | \"premium\" | \"unlimited\";\n\n// ==================== Operation Types ====================\n\n/**\n * Operation types for cost tracking (provider-agnostic)\n *\n * Dynamic string type - any operation configured in the system is valid.\n */\nexport type CreditOperationType = string;\n\n/**\n * AI providers\n */\nexport type AIProviderType = \"gemini\";\n\n/**\n * Resource type for usage logging\n */\nexport type ResourceType = string;\n\n// ==================== User Credits ====================\n\n/**\n * Portable user credits balance\n *\n * Uses ISO string timestamps instead of Firebase Timestamp.\n * This type is safe for JSON serialization and works across frameworks.\n */\nexport interface PortableUserCredits {\n userId: string;\n /** Current available balance (monthly credits, reset each month) */\n balance: number;\n /** Bonus credits from purchases/admin (never reset, persist until used) */\n bonusCredits: number;\n /** Credits currently reserved for in-flight operations */\n reserved: number;\n /** User's subscription tier */\n tier: SubscriptionTier;\n /** Monthly credit limit based on tier (0 = unlimited) */\n monthlyLimit: number;\n /** Credits used this month (resets monthly) */\n monthlyUsed: number;\n /** When monthly credits reset (start of next month) - ISO 8601 */\n monthlyResetAt: string;\n /** Subscription expiry (null/undefined for free tier) - ISO 8601 or null */\n subscriptionExpiresAt?: string | null;\n /** Creation timestamp - ISO 8601 */\n createdAt: string;\n /** Last update timestamp - ISO 8601 */\n updatedAt: string;\n}\n\n// ==================== Reservation Types ====================\n\n/**\n * Credit reservation status for two-phase commit\n */\nexport type ReservationStatus = \"reserved\" | \"committed\" | \"released\" | \"expired\";\n\n/**\n * Portable credit reservation\n *\n * Used for two-phase commit to prevent double-spending.\n */\nexport interface PortableReservation {\n id: string;\n userId: string;\n /** Amount of credits reserved */\n amount: number;\n /** Operation type for tracking */\n operationType: CreditOperationType;\n /** Current status of reservation */\n status: ReservationStatus;\n /** When reservation was created - ISO 8601 */\n createdAt: string;\n /** When reservation expires (for cleanup) - ISO 8601 */\n expiresAt: string;\n /** When reservation was committed/released - ISO 8601 or undefined */\n completedAt?: string;\n}\n\n// ==================== Result Types ====================\n\n/**\n * Result of a credit check operation\n */\nexport interface CreditCheckResult {\n /** Whether user has sufficient credits */\n hasCredits: boolean;\n /** Current balance (balance + bonusCredits) */\n balance: number;\n /** Required credits for operation */\n required: number;\n /** Shortfall if insufficient */\n shortfall: number;\n}\n\n/**\n * Result of monthly reset operation\n */\nexport interface MonthlyResetResult {\n /** Whether the reset was performed */\n wasReset: boolean;\n /** The user credits after the operation */\n credits: PortableUserCredits;\n}\n\n/**\n * Result of subscription expiry check\n */\nexport interface SubscriptionExpiryResult {\n /** Whether the subscription was downgraded */\n wasDowngraded: boolean;\n /** Whether user is in grace period */\n inGracePeriod: boolean;\n /** Days remaining in grace period (0 if not in grace) */\n graceDaysRemaining: number;\n /** The user credits after the operation */\n credits: PortableUserCredits;\n}\n\n// ==================== Transaction Types ====================\n\n/**\n * Credit transaction type\n */\nexport type TransactionType = \"purchase\" | \"subscription\" | \"bonus\" | \"refund\" | \"adjustment\";\n\n/**\n * Portable credit transaction\n */\nexport interface PortableTransaction {\n id: string;\n userId: string;\n type: TransactionType;\n /** Amount of credits (positive for additions, negative for deductions) */\n amount: number;\n description: string;\n /** Reference to payment provider */\n paymentRef?: string;\n previousBalance: number;\n newBalance: number;\n createdAt: string;\n}\n\n// ==================== Journal Types ====================\n\n/**\n * Source of a credit journal entry\n */\nexport type CreditSource =\n | \"operation_commit\"\n | \"operation_release\"\n | \"purchase\"\n | \"subscription_grant\"\n | \"subscription_upgrade\"\n | \"subscription_downgrade\"\n | \"monthly_reset\"\n | \"bonus\"\n | \"refund\"\n | \"admin_adjustment\"\n | \"expiry\"\n | \"reservation_expired\";\n\n/**\n * Type of reference for a journal entry\n */\nexport type JournalReferenceType =\n | \"reservation\"\n | \"transaction\"\n | \"adjustment\"\n | \"reset\"\n | \"subscription\";\n\n/**\n * Portable credit journal entry for audit trail\n */\nexport interface PortableJournalEntry {\n id: string;\n userId: string;\n /** Type of entry - debit decreases balance, credit increases balance */\n entryType: \"debit\" | \"credit\";\n /** Amount of credits involved */\n amount: number;\n /** Balance after this entry */\n balanceAfter: number;\n /** Source of the credit change */\n source: CreditSource;\n /** Reference ID (e.g., reservationId, transactionId) */\n referenceId: string;\n /** Type of reference */\n referenceType: JournalReferenceType;\n /** Human-readable description */\n description: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n /** When this entry was created - ISO 8601 */\n createdAt: string;\n}\n\n// ==================== Usage Types ====================\n\n/**\n * Portable usage log entry\n */\nexport interface PortableUsageLog {\n id: string;\n userId: string;\n operationType: CreditOperationType;\n provider: AIProviderType;\n creditsUsed: number;\n success: boolean;\n errorMessage?: string;\n resourceId?: string;\n resourceType?: ResourceType;\n requestId?: string;\n metadata?: Record<string, unknown>;\n createdAt: string;\n}\n\n/**\n * User-friendly usage history entry\n */\nexport interface UsageHistoryEntry {\n id: string;\n /** Type of entry for display */\n type: \"usage\" | \"purchase\" | \"bonus\" | \"reset\" | \"refund\" | \"adjustment\";\n /** Positive = earned, Negative = spent */\n creditsChange: number;\n /** Balance after this entry */\n balanceAfter: number;\n /** Human-readable description */\n description: string;\n /** When this occurred - ISO 8601 */\n createdAt: string;\n}\n\n/**\n * Paginated usage history response\n */\nexport interface UsageHistoryResponse {\n entries: UsageHistoryEntry[];\n pagination: {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n };\n}\n\n// ==================== Tier Configuration ====================\n\n/**\n * Tier configuration for credit limits and pricing\n */\nexport interface TierConfig {\n tier: SubscriptionTier;\n monthlyCredits: number;\n priceUsd: number;\n features: string[];\n}\n\n// ==================== Options Types ====================\n\n/**\n * Options for withCredits HOF\n */\nexport interface WithCreditsOptions {\n /** Operation type for cost lookup */\n operationType: CreditOperationType;\n /** AI provider (defaults to \"gemini\") */\n provider?: AIProviderType;\n /** Custom cost override (use with caution) */\n customCost?: number;\n /** Resource ID for usage logging */\n resourceId?: string;\n /** Resource type for usage logging */\n resourceType?: ResourceType;\n}\n\n// ==================== Constants ====================\n","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","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","import type {\n PortableUserCredits,\n PortableReservation,\n CreditCheckResult,\n CreditOperationType,\n SubscriptionTier,\n PortableUsageLog,\n PortableJournalEntry,\n UsageHistoryEntry,\n UsageHistoryResponse,\n} from \"../core/types.js\";\nimport { toDate } from \"../core/types.js\";\nimport type { ICreditRepository, CreateUsageLogInput, JournalEntryQuery } from \"../repository/types.js\";\nimport { toClientUserCredits } from \"../repository/types.js\";\nimport { getConfig, getConfigMonthlyLimit, getOperationLabel } from \"../config/index.js\";\n\n/**\n * Check if a date is past the monthly reset date\n */\nfunction isPastMonthlyReset(resetAt: unknown): boolean {\n if (!resetAt) return false;\n const resetDate = toDate(resetAt);\n return new Date() >= resetDate;\n}\n\n/**\n * Notification callback type for low balance notifications\n */\nexport type LowBalanceNotificationCallback = (userId: string, balance: number) => Promise<void>;\n\n/**\n * Notification callback type for subscription expired notifications\n */\nexport type SubscriptionExpiredNotificationCallback = (userId: string, wasDowngraded: boolean) => Promise<void>;\n\n/**\n * Credits service with dependency injection for repository\n *\n * Provides business logic for credit operations, delegating\n * database operations to the injected repository.\n */\nexport class CreditsService {\n private lowBalanceCallback?: LowBalanceNotificationCallback;\n private subscriptionExpiredCallback?: SubscriptionExpiredNotificationCallback;\n\n constructor(private readonly repository: ICreditRepository) {}\n\n /**\n * Set callback for low balance notifications\n */\n setLowBalanceCallback(callback: LowBalanceNotificationCallback): void {\n this.lowBalanceCallback = callback;\n }\n\n /**\n * Set callback for subscription expired notifications\n */\n setSubscriptionExpiredCallback(callback: SubscriptionExpiredNotificationCallback): void {\n this.subscriptionExpiredCallback = callback;\n }\n\n /**\n * Get user credits, performing monthly reset and subscription expiry checks if needed\n *\n * This method uses atomic operations to prevent race conditions:\n * 1. Checks subscription expiry with grace period\n * 2. Atomically performs monthly reset if needed (with optimistic locking)\n *\n * @param userId - User ID\n * @returns User credits or null if not found\n */\n async getUserCredits(userId: string): Promise<PortableUserCredits | null> {\n let data = await this.repository.getUserCredits(userId);\n\n if (!data) {\n return null;\n }\n\n // Step 1: Check subscription expiry (for non-free tiers)\n if (data.tier !== \"free\" && data.subscriptionExpiresAt) {\n const expiryResult = await this.repository.checkAndHandleSubscriptionExpiry(\n userId,\n getConfig().subscriptionGracePeriodDays\n );\n\n if (expiryResult.wasDowngraded) {\n // Create journal entry for downgrade\n await this.repository.createJournalEntry({\n userId,\n entryType: \"debit\",\n amount: 0, // No credits deducted, just tier change\n balanceAfter: expiryResult.credits.balance,\n source: \"subscription_downgrade\",\n referenceId: `downgrade-${Date.now()}`,\n referenceType: \"subscription\",\n description: `Subscription expired. Downgraded from ${data.tier} to free tier.`,\n metadata: {\n previousTier: data.tier,\n previousBalance: data.balance,\n newBalance: expiryResult.credits.balance,\n },\n });\n\n // Trigger subscription expired notification (non-blocking)\n if (this.subscriptionExpiredCallback) {\n this.subscriptionExpiredCallback(userId, true).catch((error) => {\n console.error(\"[Credits] Failed to send subscription expired notification:\", error);\n });\n }\n }\n\n // Use the potentially updated credits\n data = expiryResult.credits;\n }\n\n // Step 2: Check if monthly reset is needed (use atomic operation)\n if (isPastMonthlyReset(data.monthlyResetAt)) {\n // Convert monthlyResetAt to a compatible type (Date or string)\n const expectedResetAt = toDate(data.monthlyResetAt);\n const resetResult = await this.repository.atomicMonthlyReset(\n userId,\n data.tier,\n expectedResetAt\n );\n\n if (resetResult.wasReset) {\n // Create journal entry for monthly reset\n const balanceChange = resetResult.credits.balance - data.balance;\n if (balanceChange !== 0) {\n await this.repository.createJournalEntry({\n userId,\n entryType: balanceChange > 0 ? \"credit\" : \"debit\",\n amount: Math.abs(balanceChange),\n balanceAfter: resetResult.credits.balance,\n source: \"monthly_reset\",\n referenceId: `reset-${Date.now()}`,\n referenceType: \"reset\",\n description: `Monthly credit reset for ${data.tier} tier.`,\n metadata: {\n tier: data.tier,\n previousBalance: data.balance,\n newBalance: resetResult.credits.balance,\n },\n });\n }\n }\n\n // Use the potentially updated credits\n data = resetResult.credits;\n }\n\n return toClientUserCredits(data);\n }\n\n /**\n * Initialize credits for a new user with free tier\n * @param userId - User ID\n * @returns Initialized user credits\n */\n async initializeUserCredits(userId: string): Promise<PortableUserCredits> {\n const credits = await this.repository.initializeUserCredits(\n userId,\n \"free\",\n getConfig().defaultFreeCredits\n );\n return toClientUserCredits(credits);\n }\n\n /**\n * Get or create user credits\n * Initializes with free tier if not exists\n * @param userId - User ID\n * @returns User credits\n */\n async getOrCreateUserCredits(userId: string): Promise<PortableUserCredits> {\n const existing = await this.getUserCredits(userId);\n if (existing) {\n return existing;\n }\n return this.initializeUserCredits(userId);\n }\n\n /**\n * Check if user has sufficient credits for an operation\n * @param userId - User ID\n * @param requiredCredits - Credits required\n * @returns Credit check result\n */\n async checkCredits(userId: string, requiredCredits: number): Promise<CreditCheckResult> {\n const credits = await this.getOrCreateUserCredits(userId);\n\n // Available = balance + bonusCredits - reserved\n const totalBalance = credits.balance + credits.bonusCredits;\n const available = totalBalance - credits.reserved;\n const hasCredits = available >= requiredCredits;\n\n return {\n hasCredits,\n balance: totalBalance,\n required: requiredCredits,\n shortfall: hasCredits ? 0 : requiredCredits - available,\n };\n }\n\n /**\n * Reserve credits for an operation (phase 1 of two-phase commit)\n * Creates a reservation and locks the credits\n * @param userId - User ID\n * @param amount - Credits to reserve\n * @param operationType - Operation type for tracking\n * @returns Reservation object\n * @throws Error if insufficient credits\n */\n async reserveCredits(\n userId: string,\n amount: number,\n operationType: CreditOperationType\n ): Promise<PortableReservation> {\n const expiresAt = new Date(Date.now() + getConfig().reservationExpiryMs);\n return this.repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);\n }\n\n /**\n * Commit a reservation (phase 2 of two-phase commit - success)\n * Deducts credits and marks reservation as committed\n * Also triggers low balance notifications if balance drops below threshold\n */\n async commitCredits(userId: string, reservationId: string): Promise<void> {\n // Get the reservation to know the amount\n const reservation = await this.repository.getReservation(userId, reservationId);\n if (!reservation) {\n throw new Error(`Reservation ${reservationId} not found`);\n }\n\n // Commit the reservation atomically\n await this.repository.commitReservationAtomic(userId, reservationId);\n\n // Create journal entry\n const credits = await this.repository.getUserCredits(userId);\n if (credits) {\n await this.repository.createJournalEntry({\n userId,\n entryType: \"debit\",\n amount: reservation.amount,\n balanceAfter: credits.balance,\n source: \"operation_commit\",\n referenceId: reservationId,\n referenceType: \"reservation\",\n description: `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,\n metadata: {\n operationType: reservation.operationType,\n },\n });\n\n // Trigger low balance notification (non-blocking)\n if (this.lowBalanceCallback) {\n this.lowBalanceCallback(userId, credits.balance).catch((error) => {\n console.error(\"[Credits] Failed to send low balance notification:\", error);\n });\n }\n }\n }\n\n /**\n * Release a reservation (phase 2 of two-phase commit - failure)\n * Returns reserved credits and marks reservation as released\n */\n async releaseCredits(userId: string, reservationId: string): Promise<void> {\n // Get the reservation to check its state\n const reservation = await this.repository.getReservation(userId, reservationId);\n\n // Release the reservation atomically\n await this.repository.releaseReservationAtomic(userId, reservationId);\n\n // Create journal entry only if reservation was in reserved state\n if (reservation?.status === \"reserved\") {\n const credits = await this.repository.getUserCredits(userId);\n if (credits) {\n await this.repository.createJournalEntry({\n userId,\n entryType: \"credit\",\n amount: 0, // No actual credits returned (they were reserved, not spent)\n balanceAfter: credits.balance,\n source: \"operation_release\",\n referenceId: reservationId,\n referenceType: \"reservation\",\n description: `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,\n metadata: {\n operationType: reservation.operationType,\n amount: reservation.amount,\n },\n });\n }\n }\n }\n\n /**\n * Log usage for audit trail\n * @param log - Usage log data\n */\n async logUsage(log: Omit<PortableUsageLog, \"id\" | \"createdAt\">): Promise<void> {\n await this.repository.logUsage(log as CreateUsageLogInput);\n }\n\n /**\n * Add credits to user account (for purchases, bonuses, etc.)\n * @param userId - User ID\n * @param amount - Credits to add\n * @param description - Transaction description\n * @param paymentRef - Optional payment reference\n */\n async addCredits(\n userId: string,\n amount: number,\n description: string,\n paymentRef?: string\n ): Promise<void> {\n return this.repository.addCreditsAtomic(userId, amount, description, paymentRef);\n }\n\n /**\n * Update user subscription tier\n * @param userId - User ID\n * @param tier - New subscription tier\n * @param expiresAt - Subscription expiry date (optional)\n */\n async updateTier(\n userId: string,\n tier: SubscriptionTier,\n expiresAt?: Date\n ): Promise<void> {\n const monthlyLimit = getConfigMonthlyLimit(tier);\n\n await this.repository.updateUserTier(userId, {\n tier,\n monthlyLimit: monthlyLimit === Infinity ? 0 : monthlyLimit,\n // Reset balance to new tier limit if upgrading\n balance: tier !== \"free\"\n ? (monthlyLimit === Infinity ? 999999 : monthlyLimit)\n : undefined,\n monthlyUsed: tier !== \"free\" ? 0 : undefined,\n subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null,\n });\n }\n\n /**\n * Get usage logs with optional filtering\n * @param userId - Optional user ID filter\n * @param limit - Max results\n * @param offset - Skip results\n * @returns List of usage logs\n */\n async getUsageLogs(\n userId?: string,\n limit = 50,\n offset = 0\n ): Promise<PortableUsageLog[]> {\n return this.repository.getUsageLogs({ userId, limit, offset });\n }\n\n /**\n * Get user-friendly usage history\n * Combines journal entries into a user-facing format\n *\n * @param userId - User ID\n * @param limit - Max results per page\n * @param offset - Skip results for pagination\n * @returns Paginated usage history response\n */\n async getUsageHistory(\n userId: string,\n limit = 20,\n offset = 0\n ): Promise<UsageHistoryResponse> {\n // Get journal entries\n const [entries, total] = await Promise.all([\n this.repository.getJournalEntries({ userId, limit, offset }),\n this.repository.getJournalEntriesCount({ userId }),\n ]);\n\n // Convert journal entries to user-friendly format\n const historyEntries: UsageHistoryEntry[] = entries.map((entry) => {\n const type = this.mapSourceToHistoryType(entry.source);\n const creditsChange = entry.entryType === \"credit\" ? entry.amount : -entry.amount;\n\n return {\n id: entry.id,\n type,\n creditsChange,\n balanceAfter: entry.balanceAfter,\n description: entry.description,\n createdAt: typeof entry.createdAt === \"string\"\n ? entry.createdAt\n : toDate(entry.createdAt).toISOString(),\n };\n });\n\n return {\n entries: historyEntries,\n pagination: {\n total,\n limit,\n offset,\n hasMore: offset + entries.length < total,\n },\n };\n }\n\n /**\n * Map journal entry source to user-friendly history type\n */\n private mapSourceToHistoryType(\n source: PortableJournalEntry[\"source\"]\n ): UsageHistoryEntry[\"type\"] {\n switch (source) {\n case \"operation_commit\":\n case \"reservation_expired\":\n return \"usage\";\n case \"purchase\":\n return \"purchase\";\n case \"subscription_grant\":\n case \"subscription_upgrade\":\n case \"bonus\":\n return \"bonus\";\n case \"monthly_reset\":\n return \"reset\";\n case \"refund\":\n case \"operation_release\":\n return \"refund\";\n case \"admin_adjustment\":\n case \"subscription_downgrade\":\n case \"expiry\":\n default:\n return \"adjustment\";\n }\n }\n\n /**\n * Get journal entries directly (for admin or debugging)\n * @param query - Journal entry query parameters\n * @returns List of journal entries\n */\n async getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]> {\n return this.repository.getJournalEntries(query);\n }\n\n /**\n * Get the underlying repository (for advanced use cases)\n */\n getRepository(): ICreditRepository {\n return this.repository;\n }\n}\n\n/**\n * Create a credits service with a repository\n */\nexport function createCreditsService(repository: ICreditRepository): CreditsService {\n return new CreditsService(repository);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { b as PortableUserCredits, g as CreditCheckResult, a as CreditOperationType, c as PortableReservation, d as PortableUsageLog, S as SubscriptionTier, j as UsageHistoryResponse, f as PortableJournalEntry } from '../types-BBxIWHK2.cjs';
|
|
2
|
+
import { I as ICreditRepository, J as JournalEntryQuery } from '../types-D1vdeBcA.cjs';
|
|
3
|
+
|
|
3
4
|
/**
|
|
4
5
|
* Notification callback type for low balance notifications
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
+
type LowBalanceNotificationCallback = (userId: string, balance: number) => Promise<void>;
|
|
7
8
|
/**
|
|
8
9
|
* Notification callback type for subscription expired notifications
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
+
type SubscriptionExpiredNotificationCallback = (userId: string, wasDowngraded: boolean) => Promise<void>;
|
|
11
12
|
/**
|
|
12
13
|
* Credits service with dependency injection for repository
|
|
13
14
|
*
|
|
14
15
|
* Provides business logic for credit operations, delegating
|
|
15
16
|
* database operations to the injected repository.
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
+
declare class CreditsService {
|
|
18
19
|
private readonly repository;
|
|
19
20
|
private lowBalanceCallback?;
|
|
20
21
|
private subscriptionExpiredCallback?;
|
|
@@ -135,5 +136,6 @@ export declare class CreditsService {
|
|
|
135
136
|
/**
|
|
136
137
|
* Create a credits service with a repository
|
|
137
138
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
declare function createCreditsService(repository: ICreditRepository): CreditsService;
|
|
140
|
+
|
|
141
|
+
export { CreditsService, type LowBalanceNotificationCallback, type SubscriptionExpiredNotificationCallback, createCreditsService };
|
package/dist/service/index.d.ts
CHANGED
|
@@ -1,3 +1,141 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { b as PortableUserCredits, g as CreditCheckResult, a as CreditOperationType, c as PortableReservation, d as PortableUsageLog, S as SubscriptionTier, j as UsageHistoryResponse, f as PortableJournalEntry } from '../types-BBxIWHK2.js';
|
|
2
|
+
import { I as ICreditRepository, J as JournalEntryQuery } from '../types-aJsyv8df.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Notification callback type for low balance notifications
|
|
6
|
+
*/
|
|
7
|
+
type LowBalanceNotificationCallback = (userId: string, balance: number) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Notification callback type for subscription expired notifications
|
|
10
|
+
*/
|
|
11
|
+
type SubscriptionExpiredNotificationCallback = (userId: string, wasDowngraded: boolean) => Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Credits service with dependency injection for repository
|
|
14
|
+
*
|
|
15
|
+
* Provides business logic for credit operations, delegating
|
|
16
|
+
* database operations to the injected repository.
|
|
17
|
+
*/
|
|
18
|
+
declare class CreditsService {
|
|
19
|
+
private readonly repository;
|
|
20
|
+
private lowBalanceCallback?;
|
|
21
|
+
private subscriptionExpiredCallback?;
|
|
22
|
+
constructor(repository: ICreditRepository);
|
|
23
|
+
/**
|
|
24
|
+
* Set callback for low balance notifications
|
|
25
|
+
*/
|
|
26
|
+
setLowBalanceCallback(callback: LowBalanceNotificationCallback): void;
|
|
27
|
+
/**
|
|
28
|
+
* Set callback for subscription expired notifications
|
|
29
|
+
*/
|
|
30
|
+
setSubscriptionExpiredCallback(callback: SubscriptionExpiredNotificationCallback): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get user credits, performing monthly reset and subscription expiry checks if needed
|
|
33
|
+
*
|
|
34
|
+
* This method uses atomic operations to prevent race conditions:
|
|
35
|
+
* 1. Checks subscription expiry with grace period
|
|
36
|
+
* 2. Atomically performs monthly reset if needed (with optimistic locking)
|
|
37
|
+
*
|
|
38
|
+
* @param userId - User ID
|
|
39
|
+
* @returns User credits or null if not found
|
|
40
|
+
*/
|
|
41
|
+
getUserCredits(userId: string): Promise<PortableUserCredits | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Initialize credits for a new user with free tier
|
|
44
|
+
* @param userId - User ID
|
|
45
|
+
* @returns Initialized user credits
|
|
46
|
+
*/
|
|
47
|
+
initializeUserCredits(userId: string): Promise<PortableUserCredits>;
|
|
48
|
+
/**
|
|
49
|
+
* Get or create user credits
|
|
50
|
+
* Initializes with free tier if not exists
|
|
51
|
+
* @param userId - User ID
|
|
52
|
+
* @returns User credits
|
|
53
|
+
*/
|
|
54
|
+
getOrCreateUserCredits(userId: string): Promise<PortableUserCredits>;
|
|
55
|
+
/**
|
|
56
|
+
* Check if user has sufficient credits for an operation
|
|
57
|
+
* @param userId - User ID
|
|
58
|
+
* @param requiredCredits - Credits required
|
|
59
|
+
* @returns Credit check result
|
|
60
|
+
*/
|
|
61
|
+
checkCredits(userId: string, requiredCredits: number): Promise<CreditCheckResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Reserve credits for an operation (phase 1 of two-phase commit)
|
|
64
|
+
* Creates a reservation and locks the credits
|
|
65
|
+
* @param userId - User ID
|
|
66
|
+
* @param amount - Credits to reserve
|
|
67
|
+
* @param operationType - Operation type for tracking
|
|
68
|
+
* @returns Reservation object
|
|
69
|
+
* @throws Error if insufficient credits
|
|
70
|
+
*/
|
|
71
|
+
reserveCredits(userId: string, amount: number, operationType: CreditOperationType): Promise<PortableReservation>;
|
|
72
|
+
/**
|
|
73
|
+
* Commit a reservation (phase 2 of two-phase commit - success)
|
|
74
|
+
* Deducts credits and marks reservation as committed
|
|
75
|
+
* Also triggers low balance notifications if balance drops below threshold
|
|
76
|
+
*/
|
|
77
|
+
commitCredits(userId: string, reservationId: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Release a reservation (phase 2 of two-phase commit - failure)
|
|
80
|
+
* Returns reserved credits and marks reservation as released
|
|
81
|
+
*/
|
|
82
|
+
releaseCredits(userId: string, reservationId: string): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Log usage for audit trail
|
|
85
|
+
* @param log - Usage log data
|
|
86
|
+
*/
|
|
87
|
+
logUsage(log: Omit<PortableUsageLog, "id" | "createdAt">): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Add credits to user account (for purchases, bonuses, etc.)
|
|
90
|
+
* @param userId - User ID
|
|
91
|
+
* @param amount - Credits to add
|
|
92
|
+
* @param description - Transaction description
|
|
93
|
+
* @param paymentRef - Optional payment reference
|
|
94
|
+
*/
|
|
95
|
+
addCredits(userId: string, amount: number, description: string, paymentRef?: string): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Update user subscription tier
|
|
98
|
+
* @param userId - User ID
|
|
99
|
+
* @param tier - New subscription tier
|
|
100
|
+
* @param expiresAt - Subscription expiry date (optional)
|
|
101
|
+
*/
|
|
102
|
+
updateTier(userId: string, tier: SubscriptionTier, expiresAt?: Date): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Get usage logs with optional filtering
|
|
105
|
+
* @param userId - Optional user ID filter
|
|
106
|
+
* @param limit - Max results
|
|
107
|
+
* @param offset - Skip results
|
|
108
|
+
* @returns List of usage logs
|
|
109
|
+
*/
|
|
110
|
+
getUsageLogs(userId?: string, limit?: number, offset?: number): Promise<PortableUsageLog[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Get user-friendly usage history
|
|
113
|
+
* Combines journal entries into a user-facing format
|
|
114
|
+
*
|
|
115
|
+
* @param userId - User ID
|
|
116
|
+
* @param limit - Max results per page
|
|
117
|
+
* @param offset - Skip results for pagination
|
|
118
|
+
* @returns Paginated usage history response
|
|
119
|
+
*/
|
|
120
|
+
getUsageHistory(userId: string, limit?: number, offset?: number): Promise<UsageHistoryResponse>;
|
|
121
|
+
/**
|
|
122
|
+
* Map journal entry source to user-friendly history type
|
|
123
|
+
*/
|
|
124
|
+
private mapSourceToHistoryType;
|
|
125
|
+
/**
|
|
126
|
+
* Get journal entries directly (for admin or debugging)
|
|
127
|
+
* @param query - Journal entry query parameters
|
|
128
|
+
* @returns List of journal entries
|
|
129
|
+
*/
|
|
130
|
+
getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]>;
|
|
131
|
+
/**
|
|
132
|
+
* Get the underlying repository (for advanced use cases)
|
|
133
|
+
*/
|
|
134
|
+
getRepository(): ICreditRepository;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Create a credits service with a repository
|
|
138
|
+
*/
|
|
139
|
+
declare function createCreditsService(repository: ICreditRepository): CreditsService;
|
|
140
|
+
|
|
141
|
+
export { CreditsService, type LowBalanceNotificationCallback, type SubscriptionExpiredNotificationCallback, createCreditsService };
|