@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.
Files changed (44) hide show
  1. package/README.md +226 -2
  2. package/dist/adapters/index.cjs +298 -40
  3. package/dist/adapters/index.cjs.map +1 -1
  4. package/dist/adapters/index.d.cts +2 -2
  5. package/dist/adapters/index.d.ts +2 -2
  6. package/dist/adapters/index.js +298 -40
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/core/index.cjs +696 -74
  9. package/dist/core/index.cjs.map +1 -1
  10. package/dist/core/index.d.cts +583 -5
  11. package/dist/core/index.d.ts +583 -5
  12. package/dist/core/index.js +648 -74
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/index.cjs +1762 -365
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +41 -6
  17. package/dist/index.d.ts +41 -6
  18. package/dist/index.js +1706 -365
  19. package/dist/index.js.map +1 -1
  20. package/dist/repository/index.cjs +1399 -183
  21. package/dist/repository/index.cjs.map +1 -1
  22. package/dist/repository/index.d.cts +141 -15
  23. package/dist/repository/index.d.ts +141 -15
  24. package/dist/repository/index.js +1392 -183
  25. package/dist/repository/index.js.map +1 -1
  26. package/dist/sdk/index.cjs +23 -1
  27. package/dist/sdk/index.cjs.map +1 -1
  28. package/dist/sdk/index.d.cts +2 -2
  29. package/dist/sdk/index.d.ts +2 -2
  30. package/dist/sdk/index.js +23 -1
  31. package/dist/sdk/index.js.map +1 -1
  32. package/dist/service/index.cjs +480 -104
  33. package/dist/service/index.cjs.map +1 -1
  34. package/dist/service/index.d.cts +68 -10
  35. package/dist/service/index.d.ts +68 -10
  36. package/dist/service/index.js +480 -104
  37. package/dist/service/index.js.map +1 -1
  38. package/dist/types-BmG5Omzj.d.ts +646 -0
  39. package/dist/types-C8hZMe8a.d.cts +646 -0
  40. package/dist/{types-CjLj3GW5.d.cts → types-DJ8yqaVr.d.cts} +39 -1
  41. package/dist/{types-CjLj3GW5.d.ts → types-DJ8yqaVr.d.ts} +39 -1
  42. package/package.json +1 -1
  43. package/dist/types-DFe5BoJY.d.cts +0 -329
  44. 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 };