@nehorai/credits 1.2.0 → 1.4.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 +2514 -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 +2407 -63
  27. package/dist/index.js.map +1 -1
  28. package/dist/repository/index.cjs +819 -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 +786 -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 +675 -0
  41. package/dist/service/index.cjs.map +1 -0
  42. package/dist/service/{credits-service.d.ts → index.d.cts} +25 -8
  43. package/dist/service/index.d.ts +156 -3
  44. package/dist/service/index.js +646 -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,675 @@
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
+ * @param options - Optional settings (e.g. a custom `ttlMs` for long-running async jobs)
474
+ * @returns Reservation object
475
+ * @throws Error if insufficient credits
476
+ */
477
+ async reserveCredits(userId, amount, operationType, options) {
478
+ const ttlMs = options?.ttlMs ?? getConfig().reservationExpiryMs;
479
+ const expiresAt = new Date(Date.now() + ttlMs);
480
+ return this.repository.reserveCreditsAtomic(userId, amount, operationType, expiresAt);
481
+ }
482
+ /**
483
+ * Commit a reservation (phase 2 of two-phase commit - success)
484
+ * Deducts credits and marks reservation as committed
485
+ * Also triggers low balance notifications if balance drops below threshold
486
+ */
487
+ async commitCredits(userId, reservationId) {
488
+ const reservation = await this.repository.getReservation(userId, reservationId);
489
+ if (!reservation) {
490
+ throw new Error(`Reservation ${reservationId} not found`);
491
+ }
492
+ if (reservation.status === "committed") {
493
+ return;
494
+ }
495
+ await this.repository.commitReservationAtomic(userId, reservationId);
496
+ const credits = await this.repository.getUserCredits(userId);
497
+ if (credits) {
498
+ await this.repository.createJournalEntry({
499
+ userId,
500
+ entryType: "debit",
501
+ amount: reservation.amount,
502
+ balanceAfter: credits.balance,
503
+ source: "operation_commit",
504
+ referenceId: reservationId,
505
+ referenceType: "reservation",
506
+ description: `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,
507
+ metadata: {
508
+ operationType: reservation.operationType
509
+ }
510
+ });
511
+ if (this.lowBalanceCallback) {
512
+ this.lowBalanceCallback(userId, credits.balance).catch((error) => {
513
+ console.error("[Credits] Failed to send low balance notification:", error);
514
+ });
515
+ }
516
+ }
517
+ }
518
+ /**
519
+ * Release a reservation (phase 2 of two-phase commit - failure)
520
+ * Returns reserved credits and marks reservation as released
521
+ */
522
+ async releaseCredits(userId, reservationId) {
523
+ const reservation = await this.repository.getReservation(userId, reservationId);
524
+ await this.repository.releaseReservationAtomic(userId, reservationId);
525
+ if (reservation?.status === "reserved") {
526
+ const credits = await this.repository.getUserCredits(userId);
527
+ if (credits) {
528
+ await this.repository.createJournalEntry({
529
+ userId,
530
+ entryType: "credit",
531
+ amount: 0,
532
+ // No actual credits returned (they were reserved, not spent)
533
+ balanceAfter: credits.balance,
534
+ source: "operation_release",
535
+ referenceId: reservationId,
536
+ referenceType: "reservation",
537
+ description: `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
538
+ metadata: {
539
+ operationType: reservation.operationType,
540
+ amount: reservation.amount
541
+ }
542
+ });
543
+ }
544
+ }
545
+ }
546
+ /**
547
+ * Log usage for audit trail
548
+ * @param log - Usage log data
549
+ */
550
+ async logUsage(log) {
551
+ await this.repository.logUsage(log);
552
+ }
553
+ /**
554
+ * Add credits to user account (for purchases, bonuses, etc.)
555
+ * @param userId - User ID
556
+ * @param amount - Credits to add
557
+ * @param description - Transaction description
558
+ * @param paymentRef - Optional payment reference
559
+ */
560
+ async addCredits(userId, amount, description, paymentRef) {
561
+ return this.repository.addCreditsAtomic(userId, amount, description, paymentRef);
562
+ }
563
+ /**
564
+ * Update user subscription tier
565
+ * @param userId - User ID
566
+ * @param tier - New subscription tier
567
+ * @param expiresAt - Subscription expiry date (optional)
568
+ */
569
+ async updateTier(userId, tier, expiresAt) {
570
+ const monthlyLimit = getConfigMonthlyLimit(tier);
571
+ await this.repository.updateUserTier(userId, {
572
+ tier,
573
+ monthlyLimit: monthlyLimit === Infinity ? 0 : monthlyLimit,
574
+ // Reset balance to new tier limit if upgrading
575
+ balance: tier !== "free" ? monthlyLimit === Infinity ? 999999 : monthlyLimit : void 0,
576
+ monthlyUsed: tier !== "free" ? 0 : void 0,
577
+ subscriptionExpiresAt: expiresAt ? expiresAt.toISOString() : null
578
+ });
579
+ }
580
+ /**
581
+ * Get usage logs with optional filtering
582
+ * @param userId - Optional user ID filter
583
+ * @param limit - Max results
584
+ * @param offset - Skip results
585
+ * @returns List of usage logs
586
+ */
587
+ async getUsageLogs(userId, limit = 50, offset = 0) {
588
+ return this.repository.getUsageLogs({ userId, limit, offset });
589
+ }
590
+ /**
591
+ * Get user-friendly usage history
592
+ * Combines journal entries into a user-facing format
593
+ *
594
+ * @param userId - User ID
595
+ * @param limit - Max results per page
596
+ * @param offset - Skip results for pagination
597
+ * @returns Paginated usage history response
598
+ */
599
+ async getUsageHistory(userId, limit = 20, offset = 0) {
600
+ const [entries, total] = await Promise.all([
601
+ this.repository.getJournalEntries({ userId, limit, offset }),
602
+ this.repository.getJournalEntriesCount({ userId })
603
+ ]);
604
+ const historyEntries = entries.map((entry) => {
605
+ const type = this.mapSourceToHistoryType(entry.source);
606
+ const creditsChange = entry.entryType === "credit" ? entry.amount : -entry.amount;
607
+ return {
608
+ id: entry.id,
609
+ type,
610
+ creditsChange,
611
+ balanceAfter: entry.balanceAfter,
612
+ description: entry.description,
613
+ createdAt: typeof entry.createdAt === "string" ? entry.createdAt : toDate(entry.createdAt).toISOString()
614
+ };
615
+ });
616
+ return {
617
+ entries: historyEntries,
618
+ pagination: {
619
+ total,
620
+ limit,
621
+ offset,
622
+ hasMore: offset + entries.length < total
623
+ }
624
+ };
625
+ }
626
+ /**
627
+ * Map journal entry source to user-friendly history type
628
+ */
629
+ mapSourceToHistoryType(source) {
630
+ switch (source) {
631
+ case "operation_commit":
632
+ case "reservation_expired":
633
+ return "usage";
634
+ case "purchase":
635
+ return "purchase";
636
+ case "subscription_grant":
637
+ case "subscription_upgrade":
638
+ case "bonus":
639
+ return "bonus";
640
+ case "monthly_reset":
641
+ return "reset";
642
+ case "refund":
643
+ case "operation_release":
644
+ return "refund";
645
+ case "admin_adjustment":
646
+ case "subscription_downgrade":
647
+ case "expiry":
648
+ default:
649
+ return "adjustment";
650
+ }
651
+ }
652
+ /**
653
+ * Get journal entries directly (for admin or debugging)
654
+ * @param query - Journal entry query parameters
655
+ * @returns List of journal entries
656
+ */
657
+ async getJournalEntries(query) {
658
+ return this.repository.getJournalEntries(query);
659
+ }
660
+ /**
661
+ * Get the underlying repository (for advanced use cases)
662
+ */
663
+ getRepository() {
664
+ return this.repository;
665
+ }
666
+ };
667
+ function createCreditsService(repository) {
668
+ return new CreditsService(repository);
669
+ }
670
+ // Annotate the CommonJS export names for ESM import in node:
671
+ 0 && (module.exports = {
672
+ CreditsService,
673
+ createCreditsService
674
+ });
675
+ //# sourceMappingURL=index.cjs.map