@nehorai/credits 1.0.2 → 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.
Files changed (134) hide show
  1. package/dist/adapters/index.cjs +463 -0
  2. package/dist/adapters/index.cjs.map +1 -0
  3. package/dist/adapters/index.d.cts +66 -0
  4. package/dist/adapters/index.d.ts +66 -3
  5. package/dist/adapters/index.js +435 -1
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/auth/index.cjs +96 -0
  8. package/dist/auth/index.cjs.map +1 -0
  9. package/dist/auth/{api-key.d.ts → index.d.cts} +9 -6
  10. package/dist/auth/index.d.ts +37 -5
  11. package/dist/auth/index.js +65 -7
  12. package/dist/auth/index.js.map +1 -1
  13. package/dist/core/index.cjs +522 -0
  14. package/dist/core/index.cjs.map +1 -0
  15. package/dist/core/index.d.cts +119 -0
  16. package/dist/core/index.d.ts +115 -25
  17. package/dist/core/index.js +475 -27
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/{core/deferred.d.ts → deferred-C7rrRfkm.d.cts} +7 -6
  20. package/dist/{core/deferred.js → deferred-C7rrRfkm.d.ts} +23 -29
  21. package/dist/index.cjs +2506 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.cts +437 -0
  24. package/dist/index.d.ts +429 -49
  25. package/dist/index.js +2399 -62
  26. package/dist/index.js.map +1 -1
  27. package/dist/repository/index.cjs +816 -0
  28. package/dist/repository/index.cjs.map +1 -0
  29. package/dist/repository/{memory/index.d.ts → index.d.cts} +35 -5
  30. package/dist/repository/index.d.ts +97 -7
  31. package/dist/repository/index.js +783 -10
  32. package/dist/repository/index.js.map +1 -1
  33. package/dist/sdk/index.cjs +608 -0
  34. package/dist/sdk/index.cjs.map +1 -0
  35. package/dist/sdk/index.d.cts +495 -0
  36. package/dist/sdk/index.d.ts +491 -9
  37. package/dist/sdk/index.js +568 -12
  38. package/dist/sdk/index.js.map +1 -1
  39. package/dist/service/index.cjs +670 -0
  40. package/dist/service/index.cjs.map +1 -0
  41. package/dist/service/{credits-service.d.ts → index.d.cts} +9 -7
  42. package/dist/service/index.d.ts +141 -3
  43. package/dist/service/index.js +641 -1
  44. package/dist/service/index.js.map +1 -1
  45. package/dist/{core/types.d.ts → types-BBxIWHK2.d.cts} +25 -35
  46. package/dist/types-BBxIWHK2.d.ts +277 -0
  47. package/dist/types-D1vdeBcA.d.cts +305 -0
  48. package/dist/{auth/types.d.ts → types-OZU3I5OM.d.cts} +5 -4
  49. package/dist/types-OZU3I5OM.d.ts +32 -0
  50. package/dist/{repository/types.d.ts → types-aJsyv8df.d.ts} +35 -13
  51. package/package.json +14 -13
  52. package/dist/adapters/generic.d.ts +0 -18
  53. package/dist/adapters/generic.d.ts.map +0 -1
  54. package/dist/adapters/generic.js +0 -147
  55. package/dist/adapters/generic.js.map +0 -1
  56. package/dist/adapters/index.d.ts.map +0 -1
  57. package/dist/adapters/types.d.ts +0 -45
  58. package/dist/adapters/types.d.ts.map +0 -1
  59. package/dist/adapters/types.js +0 -8
  60. package/dist/adapters/types.js.map +0 -1
  61. package/dist/auth/api-key.d.ts.map +0 -1
  62. package/dist/auth/api-key.js +0 -94
  63. package/dist/auth/api-key.js.map +0 -1
  64. package/dist/auth/index.d.ts.map +0 -1
  65. package/dist/auth/types.d.ts.map +0 -1
  66. package/dist/auth/types.js +0 -2
  67. package/dist/auth/types.js.map +0 -1
  68. package/dist/config/costs.d.ts +0 -61
  69. package/dist/config/costs.d.ts.map +0 -1
  70. package/dist/config/costs.js +0 -86
  71. package/dist/config/costs.js.map +0 -1
  72. package/dist/config/index.d.ts +0 -165
  73. package/dist/config/index.d.ts.map +0 -1
  74. package/dist/config/index.js +0 -286
  75. package/dist/config/index.js.map +0 -1
  76. package/dist/core/deferred.d.ts.map +0 -1
  77. package/dist/core/deferred.js.map +0 -1
  78. package/dist/core/errors.d.ts +0 -78
  79. package/dist/core/errors.d.ts.map +0 -1
  80. package/dist/core/errors.js +0 -110
  81. package/dist/core/errors.js.map +0 -1
  82. package/dist/core/index.d.ts.map +0 -1
  83. package/dist/core/operations.d.ts +0 -36
  84. package/dist/core/operations.d.ts.map +0 -1
  85. package/dist/core/operations.js +0 -87
  86. package/dist/core/operations.js.map +0 -1
  87. package/dist/core/types.d.ts.map +0 -1
  88. package/dist/core/types.js +0 -93
  89. package/dist/core/types.js.map +0 -1
  90. package/dist/index.d.ts.map +0 -1
  91. package/dist/notifications/index.d.ts +0 -76
  92. package/dist/notifications/index.d.ts.map +0 -1
  93. package/dist/notifications/index.js +0 -182
  94. package/dist/notifications/index.js.map +0 -1
  95. package/dist/repository/index.d.ts.map +0 -1
  96. package/dist/repository/memory/index.d.ts.map +0 -1
  97. package/dist/repository/memory/index.js +0 -518
  98. package/dist/repository/memory/index.js.map +0 -1
  99. package/dist/repository/types.d.ts.map +0 -1
  100. package/dist/repository/types.js +0 -39
  101. package/dist/repository/types.js.map +0 -1
  102. package/dist/repository/utils.d.ts +0 -22
  103. package/dist/repository/utils.d.ts.map +0 -1
  104. package/dist/repository/utils.js +0 -40
  105. package/dist/repository/utils.js.map +0 -1
  106. package/dist/sdk/admin-client.d.ts +0 -146
  107. package/dist/sdk/admin-client.d.ts.map +0 -1
  108. package/dist/sdk/admin-client.js +0 -196
  109. package/dist/sdk/admin-client.js.map +0 -1
  110. package/dist/sdk/client.d.ts +0 -144
  111. package/dist/sdk/client.d.ts.map +0 -1
  112. package/dist/sdk/client.js +0 -247
  113. package/dist/sdk/client.js.map +0 -1
  114. package/dist/sdk/errors.d.ts +0 -91
  115. package/dist/sdk/errors.d.ts.map +0 -1
  116. package/dist/sdk/errors.js +0 -185
  117. package/dist/sdk/errors.js.map +0 -1
  118. package/dist/sdk/index.d.ts.map +0 -1
  119. package/dist/sdk/types.d.ts +0 -110
  120. package/dist/sdk/types.d.ts.map +0 -1
  121. package/dist/sdk/types.js +0 -20
  122. package/dist/sdk/types.js.map +0 -1
  123. package/dist/service/credits-service.d.ts.map +0 -1
  124. package/dist/service/credits-service.js +0 -372
  125. package/dist/service/credits-service.js.map +0 -1
  126. package/dist/service/index.d.ts.map +0 -1
  127. package/dist/utils/error-utils.d.ts +0 -48
  128. package/dist/utils/error-utils.d.ts.map +0 -1
  129. package/dist/utils/error-utils.js +0 -57
  130. package/dist/utils/error-utils.js.map +0 -1
  131. package/dist/utils/index.d.ts +0 -54
  132. package/dist/utils/index.d.ts.map +0 -1
  133. package/dist/utils/index.js +0 -72
  134. package/dist/utils/index.js.map +0 -1
@@ -1,2 +1,642 @@
1
- export { CreditsService, createCreditsService, } from "./credits-service.js";
1
+ // src/core/types.ts
2
+ function toDate(value) {
3
+ if (value instanceof Date) {
4
+ return value;
5
+ }
6
+ if (typeof value === "string") {
7
+ return new Date(value);
8
+ }
9
+ if (typeof value === "object" && value !== null && "toDate" in value && typeof value.toDate === "function") {
10
+ return value.toDate();
11
+ }
12
+ return /* @__PURE__ */ new Date();
13
+ }
14
+
15
+ // src/repository/types.ts
16
+ function toClientUserCredits(credits) {
17
+ return {
18
+ userId: credits.userId,
19
+ balance: credits.balance,
20
+ bonusCredits: credits.bonusCredits ?? 0,
21
+ reserved: credits.reserved,
22
+ tier: credits.tier,
23
+ monthlyLimit: credits.monthlyLimit,
24
+ monthlyUsed: credits.monthlyUsed,
25
+ monthlyResetAt: toISOString(credits.monthlyResetAt),
26
+ subscriptionExpiresAt: credits.subscriptionExpiresAt ? toISOString(credits.subscriptionExpiresAt) : null,
27
+ createdAt: toISOString(credits.createdAt),
28
+ updatedAt: toISOString(credits.updatedAt)
29
+ };
30
+ }
31
+ function toISOString(value) {
32
+ if (!value) return (/* @__PURE__ */ new Date()).toISOString();
33
+ if (typeof value === "string") return value;
34
+ if (value instanceof Date) return value.toISOString();
35
+ if (typeof value === "object" && "toDate" in value && typeof value.toDate === "function") {
36
+ return value.toDate().toISOString();
37
+ }
38
+ return (/* @__PURE__ */ new Date()).toISOString();
39
+ }
40
+
41
+ // src/config/index.ts
42
+ import { z } from "zod";
43
+
44
+ // src/config/provider.ts
45
+ var configProvider = null;
46
+ function getConfigProvider() {
47
+ return configProvider;
48
+ }
49
+
50
+ // src/config/index.ts
51
+ var tierConfigSchema = z.object({
52
+ tier: z.enum(["free", "basic", "premium", "unlimited"]),
53
+ monthlyCredits: z.number().min(0),
54
+ priceUsd: z.number().min(0),
55
+ features: z.array(z.string())
56
+ });
57
+ var creditSystemConfigSchema = z.object({
58
+ /**
59
+ * Operation costs in credits
60
+ * Keys are operation type names (any non-empty string)
61
+ * Values are positive numbers representing credit cost
62
+ */
63
+ operationCosts: z.record(
64
+ z.string().min(1),
65
+ z.number().positive()
66
+ ),
67
+ /**
68
+ * Display labels for operation types (English canonical labels).
69
+ * Falls back to auto-generating from snake_case keys if not provided.
70
+ */
71
+ operationLabels: z.record(
72
+ z.string().min(1),
73
+ z.string().min(1)
74
+ ).optional().default({}),
75
+ /**
76
+ * Tier configurations
77
+ */
78
+ tierConfigs: z.record(
79
+ z.enum(["free", "basic", "premium", "unlimited"]),
80
+ tierConfigSchema
81
+ ),
82
+ /**
83
+ * Reservation expiry time in milliseconds
84
+ */
85
+ reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
86
+ /**
87
+ * Default credits for new users (free tier)
88
+ */
89
+ defaultFreeCredits: z.number().positive().default(25),
90
+ /**
91
+ * Grace period in days after subscription expires before downgrade
92
+ */
93
+ subscriptionGracePeriodDays: z.number().min(0).default(3),
94
+ /**
95
+ * Low balance notification threshold
96
+ */
97
+ lowBalanceThreshold: z.number().min(0).default(10),
98
+ /**
99
+ * Cooldown in hours between low balance notifications
100
+ */
101
+ lowBalanceNotificationCooldownHours: z.number().positive().default(24),
102
+ /**
103
+ * Feature flags for optional features
104
+ */
105
+ features: z.object({
106
+ journalEntries: z.boolean().default(true),
107
+ notifications: z.boolean().default(true),
108
+ subscriptionExpiry: z.boolean().default(true),
109
+ usageHistory: z.boolean().default(true)
110
+ }).default({
111
+ journalEntries: true,
112
+ notifications: true,
113
+ subscriptionExpiry: true,
114
+ usageHistory: true
115
+ })
116
+ });
117
+ var DEFAULT_CONFIG = {
118
+ operationCosts: {
119
+ story_generation: 5,
120
+ conversation: 2,
121
+ image_generation: 10,
122
+ template_generation: 10
123
+ },
124
+ operationLabels: {
125
+ story_generation: "Story generation",
126
+ conversation: "Conversation",
127
+ image_generation: "Image generation",
128
+ template_generation: "Template generation"
129
+ },
130
+ tierConfigs: {
131
+ free: {
132
+ tier: "free",
133
+ monthlyCredits: 25,
134
+ priceUsd: 0,
135
+ features: [
136
+ "25 credits per month",
137
+ "Basic story generation",
138
+ "Standard image quality"
139
+ ]
140
+ },
141
+ basic: {
142
+ tier: "basic",
143
+ monthlyCredits: 200,
144
+ priceUsd: 9.99,
145
+ features: [
146
+ "200 credits per month",
147
+ "All story features",
148
+ "High quality images",
149
+ "Priority support"
150
+ ]
151
+ },
152
+ premium: {
153
+ tier: "premium",
154
+ monthlyCredits: 500,
155
+ priceUsd: 19.99,
156
+ features: [
157
+ "500 credits per month",
158
+ "All features",
159
+ "Highest quality images",
160
+ "Early access to new features",
161
+ "Priority support"
162
+ ]
163
+ },
164
+ unlimited: {
165
+ tier: "unlimited",
166
+ monthlyCredits: 0,
167
+ // 0 = unlimited
168
+ priceUsd: 49.99,
169
+ features: [
170
+ "Unlimited credits",
171
+ "All features",
172
+ "Highest quality images",
173
+ "Early access to new features",
174
+ "Dedicated support"
175
+ ]
176
+ }
177
+ },
178
+ reservationExpiryMs: 5 * 60 * 1e3,
179
+ // 5 minutes
180
+ defaultFreeCredits: 25,
181
+ subscriptionGracePeriodDays: 3,
182
+ lowBalanceThreshold: 10,
183
+ lowBalanceNotificationCooldownHours: 24,
184
+ features: {
185
+ journalEntries: true,
186
+ notifications: true,
187
+ subscriptionExpiry: true,
188
+ usageHistory: true
189
+ }
190
+ };
191
+ var currentConfig = DEFAULT_CONFIG;
192
+ function loadConfigFromEnv() {
193
+ const envConfig = {};
194
+ const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;
195
+ if (operationCostsEnv) {
196
+ try {
197
+ const parsed = JSON.parse(operationCostsEnv);
198
+ if (typeof parsed === "object" && parsed !== null) {
199
+ envConfig.operationCosts = {
200
+ ...DEFAULT_CONFIG.operationCosts,
201
+ ...parsed
202
+ };
203
+ }
204
+ } catch {
205
+ console.warn("[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON");
206
+ }
207
+ }
208
+ const reservationExpiry = process.env.CREDITS_RESERVATION_EXPIRY_MS;
209
+ if (reservationExpiry) {
210
+ envConfig.reservationExpiryMs = parseInt(reservationExpiry, 10);
211
+ }
212
+ const defaultCredits = process.env.CREDITS_DEFAULT_FREE;
213
+ if (defaultCredits) {
214
+ envConfig.defaultFreeCredits = parseInt(defaultCredits, 10);
215
+ }
216
+ const gracePeriod = process.env.CREDITS_GRACE_PERIOD_DAYS;
217
+ if (gracePeriod) {
218
+ envConfig.subscriptionGracePeriodDays = parseInt(gracePeriod, 10);
219
+ }
220
+ const lowBalanceThreshold = process.env.CREDITS_LOW_BALANCE_THRESHOLD;
221
+ if (lowBalanceThreshold) {
222
+ envConfig.lowBalanceThreshold = parseInt(lowBalanceThreshold, 10);
223
+ }
224
+ const featuresEnv = process.env.CREDITS_FEATURES;
225
+ if (featuresEnv) {
226
+ try {
227
+ const features = JSON.parse(featuresEnv);
228
+ envConfig.features = {
229
+ ...DEFAULT_CONFIG.features,
230
+ ...features
231
+ };
232
+ } catch {
233
+ console.warn("[Credits Config] Failed to parse CREDITS_FEATURES env var");
234
+ }
235
+ }
236
+ return envConfig;
237
+ }
238
+ function initializeConfig(overrides) {
239
+ const envConfig = loadConfigFromEnv();
240
+ const mergedConfig = {
241
+ ...DEFAULT_CONFIG,
242
+ ...envConfig,
243
+ ...overrides,
244
+ operationCosts: {
245
+ ...DEFAULT_CONFIG.operationCosts,
246
+ ...envConfig.operationCosts,
247
+ ...overrides?.operationCosts
248
+ },
249
+ operationLabels: {
250
+ ...DEFAULT_CONFIG.operationLabels,
251
+ ...envConfig.operationLabels,
252
+ ...overrides?.operationLabels
253
+ },
254
+ tierConfigs: {
255
+ ...DEFAULT_CONFIG.tierConfigs,
256
+ ...envConfig.tierConfigs,
257
+ ...overrides?.tierConfigs
258
+ },
259
+ features: {
260
+ ...DEFAULT_CONFIG.features,
261
+ ...envConfig.features,
262
+ ...overrides?.features
263
+ }
264
+ };
265
+ const result = creditSystemConfigSchema.safeParse(mergedConfig);
266
+ if (!result.success) {
267
+ console.error("[Credits Config] Invalid configuration:", result.error.issues);
268
+ throw new Error("Invalid credit system configuration");
269
+ }
270
+ currentConfig = result.data;
271
+ return currentConfig;
272
+ }
273
+ function getConfig() {
274
+ const provider = getConfigProvider();
275
+ if (provider) {
276
+ return provider.getConfig();
277
+ }
278
+ return currentConfig;
279
+ }
280
+ function getConfigMonthlyLimit(tier) {
281
+ const config = getConfig().tierConfigs[tier];
282
+ return config.monthlyCredits === 0 ? Infinity : config.monthlyCredits;
283
+ }
284
+ function snakeCaseToTitleCase(str) {
285
+ return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
286
+ }
287
+ function getOperationLabel(operationType) {
288
+ const labels = getConfig().operationLabels;
289
+ if (labels && operationType in labels) {
290
+ return labels[operationType];
291
+ }
292
+ return snakeCaseToTitleCase(operationType);
293
+ }
294
+ initializeConfig();
295
+
296
+ // src/service/credits-service.ts
297
+ function isPastMonthlyReset(resetAt) {
298
+ if (!resetAt) return false;
299
+ const resetDate = toDate(resetAt);
300
+ return /* @__PURE__ */ new Date() >= resetDate;
301
+ }
302
+ var CreditsService = class {
303
+ constructor(repository) {
304
+ this.repository = repository;
305
+ }
306
+ lowBalanceCallback;
307
+ subscriptionExpiredCallback;
308
+ /**
309
+ * Set callback for low balance notifications
310
+ */
311
+ setLowBalanceCallback(callback) {
312
+ this.lowBalanceCallback = callback;
313
+ }
314
+ /**
315
+ * Set callback for subscription expired notifications
316
+ */
317
+ setSubscriptionExpiredCallback(callback) {
318
+ this.subscriptionExpiredCallback = callback;
319
+ }
320
+ /**
321
+ * Get user credits, performing monthly reset and subscription expiry checks if needed
322
+ *
323
+ * This method uses atomic operations to prevent race conditions:
324
+ * 1. Checks subscription expiry with grace period
325
+ * 2. Atomically performs monthly reset if needed (with optimistic locking)
326
+ *
327
+ * @param userId - User ID
328
+ * @returns User credits or null if not found
329
+ */
330
+ async getUserCredits(userId) {
331
+ let data = await this.repository.getUserCredits(userId);
332
+ if (!data) {
333
+ return null;
334
+ }
335
+ if (data.tier !== "free" && data.subscriptionExpiresAt) {
336
+ const expiryResult = await this.repository.checkAndHandleSubscriptionExpiry(
337
+ userId,
338
+ getConfig().subscriptionGracePeriodDays
339
+ );
340
+ if (expiryResult.wasDowngraded) {
341
+ await this.repository.createJournalEntry({
342
+ userId,
343
+ entryType: "debit",
344
+ amount: 0,
345
+ // No credits deducted, just tier change
346
+ balanceAfter: expiryResult.credits.balance,
347
+ source: "subscription_downgrade",
348
+ referenceId: `downgrade-${Date.now()}`,
349
+ referenceType: "subscription",
350
+ description: `Subscription expired. Downgraded from ${data.tier} to free tier.`,
351
+ metadata: {
352
+ previousTier: data.tier,
353
+ previousBalance: data.balance,
354
+ newBalance: expiryResult.credits.balance
355
+ }
356
+ });
357
+ if (this.subscriptionExpiredCallback) {
358
+ this.subscriptionExpiredCallback(userId, true).catch((error) => {
359
+ console.error("[Credits] Failed to send subscription expired notification:", error);
360
+ });
361
+ }
362
+ }
363
+ data = expiryResult.credits;
364
+ }
365
+ if (isPastMonthlyReset(data.monthlyResetAt)) {
366
+ const expectedResetAt = toDate(data.monthlyResetAt);
367
+ const resetResult = await this.repository.atomicMonthlyReset(
368
+ userId,
369
+ data.tier,
370
+ expectedResetAt
371
+ );
372
+ if (resetResult.wasReset) {
373
+ const balanceChange = resetResult.credits.balance - data.balance;
374
+ if (balanceChange !== 0) {
375
+ await this.repository.createJournalEntry({
376
+ userId,
377
+ entryType: balanceChange > 0 ? "credit" : "debit",
378
+ amount: Math.abs(balanceChange),
379
+ balanceAfter: resetResult.credits.balance,
380
+ source: "monthly_reset",
381
+ referenceId: `reset-${Date.now()}`,
382
+ referenceType: "reset",
383
+ description: `Monthly credit reset for ${data.tier} tier.`,
384
+ metadata: {
385
+ tier: data.tier,
386
+ previousBalance: data.balance,
387
+ newBalance: resetResult.credits.balance
388
+ }
389
+ });
390
+ }
391
+ }
392
+ data = resetResult.credits;
393
+ }
394
+ return toClientUserCredits(data);
395
+ }
396
+ /**
397
+ * Initialize credits for a new user with free tier
398
+ * @param userId - User ID
399
+ * @returns Initialized user credits
400
+ */
401
+ async initializeUserCredits(userId) {
402
+ const credits = await this.repository.initializeUserCredits(
403
+ userId,
404
+ "free",
405
+ getConfig().defaultFreeCredits
406
+ );
407
+ return toClientUserCredits(credits);
408
+ }
409
+ /**
410
+ * Get or create user credits
411
+ * Initializes with free tier if not exists
412
+ * @param userId - User ID
413
+ * @returns User credits
414
+ */
415
+ async getOrCreateUserCredits(userId) {
416
+ const existing = await this.getUserCredits(userId);
417
+ if (existing) {
418
+ return existing;
419
+ }
420
+ return this.initializeUserCredits(userId);
421
+ }
422
+ /**
423
+ * Check if user has sufficient credits for an operation
424
+ * @param userId - User ID
425
+ * @param requiredCredits - Credits required
426
+ * @returns Credit check result
427
+ */
428
+ async checkCredits(userId, requiredCredits) {
429
+ const credits = await this.getOrCreateUserCredits(userId);
430
+ const totalBalance = credits.balance + credits.bonusCredits;
431
+ const available = totalBalance - credits.reserved;
432
+ const hasCredits = available >= requiredCredits;
433
+ return {
434
+ hasCredits,
435
+ balance: totalBalance,
436
+ required: requiredCredits,
437
+ shortfall: hasCredits ? 0 : requiredCredits - available
438
+ };
439
+ }
440
+ /**
441
+ * Reserve credits for an operation (phase 1 of two-phase commit)
442
+ * Creates a reservation and locks the credits
443
+ * @param userId - User ID
444
+ * @param amount - Credits to reserve
445
+ * @param operationType - Operation type for tracking
446
+ * @returns Reservation object
447
+ * @throws Error if insufficient credits
448
+ */
449
+ async reserveCredits(userId, amount, operationType) {
450
+ const expiresAt = new Date(Date.now() + getConfig().reservationExpiryMs);
451
+ return this.repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);
452
+ }
453
+ /**
454
+ * Commit a reservation (phase 2 of two-phase commit - success)
455
+ * Deducts credits and marks reservation as committed
456
+ * Also triggers low balance notifications if balance drops below threshold
457
+ */
458
+ async commitCredits(userId, reservationId) {
459
+ const reservation = await this.repository.getReservation(userId, reservationId);
460
+ if (!reservation) {
461
+ throw new Error(`Reservation ${reservationId} not found`);
462
+ }
463
+ await this.repository.commitReservationAtomic(userId, reservationId);
464
+ const credits = await this.repository.getUserCredits(userId);
465
+ if (credits) {
466
+ await this.repository.createJournalEntry({
467
+ userId,
468
+ entryType: "debit",
469
+ amount: reservation.amount,
470
+ balanceAfter: credits.balance,
471
+ source: "operation_commit",
472
+ referenceId: reservationId,
473
+ referenceType: "reservation",
474
+ description: `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,
475
+ metadata: {
476
+ operationType: reservation.operationType
477
+ }
478
+ });
479
+ if (this.lowBalanceCallback) {
480
+ this.lowBalanceCallback(userId, credits.balance).catch((error) => {
481
+ console.error("[Credits] Failed to send low balance notification:", error);
482
+ });
483
+ }
484
+ }
485
+ }
486
+ /**
487
+ * Release a reservation (phase 2 of two-phase commit - failure)
488
+ * Returns reserved credits and marks reservation as released
489
+ */
490
+ async releaseCredits(userId, reservationId) {
491
+ const reservation = await this.repository.getReservation(userId, reservationId);
492
+ await this.repository.releaseReservationAtomic(userId, reservationId);
493
+ if (reservation?.status === "reserved") {
494
+ const credits = await this.repository.getUserCredits(userId);
495
+ if (credits) {
496
+ await this.repository.createJournalEntry({
497
+ userId,
498
+ entryType: "credit",
499
+ amount: 0,
500
+ // No actual credits returned (they were reserved, not spent)
501
+ balanceAfter: credits.balance,
502
+ source: "operation_release",
503
+ referenceId: reservationId,
504
+ referenceType: "reservation",
505
+ description: `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
506
+ metadata: {
507
+ operationType: reservation.operationType,
508
+ amount: reservation.amount
509
+ }
510
+ });
511
+ }
512
+ }
513
+ }
514
+ /**
515
+ * Log usage for audit trail
516
+ * @param log - Usage log data
517
+ */
518
+ async logUsage(log) {
519
+ await this.repository.logUsage(log);
520
+ }
521
+ /**
522
+ * Add credits to user account (for purchases, bonuses, etc.)
523
+ * @param userId - User ID
524
+ * @param amount - Credits to add
525
+ * @param description - Transaction description
526
+ * @param paymentRef - Optional payment reference
527
+ */
528
+ async addCredits(userId, amount, description, paymentRef) {
529
+ return this.repository.addCreditsAtomic(userId, amount, description, paymentRef);
530
+ }
531
+ /**
532
+ * Update user subscription tier
533
+ * @param userId - User ID
534
+ * @param tier - New subscription tier
535
+ * @param expiresAt - Subscription expiry date (optional)
536
+ */
537
+ async updateTier(userId, tier, expiresAt) {
538
+ const monthlyLimit = getConfigMonthlyLimit(tier);
539
+ await this.repository.updateUserTier(userId, {
540
+ tier,
541
+ monthlyLimit: monthlyLimit === Infinity ? 0 : monthlyLimit,
542
+ // Reset balance to new tier limit if upgrading
543
+ balance: tier !== "free" ? monthlyLimit === Infinity ? 999999 : monthlyLimit : void 0,
544
+ monthlyUsed: tier !== "free" ? 0 : void 0,
545
+ subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null
546
+ });
547
+ }
548
+ /**
549
+ * Get usage logs with optional filtering
550
+ * @param userId - Optional user ID filter
551
+ * @param limit - Max results
552
+ * @param offset - Skip results
553
+ * @returns List of usage logs
554
+ */
555
+ async getUsageLogs(userId, limit = 50, offset = 0) {
556
+ return this.repository.getUsageLogs({ userId, limit, offset });
557
+ }
558
+ /**
559
+ * Get user-friendly usage history
560
+ * Combines journal entries into a user-facing format
561
+ *
562
+ * @param userId - User ID
563
+ * @param limit - Max results per page
564
+ * @param offset - Skip results for pagination
565
+ * @returns Paginated usage history response
566
+ */
567
+ async getUsageHistory(userId, limit = 20, offset = 0) {
568
+ const [entries, total] = await Promise.all([
569
+ this.repository.getJournalEntries({ userId, limit, offset }),
570
+ this.repository.getJournalEntriesCount({ userId })
571
+ ]);
572
+ const historyEntries = entries.map((entry) => {
573
+ const type = this.mapSourceToHistoryType(entry.source);
574
+ const creditsChange = entry.entryType === "credit" ? entry.amount : -entry.amount;
575
+ return {
576
+ id: entry.id,
577
+ type,
578
+ creditsChange,
579
+ balanceAfter: entry.balanceAfter,
580
+ description: entry.description,
581
+ createdAt: typeof entry.createdAt === "string" ? entry.createdAt : toDate(entry.createdAt).toISOString()
582
+ };
583
+ });
584
+ return {
585
+ entries: historyEntries,
586
+ pagination: {
587
+ total,
588
+ limit,
589
+ offset,
590
+ hasMore: offset + entries.length < total
591
+ }
592
+ };
593
+ }
594
+ /**
595
+ * Map journal entry source to user-friendly history type
596
+ */
597
+ mapSourceToHistoryType(source) {
598
+ switch (source) {
599
+ case "operation_commit":
600
+ case "reservation_expired":
601
+ return "usage";
602
+ case "purchase":
603
+ return "purchase";
604
+ case "subscription_grant":
605
+ case "subscription_upgrade":
606
+ case "bonus":
607
+ return "bonus";
608
+ case "monthly_reset":
609
+ return "reset";
610
+ case "refund":
611
+ case "operation_release":
612
+ return "refund";
613
+ case "admin_adjustment":
614
+ case "subscription_downgrade":
615
+ case "expiry":
616
+ default:
617
+ return "adjustment";
618
+ }
619
+ }
620
+ /**
621
+ * Get journal entries directly (for admin or debugging)
622
+ * @param query - Journal entry query parameters
623
+ * @returns List of journal entries
624
+ */
625
+ async getJournalEntries(query) {
626
+ return this.repository.getJournalEntries(query);
627
+ }
628
+ /**
629
+ * Get the underlying repository (for advanced use cases)
630
+ */
631
+ getRepository() {
632
+ return this.repository;
633
+ }
634
+ };
635
+ function createCreditsService(repository) {
636
+ return new CreditsService(repository);
637
+ }
638
+ export {
639
+ CreditsService,
640
+ createCreditsService
641
+ };
2
642
  //# sourceMappingURL=index.js.map