@nehorai/credits-drizzle 0.1.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/chunk-7R6F67RH.js +113 -0
- package/dist/chunk-7R6F67RH.js.map +1 -0
- package/dist/chunk-ZIOAIRV6.js +488 -0
- package/dist/chunk-ZIOAIRV6.js.map +1 -0
- package/dist/index.cjs +609 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/repository/index.cjs +599 -0
- package/dist/repository/index.cjs.map +1 -0
- package/dist/repository/index.d.cts +49 -0
- package/dist/repository/index.d.ts +49 -0
- package/dist/repository/index.js +10 -0
- package/dist/repository/index.js.map +1 -0
- package/dist/schema/index.cjs +131 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.cts +915 -0
- package/dist/schema/index.d.ts +915 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/index.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,599 @@
|
|
|
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, desc2) => {
|
|
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: !(desc2 = __getOwnPropDesc(from, key)) || desc2.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
|
+
DrizzleCreditRepository: () => DrizzleCreditRepository,
|
|
24
|
+
createDrizzleCreditRepository: () => createDrizzleCreditRepository
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(repository_exports);
|
|
27
|
+
var import_drizzle_orm2 = require("drizzle-orm");
|
|
28
|
+
var import_credits = require("@nehorai/credits");
|
|
29
|
+
var import_credits2 = require("@nehorai/credits");
|
|
30
|
+
|
|
31
|
+
// src/schema/index.ts
|
|
32
|
+
var import_drizzle_orm = require("drizzle-orm");
|
|
33
|
+
var import_pg_core = require("drizzle-orm/pg-core");
|
|
34
|
+
var creditBalances = (0, import_pg_core.pgTable)("credit_balances", {
|
|
35
|
+
userId: (0, import_pg_core.uuid)("user_id").primaryKey(),
|
|
36
|
+
balance: (0, import_pg_core.numeric)("balance", { precision: 12, scale: 2 }).notNull().default("0"),
|
|
37
|
+
bonusCredits: (0, import_pg_core.numeric)("bonus_credits", { precision: 12, scale: 2 }).notNull().default("0"),
|
|
38
|
+
reserved: (0, import_pg_core.numeric)("reserved", { precision: 12, scale: 2 }).notNull().default("0"),
|
|
39
|
+
tier: (0, import_pg_core.text)("tier").notNull().default("free"),
|
|
40
|
+
monthlyLimit: (0, import_pg_core.numeric)("monthly_limit", { precision: 12, scale: 2 }).notNull().default("0"),
|
|
41
|
+
monthlyUsed: (0, import_pg_core.numeric)("monthly_used", { precision: 12, scale: 2 }).notNull().default("0"),
|
|
42
|
+
monthlyResetAt: (0, import_pg_core.timestamp)("monthly_reset_at", { withTimezone: true }).notNull(),
|
|
43
|
+
subscriptionExpiresAt: (0, import_pg_core.timestamp)("subscription_expires_at", { withTimezone: true }),
|
|
44
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
45
|
+
updatedAt: (0, import_pg_core.timestamp)("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
46
|
+
});
|
|
47
|
+
var creditReservations = (0, import_pg_core.pgTable)(
|
|
48
|
+
"credit_reservations",
|
|
49
|
+
{
|
|
50
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
51
|
+
userId: (0, import_pg_core.uuid)("user_id").notNull(),
|
|
52
|
+
amount: (0, import_pg_core.numeric)("amount", { precision: 12, scale: 2 }).notNull(),
|
|
53
|
+
operationType: (0, import_pg_core.text)("operation_type").notNull(),
|
|
54
|
+
status: (0, import_pg_core.text)("status").notNull().default("reserved"),
|
|
55
|
+
expiresAt: (0, import_pg_core.timestamp)("expires_at", { withTimezone: true }).notNull(),
|
|
56
|
+
completedAt: (0, import_pg_core.timestamp)("completed_at", { withTimezone: true }),
|
|
57
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
58
|
+
},
|
|
59
|
+
(table) => ({
|
|
60
|
+
userIdx: (0, import_pg_core.index)("credit_reservations_user_idx").on(table.userId),
|
|
61
|
+
statusExpiresIdx: (0, import_pg_core.index)("credit_reservations_status_expires_idx").on(table.status, table.expiresAt)
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
var creditPluginTransactions = (0, import_pg_core.pgTable)(
|
|
65
|
+
"credit_plugin_transactions",
|
|
66
|
+
{
|
|
67
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
68
|
+
userId: (0, import_pg_core.uuid)("user_id").notNull(),
|
|
69
|
+
type: (0, import_pg_core.text)("type").notNull(),
|
|
70
|
+
amount: (0, import_pg_core.numeric)("amount", { precision: 12, scale: 2 }).notNull(),
|
|
71
|
+
description: (0, import_pg_core.text)("description").notNull(),
|
|
72
|
+
paymentRef: (0, import_pg_core.text)("payment_ref"),
|
|
73
|
+
previousBalance: (0, import_pg_core.numeric)("previous_balance", { precision: 12, scale: 2 }).notNull(),
|
|
74
|
+
newBalance: (0, import_pg_core.numeric)("new_balance", { precision: 12, scale: 2 }).notNull(),
|
|
75
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
76
|
+
},
|
|
77
|
+
(table) => ({
|
|
78
|
+
userCreatedIdx: (0, import_pg_core.index)("credit_plugin_transactions_user_created_idx").on(table.userId, table.createdAt),
|
|
79
|
+
paymentRefUnique: (0, import_pg_core.uniqueIndex)("credit_plugin_transactions_payment_ref_unique").on(table.paymentRef).where(import_drizzle_orm.sql`${table.paymentRef} is not null`)
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
var creditUsageLogs = (0, import_pg_core.pgTable)(
|
|
83
|
+
"credit_usage_logs",
|
|
84
|
+
{
|
|
85
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
86
|
+
userId: (0, import_pg_core.uuid)("user_id").notNull(),
|
|
87
|
+
operationType: (0, import_pg_core.text)("operation_type").notNull(),
|
|
88
|
+
provider: (0, import_pg_core.text)("provider").notNull(),
|
|
89
|
+
creditsUsed: (0, import_pg_core.numeric)("credits_used", { precision: 12, scale: 2 }).notNull(),
|
|
90
|
+
success: (0, import_pg_core.boolean)("success").notNull(),
|
|
91
|
+
errorMessage: (0, import_pg_core.text)("error_message"),
|
|
92
|
+
resourceId: (0, import_pg_core.text)("resource_id"),
|
|
93
|
+
resourceType: (0, import_pg_core.text)("resource_type"),
|
|
94
|
+
requestId: (0, import_pg_core.text)("request_id"),
|
|
95
|
+
metadata: (0, import_pg_core.jsonb)("metadata").$type(),
|
|
96
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
97
|
+
},
|
|
98
|
+
(table) => ({
|
|
99
|
+
userCreatedIdx: (0, import_pg_core.index)("credit_usage_logs_user_created_idx").on(table.userId, table.createdAt),
|
|
100
|
+
operationIdx: (0, import_pg_core.index)("credit_usage_logs_operation_idx").on(table.operationType),
|
|
101
|
+
successIdx: (0, import_pg_core.index)("credit_usage_logs_success_idx").on(table.success)
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
var creditJournalEntries = (0, import_pg_core.pgTable)(
|
|
105
|
+
"credit_journal_entries",
|
|
106
|
+
{
|
|
107
|
+
id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
|
|
108
|
+
userId: (0, import_pg_core.uuid)("user_id").notNull(),
|
|
109
|
+
entryType: (0, import_pg_core.text)("entry_type").notNull(),
|
|
110
|
+
amount: (0, import_pg_core.numeric)("amount", { precision: 12, scale: 2 }).notNull(),
|
|
111
|
+
balanceAfter: (0, import_pg_core.numeric)("balance_after", { precision: 12, scale: 2 }).notNull(),
|
|
112
|
+
source: (0, import_pg_core.text)("source").notNull(),
|
|
113
|
+
referenceId: (0, import_pg_core.text)("reference_id").notNull(),
|
|
114
|
+
referenceType: (0, import_pg_core.text)("reference_type").notNull(),
|
|
115
|
+
description: (0, import_pg_core.text)("description").notNull(),
|
|
116
|
+
metadata: (0, import_pg_core.jsonb)("metadata").$type(),
|
|
117
|
+
createdAt: (0, import_pg_core.timestamp)("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
118
|
+
},
|
|
119
|
+
(table) => ({
|
|
120
|
+
userCreatedIdx: (0, import_pg_core.index)("credit_journal_entries_user_created_idx").on(table.userId, table.createdAt),
|
|
121
|
+
sourceIdx: (0, import_pg_core.index)("credit_journal_entries_source_idx").on(table.source),
|
|
122
|
+
referenceIdx: (0, import_pg_core.index)("credit_journal_entries_reference_idx").on(table.referenceId, table.referenceType)
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// src/repository/index.ts
|
|
127
|
+
function numberValue(value) {
|
|
128
|
+
if (value === null || value === void 0) return 0;
|
|
129
|
+
if (typeof value === "number") return value;
|
|
130
|
+
const parsed = Number(value);
|
|
131
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
132
|
+
}
|
|
133
|
+
function dateValue(value) {
|
|
134
|
+
if (!value) return null;
|
|
135
|
+
return value instanceof Date ? value : new Date(value);
|
|
136
|
+
}
|
|
137
|
+
function iso(value) {
|
|
138
|
+
if (!value) return (/* @__PURE__ */ new Date()).toISOString();
|
|
139
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
140
|
+
}
|
|
141
|
+
function toUserCredits(row) {
|
|
142
|
+
return {
|
|
143
|
+
userId: row.userId,
|
|
144
|
+
balance: numberValue(row.balance),
|
|
145
|
+
bonusCredits: numberValue(row.bonusCredits),
|
|
146
|
+
reserved: numberValue(row.reserved),
|
|
147
|
+
tier: row.tier,
|
|
148
|
+
monthlyLimit: numberValue(row.monthlyLimit),
|
|
149
|
+
monthlyUsed: numberValue(row.monthlyUsed),
|
|
150
|
+
monthlyResetAt: iso(row.monthlyResetAt),
|
|
151
|
+
subscriptionExpiresAt: row.subscriptionExpiresAt ? iso(row.subscriptionExpiresAt) : null,
|
|
152
|
+
createdAt: iso(row.createdAt),
|
|
153
|
+
updatedAt: iso(row.updatedAt)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function toReservation(row) {
|
|
157
|
+
return {
|
|
158
|
+
id: row.id,
|
|
159
|
+
userId: row.userId,
|
|
160
|
+
amount: numberValue(row.amount),
|
|
161
|
+
operationType: row.operationType,
|
|
162
|
+
status: row.status,
|
|
163
|
+
createdAt: iso(row.createdAt),
|
|
164
|
+
expiresAt: iso(row.expiresAt),
|
|
165
|
+
completedAt: row.completedAt ? iso(row.completedAt) : void 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function toTransaction(row) {
|
|
169
|
+
return {
|
|
170
|
+
id: row.id,
|
|
171
|
+
userId: row.userId,
|
|
172
|
+
type: row.type,
|
|
173
|
+
amount: numberValue(row.amount),
|
|
174
|
+
description: row.description,
|
|
175
|
+
paymentRef: row.paymentRef ?? void 0,
|
|
176
|
+
previousBalance: numberValue(row.previousBalance),
|
|
177
|
+
newBalance: numberValue(row.newBalance),
|
|
178
|
+
createdAt: iso(row.createdAt)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function toUsageLog(row) {
|
|
182
|
+
return {
|
|
183
|
+
id: row.id,
|
|
184
|
+
userId: row.userId,
|
|
185
|
+
operationType: row.operationType,
|
|
186
|
+
provider: row.provider,
|
|
187
|
+
creditsUsed: numberValue(row.creditsUsed),
|
|
188
|
+
success: row.success,
|
|
189
|
+
errorMessage: row.errorMessage ?? void 0,
|
|
190
|
+
resourceId: row.resourceId ?? void 0,
|
|
191
|
+
resourceType: row.resourceType ?? void 0,
|
|
192
|
+
requestId: row.requestId ?? void 0,
|
|
193
|
+
metadata: row.metadata ?? void 0,
|
|
194
|
+
createdAt: iso(row.createdAt)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function toJournalEntry(row) {
|
|
198
|
+
return {
|
|
199
|
+
id: row.id,
|
|
200
|
+
userId: row.userId,
|
|
201
|
+
entryType: row.entryType,
|
|
202
|
+
amount: numberValue(row.amount),
|
|
203
|
+
balanceAfter: numberValue(row.balanceAfter),
|
|
204
|
+
source: row.source,
|
|
205
|
+
referenceId: row.referenceId,
|
|
206
|
+
referenceType: row.referenceType,
|
|
207
|
+
description: row.description,
|
|
208
|
+
metadata: row.metadata ?? void 0,
|
|
209
|
+
createdAt: iso(row.createdAt)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
var DrizzleCreditRepository = class {
|
|
213
|
+
constructor(db) {
|
|
214
|
+
this.db = db;
|
|
215
|
+
}
|
|
216
|
+
async withTx(callback) {
|
|
217
|
+
if (this.db.transaction) {
|
|
218
|
+
return this.db.transaction(callback);
|
|
219
|
+
}
|
|
220
|
+
return callback(this.db);
|
|
221
|
+
}
|
|
222
|
+
async ensureUserCredits(db, userId, tier = "free") {
|
|
223
|
+
const existing = await db.select().from(creditBalances).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId)).limit(1);
|
|
224
|
+
if (existing[0]) return toUserCredits(existing[0]);
|
|
225
|
+
const monthlyLimit = (0, import_credits.getConfigMonthlyLimit)(tier);
|
|
226
|
+
const initialBalance = Number.isFinite(monthlyLimit) ? monthlyLimit : 0;
|
|
227
|
+
const inserted = await db.insert(creditBalances).values({
|
|
228
|
+
userId,
|
|
229
|
+
tier,
|
|
230
|
+
balance: String(initialBalance),
|
|
231
|
+
monthlyLimit: String(initialBalance),
|
|
232
|
+
monthlyResetAt: (0, import_credits2.getNextMonthlyReset)()
|
|
233
|
+
}).onConflictDoNothing().returning();
|
|
234
|
+
if (inserted[0]) return toUserCredits(inserted[0]);
|
|
235
|
+
const afterConflict = await db.select().from(creditBalances).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId)).limit(1);
|
|
236
|
+
if (!afterConflict[0]) {
|
|
237
|
+
throw new Error(`Failed to initialize credits for user ${userId}`);
|
|
238
|
+
}
|
|
239
|
+
return toUserCredits(afterConflict[0]);
|
|
240
|
+
}
|
|
241
|
+
async getUserCredits(userId) {
|
|
242
|
+
const rows = await this.db.select().from(creditBalances).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId)).limit(1);
|
|
243
|
+
return rows[0] ? toUserCredits(rows[0]) : null;
|
|
244
|
+
}
|
|
245
|
+
async initializeUserCredits(userId, tier, initialBalance) {
|
|
246
|
+
const monthlyLimit = (0, import_credits.getConfigMonthlyLimit)(tier);
|
|
247
|
+
const rows = await this.db.insert(creditBalances).values({
|
|
248
|
+
userId,
|
|
249
|
+
tier,
|
|
250
|
+
balance: String(initialBalance),
|
|
251
|
+
monthlyLimit: String(Number.isFinite(monthlyLimit) ? monthlyLimit : 0),
|
|
252
|
+
monthlyResetAt: (0, import_credits2.getNextMonthlyReset)()
|
|
253
|
+
}).onConflictDoUpdate({
|
|
254
|
+
target: creditBalances.userId,
|
|
255
|
+
set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
256
|
+
}).returning();
|
|
257
|
+
return toUserCredits(rows[0]);
|
|
258
|
+
}
|
|
259
|
+
async updateUserCredits(userId, updates) {
|
|
260
|
+
const set = { updatedAt: /* @__PURE__ */ new Date() };
|
|
261
|
+
if (updates.balance !== void 0) set.balance = String(updates.balance);
|
|
262
|
+
if (updates.bonusCredits !== void 0) set.bonusCredits = String(updates.bonusCredits);
|
|
263
|
+
if (updates.reserved !== void 0) set.reserved = String(updates.reserved);
|
|
264
|
+
if (updates.tier !== void 0) set.tier = updates.tier;
|
|
265
|
+
if (updates.monthlyLimit !== void 0) set.monthlyLimit = String(updates.monthlyLimit);
|
|
266
|
+
if (updates.monthlyUsed !== void 0) set.monthlyUsed = String(updates.monthlyUsed);
|
|
267
|
+
if (updates.monthlyResetAt !== void 0) set.monthlyResetAt = dateValue(updates.monthlyResetAt);
|
|
268
|
+
if (updates.subscriptionExpiresAt !== void 0) set.subscriptionExpiresAt = dateValue(updates.subscriptionExpiresAt);
|
|
269
|
+
await this.db.update(creditBalances).set({
|
|
270
|
+
...set,
|
|
271
|
+
balance: updates.balanceIncrement !== void 0 ? import_drizzle_orm2.sql`${creditBalances.balance} + ${updates.balanceIncrement}` : set.balance,
|
|
272
|
+
bonusCredits: updates.bonusCreditsIncrement !== void 0 ? import_drizzle_orm2.sql`${creditBalances.bonusCredits} + ${updates.bonusCreditsIncrement}` : set.bonusCredits,
|
|
273
|
+
reserved: updates.reservedIncrement !== void 0 ? import_drizzle_orm2.sql`${creditBalances.reserved} + ${updates.reservedIncrement}` : set.reserved,
|
|
274
|
+
monthlyUsed: updates.monthlyUsedIncrement !== void 0 ? import_drizzle_orm2.sql`${creditBalances.monthlyUsed} + ${updates.monthlyUsedIncrement}` : set.monthlyUsed
|
|
275
|
+
}).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId));
|
|
276
|
+
}
|
|
277
|
+
async updateUserTier(userId, input) {
|
|
278
|
+
await this.db.update(creditBalances).set({
|
|
279
|
+
tier: input.tier,
|
|
280
|
+
monthlyLimit: String(input.monthlyLimit),
|
|
281
|
+
balance: input.balance !== void 0 ? String(input.balance) : void 0,
|
|
282
|
+
monthlyUsed: input.monthlyUsed !== void 0 ? String(input.monthlyUsed) : void 0,
|
|
283
|
+
subscriptionExpiresAt: input.subscriptionExpiresAt !== void 0 ? dateValue(input.subscriptionExpiresAt) : void 0,
|
|
284
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
285
|
+
}).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId));
|
|
286
|
+
}
|
|
287
|
+
async createReservation(input) {
|
|
288
|
+
const rows = await this.db.insert(creditReservations).values({
|
|
289
|
+
userId: input.userId,
|
|
290
|
+
amount: String(input.amount),
|
|
291
|
+
operationType: input.operationType,
|
|
292
|
+
expiresAt: input.expiresAt
|
|
293
|
+
}).returning();
|
|
294
|
+
return toReservation(rows[0]);
|
|
295
|
+
}
|
|
296
|
+
async getReservation(userId, reservationId) {
|
|
297
|
+
const rows = await this.db.select().from(creditReservations).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(creditReservations.userId, userId), (0, import_drizzle_orm2.eq)(creditReservations.id, reservationId))).limit(1);
|
|
298
|
+
return rows[0] ? toReservation(rows[0]) : null;
|
|
299
|
+
}
|
|
300
|
+
async updateReservationStatus(userId, reservationId, status, completedAt) {
|
|
301
|
+
await this.db.update(creditReservations).set({ status, completedAt: completedAt ?? /* @__PURE__ */ new Date() }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(creditReservations.userId, userId), (0, import_drizzle_orm2.eq)(creditReservations.id, reservationId)));
|
|
302
|
+
}
|
|
303
|
+
async reserveCreditsAtomic(userId, amount, operationType, expiresAt) {
|
|
304
|
+
return this.withTx(async (tx) => {
|
|
305
|
+
await this.ensureUserCredits(tx, userId);
|
|
306
|
+
const updated = await tx.update(creditBalances).set({
|
|
307
|
+
reserved: import_drizzle_orm2.sql`${creditBalances.reserved} + ${amount}`,
|
|
308
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
309
|
+
}).where(
|
|
310
|
+
(0, import_drizzle_orm2.and)(
|
|
311
|
+
(0, import_drizzle_orm2.eq)(creditBalances.userId, userId),
|
|
312
|
+
import_drizzle_orm2.sql`${creditBalances.balance} + ${creditBalances.bonusCredits} - ${creditBalances.reserved} >= ${amount}`
|
|
313
|
+
)
|
|
314
|
+
).returning();
|
|
315
|
+
if (!updated[0]) {
|
|
316
|
+
throw new Error(`Insufficient credits for user ${userId}`);
|
|
317
|
+
}
|
|
318
|
+
const reservation = await tx.insert(creditReservations).values({
|
|
319
|
+
userId,
|
|
320
|
+
amount: String(amount),
|
|
321
|
+
operationType,
|
|
322
|
+
expiresAt
|
|
323
|
+
}).returning();
|
|
324
|
+
return toReservation(reservation[0]);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async commitReservationAtomic(userId, reservationId) {
|
|
328
|
+
await this.withTx(async (tx) => {
|
|
329
|
+
const reservationRows = await tx.select().from(creditReservations).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(creditReservations.userId, userId), (0, import_drizzle_orm2.eq)(creditReservations.id, reservationId))).limit(1);
|
|
330
|
+
const reservation = reservationRows[0];
|
|
331
|
+
if (!reservation) throw new Error(`Reservation ${reservationId} not found`);
|
|
332
|
+
if (reservation.status === "committed") return;
|
|
333
|
+
if (reservation.status !== "reserved") {
|
|
334
|
+
throw new Error(`Cannot commit reservation in ${reservation.status} state`);
|
|
335
|
+
}
|
|
336
|
+
const creditRows = await tx.select().from(creditBalances).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId)).limit(1);
|
|
337
|
+
const credits = creditRows[0];
|
|
338
|
+
if (!credits) throw new Error(`User credits not found for user ${userId}`);
|
|
339
|
+
const amount = numberValue(reservation.amount);
|
|
340
|
+
const balance = numberValue(credits.balance);
|
|
341
|
+
const bonusCredits = numberValue(credits.bonusCredits);
|
|
342
|
+
if (balance + bonusCredits < amount) {
|
|
343
|
+
throw new Error(`Insufficient credits to commit reservation ${reservationId}`);
|
|
344
|
+
}
|
|
345
|
+
const balanceDeduction = Math.min(balance, amount);
|
|
346
|
+
const bonusDeduction = amount - balanceDeduction;
|
|
347
|
+
const previousTotal = balance + bonusCredits;
|
|
348
|
+
const newTotal = previousTotal - amount;
|
|
349
|
+
await tx.update(creditBalances).set({
|
|
350
|
+
balance: String(balance - balanceDeduction),
|
|
351
|
+
bonusCredits: String(bonusCredits - bonusDeduction),
|
|
352
|
+
reserved: import_drizzle_orm2.sql`greatest(${creditBalances.reserved} - ${amount}, 0)`,
|
|
353
|
+
monthlyUsed: import_drizzle_orm2.sql`${creditBalances.monthlyUsed} + ${amount}`,
|
|
354
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
355
|
+
}).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId));
|
|
356
|
+
await tx.update(creditReservations).set({ status: "committed", completedAt: /* @__PURE__ */ new Date() }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(creditReservations.userId, userId), (0, import_drizzle_orm2.eq)(creditReservations.id, reservationId)));
|
|
357
|
+
await tx.insert(creditJournalEntries).values({
|
|
358
|
+
userId,
|
|
359
|
+
entryType: "debit",
|
|
360
|
+
amount: String(amount),
|
|
361
|
+
balanceAfter: String(newTotal),
|
|
362
|
+
source: "operation_commit",
|
|
363
|
+
referenceId: reservationId,
|
|
364
|
+
referenceType: "reservation",
|
|
365
|
+
description: `Committed ${amount} credits`
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async releaseReservationAtomic(userId, reservationId) {
|
|
370
|
+
await this.withTx(async (tx) => {
|
|
371
|
+
const reservationRows = await tx.select().from(creditReservations).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(creditReservations.userId, userId), (0, import_drizzle_orm2.eq)(creditReservations.id, reservationId))).limit(1);
|
|
372
|
+
const reservation = reservationRows[0];
|
|
373
|
+
if (!reservation) throw new Error(`Reservation ${reservationId} not found`);
|
|
374
|
+
if (reservation.status !== "reserved") return;
|
|
375
|
+
const amount = numberValue(reservation.amount);
|
|
376
|
+
await tx.update(creditBalances).set({
|
|
377
|
+
reserved: import_drizzle_orm2.sql`greatest(${creditBalances.reserved} - ${amount}, 0)`,
|
|
378
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
379
|
+
}).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId));
|
|
380
|
+
await tx.update(creditReservations).set({ status: "released", completedAt: /* @__PURE__ */ new Date() }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(creditReservations.userId, userId), (0, import_drizzle_orm2.eq)(creditReservations.id, reservationId)));
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
async addCreditsAtomic(userId, amount, description, paymentRef) {
|
|
384
|
+
await this.withTx(async (tx) => {
|
|
385
|
+
if (paymentRef) {
|
|
386
|
+
const existing = await tx.select().from(creditPluginTransactions).where((0, import_drizzle_orm2.eq)(creditPluginTransactions.paymentRef, paymentRef)).limit(1);
|
|
387
|
+
if (existing[0]) return;
|
|
388
|
+
}
|
|
389
|
+
const credits = await this.ensureUserCredits(tx, userId);
|
|
390
|
+
const previousBalance = credits.balance + credits.bonusCredits;
|
|
391
|
+
const newBalance = previousBalance + amount;
|
|
392
|
+
await tx.update(creditBalances).set({
|
|
393
|
+
bonusCredits: import_drizzle_orm2.sql`${creditBalances.bonusCredits} + ${amount}`,
|
|
394
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
395
|
+
}).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId));
|
|
396
|
+
const inserted = await tx.insert(creditPluginTransactions).values({
|
|
397
|
+
userId,
|
|
398
|
+
type: "purchase",
|
|
399
|
+
amount: String(amount),
|
|
400
|
+
description,
|
|
401
|
+
paymentRef,
|
|
402
|
+
previousBalance: String(previousBalance),
|
|
403
|
+
newBalance: String(newBalance)
|
|
404
|
+
}).returning();
|
|
405
|
+
await tx.insert(creditJournalEntries).values({
|
|
406
|
+
userId,
|
|
407
|
+
entryType: "credit",
|
|
408
|
+
amount: String(amount),
|
|
409
|
+
balanceAfter: String(newBalance),
|
|
410
|
+
source: "purchase",
|
|
411
|
+
referenceId: inserted[0]?.id ?? paymentRef ?? "unknown",
|
|
412
|
+
referenceType: "transaction",
|
|
413
|
+
description,
|
|
414
|
+
metadata: paymentRef ? { paymentRef } : void 0
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async deductCreditsAtomic(userId, amount) {
|
|
419
|
+
return this.withTx(async (tx) => {
|
|
420
|
+
const creditRows = await tx.select().from(creditBalances).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId)).limit(1);
|
|
421
|
+
const credits = creditRows[0];
|
|
422
|
+
if (!credits) throw new Error(`User credits not found for user ${userId}`);
|
|
423
|
+
const balance = numberValue(credits.balance);
|
|
424
|
+
const bonusCredits = numberValue(credits.bonusCredits);
|
|
425
|
+
const reserved = numberValue(credits.reserved);
|
|
426
|
+
const available = balance + bonusCredits - reserved;
|
|
427
|
+
if (available < amount) {
|
|
428
|
+
throw new Error(`Insufficient credits. Available: ${available}, requested: ${amount}`);
|
|
429
|
+
}
|
|
430
|
+
const balanceDeduction = Math.min(balance, amount);
|
|
431
|
+
const bonusDeduction = amount - balanceDeduction;
|
|
432
|
+
const previousBalance = balance + bonusCredits;
|
|
433
|
+
const newBalance = previousBalance - amount;
|
|
434
|
+
await tx.update(creditBalances).set({
|
|
435
|
+
balance: String(balance - balanceDeduction),
|
|
436
|
+
bonusCredits: String(bonusCredits - bonusDeduction),
|
|
437
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
438
|
+
}).where((0, import_drizzle_orm2.eq)(creditBalances.userId, userId));
|
|
439
|
+
return { previousBalance, newBalance };
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async createTransaction(input) {
|
|
443
|
+
const rows = await this.db.insert(creditPluginTransactions).values({
|
|
444
|
+
userId: input.userId,
|
|
445
|
+
type: input.type,
|
|
446
|
+
amount: String(input.amount),
|
|
447
|
+
description: input.description,
|
|
448
|
+
paymentRef: input.paymentRef,
|
|
449
|
+
previousBalance: String(input.previousBalance),
|
|
450
|
+
newBalance: String(input.newBalance)
|
|
451
|
+
}).returning();
|
|
452
|
+
return toTransaction(rows[0]);
|
|
453
|
+
}
|
|
454
|
+
async getTransactions(userId, limit = 50, offset = 0) {
|
|
455
|
+
const rows = await this.db.select().from(creditPluginTransactions).where((0, import_drizzle_orm2.eq)(creditPluginTransactions.userId, userId)).orderBy((0, import_drizzle_orm2.desc)(creditPluginTransactions.createdAt)).limit(limit).offset(offset);
|
|
456
|
+
return rows.map(toTransaction);
|
|
457
|
+
}
|
|
458
|
+
async logUsage(input) {
|
|
459
|
+
const rows = await this.db.insert(creditUsageLogs).values({
|
|
460
|
+
userId: input.userId,
|
|
461
|
+
operationType: input.operationType,
|
|
462
|
+
provider: input.provider,
|
|
463
|
+
creditsUsed: String(input.creditsUsed),
|
|
464
|
+
success: input.success,
|
|
465
|
+
errorMessage: input.errorMessage,
|
|
466
|
+
resourceId: input.resourceId,
|
|
467
|
+
resourceType: input.resourceType,
|
|
468
|
+
requestId: input.requestId,
|
|
469
|
+
metadata: input.metadata
|
|
470
|
+
}).returning();
|
|
471
|
+
return toUsageLog(rows[0]);
|
|
472
|
+
}
|
|
473
|
+
async getUsageLogs(query) {
|
|
474
|
+
const filters = this.usageFilters(query);
|
|
475
|
+
const rows = await this.db.select().from(creditUsageLogs).where(filters.length ? (0, import_drizzle_orm2.and)(...filters) : void 0).orderBy((0, import_drizzle_orm2.desc)(creditUsageLogs.createdAt)).limit(query.limit ?? 50).offset(query.offset ?? 0);
|
|
476
|
+
return rows.map(toUsageLog);
|
|
477
|
+
}
|
|
478
|
+
async getUsageLogsCount(query) {
|
|
479
|
+
const filters = this.usageFilters(query);
|
|
480
|
+
const rows = await this.db.select({ value: (0, import_drizzle_orm2.count)() }).from(creditUsageLogs).where(filters.length ? (0, import_drizzle_orm2.and)(...filters) : void 0);
|
|
481
|
+
return Number(rows[0]?.value ?? 0);
|
|
482
|
+
}
|
|
483
|
+
async findAndExpireReservations(batchSize = 100, maxIterations = 100) {
|
|
484
|
+
const errors = [];
|
|
485
|
+
let expiredCount = 0;
|
|
486
|
+
let creditsReleased = 0;
|
|
487
|
+
for (let i = 0; i < maxIterations; i += 1) {
|
|
488
|
+
const rows = await this.db.select().from(creditReservations).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(creditReservations.status, "reserved"), (0, import_drizzle_orm2.lt)(creditReservations.expiresAt, /* @__PURE__ */ new Date()))).limit(batchSize);
|
|
489
|
+
if (rows.length === 0) break;
|
|
490
|
+
for (const row of rows) {
|
|
491
|
+
try {
|
|
492
|
+
await this.releaseReservationAtomic(row.userId, row.id);
|
|
493
|
+
await this.db.update(creditReservations).set({ status: "expired", completedAt: /* @__PURE__ */ new Date() }).where((0, import_drizzle_orm2.eq)(creditReservations.id, row.id));
|
|
494
|
+
expiredCount += 1;
|
|
495
|
+
creditsReleased += numberValue(row.amount);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
errors.push(`Failed to expire reservation ${row.id}: ${String(error)}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { expiredCount, creditsReleased, errors };
|
|
502
|
+
}
|
|
503
|
+
async atomicMonthlyReset(userId, tier, expectedResetAt) {
|
|
504
|
+
const newBalance = (0, import_credits.getConfigMonthlyLimit)(tier);
|
|
505
|
+
const nextReset = (0, import_credits2.getNextMonthlyReset)();
|
|
506
|
+
const expected = dateValue(expectedResetAt);
|
|
507
|
+
const rows = await this.db.update(creditBalances).set({
|
|
508
|
+
balance: Number.isFinite(newBalance) ? String(newBalance) : import_drizzle_orm2.sql`${creditBalances.balance}`,
|
|
509
|
+
monthlyUsed: "0",
|
|
510
|
+
monthlyResetAt: nextReset,
|
|
511
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
512
|
+
}).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(creditBalances.userId, userId), (0, import_drizzle_orm2.eq)(creditBalances.monthlyResetAt, expected))).returning();
|
|
513
|
+
if (rows[0]) return { wasReset: true, credits: toUserCredits(rows[0]) };
|
|
514
|
+
const current = await this.getUserCredits(userId);
|
|
515
|
+
if (!current) throw new Error(`User ${userId} not found`);
|
|
516
|
+
return { wasReset: false, credits: current };
|
|
517
|
+
}
|
|
518
|
+
async checkAndHandleSubscriptionExpiry(userId, gracePeriodDays = 3) {
|
|
519
|
+
const credits = await this.getUserCredits(userId);
|
|
520
|
+
if (!credits) throw new Error(`User ${userId} not found`);
|
|
521
|
+
const tierConfig = (0, import_credits.getConfigTierConfig)(credits.tier);
|
|
522
|
+
if ((tierConfig.isFree ?? credits.tier === "free") || !credits.subscriptionExpiresAt) {
|
|
523
|
+
return { wasDowngraded: false, inGracePeriod: false, graceDaysRemaining: 0, credits };
|
|
524
|
+
}
|
|
525
|
+
const expiresAt = new Date(credits.subscriptionExpiresAt);
|
|
526
|
+
const daysSinceExpiry = (Date.now() - expiresAt.getTime()) / (1e3 * 60 * 60 * 24);
|
|
527
|
+
if (daysSinceExpiry <= 0) {
|
|
528
|
+
return { wasDowngraded: false, inGracePeriod: false, graceDaysRemaining: 0, credits };
|
|
529
|
+
}
|
|
530
|
+
if (daysSinceExpiry <= gracePeriodDays) {
|
|
531
|
+
return {
|
|
532
|
+
wasDowngraded: false,
|
|
533
|
+
inGracePeriod: true,
|
|
534
|
+
graceDaysRemaining: Math.ceil(gracePeriodDays - daysSinceExpiry),
|
|
535
|
+
credits
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const defaultTier = "free";
|
|
539
|
+
const defaultTierConfig = (0, import_credits.getConfigTierConfig)(defaultTier);
|
|
540
|
+
await this.updateUserTier(userId, {
|
|
541
|
+
tier: defaultTier,
|
|
542
|
+
monthlyLimit: defaultTierConfig.monthlyCredits,
|
|
543
|
+
balance: Math.min(credits.balance, defaultTierConfig.monthlyCredits),
|
|
544
|
+
subscriptionExpiresAt: null
|
|
545
|
+
});
|
|
546
|
+
const updatedCredits = await this.getUserCredits(userId) ?? credits;
|
|
547
|
+
return { wasDowngraded: true, inGracePeriod: false, graceDaysRemaining: 0, credits: updatedCredits };
|
|
548
|
+
}
|
|
549
|
+
async createJournalEntry(input) {
|
|
550
|
+
const rows = await this.db.insert(creditJournalEntries).values({
|
|
551
|
+
userId: input.userId,
|
|
552
|
+
entryType: input.entryType,
|
|
553
|
+
amount: String(input.amount),
|
|
554
|
+
balanceAfter: String(input.balanceAfter),
|
|
555
|
+
source: input.source,
|
|
556
|
+
referenceId: input.referenceId,
|
|
557
|
+
referenceType: input.referenceType,
|
|
558
|
+
description: input.description,
|
|
559
|
+
metadata: input.metadata
|
|
560
|
+
}).returning();
|
|
561
|
+
return toJournalEntry(rows[0]);
|
|
562
|
+
}
|
|
563
|
+
async getJournalEntries(query) {
|
|
564
|
+
const filters = this.journalFilters(query);
|
|
565
|
+
const rows = await this.db.select().from(creditJournalEntries).where(filters.length ? (0, import_drizzle_orm2.and)(...filters) : void 0).orderBy((0, import_drizzle_orm2.desc)(creditJournalEntries.createdAt)).limit(query.limit ?? 50).offset(query.offset ?? 0);
|
|
566
|
+
return rows.map(toJournalEntry);
|
|
567
|
+
}
|
|
568
|
+
async getJournalEntriesCount(query) {
|
|
569
|
+
const filters = this.journalFilters(query);
|
|
570
|
+
const rows = await this.db.select({ value: (0, import_drizzle_orm2.count)() }).from(creditJournalEntries).where(filters.length ? (0, import_drizzle_orm2.and)(...filters) : void 0);
|
|
571
|
+
return Number(rows[0]?.value ?? 0);
|
|
572
|
+
}
|
|
573
|
+
usageFilters(query) {
|
|
574
|
+
const filters = [];
|
|
575
|
+
if (query.userId) filters.push((0, import_drizzle_orm2.eq)(creditUsageLogs.userId, query.userId));
|
|
576
|
+
if (query.operationType) filters.push((0, import_drizzle_orm2.eq)(creditUsageLogs.operationType, query.operationType));
|
|
577
|
+
if (query.success !== void 0) filters.push((0, import_drizzle_orm2.eq)(creditUsageLogs.success, query.success));
|
|
578
|
+
if (query.startDate) filters.push((0, import_drizzle_orm2.gte)(creditUsageLogs.createdAt, query.startDate));
|
|
579
|
+
if (query.endDate) filters.push((0, import_drizzle_orm2.lte)(creditUsageLogs.createdAt, query.endDate));
|
|
580
|
+
return filters;
|
|
581
|
+
}
|
|
582
|
+
journalFilters(query) {
|
|
583
|
+
const filters = [(0, import_drizzle_orm2.eq)(creditJournalEntries.userId, query.userId)];
|
|
584
|
+
if (query.source) filters.push((0, import_drizzle_orm2.eq)(creditJournalEntries.source, query.source));
|
|
585
|
+
if (query.referenceType) filters.push((0, import_drizzle_orm2.eq)(creditJournalEntries.referenceType, query.referenceType));
|
|
586
|
+
if (query.startDate) filters.push((0, import_drizzle_orm2.gte)(creditJournalEntries.createdAt, query.startDate));
|
|
587
|
+
if (query.endDate) filters.push((0, import_drizzle_orm2.lte)(creditJournalEntries.createdAt, query.endDate));
|
|
588
|
+
return filters;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
function createDrizzleCreditRepository(db) {
|
|
592
|
+
return new DrizzleCreditRepository(db);
|
|
593
|
+
}
|
|
594
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
595
|
+
0 && (module.exports = {
|
|
596
|
+
DrizzleCreditRepository,
|
|
597
|
+
createDrizzleCreditRepository
|
|
598
|
+
});
|
|
599
|
+
//# sourceMappingURL=index.cjs.map
|