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