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