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