@nehorai/credits 1.7.0 → 2.0.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/README.md +226 -2
- package/dist/adapters/index.cjs +298 -40
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +298 -40
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +696 -74
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +583 -5
- package/dist/core/index.d.ts +583 -5
- package/dist/core/index.js +648 -74
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +1762 -365
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -6
- package/dist/index.d.ts +41 -6
- package/dist/index.js +1706 -365
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +1399 -183
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +141 -15
- package/dist/repository/index.d.ts +141 -15
- package/dist/repository/index.js +1392 -183
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.cjs +23 -1
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.d.cts +2 -2
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +23 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/service/index.cjs +480 -104
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +68 -10
- package/dist/service/index.d.ts +68 -10
- package/dist/service/index.js +480 -104
- package/dist/service/index.js.map +1 -1
- package/dist/types-BmG5Omzj.d.ts +646 -0
- package/dist/types-C8hZMe8a.d.cts +646 -0
- package/dist/{types-CjLj3GW5.d.cts → types-DJ8yqaVr.d.cts} +39 -1
- package/dist/{types-CjLj3GW5.d.ts → types-DJ8yqaVr.d.ts} +39 -1
- package/package.json +1 -1
- package/dist/types-DFe5BoJY.d.cts +0 -329
- package/dist/types-_bSybQnb.d.ts +0 -329
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { P as PortableTransaction, a as PortableReservation, R as ReservationStatus, C as CreditOperationType, b as CreditSource, J as JournalReferenceType, A as AIProviderType, S as SubscriptionTier, c as PortableUserCredits, d as PortableUsageLog, M as MonthlyResetResult, e as SubscriptionExpiryResult, f as PortableJournalEntry } from './types-DJ8yqaVr.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed outcomes for the V2 reservation boundary.
|
|
5
|
+
*
|
|
6
|
+
* The legacy `*Atomic` repository methods signal "you lost the race" by
|
|
7
|
+
* throwing (or, worse, by silently returning). V2 makes every terminal
|
|
8
|
+
* transition an explicit, serialisable value so callers can tell a winner
|
|
9
|
+
* from a duplicate delivery without string-matching an error message.
|
|
10
|
+
*
|
|
11
|
+
* Every outcome is discriminated on `outcome` (never `status`, which is the
|
|
12
|
+
* reservation's own lifecycle field and would collide).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Statuses a reservation can no longer transition out of. */
|
|
16
|
+
type TerminalReservationStatus = Exclude<ReservationStatus, "reserved">;
|
|
17
|
+
/**
|
|
18
|
+
* Result of `reserveCreditsV2`.
|
|
19
|
+
*
|
|
20
|
+
* - `created` — this caller minted the hold.
|
|
21
|
+
* - `replayed` — same `(userId, idempotencyKey)` with an identical immutable
|
|
22
|
+
* payload; the original reservation is returned and no new hold was placed.
|
|
23
|
+
* - `insufficient` — the guarded hold could not be placed; nothing was written.
|
|
24
|
+
* - `idempotency_conflict` — the key was reused with a different
|
|
25
|
+
* amount/operation. Nothing was written; the existing reservation is
|
|
26
|
+
* returned so the caller can diff it.
|
|
27
|
+
*/
|
|
28
|
+
type ReserveOutcome = {
|
|
29
|
+
outcome: "created";
|
|
30
|
+
reservation: PortableReservation;
|
|
31
|
+
} | {
|
|
32
|
+
outcome: "replayed";
|
|
33
|
+
reservation: PortableReservation;
|
|
34
|
+
} | {
|
|
35
|
+
outcome: "insufficient";
|
|
36
|
+
available: number;
|
|
37
|
+
required: number;
|
|
38
|
+
shortfall: number;
|
|
39
|
+
} | {
|
|
40
|
+
outcome: "idempotency_conflict";
|
|
41
|
+
idempotencyKey: string;
|
|
42
|
+
existing: PortableReservation;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Result of `commitReservationV2`.
|
|
46
|
+
*
|
|
47
|
+
* Exactly one concurrent caller ever observes `committed`; that call — and
|
|
48
|
+
* only that call — moved the balance and wrote the journal entry.
|
|
49
|
+
*/
|
|
50
|
+
type CommitOutcome = {
|
|
51
|
+
outcome: "committed";
|
|
52
|
+
reservation: PortableReservation;
|
|
53
|
+
amount: number;
|
|
54
|
+
/** `balance + bonusCredits` after the deduction, read from the same UPDATE. */
|
|
55
|
+
balanceAfter: number;
|
|
56
|
+
journalEntryId: string;
|
|
57
|
+
} | {
|
|
58
|
+
outcome: "already_terminal";
|
|
59
|
+
reservation: PortableReservation;
|
|
60
|
+
terminalStatus: TerminalReservationStatus;
|
|
61
|
+
} | {
|
|
62
|
+
outcome: "not_found";
|
|
63
|
+
reservationId: string;
|
|
64
|
+
};
|
|
65
|
+
/** Result of `releaseReservationV2`. Mirrors {@link CommitOutcome}. */
|
|
66
|
+
type ReleaseOutcome = {
|
|
67
|
+
outcome: "released";
|
|
68
|
+
reservation: PortableReservation;
|
|
69
|
+
amount: number;
|
|
70
|
+
journalEntryId: string;
|
|
71
|
+
} | {
|
|
72
|
+
outcome: "already_terminal";
|
|
73
|
+
reservation: PortableReservation;
|
|
74
|
+
terminalStatus: TerminalReservationStatus;
|
|
75
|
+
} | {
|
|
76
|
+
outcome: "not_found";
|
|
77
|
+
reservationId: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Result of `expireReservationV2`.
|
|
81
|
+
*
|
|
82
|
+
* `not_due` means the reservation is still `reserved` but its `expiresAt` is
|
|
83
|
+
* in the future relative to the sweep's `asOf` — a no-op, not a failure.
|
|
84
|
+
*/
|
|
85
|
+
type ExpireOutcome = {
|
|
86
|
+
outcome: "expired";
|
|
87
|
+
reservation: PortableReservation;
|
|
88
|
+
amount: number;
|
|
89
|
+
journalEntryId: string;
|
|
90
|
+
} | {
|
|
91
|
+
outcome: "not_due";
|
|
92
|
+
reservation: PortableReservation;
|
|
93
|
+
} | {
|
|
94
|
+
outcome: "already_terminal";
|
|
95
|
+
reservation: PortableReservation;
|
|
96
|
+
terminalStatus: TerminalReservationStatus;
|
|
97
|
+
} | {
|
|
98
|
+
outcome: "not_found";
|
|
99
|
+
reservationId: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Result of crediting an account against a `paymentRef`.
|
|
103
|
+
*
|
|
104
|
+
* - `created` — this delivery credited the account.
|
|
105
|
+
* - `replayed` — the reference was already used for the *same* credit event;
|
|
106
|
+
* nothing was written and the original transaction is returned.
|
|
107
|
+
* - `conflict` — the reference was already used for a *different* event
|
|
108
|
+
* (another user, another amount, another source). Nothing was written;
|
|
109
|
+
* `mismatch` names the first field that disagreed.
|
|
110
|
+
*
|
|
111
|
+
* A call with no `paymentRef` is always `created`: without a reference there is
|
|
112
|
+
* nothing to deduplicate on, and every delivery is a distinct credit.
|
|
113
|
+
*
|
|
114
|
+
* **`created` is only a single-delivery guarantee on an adapter that implements
|
|
115
|
+
* `addCreditsV2`.** Routed through a legacy adapter, `created` means "the call
|
|
116
|
+
* was made and did not throw" — there is no global unique index behind it, so a
|
|
117
|
+
* re-delivered reference credits again. `supportsIdempotentCredit` is the probe
|
|
118
|
+
* that tells the two apart, and it is also why `transaction` is optional here:
|
|
119
|
+
* the legacy method returns nothing, and inventing a transaction to fill the
|
|
120
|
+
* field would be a fabricated audit record.
|
|
121
|
+
*/
|
|
122
|
+
type AddCreditsOutcome = {
|
|
123
|
+
outcome: "created";
|
|
124
|
+
/** The reference this credit was recorded under, if there was one. */
|
|
125
|
+
paymentRef?: string;
|
|
126
|
+
/** Absent only on the legacy fallback path; see above. */
|
|
127
|
+
transaction?: PortableTransaction;
|
|
128
|
+
/** Absent only on the legacy fallback path; see above. */
|
|
129
|
+
journalEntryId?: string;
|
|
130
|
+
} | {
|
|
131
|
+
outcome: "replayed";
|
|
132
|
+
paymentRef: string;
|
|
133
|
+
transaction: PortableTransaction;
|
|
134
|
+
} | {
|
|
135
|
+
outcome: "conflict";
|
|
136
|
+
paymentRef: string;
|
|
137
|
+
existing: PortableTransaction;
|
|
138
|
+
/** The first field of the canonical payload that disagreed. */
|
|
139
|
+
mismatch: string;
|
|
140
|
+
};
|
|
141
|
+
/** True when this delivery actually moved credits. */
|
|
142
|
+
declare function isCreditedOutcome(outcome: AddCreditsOutcome): outcome is Extract<AddCreditsOutcome, {
|
|
143
|
+
outcome: "created";
|
|
144
|
+
}>;
|
|
145
|
+
/** True when this caller performed the state transition. */
|
|
146
|
+
declare function isWinningOutcome(outcome: CommitOutcome | ReleaseOutcome | ExpireOutcome): boolean;
|
|
147
|
+
/** Narrow a reserve outcome to the two cases that yield a usable reservation. */
|
|
148
|
+
declare function isReservedOutcome(outcome: ReserveOutcome): outcome is Extract<ReserveOutcome, {
|
|
149
|
+
reservation: PortableReservation;
|
|
150
|
+
}>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Inputs for the V2 reservation boundary.
|
|
154
|
+
*
|
|
155
|
+
* V2 is additive: every method is optional on {@link ICreditRepository} so
|
|
156
|
+
* existing adapters keep compiling and keep working through the legacy
|
|
157
|
+
* `*Atomic` methods. Adapters that implement V2 must satisfy the contract
|
|
158
|
+
* documented on {@link ICreditRepositoryV2}.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Input for `reserveCreditsV2`.
|
|
163
|
+
*
|
|
164
|
+
* `userId`, `amount` and `operationType` form the immutable payload bound to
|
|
165
|
+
* `idempotencyKey`: replaying the same key with the same payload returns the
|
|
166
|
+
* original reservation, replaying it with a different one is a conflict.
|
|
167
|
+
* `expiresAt` is deliberately *not* part of the payload — a retry naturally
|
|
168
|
+
* computes a later deadline and that must not be treated as a conflict.
|
|
169
|
+
*/
|
|
170
|
+
interface ReserveCreditsV2Input {
|
|
171
|
+
userId: string;
|
|
172
|
+
amount: number;
|
|
173
|
+
operationType: CreditOperationType;
|
|
174
|
+
expiresAt: Date;
|
|
175
|
+
/**
|
|
176
|
+
* Caller-supplied key, unique per user. Omit it to get legacy
|
|
177
|
+
* (non-idempotent) reserve semantics — every call mints a new hold.
|
|
178
|
+
*/
|
|
179
|
+
idempotencyKey?: string;
|
|
180
|
+
}
|
|
181
|
+
/** Optional journal customisation for a V2 state transition. */
|
|
182
|
+
interface ReservationTransitionOptions {
|
|
183
|
+
/** Overrides the generated journal description. */
|
|
184
|
+
description?: string;
|
|
185
|
+
/** Merged onto the journal entry's metadata. */
|
|
186
|
+
metadata?: Record<string, unknown>;
|
|
187
|
+
}
|
|
188
|
+
/** Options for `expireReservationV2`. */
|
|
189
|
+
interface ExpireReservationV2Options extends ReservationTransitionOptions {
|
|
190
|
+
/**
|
|
191
|
+
* Sweep timestamp. A reservation only expires when `expiresAt <= asOf`;
|
|
192
|
+
* otherwise the outcome is `not_due`. Defaults to now.
|
|
193
|
+
*/
|
|
194
|
+
asOf?: Date;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The V2 surface. Every method must:
|
|
198
|
+
*
|
|
199
|
+
* 1. run in a single database transaction;
|
|
200
|
+
* 2. take row locks in a consistent order — reservation first, then balance —
|
|
201
|
+
* so concurrent transitions cannot deadlock;
|
|
202
|
+
* 3. transition status with a compare-and-set (`WHERE status = 'reserved'`)
|
|
203
|
+
* and report the loser truthfully instead of silently succeeding;
|
|
204
|
+
* 4. mutate balances with column expressions, never with values read earlier
|
|
205
|
+
* in the transaction;
|
|
206
|
+
* 5. write at most one journal entry, inside the same transaction, keyed so a
|
|
207
|
+
* retry cannot duplicate it;
|
|
208
|
+
* 6. perform no callbacks and no network I/O inside the transaction.
|
|
209
|
+
*/
|
|
210
|
+
interface ICreditRepositoryV2 {
|
|
211
|
+
reserveCreditsV2(input: ReserveCreditsV2Input): Promise<ReserveOutcome>;
|
|
212
|
+
commitReservationV2(userId: string, reservationId: string, options?: ReservationTransitionOptions): Promise<CommitOutcome>;
|
|
213
|
+
releaseReservationV2(userId: string, reservationId: string, options?: ReservationTransitionOptions): Promise<ReleaseOutcome>;
|
|
214
|
+
expireReservationV2(userId: string, reservationId: string, options?: ExpireReservationV2Options): Promise<ExpireOutcome>;
|
|
215
|
+
}
|
|
216
|
+
/** Input for `addCreditsV2`. */
|
|
217
|
+
interface AddCreditsV2Input {
|
|
218
|
+
userId: string;
|
|
219
|
+
amount: number;
|
|
220
|
+
description: string;
|
|
221
|
+
/**
|
|
222
|
+
* Global idempotency key for the credit event. Empty and whitespace-only
|
|
223
|
+
* strings normalise to "no reference" — see `normalizePaymentRef`.
|
|
224
|
+
*/
|
|
225
|
+
paymentRef?: string;
|
|
226
|
+
options?: AddCreditsAtomicOptions;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The idempotent credit surface, separate from {@link ICreditRepositoryV2}.
|
|
230
|
+
*
|
|
231
|
+
* It is its own interface with its own probe because the two capabilities are
|
|
232
|
+
* genuinely independent: an adapter can serialise reservations without having a
|
|
233
|
+
* unique index over `payment_ref`, and folding this into the reservation probe
|
|
234
|
+
* would make `supportsCreditsV2` narrow third-party adapters to a method they
|
|
235
|
+
* never wrote.
|
|
236
|
+
*
|
|
237
|
+
* An implementation must:
|
|
238
|
+
*
|
|
239
|
+
* 1. run in a single transaction;
|
|
240
|
+
* 2. resolve a non-empty reference to exactly one of `created` / `replayed` /
|
|
241
|
+
* `conflict`, comparing the canonical payload — not merely the reference's
|
|
242
|
+
* presence — before reporting a replay;
|
|
243
|
+
* 3. write nothing at all on `replayed` or `conflict`;
|
|
244
|
+
* 4. enforce the reference *globally*, so the same reference cannot credit two
|
|
245
|
+
* different users;
|
|
246
|
+
* 5. fail closed if the uniqueness it relies on is not actually in place.
|
|
247
|
+
*/
|
|
248
|
+
interface ICreditRepositoryCreditsV2 {
|
|
249
|
+
addCreditsV2(input: AddCreditsV2Input): Promise<AddCreditsOutcome>;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Deterministic journal key for a reservation transition.
|
|
253
|
+
*
|
|
254
|
+
* Derived purely from the reservation id and the transition, so a retried
|
|
255
|
+
* commit computes the same key and collides with the first entry on the
|
|
256
|
+
* journal's partial unique index instead of writing a second row.
|
|
257
|
+
*/
|
|
258
|
+
declare function reservationJournalKey(reservationId: string, transition: "commit" | "release" | "expire"): string;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Input for creating a credit reservation
|
|
262
|
+
*/
|
|
263
|
+
interface CreateReservationInput {
|
|
264
|
+
userId: string;
|
|
265
|
+
amount: number;
|
|
266
|
+
operationType: CreditOperationType;
|
|
267
|
+
expiresAt: Date;
|
|
268
|
+
/**
|
|
269
|
+
* @deprecated Rejected — this writer does not place the hold.
|
|
270
|
+
*
|
|
271
|
+
* `createReservation` inserts a row and leaves `reserved` alone, so a keyed
|
|
272
|
+
* row written here names a hold that does not exist, and `reserveCreditsV2`
|
|
273
|
+
* would adopt it as a `replayed` reservation whose commit then spends another
|
|
274
|
+
* hold's coverage. Both adapters throw `UNSUPPORTED_OPERATION` when this is
|
|
275
|
+
* set. Pass the key to `reserveCredits` / `reserveCreditsV2` instead, which
|
|
276
|
+
* claims the key and places the hold in one transaction.
|
|
277
|
+
*
|
|
278
|
+
* The field is kept rather than deleted so the refusal reaches a JavaScript
|
|
279
|
+
* caller as a typed error instead of a silently ignored property.
|
|
280
|
+
*/
|
|
281
|
+
idempotencyKey?: string;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Input for creating a credit transaction
|
|
285
|
+
*/
|
|
286
|
+
interface CreateTransactionInput {
|
|
287
|
+
userId: string;
|
|
288
|
+
type: PortableTransaction["type"];
|
|
289
|
+
amount: number;
|
|
290
|
+
description: string;
|
|
291
|
+
paymentRef?: string;
|
|
292
|
+
previousBalance: number;
|
|
293
|
+
newBalance: number;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Input for logging usage
|
|
297
|
+
*/
|
|
298
|
+
interface CreateUsageLogInput {
|
|
299
|
+
userId: string;
|
|
300
|
+
operationType: CreditOperationType;
|
|
301
|
+
provider: AIProviderType;
|
|
302
|
+
creditsUsed: number;
|
|
303
|
+
success: boolean;
|
|
304
|
+
errorMessage?: string;
|
|
305
|
+
resourceId?: string;
|
|
306
|
+
resourceType?: string;
|
|
307
|
+
requestId?: string;
|
|
308
|
+
metadata?: Record<string, unknown>;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Query options for usage logs
|
|
312
|
+
*/
|
|
313
|
+
interface UsageLogQuery {
|
|
314
|
+
userId?: string;
|
|
315
|
+
operationType?: CreditOperationType;
|
|
316
|
+
success?: boolean;
|
|
317
|
+
limit?: number;
|
|
318
|
+
offset?: number;
|
|
319
|
+
startDate?: Date;
|
|
320
|
+
endDate?: Date;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Input for creating a journal entry
|
|
324
|
+
*/
|
|
325
|
+
interface CreateJournalEntryInput {
|
|
326
|
+
userId: string;
|
|
327
|
+
entryType: "debit" | "credit";
|
|
328
|
+
amount: number;
|
|
329
|
+
balanceAfter: number;
|
|
330
|
+
source: CreditSource;
|
|
331
|
+
referenceId: string;
|
|
332
|
+
referenceType: JournalReferenceType;
|
|
333
|
+
description: string;
|
|
334
|
+
metadata?: Record<string, unknown>;
|
|
335
|
+
/** Deterministic key making this entry unique per user (V2, optional). */
|
|
336
|
+
idempotencyKey?: string;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Query options for journal entries
|
|
340
|
+
*/
|
|
341
|
+
interface JournalEntryQuery {
|
|
342
|
+
userId: string;
|
|
343
|
+
source?: CreditSource;
|
|
344
|
+
referenceType?: JournalReferenceType;
|
|
345
|
+
limit?: number;
|
|
346
|
+
offset?: number;
|
|
347
|
+
startDate?: Date;
|
|
348
|
+
endDate?: Date;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Partial update for user credits balance
|
|
352
|
+
*/
|
|
353
|
+
interface CreditBalanceUpdate {
|
|
354
|
+
balance?: number;
|
|
355
|
+
bonusCredits?: number;
|
|
356
|
+
reserved?: number;
|
|
357
|
+
tier?: SubscriptionTier;
|
|
358
|
+
monthlyLimit?: number;
|
|
359
|
+
monthlyUsed?: number;
|
|
360
|
+
monthlyResetAt?: Date | string;
|
|
361
|
+
subscriptionExpiresAt?: Date | string | null;
|
|
362
|
+
/** Increment balance by this amount (alternative to absolute value) */
|
|
363
|
+
balanceIncrement?: number;
|
|
364
|
+
/** Increment bonusCredits by this amount (alternative to absolute value) */
|
|
365
|
+
bonusCreditsIncrement?: number;
|
|
366
|
+
/** Increment reserved by this amount (alternative to absolute value) */
|
|
367
|
+
reservedIncrement?: number;
|
|
368
|
+
/** Increment monthlyUsed by this amount */
|
|
369
|
+
monthlyUsedIncrement?: number;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Input for tier update
|
|
373
|
+
*/
|
|
374
|
+
interface TierUpdateInput {
|
|
375
|
+
tier: SubscriptionTier;
|
|
376
|
+
monthlyLimit: number;
|
|
377
|
+
balance?: number;
|
|
378
|
+
monthlyUsed?: number;
|
|
379
|
+
subscriptionExpiresAt?: Date | string | null;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Optional journal customization for `addCreditsAtomic`.
|
|
383
|
+
*
|
|
384
|
+
* By default a credit add records a journal entry with `source: "purchase"`,
|
|
385
|
+
* `referenceType: "transaction"`, and `{ paymentRef }` metadata. These options
|
|
386
|
+
* let callers override that — e.g. tag a signup grant as `source: "bonus"`, or
|
|
387
|
+
* carry revenue-attribution fields (gross amount, credits granted) so a single
|
|
388
|
+
* journal can serve as the app's revenue ledger.
|
|
389
|
+
*/
|
|
390
|
+
interface AddCreditsAtomicOptions {
|
|
391
|
+
/** Journal entry source (defaults to `"purchase"`). */
|
|
392
|
+
source?: CreditSource;
|
|
393
|
+
/** Journal reference type (defaults to `"transaction"`). */
|
|
394
|
+
referenceType?: JournalReferenceType;
|
|
395
|
+
/**
|
|
396
|
+
* Extra metadata merged onto the journal entry. When `paymentRef` is present
|
|
397
|
+
* it is merged in too (explicit keys here win on collision).
|
|
398
|
+
*/
|
|
399
|
+
metadata?: Record<string, unknown>;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Repository interface for credits database operations
|
|
403
|
+
*
|
|
404
|
+
* Implementations can use any database (Firestore, PostgreSQL, etc.)
|
|
405
|
+
* All methods should handle their own error handling and transactions
|
|
406
|
+
*/
|
|
407
|
+
interface ICreditRepository extends Partial<ICreditRepositoryV2>, Partial<ICreditRepositoryCreditsV2> {
|
|
408
|
+
/**
|
|
409
|
+
* Get user credits balance
|
|
410
|
+
* @param userId - User ID
|
|
411
|
+
* @returns User credits or null if not found
|
|
412
|
+
*/
|
|
413
|
+
getUserCredits(userId: string): Promise<PortableUserCredits | null>;
|
|
414
|
+
/**
|
|
415
|
+
* Initialize credits for a new user
|
|
416
|
+
* @param userId - User ID
|
|
417
|
+
* @param tier - Initial subscription tier
|
|
418
|
+
* @param initialBalance - Initial credit balance
|
|
419
|
+
* @returns Initialized user credits
|
|
420
|
+
*/
|
|
421
|
+
initializeUserCredits(userId: string, tier: SubscriptionTier, initialBalance: number): Promise<PortableUserCredits>;
|
|
422
|
+
/**
|
|
423
|
+
* Update user credits balance
|
|
424
|
+
* @param userId - User ID
|
|
425
|
+
* @param updates - Partial updates to apply
|
|
426
|
+
*/
|
|
427
|
+
updateUserCredits(userId: string, updates: CreditBalanceUpdate): Promise<void>;
|
|
428
|
+
/**
|
|
429
|
+
* Update user subscription tier
|
|
430
|
+
* @param userId - User ID
|
|
431
|
+
* @param input - Tier update data
|
|
432
|
+
*/
|
|
433
|
+
updateUserTier(userId: string, input: TierUpdateInput): Promise<void>;
|
|
434
|
+
/**
|
|
435
|
+
* Create a reservation *record* — not a hold.
|
|
436
|
+
*
|
|
437
|
+
* Writes a row and leaves `reserved` untouched, so the row is not backed by
|
|
438
|
+
* credits and no settlement path will move money for it: the commit /
|
|
439
|
+
* release / expire transitions (legacy and V2 alike) refuse it with
|
|
440
|
+
* `UNBACKED_RESERVATION`, and it refuses an `idempotencyKey` so it can never
|
|
441
|
+
* be adopted as a V2 replay. Use it for imported or annotation-only records.
|
|
442
|
+
*
|
|
443
|
+
* To place a hold that can actually be committed, call `reserveCreditsAtomic`
|
|
444
|
+
* or `reserveCreditsV2` — they raise `reserved` and write the row in one
|
|
445
|
+
* atomic step.
|
|
446
|
+
*
|
|
447
|
+
* @param input - Reservation data
|
|
448
|
+
* @returns Created reservation record
|
|
449
|
+
*/
|
|
450
|
+
createReservation(input: CreateReservationInput): Promise<PortableReservation>;
|
|
451
|
+
/**
|
|
452
|
+
* Get a reservation by ID
|
|
453
|
+
* @param userId - User ID
|
|
454
|
+
* @param reservationId - Reservation ID
|
|
455
|
+
* @returns Reservation or null
|
|
456
|
+
*/
|
|
457
|
+
getReservation(userId: string, reservationId: string): Promise<PortableReservation | null>;
|
|
458
|
+
/**
|
|
459
|
+
* Annotate a reservation *record* with a status.
|
|
460
|
+
*
|
|
461
|
+
* Assigns a status and nothing else — no balance movement, no `reserved`
|
|
462
|
+
* adjustment, no journal entry. It therefore refuses two writes that would
|
|
463
|
+
* make the status lie about the ledger: any write to a row whose hold was
|
|
464
|
+
* atomically placed (those statuses belong to commit/release/expire), and
|
|
465
|
+
* any write back to `reserved` (nothing here re-places a hold). Rows written
|
|
466
|
+
* by `createReservation` accept terminal statuses freely.
|
|
467
|
+
*
|
|
468
|
+
* @param userId - User ID
|
|
469
|
+
* @param reservationId - Reservation ID
|
|
470
|
+
* @param status - New status
|
|
471
|
+
* @param completedAt - Completion timestamp
|
|
472
|
+
*/
|
|
473
|
+
updateReservationStatus(userId: string, reservationId: string, status: ReservationStatus, completedAt?: Date): Promise<void>;
|
|
474
|
+
/**
|
|
475
|
+
* Reserve credits atomically (creates reservation + updates balance in transaction)
|
|
476
|
+
* @param userId - User ID
|
|
477
|
+
* @param amount - Credits to reserve
|
|
478
|
+
* @param operationType - Operation type for tracking
|
|
479
|
+
* @param expiresAt - Reservation expiry time
|
|
480
|
+
* @returns Created reservation
|
|
481
|
+
* @throws Error if insufficient credits
|
|
482
|
+
*/
|
|
483
|
+
reserveCreditsAtomic(userId: string, amount: number, operationType: CreditOperationType, expiresAt: Date): Promise<PortableReservation>;
|
|
484
|
+
/**
|
|
485
|
+
* Commit a reservation atomically (deducts credits + marks reservation committed)
|
|
486
|
+
* @param userId - User ID
|
|
487
|
+
* @param reservationId - Reservation ID
|
|
488
|
+
* @throws Error if reservation not found or not in reserved state
|
|
489
|
+
*/
|
|
490
|
+
commitReservationAtomic(userId: string, reservationId: string): Promise<void>;
|
|
491
|
+
/**
|
|
492
|
+
* Release a reservation atomically (releases reserved credits + marks reservation released)
|
|
493
|
+
* @param userId - User ID
|
|
494
|
+
* @param reservationId - Reservation ID
|
|
495
|
+
*/
|
|
496
|
+
releaseReservationAtomic(userId: string, reservationId: string): Promise<void>;
|
|
497
|
+
/**
|
|
498
|
+
* Add credits atomically (creates transaction + updates balance)
|
|
499
|
+
* @param userId - User ID
|
|
500
|
+
* @param amount - Credits to add
|
|
501
|
+
* @param description - Transaction description
|
|
502
|
+
* @param paymentRef - Optional payment reference
|
|
503
|
+
* @param options - Optional journal source / reference type / extra metadata
|
|
504
|
+
* for the credit journal entry (e.g. revenue attribution fields). Lets
|
|
505
|
+
* callers record a purchase's gross amount, a bonus/refund source, etc.,
|
|
506
|
+
* instead of the default `source: "purchase"` + `{ paymentRef }` metadata.
|
|
507
|
+
*/
|
|
508
|
+
addCreditsAtomic(userId: string, amount: number, description: string, paymentRef?: string, options?: AddCreditsAtomicOptions): Promise<void>;
|
|
509
|
+
/**
|
|
510
|
+
* Deduct credits from a user's balance atomically.
|
|
511
|
+
*
|
|
512
|
+
* Splits the deduction across `balance` and `bonusCredits`, draining
|
|
513
|
+
* `balance` (monthly, resets each cycle) first so persistent bonus
|
|
514
|
+
* credits survive longer. Runs in a single atomic transaction so
|
|
515
|
+
* concurrent deducts cannot drive either field negative.
|
|
516
|
+
*
|
|
517
|
+
* Callers are responsible for writing any audit record (journal /
|
|
518
|
+
* transaction) — this method only moves credits.
|
|
519
|
+
*
|
|
520
|
+
* @param userId - User ID
|
|
521
|
+
* @param amount - Credits to deduct (positive)
|
|
522
|
+
* @returns Combined totals (balance + bonusCredits) before and after.
|
|
523
|
+
* @throws If user has no credits doc or insufficient credits
|
|
524
|
+
*/
|
|
525
|
+
deductCreditsAtomic(userId: string, amount: number): Promise<{
|
|
526
|
+
previousBalance: number;
|
|
527
|
+
newBalance: number;
|
|
528
|
+
}>;
|
|
529
|
+
/**
|
|
530
|
+
* Create a credit transaction *record* — no balance moves.
|
|
531
|
+
*
|
|
532
|
+
* Refuses a `paymentRef`: the reference is the global identity of a credit
|
|
533
|
+
* event, and a record that carried one without crediting anyone would make a
|
|
534
|
+
* later `addCredits` with the same reference report `replayed` and credit
|
|
535
|
+
* nothing. Deliver referenced payments through `addCreditsAtomic` /
|
|
536
|
+
* `addCreditsV2`, which claim the reference and move the balance atomically.
|
|
537
|
+
*
|
|
538
|
+
* @param input - Transaction data
|
|
539
|
+
* @returns Created transaction
|
|
540
|
+
*/
|
|
541
|
+
createTransaction(input: CreateTransactionInput): Promise<PortableTransaction>;
|
|
542
|
+
/**
|
|
543
|
+
* Get user's transaction history
|
|
544
|
+
* @param userId - User ID
|
|
545
|
+
* @param limit - Max results
|
|
546
|
+
* @param offset - Skip results
|
|
547
|
+
* @returns List of transactions
|
|
548
|
+
*/
|
|
549
|
+
getTransactions(userId: string, limit?: number, offset?: number): Promise<PortableTransaction[]>;
|
|
550
|
+
/**
|
|
551
|
+
* Log a usage event
|
|
552
|
+
* @param input - Usage log data
|
|
553
|
+
* @returns Created usage log
|
|
554
|
+
*/
|
|
555
|
+
logUsage(input: CreateUsageLogInput): Promise<PortableUsageLog>;
|
|
556
|
+
/**
|
|
557
|
+
* Query usage logs
|
|
558
|
+
* @param query - Query parameters
|
|
559
|
+
* @returns List of usage logs
|
|
560
|
+
*/
|
|
561
|
+
getUsageLogs(query: UsageLogQuery): Promise<PortableUsageLog[]>;
|
|
562
|
+
/**
|
|
563
|
+
* Get usage log count (for pagination)
|
|
564
|
+
* @param query - Query parameters (without limit/offset)
|
|
565
|
+
* @returns Count of matching logs
|
|
566
|
+
*/
|
|
567
|
+
getUsageLogsCount(query: Omit<UsageLogQuery, "limit" | "offset">): Promise<number>;
|
|
568
|
+
/**
|
|
569
|
+
* Find and expire reservations past their expiration time
|
|
570
|
+
* Used by cron job to clean up stale reservations
|
|
571
|
+
* @param batchSize - Maximum number of reservations to process per batch (default: 100)
|
|
572
|
+
* @param maxIterations - Maximum number of pagination iterations to prevent infinite loops (default: 100)
|
|
573
|
+
* @returns Cleanup results with counts and errors
|
|
574
|
+
*/
|
|
575
|
+
findAndExpireReservations(batchSize?: number, maxIterations?: number): Promise<{
|
|
576
|
+
expiredCount: number;
|
|
577
|
+
creditsReleased: number;
|
|
578
|
+
errors: string[];
|
|
579
|
+
}>;
|
|
580
|
+
/**
|
|
581
|
+
* Atomically perform monthly reset if needed
|
|
582
|
+
* Uses optimistic locking to prevent race conditions
|
|
583
|
+
*
|
|
584
|
+
* @param userId - User ID
|
|
585
|
+
* @param tier - User's current subscription tier (for determining new balance)
|
|
586
|
+
* @param expectedResetAt - The expected monthlyResetAt value (for optimistic locking)
|
|
587
|
+
* @returns Result indicating whether reset was performed and updated credits
|
|
588
|
+
*/
|
|
589
|
+
atomicMonthlyReset(userId: string, tier: SubscriptionTier, expectedResetAt: Date | string): Promise<MonthlyResetResult>;
|
|
590
|
+
/**
|
|
591
|
+
* Check and handle subscription expiry with grace period
|
|
592
|
+
* Auto-downgrades expired subscriptions after grace period
|
|
593
|
+
*
|
|
594
|
+
* @param userId - User ID
|
|
595
|
+
* @param gracePeriodDays - Days to allow after expiry before downgrade (default: 3)
|
|
596
|
+
* @returns Result indicating whether downgrade occurred
|
|
597
|
+
*/
|
|
598
|
+
checkAndHandleSubscriptionExpiry(userId: string, gracePeriodDays?: number): Promise<SubscriptionExpiryResult>;
|
|
599
|
+
/**
|
|
600
|
+
* Create a journal entry for audit trail
|
|
601
|
+
* @param input - Journal entry data
|
|
602
|
+
* @returns Created journal entry
|
|
603
|
+
*/
|
|
604
|
+
createJournalEntry(input: CreateJournalEntryInput): Promise<PortableJournalEntry>;
|
|
605
|
+
/**
|
|
606
|
+
* Get journal entries for a user
|
|
607
|
+
* @param query - Query parameters
|
|
608
|
+
* @returns List of journal entries
|
|
609
|
+
*/
|
|
610
|
+
getJournalEntries(query: JournalEntryQuery): Promise<PortableJournalEntry[]>;
|
|
611
|
+
/**
|
|
612
|
+
* Get journal entry count for pagination
|
|
613
|
+
* @param query - Query parameters (without limit/offset)
|
|
614
|
+
* @returns Count of matching entries
|
|
615
|
+
*/
|
|
616
|
+
getJournalEntriesCount(query: Omit<JournalEntryQuery, "limit" | "offset">): Promise<number>;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Factory type for creating repository instances
|
|
620
|
+
*/
|
|
621
|
+
type CreditRepositoryFactory = () => ICreditRepository;
|
|
622
|
+
/**
|
|
623
|
+
* Narrow a repository to one that implements the full V2 boundary.
|
|
624
|
+
*
|
|
625
|
+
* V2 methods are optional on {@link ICreditRepository}, so callers that need
|
|
626
|
+
* the idempotent/race-safe path must probe rather than assume. A repository
|
|
627
|
+
* only counts as V2 when it implements *all four* transitions — a partial
|
|
628
|
+
* implementation would leave one path silently unsafe.
|
|
629
|
+
*/
|
|
630
|
+
declare function supportsCreditsV2(repository: ICreditRepository): repository is ICreditRepository & ICreditRepositoryV2;
|
|
631
|
+
/**
|
|
632
|
+
* Narrow a repository to one that credits idempotently against a `paymentRef`.
|
|
633
|
+
*
|
|
634
|
+
* Deliberately a separate probe from {@link supportsCreditsV2}: serialising
|
|
635
|
+
* reservations and enforcing a global payment reference are different
|
|
636
|
+
* guarantees resting on different database objects, and an adapter may have one
|
|
637
|
+
* without the other.
|
|
638
|
+
*/
|
|
639
|
+
declare function supportsIdempotentCredit(repository: ICreditRepository): repository is ICreditRepository & ICreditRepositoryCreditsV2;
|
|
640
|
+
/**
|
|
641
|
+
* Convert PortableUserCredits to client-safe format
|
|
642
|
+
* Utility function that implementations can use
|
|
643
|
+
*/
|
|
644
|
+
declare function toClientUserCredits(credits: PortableUserCredits): PortableUserCredits;
|
|
645
|
+
|
|
646
|
+
export { type AddCreditsAtomicOptions as A, type CommitOutcome as C, type ExpireOutcome as E, type ICreditRepository as I, type JournalEntryQuery as J, type ReleaseOutcome as R, type TerminalReservationStatus as T, type UsageLogQuery as U, type AddCreditsOutcome as a, type AddCreditsV2Input as b, type CreateJournalEntryInput as c, type CreateReservationInput as d, type CreateTransactionInput as e, type CreateUsageLogInput as f, type CreditBalanceUpdate as g, type CreditRepositoryFactory as h, type ExpireReservationV2Options as i, type ICreditRepositoryCreditsV2 as j, type ICreditRepositoryV2 as k, type ReservationTransitionOptions as l, type ReserveCreditsV2Input as m, type ReserveOutcome as n, type TierUpdateInput as o, isCreditedOutcome as p, isReservedOutcome as q, isWinningOutcome as r, reservationJournalKey as s, supportsCreditsV2 as t, supportsIdempotentCredit as u, toClientUserCredits as v };
|