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