@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,11 +1,784 @@
1
- /**
2
- * Credits repository - barrel exports
3
- *
4
- * Provides database-agnostic interface for credits storage
5
- */
6
- export { toClientUserCredits } from "./types.js";
7
- // Shared utilities
8
- export { generateId, toDate, getNextMonthlyReset } from "./utils.js";
9
- // In-memory implementation (for testing and prototyping)
10
- export { InMemoryCreditRepository, createInMemoryCreditRepository, } from "./memory/index.js";
1
+ // src/repository/types.ts
2
+ function toClientUserCredits(credits) {
3
+ return {
4
+ userId: credits.userId,
5
+ balance: credits.balance,
6
+ bonusCredits: credits.bonusCredits ?? 0,
7
+ reserved: credits.reserved,
8
+ tier: credits.tier,
9
+ monthlyLimit: credits.monthlyLimit,
10
+ monthlyUsed: credits.monthlyUsed,
11
+ monthlyResetAt: toISOString(credits.monthlyResetAt),
12
+ subscriptionExpiresAt: credits.subscriptionExpiresAt ? toISOString(credits.subscriptionExpiresAt) : null,
13
+ createdAt: toISOString(credits.createdAt),
14
+ updatedAt: toISOString(credits.updatedAt)
15
+ };
16
+ }
17
+ function toISOString(value) {
18
+ if (!value) return (/* @__PURE__ */ new Date()).toISOString();
19
+ if (typeof value === "string") return value;
20
+ if (value instanceof Date) return value.toISOString();
21
+ if (typeof value === "object" && "toDate" in value && typeof value.toDate === "function") {
22
+ return value.toDate().toISOString();
23
+ }
24
+ return (/* @__PURE__ */ new Date()).toISOString();
25
+ }
26
+
27
+ // src/repository/utils.ts
28
+ function generateId() {
29
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
30
+ }
31
+ function toDate(value) {
32
+ if (value instanceof Date) return value;
33
+ if (typeof value === "string") return new Date(value);
34
+ if (typeof value === "object" && value !== null && "toDate" in value) {
35
+ return value.toDate();
36
+ }
37
+ return /* @__PURE__ */ new Date();
38
+ }
39
+ function getNextMonthlyReset(from = /* @__PURE__ */ new Date()) {
40
+ const next = new Date(from);
41
+ next.setMonth(next.getMonth() + 1);
42
+ next.setDate(1);
43
+ next.setHours(0, 0, 0, 0);
44
+ return next;
45
+ }
46
+
47
+ // src/config/index.ts
48
+ import { z } from "zod";
49
+
50
+ // src/config/provider.ts
51
+ var configProvider = null;
52
+ function getConfigProvider() {
53
+ return configProvider;
54
+ }
55
+
56
+ // src/config/index.ts
57
+ var tierConfigSchema = z.object({
58
+ tier: z.enum(["free", "basic", "premium", "unlimited"]),
59
+ monthlyCredits: z.number().min(0),
60
+ priceUsd: z.number().min(0),
61
+ features: z.array(z.string())
62
+ });
63
+ var creditSystemConfigSchema = z.object({
64
+ /**
65
+ * Operation costs in credits
66
+ * Keys are operation type names (any non-empty string)
67
+ * Values are positive numbers representing credit cost
68
+ */
69
+ operationCosts: z.record(
70
+ z.string().min(1),
71
+ z.number().positive()
72
+ ),
73
+ /**
74
+ * Display labels for operation types (English canonical labels).
75
+ * Falls back to auto-generating from snake_case keys if not provided.
76
+ */
77
+ operationLabels: z.record(
78
+ z.string().min(1),
79
+ z.string().min(1)
80
+ ).optional().default({}),
81
+ /**
82
+ * Tier configurations
83
+ */
84
+ tierConfigs: z.record(
85
+ z.enum(["free", "basic", "premium", "unlimited"]),
86
+ tierConfigSchema
87
+ ),
88
+ /**
89
+ * Reservation expiry time in milliseconds
90
+ */
91
+ reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
92
+ /**
93
+ * Default credits for new users (free tier)
94
+ */
95
+ defaultFreeCredits: z.number().positive().default(25),
96
+ /**
97
+ * Grace period in days after subscription expires before downgrade
98
+ */
99
+ subscriptionGracePeriodDays: z.number().min(0).default(3),
100
+ /**
101
+ * Low balance notification threshold
102
+ */
103
+ lowBalanceThreshold: z.number().min(0).default(10),
104
+ /**
105
+ * Cooldown in hours between low balance notifications
106
+ */
107
+ lowBalanceNotificationCooldownHours: z.number().positive().default(24),
108
+ /**
109
+ * Feature flags for optional features
110
+ */
111
+ features: z.object({
112
+ journalEntries: z.boolean().default(true),
113
+ notifications: z.boolean().default(true),
114
+ subscriptionExpiry: z.boolean().default(true),
115
+ usageHistory: z.boolean().default(true)
116
+ }).default({
117
+ journalEntries: true,
118
+ notifications: true,
119
+ subscriptionExpiry: true,
120
+ usageHistory: true
121
+ })
122
+ });
123
+ var DEFAULT_CONFIG = {
124
+ operationCosts: {
125
+ story_generation: 5,
126
+ conversation: 2,
127
+ image_generation: 10,
128
+ template_generation: 10
129
+ },
130
+ operationLabels: {
131
+ story_generation: "Story generation",
132
+ conversation: "Conversation",
133
+ image_generation: "Image generation",
134
+ template_generation: "Template generation"
135
+ },
136
+ tierConfigs: {
137
+ free: {
138
+ tier: "free",
139
+ monthlyCredits: 25,
140
+ priceUsd: 0,
141
+ features: [
142
+ "25 credits per month",
143
+ "Basic story generation",
144
+ "Standard image quality"
145
+ ]
146
+ },
147
+ basic: {
148
+ tier: "basic",
149
+ monthlyCredits: 200,
150
+ priceUsd: 9.99,
151
+ features: [
152
+ "200 credits per month",
153
+ "All story features",
154
+ "High quality images",
155
+ "Priority support"
156
+ ]
157
+ },
158
+ premium: {
159
+ tier: "premium",
160
+ monthlyCredits: 500,
161
+ priceUsd: 19.99,
162
+ features: [
163
+ "500 credits per month",
164
+ "All features",
165
+ "Highest quality images",
166
+ "Early access to new features",
167
+ "Priority support"
168
+ ]
169
+ },
170
+ unlimited: {
171
+ tier: "unlimited",
172
+ monthlyCredits: 0,
173
+ // 0 = unlimited
174
+ priceUsd: 49.99,
175
+ features: [
176
+ "Unlimited credits",
177
+ "All features",
178
+ "Highest quality images",
179
+ "Early access to new features",
180
+ "Dedicated support"
181
+ ]
182
+ }
183
+ },
184
+ reservationExpiryMs: 5 * 60 * 1e3,
185
+ // 5 minutes
186
+ defaultFreeCredits: 25,
187
+ subscriptionGracePeriodDays: 3,
188
+ lowBalanceThreshold: 10,
189
+ lowBalanceNotificationCooldownHours: 24,
190
+ features: {
191
+ journalEntries: true,
192
+ notifications: true,
193
+ subscriptionExpiry: true,
194
+ usageHistory: true
195
+ }
196
+ };
197
+ var currentConfig = DEFAULT_CONFIG;
198
+ function loadConfigFromEnv() {
199
+ const envConfig = {};
200
+ const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;
201
+ if (operationCostsEnv) {
202
+ try {
203
+ const parsed = JSON.parse(operationCostsEnv);
204
+ if (typeof parsed === "object" && parsed !== null) {
205
+ envConfig.operationCosts = {
206
+ ...DEFAULT_CONFIG.operationCosts,
207
+ ...parsed
208
+ };
209
+ }
210
+ } catch {
211
+ console.warn("[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON");
212
+ }
213
+ }
214
+ const reservationExpiry = process.env.CREDITS_RESERVATION_EXPIRY_MS;
215
+ if (reservationExpiry) {
216
+ envConfig.reservationExpiryMs = parseInt(reservationExpiry, 10);
217
+ }
218
+ const defaultCredits = process.env.CREDITS_DEFAULT_FREE;
219
+ if (defaultCredits) {
220
+ envConfig.defaultFreeCredits = parseInt(defaultCredits, 10);
221
+ }
222
+ const gracePeriod = process.env.CREDITS_GRACE_PERIOD_DAYS;
223
+ if (gracePeriod) {
224
+ envConfig.subscriptionGracePeriodDays = parseInt(gracePeriod, 10);
225
+ }
226
+ const lowBalanceThreshold = process.env.CREDITS_LOW_BALANCE_THRESHOLD;
227
+ if (lowBalanceThreshold) {
228
+ envConfig.lowBalanceThreshold = parseInt(lowBalanceThreshold, 10);
229
+ }
230
+ const featuresEnv = process.env.CREDITS_FEATURES;
231
+ if (featuresEnv) {
232
+ try {
233
+ const features = JSON.parse(featuresEnv);
234
+ envConfig.features = {
235
+ ...DEFAULT_CONFIG.features,
236
+ ...features
237
+ };
238
+ } catch {
239
+ console.warn("[Credits Config] Failed to parse CREDITS_FEATURES env var");
240
+ }
241
+ }
242
+ return envConfig;
243
+ }
244
+ function initializeConfig(overrides) {
245
+ const envConfig = loadConfigFromEnv();
246
+ const mergedConfig = {
247
+ ...DEFAULT_CONFIG,
248
+ ...envConfig,
249
+ ...overrides,
250
+ operationCosts: {
251
+ ...DEFAULT_CONFIG.operationCosts,
252
+ ...envConfig.operationCosts,
253
+ ...overrides?.operationCosts
254
+ },
255
+ operationLabels: {
256
+ ...DEFAULT_CONFIG.operationLabels,
257
+ ...envConfig.operationLabels,
258
+ ...overrides?.operationLabels
259
+ },
260
+ tierConfigs: {
261
+ ...DEFAULT_CONFIG.tierConfigs,
262
+ ...envConfig.tierConfigs,
263
+ ...overrides?.tierConfigs
264
+ },
265
+ features: {
266
+ ...DEFAULT_CONFIG.features,
267
+ ...envConfig.features,
268
+ ...overrides?.features
269
+ }
270
+ };
271
+ const result = creditSystemConfigSchema.safeParse(mergedConfig);
272
+ if (!result.success) {
273
+ console.error("[Credits Config] Invalid configuration:", result.error.issues);
274
+ throw new Error("Invalid credit system configuration");
275
+ }
276
+ currentConfig = result.data;
277
+ return currentConfig;
278
+ }
279
+ function getConfig() {
280
+ const provider = getConfigProvider();
281
+ if (provider) {
282
+ return provider.getConfig();
283
+ }
284
+ return currentConfig;
285
+ }
286
+ function getConfigMonthlyLimit(tier) {
287
+ const config = getConfig().tierConfigs[tier];
288
+ return config.monthlyCredits === 0 ? Infinity : config.monthlyCredits;
289
+ }
290
+ initializeConfig();
291
+
292
+ // src/repository/memory/index.ts
293
+ var InMemoryCreditRepository = class {
294
+ users = /* @__PURE__ */ new Map();
295
+ reservations = /* @__PURE__ */ new Map();
296
+ transactions = /* @__PURE__ */ new Map();
297
+ usageLogs = [];
298
+ journalEntries = /* @__PURE__ */ new Map();
299
+ // ==================== User Credits ====================
300
+ async getUserCredits(userId) {
301
+ return this.users.get(userId) ?? null;
302
+ }
303
+ async initializeUserCredits(userId, tier, initialBalance) {
304
+ const now = (/* @__PURE__ */ new Date()).toISOString();
305
+ const credits = {
306
+ userId,
307
+ balance: initialBalance,
308
+ bonusCredits: 0,
309
+ reserved: 0,
310
+ tier,
311
+ monthlyLimit: getConfigMonthlyLimit(tier),
312
+ monthlyUsed: 0,
313
+ monthlyResetAt: getNextMonthlyReset().toISOString(),
314
+ subscriptionExpiresAt: null,
315
+ createdAt: now,
316
+ updatedAt: now
317
+ };
318
+ this.users.set(userId, credits);
319
+ return credits;
320
+ }
321
+ async updateUserCredits(userId, updates) {
322
+ const credits = this.users.get(userId);
323
+ if (!credits) {
324
+ throw new Error(`User ${userId} not found`);
325
+ }
326
+ const now = (/* @__PURE__ */ new Date()).toISOString();
327
+ if (updates.balance !== void 0) credits.balance = updates.balance;
328
+ if (updates.bonusCredits !== void 0) credits.bonusCredits = updates.bonusCredits;
329
+ if (updates.reserved !== void 0) credits.reserved = updates.reserved;
330
+ if (updates.tier !== void 0) credits.tier = updates.tier;
331
+ if (updates.monthlyLimit !== void 0) credits.monthlyLimit = updates.monthlyLimit;
332
+ if (updates.monthlyUsed !== void 0) credits.monthlyUsed = updates.monthlyUsed;
333
+ if (updates.monthlyResetAt !== void 0) {
334
+ credits.monthlyResetAt = updates.monthlyResetAt instanceof Date ? updates.monthlyResetAt.toISOString() : updates.monthlyResetAt;
335
+ }
336
+ if (updates.subscriptionExpiresAt !== void 0) {
337
+ if (updates.subscriptionExpiresAt === null) {
338
+ credits.subscriptionExpiresAt = null;
339
+ } else {
340
+ credits.subscriptionExpiresAt = updates.subscriptionExpiresAt instanceof Date ? updates.subscriptionExpiresAt.toISOString() : updates.subscriptionExpiresAt;
341
+ }
342
+ }
343
+ if (updates.balanceIncrement !== void 0) {
344
+ credits.balance += updates.balanceIncrement;
345
+ }
346
+ if (updates.bonusCreditsIncrement !== void 0) {
347
+ credits.bonusCredits += updates.bonusCreditsIncrement;
348
+ }
349
+ if (updates.reservedIncrement !== void 0) {
350
+ credits.reserved += updates.reservedIncrement;
351
+ }
352
+ if (updates.monthlyUsedIncrement !== void 0) {
353
+ credits.monthlyUsed += updates.monthlyUsedIncrement;
354
+ }
355
+ credits.updatedAt = now;
356
+ this.users.set(userId, credits);
357
+ }
358
+ async updateUserTier(userId, input) {
359
+ const credits = this.users.get(userId);
360
+ if (!credits) {
361
+ throw new Error(`User ${userId} not found`);
362
+ }
363
+ credits.tier = input.tier;
364
+ credits.monthlyLimit = input.monthlyLimit;
365
+ if (input.balance !== void 0) credits.balance = input.balance;
366
+ if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
367
+ if (input.subscriptionExpiresAt !== void 0) {
368
+ if (input.subscriptionExpiresAt === null) {
369
+ credits.subscriptionExpiresAt = null;
370
+ } else {
371
+ credits.subscriptionExpiresAt = input.subscriptionExpiresAt instanceof Date ? input.subscriptionExpiresAt.toISOString() : input.subscriptionExpiresAt;
372
+ }
373
+ }
374
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
375
+ this.users.set(userId, credits);
376
+ }
377
+ // ==================== Reservations ====================
378
+ async createReservation(input) {
379
+ const now = (/* @__PURE__ */ new Date()).toISOString();
380
+ const reservation = {
381
+ id: generateId(),
382
+ userId: input.userId,
383
+ amount: input.amount,
384
+ operationType: input.operationType,
385
+ status: "reserved",
386
+ createdAt: now,
387
+ expiresAt: input.expiresAt.toISOString()
388
+ };
389
+ if (!this.reservations.has(input.userId)) {
390
+ this.reservations.set(input.userId, /* @__PURE__ */ new Map());
391
+ }
392
+ this.reservations.get(input.userId).set(reservation.id, reservation);
393
+ return reservation;
394
+ }
395
+ async getReservation(userId, reservationId) {
396
+ const userReservations = this.reservations.get(userId);
397
+ if (!userReservations) return null;
398
+ return userReservations.get(reservationId) ?? null;
399
+ }
400
+ async updateReservationStatus(userId, reservationId, status, completedAt) {
401
+ const userReservations = this.reservations.get(userId);
402
+ if (!userReservations) {
403
+ throw new Error(`No reservations found for user ${userId}`);
404
+ }
405
+ const reservation = userReservations.get(reservationId);
406
+ if (!reservation) {
407
+ throw new Error(`Reservation ${reservationId} not found`);
408
+ }
409
+ reservation.status = status;
410
+ if (completedAt) reservation.completedAt = completedAt.toISOString();
411
+ userReservations.set(reservationId, reservation);
412
+ }
413
+ // ==================== Atomic Operations ====================
414
+ async reserveCreditsAtomic(userId, amount, operationType, expiresAt) {
415
+ const credits = this.users.get(userId);
416
+ if (!credits) {
417
+ throw new Error(`User ${userId} not found`);
418
+ }
419
+ const available = credits.balance + credits.bonusCredits - credits.reserved;
420
+ if (available < amount) {
421
+ throw new Error(
422
+ `Insufficient credits. Available: ${available}, Required: ${amount}`
423
+ );
424
+ }
425
+ const reservation = await this.createReservation({
426
+ userId,
427
+ amount,
428
+ operationType,
429
+ expiresAt
430
+ });
431
+ credits.reserved += amount;
432
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
433
+ this.users.set(userId, credits);
434
+ return reservation;
435
+ }
436
+ async commitReservationAtomic(userId, reservationId) {
437
+ const credits = this.users.get(userId);
438
+ if (!credits) {
439
+ throw new Error(`User ${userId} not found`);
440
+ }
441
+ const reservation = await this.getReservation(userId, reservationId);
442
+ if (!reservation) {
443
+ throw new Error(`Reservation ${reservationId} not found`);
444
+ }
445
+ if (reservation.status !== "reserved") {
446
+ throw new Error(
447
+ `Cannot commit reservation in ${reservation.status} state`
448
+ );
449
+ }
450
+ const amount = reservation.amount;
451
+ credits.balance -= amount;
452
+ credits.reserved -= amount;
453
+ credits.monthlyUsed += amount;
454
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
455
+ this.users.set(userId, credits);
456
+ await this.updateReservationStatus(userId, reservationId, "committed", /* @__PURE__ */ new Date());
457
+ }
458
+ async releaseReservationAtomic(userId, reservationId) {
459
+ const credits = this.users.get(userId);
460
+ if (!credits) {
461
+ throw new Error(`User ${userId} not found`);
462
+ }
463
+ const reservation = await this.getReservation(userId, reservationId);
464
+ if (!reservation) {
465
+ throw new Error(`Reservation ${reservationId} not found`);
466
+ }
467
+ if (reservation.status !== "reserved") {
468
+ return;
469
+ }
470
+ credits.reserved -= reservation.amount;
471
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
472
+ this.users.set(userId, credits);
473
+ await this.updateReservationStatus(userId, reservationId, "released", /* @__PURE__ */ new Date());
474
+ }
475
+ async addCreditsAtomic(userId, amount, description, paymentRef) {
476
+ const credits = this.users.get(userId);
477
+ if (!credits) {
478
+ throw new Error(`User ${userId} not found`);
479
+ }
480
+ const previousBalance = credits.bonusCredits;
481
+ credits.bonusCredits += amount;
482
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
483
+ this.users.set(userId, credits);
484
+ await this.createTransaction({
485
+ userId,
486
+ type: "purchase",
487
+ amount,
488
+ description,
489
+ paymentRef,
490
+ previousBalance,
491
+ newBalance: credits.bonusCredits
492
+ });
493
+ }
494
+ async deductCreditsAtomic(userId, amount) {
495
+ if (amount <= 0) {
496
+ throw new Error(`deductCreditsAtomic amount must be positive (got ${amount})`);
497
+ }
498
+ const credits = this.users.get(userId);
499
+ if (!credits) {
500
+ throw new Error(`User credits not found for userId: ${userId}`);
501
+ }
502
+ const available = credits.balance + credits.bonusCredits - credits.reserved;
503
+ if (available < amount) {
504
+ throw new Error(
505
+ `Insufficient credits. Available: ${available}, requested: ${amount}`
506
+ );
507
+ }
508
+ const balanceDeduction = Math.min(credits.balance, amount);
509
+ const bonusDeduction = amount - balanceDeduction;
510
+ const previousBalance = credits.balance + credits.bonusCredits;
511
+ credits.balance -= balanceDeduction;
512
+ credits.bonusCredits -= bonusDeduction;
513
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
514
+ this.users.set(userId, credits);
515
+ return { previousBalance, newBalance: previousBalance - amount };
516
+ }
517
+ // ==================== Transactions ====================
518
+ async createTransaction(input) {
519
+ const transaction = {
520
+ id: generateId(),
521
+ userId: input.userId,
522
+ type: input.type,
523
+ amount: input.amount,
524
+ description: input.description,
525
+ paymentRef: input.paymentRef,
526
+ previousBalance: input.previousBalance,
527
+ newBalance: input.newBalance,
528
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
529
+ };
530
+ if (!this.transactions.has(input.userId)) {
531
+ this.transactions.set(input.userId, []);
532
+ }
533
+ this.transactions.get(input.userId).push(transaction);
534
+ return transaction;
535
+ }
536
+ async getTransactions(userId, limit = 50, offset = 0) {
537
+ const userTransactions = this.transactions.get(userId) ?? [];
538
+ const sorted = [...userTransactions].sort((a, b) => {
539
+ const aDate = toDate(a.createdAt).getTime();
540
+ const bDate = toDate(b.createdAt).getTime();
541
+ return bDate - aDate;
542
+ });
543
+ return sorted.slice(offset, offset + limit);
544
+ }
545
+ // ==================== Usage Logs ====================
546
+ async logUsage(input) {
547
+ const log = {
548
+ id: generateId(),
549
+ userId: input.userId,
550
+ operationType: input.operationType,
551
+ provider: input.provider,
552
+ creditsUsed: input.creditsUsed,
553
+ success: input.success,
554
+ errorMessage: input.errorMessage,
555
+ resourceId: input.resourceId,
556
+ resourceType: input.resourceType,
557
+ requestId: input.requestId,
558
+ metadata: input.metadata,
559
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
560
+ };
561
+ this.usageLogs.push(log);
562
+ return log;
563
+ }
564
+ async getUsageLogs(query) {
565
+ let results = [...this.usageLogs];
566
+ if (query.userId) {
567
+ results = results.filter((log) => log.userId === query.userId);
568
+ }
569
+ if (query.operationType) {
570
+ results = results.filter((log) => log.operationType === query.operationType);
571
+ }
572
+ if (query.success !== void 0) {
573
+ results = results.filter((log) => log.success === query.success);
574
+ }
575
+ if (query.startDate) {
576
+ const startTime = query.startDate.getTime();
577
+ results = results.filter(
578
+ (log) => toDate(log.createdAt).getTime() >= startTime
579
+ );
580
+ }
581
+ if (query.endDate) {
582
+ const endTime = query.endDate.getTime();
583
+ results = results.filter(
584
+ (log) => toDate(log.createdAt).getTime() <= endTime
585
+ );
586
+ }
587
+ results.sort((a, b) => {
588
+ return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();
589
+ });
590
+ const offset = query.offset ?? 0;
591
+ const limit = query.limit ?? 50;
592
+ return results.slice(offset, offset + limit);
593
+ }
594
+ async getUsageLogsCount(query) {
595
+ const results = await this.getUsageLogs({ ...query, limit: Infinity, offset: 0 });
596
+ return results.length;
597
+ }
598
+ // ==================== Journal Entries ====================
599
+ async createJournalEntry(input) {
600
+ const entry = {
601
+ id: generateId(),
602
+ userId: input.userId,
603
+ entryType: input.entryType,
604
+ amount: input.amount,
605
+ balanceAfter: input.balanceAfter,
606
+ source: input.source,
607
+ referenceId: input.referenceId,
608
+ referenceType: input.referenceType,
609
+ description: input.description,
610
+ metadata: input.metadata,
611
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
612
+ };
613
+ if (!this.journalEntries.has(input.userId)) {
614
+ this.journalEntries.set(input.userId, []);
615
+ }
616
+ this.journalEntries.get(input.userId).push(entry);
617
+ return entry;
618
+ }
619
+ async getJournalEntries(query) {
620
+ let results = this.journalEntries.get(query.userId) ?? [];
621
+ if (query.source) {
622
+ results = results.filter((entry) => entry.source === query.source);
623
+ }
624
+ if (query.referenceType) {
625
+ results = results.filter((entry) => entry.referenceType === query.referenceType);
626
+ }
627
+ if (query.startDate) {
628
+ const startTime = query.startDate.getTime();
629
+ results = results.filter(
630
+ (entry) => toDate(entry.createdAt).getTime() >= startTime
631
+ );
632
+ }
633
+ if (query.endDate) {
634
+ const endTime = query.endDate.getTime();
635
+ results = results.filter(
636
+ (entry) => toDate(entry.createdAt).getTime() <= endTime
637
+ );
638
+ }
639
+ results = [...results].sort((a, b) => {
640
+ return toDate(b.createdAt).getTime() - toDate(a.createdAt).getTime();
641
+ });
642
+ const offset = query.offset ?? 0;
643
+ const limit = query.limit ?? 50;
644
+ return results.slice(offset, offset + limit);
645
+ }
646
+ async getJournalEntriesCount(query) {
647
+ const results = await this.getJournalEntries({
648
+ ...query,
649
+ limit: Infinity,
650
+ offset: 0
651
+ });
652
+ return results.length;
653
+ }
654
+ // ==================== Cleanup Operations ====================
655
+ async findAndExpireReservations(_batchSize = 100, _maxIterations = 100) {
656
+ const now = /* @__PURE__ */ new Date();
657
+ let expiredCount = 0;
658
+ let creditsReleased = 0;
659
+ const errors = [];
660
+ for (const [userId, userReservations] of this.reservations) {
661
+ for (const [reservationId, reservation] of userReservations) {
662
+ if (reservation.status === "reserved" && toDate(reservation.expiresAt).getTime() < now.getTime()) {
663
+ try {
664
+ await this.releaseReservationAtomic(userId, reservationId);
665
+ reservation.status = "expired";
666
+ userReservations.set(reservationId, reservation);
667
+ expiredCount++;
668
+ creditsReleased += reservation.amount;
669
+ } catch (error) {
670
+ errors.push(
671
+ `Failed to expire reservation ${reservationId}: ${error}`
672
+ );
673
+ }
674
+ }
675
+ }
676
+ }
677
+ return { expiredCount, creditsReleased, errors };
678
+ }
679
+ // ==================== Atomic Monthly Reset ====================
680
+ async atomicMonthlyReset(userId, tier, expectedResetAt) {
681
+ const credits = this.users.get(userId);
682
+ if (!credits) {
683
+ throw new Error(`User ${userId} not found`);
684
+ }
685
+ const currentResetAt = toDate(credits.monthlyResetAt).getTime();
686
+ const expected = toDate(expectedResetAt).getTime();
687
+ if (currentResetAt !== expected) {
688
+ return { wasReset: false, credits };
689
+ }
690
+ const newBalance = getConfigMonthlyLimit(tier);
691
+ const nextReset = getNextMonthlyReset();
692
+ credits.balance = newBalance === Infinity ? credits.balance : newBalance;
693
+ credits.monthlyUsed = 0;
694
+ credits.monthlyResetAt = nextReset.toISOString();
695
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
696
+ this.users.set(userId, credits);
697
+ return { wasReset: true, credits };
698
+ }
699
+ // ==================== Subscription Expiry ====================
700
+ async checkAndHandleSubscriptionExpiry(userId, gracePeriodDays = 3) {
701
+ const credits = this.users.get(userId);
702
+ if (!credits) {
703
+ throw new Error(`User ${userId} not found`);
704
+ }
705
+ if (credits.tier === "free" || !credits.subscriptionExpiresAt) {
706
+ return {
707
+ wasDowngraded: false,
708
+ inGracePeriod: false,
709
+ graceDaysRemaining: 0,
710
+ credits
711
+ };
712
+ }
713
+ const now = /* @__PURE__ */ new Date();
714
+ const expiresAt = toDate(credits.subscriptionExpiresAt);
715
+ const daysSinceExpiry = (now.getTime() - expiresAt.getTime()) / (1e3 * 60 * 60 * 24);
716
+ if (daysSinceExpiry <= 0) {
717
+ return {
718
+ wasDowngraded: false,
719
+ inGracePeriod: false,
720
+ graceDaysRemaining: 0,
721
+ credits
722
+ };
723
+ }
724
+ if (daysSinceExpiry <= gracePeriodDays) {
725
+ return {
726
+ wasDowngraded: false,
727
+ inGracePeriod: true,
728
+ graceDaysRemaining: Math.ceil(gracePeriodDays - daysSinceExpiry),
729
+ credits
730
+ };
731
+ }
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);
737
+ credits.subscriptionExpiresAt = null;
738
+ credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
739
+ this.users.set(userId, credits);
740
+ return {
741
+ wasDowngraded: true,
742
+ inGracePeriod: false,
743
+ graceDaysRemaining: 0,
744
+ credits
745
+ };
746
+ }
747
+ // ==================== Testing Utilities ====================
748
+ /**
749
+ * Clear all data (useful for testing)
750
+ */
751
+ clear() {
752
+ this.users.clear();
753
+ this.reservations.clear();
754
+ this.transactions.clear();
755
+ this.usageLogs = [];
756
+ this.journalEntries.clear();
757
+ }
758
+ /**
759
+ * Get all users (useful for testing/debugging)
760
+ */
761
+ getAllUsers() {
762
+ return Array.from(this.users.values());
763
+ }
764
+ /**
765
+ * Get all reservations for a user (useful for testing)
766
+ */
767
+ getAllReservations(userId) {
768
+ const userReservations = this.reservations.get(userId);
769
+ if (!userReservations) return [];
770
+ return Array.from(userReservations.values());
771
+ }
772
+ };
773
+ function createInMemoryCreditRepository() {
774
+ return new InMemoryCreditRepository();
775
+ }
776
+ export {
777
+ InMemoryCreditRepository,
778
+ createInMemoryCreditRepository,
779
+ generateId,
780
+ getNextMonthlyReset,
781
+ toClientUserCredits,
782
+ toDate
783
+ };
11
784
  //# sourceMappingURL=index.js.map