@nehorai/credits 2.2.0 → 3.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 (41) hide show
  1. package/dist/adapters/index.cjs +1 -0
  2. package/dist/adapters/index.cjs.map +1 -1
  3. package/dist/adapters/index.d.cts +2 -2
  4. package/dist/adapters/index.d.ts +2 -2
  5. package/dist/adapters/index.js +1 -0
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/core/index.cjs +139 -23
  8. package/dist/core/index.cjs.map +1 -1
  9. package/dist/core/index.d.cts +113 -22
  10. package/dist/core/index.d.ts +113 -22
  11. package/dist/core/index.js +128 -21
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/index.cjs +246 -61
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +4 -4
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +235 -59
  18. package/dist/index.js.map +1 -1
  19. package/dist/repository/index.cjs +183 -41
  20. package/dist/repository/index.cjs.map +1 -1
  21. package/dist/repository/index.d.cts +6 -5
  22. package/dist/repository/index.d.ts +6 -5
  23. package/dist/repository/index.js +183 -41
  24. package/dist/repository/index.js.map +1 -1
  25. package/dist/sdk/index.cjs +1 -0
  26. package/dist/sdk/index.cjs.map +1 -1
  27. package/dist/sdk/index.d.cts +2 -2
  28. package/dist/sdk/index.d.ts +2 -2
  29. package/dist/sdk/index.js +1 -0
  30. package/dist/sdk/index.js.map +1 -1
  31. package/dist/service/index.cjs +25 -16
  32. package/dist/service/index.cjs.map +1 -1
  33. package/dist/service/index.d.cts +2 -2
  34. package/dist/service/index.d.ts +2 -2
  35. package/dist/service/index.js +25 -16
  36. package/dist/service/index.js.map +1 -1
  37. package/dist/{types-MaViK5tD.d.cts → types-C3sxDvNb.d.cts} +29 -6
  38. package/dist/{types-MaViK5tD.d.ts → types-C3sxDvNb.d.ts} +29 -6
  39. package/dist/{types-3c-IAwZI.d.ts → types-CY-oHIMA.d.ts} +19 -3
  40. package/dist/{types-XSjNXO5l.d.cts → types-DOjguhEX.d.cts} +19 -3
  41. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-MaViK5tD.cjs';
2
- export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-MaViK5tD.cjs';
3
- export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-XSjNXO5l.cjs';
4
- export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput, attributeExpiredBacking, backedBalanceFloor, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredHoldBackingPatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.cjs';
1
+ import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-C3sxDvNb.cjs';
2
+ export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, E as ExpiredBackingFrontier, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-C3sxDvNb.cjs';
3
+ export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, D as DeductJournalInput, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-DOjguhEX.cjs';
4
+ export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, ExpiredBackingAllocation, ExpiredBackingFields, ExpiredBackingHold, ExpiredBackingRewrite, NO_EXPIRED_BACKING_PREFIX, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, allocateExpiredBacking, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput, backedBalanceFloor, canonicalAmount, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredBackingPatch, getSqlState, hasPlacedHold, heldExpiredBacking, holdExpiredBacking, holdPlacedAtAfter, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, multiplyAmount, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, recordedExpiredBacking, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, storedAmountCeil, storedAmountFloor, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.cjs';
5
5
  export { D as DeferredExecutor, c as createDeferredExecutor, g as genericDeferred, n as noopDeferred, s as synchronousDeferred } from './deferred-C7rrRfkm.cjs';
6
6
  import { z } from 'zod';
7
7
  export { BalanceValidationResult, InMemoryCreditRepository, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, validateBalanceUpdate } from './repository/index.cjs';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-MaViK5tD.js';
2
- export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-MaViK5tD.js';
3
- export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-3c-IAwZI.js';
4
- export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput, attributeExpiredBacking, backedBalanceFloor, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredHoldBackingPatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.js';
1
+ import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-C3sxDvNb.js';
2
+ export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, E as ExpiredBackingFrontier, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-C3sxDvNb.js';
3
+ export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, D as DeductJournalInput, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-CY-oHIMA.js';
4
+ export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, ExpiredBackingAllocation, ExpiredBackingFields, ExpiredBackingHold, ExpiredBackingRewrite, NO_EXPIRED_BACKING_PREFIX, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, allocateExpiredBacking, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput, backedBalanceFloor, canonicalAmount, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredBackingPatch, getSqlState, hasPlacedHold, heldExpiredBacking, holdExpiredBacking, holdPlacedAtAfter, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, multiplyAmount, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, recordedExpiredBacking, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, storedAmountCeil, storedAmountFloor, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.js';
5
5
  export { D as DeferredExecutor, c as createDeferredExecutor, g as genericDeferred, n as noopDeferred, s as synchronousDeferred } from './deferred-C7rrRfkm.js';
6
6
  import { z } from 'zod';
7
7
  export { BalanceValidationResult, InMemoryCreditRepository, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, validateBalanceUpdate } from './repository/index.js';
package/dist/index.js CHANGED
@@ -167,6 +167,40 @@ function floatSum(values) {
167
167
  for (const value of values) total += value;
168
168
  return total;
169
169
  }
170
+ function multiplyAmount(unit, quantity) {
171
+ const cents = toCents(unit);
172
+ const product = cents === null ? Number.NaN : cents * quantity;
173
+ if (cents === null || cents < 0 || !Number.isSafeInteger(quantity) || quantity < 0 || !Number.isSafeInteger(product) || product > MAX_CENTS) {
174
+ throw new CreditError(
175
+ `Cannot price ${quantity} units at ${unit}: the price must be a non-negative amount exact to ${CREDIT_AMOUNT_SCALE} decimals, the quantity a whole number, and the result at most ${CREDIT_AMOUNT_MAX}`,
176
+ CreditErrorCode.INVALID_AMOUNT,
177
+ { unit, quantity, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE }
178
+ );
179
+ }
180
+ return product / 100;
181
+ }
182
+ var FLOAT_NOISE_FLOOR = 1e-6;
183
+ var FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;
184
+ function floatNoise(value) {
185
+ return Math.max(FLOAT_NOISE_FLOOR, Math.abs(value) * FLOAT_NOISE_RELATIVE);
186
+ }
187
+ function canonicalAmount(value) {
188
+ if (typeof value !== "number" || !Number.isFinite(value)) return value;
189
+ const cents = Math.round(value * 100);
190
+ if (!Number.isSafeInteger(cents)) return value;
191
+ const onGrid = cents / 100 + 0;
192
+ return Math.abs(value - onGrid) < floatNoise(value) ? onGrid : value;
193
+ }
194
+ function storedAmountFloor(value) {
195
+ const canonical = canonicalAmount(value);
196
+ if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
197
+ return Math.floor(canonical * 100) / 100;
198
+ }
199
+ function storedAmountCeil(value) {
200
+ const canonical = canonicalAmount(value);
201
+ if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
202
+ return Math.ceil(canonical * 100) / 100;
203
+ }
170
204
  function isValidCreditAmount(value) {
171
205
  if (typeof value !== "number") return false;
172
206
  const cents = toCents(value);
@@ -222,14 +256,19 @@ function backedBalanceFloor(reserved, bonusCredits) {
222
256
  function cappedBalance(balance, cap, reserved, bonusCredits) {
223
257
  return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
224
258
  }
225
- function spendableBalance(balance, expiredHoldBacking = 0) {
226
- return sumAmounts(balance, -Math.min(expiredHoldBacking, balance));
259
+ function heldExpiredBacking(balance, stored) {
260
+ if (!(stored !== void 0 && stored > 0)) return 0;
261
+ return Math.max(0, Math.min(stored, balance));
262
+ }
263
+ function spendableBalance(balance, expiredHoldBacking) {
264
+ return sumAmounts(balance, -heldExpiredBacking(balance, expiredHoldBacking));
227
265
  }
228
266
  function planBalanceTarget(target, reserved, bonusCredits) {
267
+ assertValidBalanceTarget(target, "balance target", { reserved, bonusCredits });
229
268
  const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
230
269
  return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
231
270
  }
232
- function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking = 0) {
271
+ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking) {
233
272
  const spendable = spendableBalance(balance, expiredHoldBacking);
234
273
  const target = Math.min(spendable, cap);
235
274
  const kept = Math.max(
@@ -240,8 +279,8 @@ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
240
279
  return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
241
280
  }
242
281
  function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
243
- const held = Math.min(expiredHoldBacking, balance);
244
- const used = Math.min(held, holdBacking);
282
+ const held = heldExpiredBacking(balance, expiredHoldBacking);
283
+ const used = Math.max(0, Math.min(held, holdBacking));
245
284
  return { used, remaining: sumAmounts(held, -used) };
246
285
  }
247
286
  function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
@@ -256,27 +295,83 @@ function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking,
256
295
  expiredHoldBacking: backing.remaining
257
296
  };
258
297
  }
259
- function attributeExpiredBacking(holds, total) {
260
- const placed = holds.filter((hold) => hold.holdPlacedAt).sort(
261
- (a, b) => a.holdPlacedAt === b.holdPlacedAt ? a.id < b.id ? -1 : 1 : a.holdPlacedAt < b.holdPlacedAt ? -1 : 1
262
- );
263
- const shares = /* @__PURE__ */ new Map();
264
- let left = total;
265
- for (const hold of placed) {
266
- const kept = Math.min(hold.expiredBacking ?? 0, hold.amount, left);
267
- shares.set(hold.id, kept);
268
- left = sumAmounts(left, -kept);
298
+ var NO_EXPIRED_BACKING_PREFIX = {
299
+ holdPlacedAt: "",
300
+ id: "",
301
+ share: 0
302
+ };
303
+ function compareHolds(a, b) {
304
+ if (a.holdPlacedAt !== b.holdPlacedAt) return a.holdPlacedAt < b.holdPlacedAt ? -1 : 1;
305
+ if (a.id === b.id) return 0;
306
+ return a.id < b.id ? -1 : 1;
307
+ }
308
+ function recordedExpiredBacking(hold) {
309
+ const stored = hold.expiredBacking;
310
+ if (typeof stored !== "number" || !Number.isFinite(stored) || stored <= 0) return 0;
311
+ if (!isValidCreditAmount(hold.amount)) return 0;
312
+ return Math.min(storedAmountCeil(stored), hold.amount);
313
+ }
314
+ function allocateExpiredBacking(holds, total) {
315
+ const placed = holds.filter((hold) => hold.holdPlacedAt && isValidCreditAmount(hold.amount)).map((hold) => ({ ...hold, holdPlacedAt: hold.holdPlacedAt })).sort(compareHolds);
316
+ const wanted = Math.max(0, total);
317
+ if (wanted === 0) return { total: 0, frontier: null };
318
+ const recorded = placed.filter((hold) => recordedExpiredBacking(hold) > 0);
319
+ const shares = new Map(recorded.map((hold) => [hold.id, recordedExpiredBacking(hold)]));
320
+ let left = sumAmounts(wanted, ...[...shares.values()].map((share) => -share));
321
+ for (const hold of [...recorded].reverse()) {
322
+ if (left >= 0) break;
323
+ const share = shares.get(hold.id);
324
+ const cut = Math.min(share, -left);
325
+ shares.set(hold.id, sumAmounts(share, -cut));
326
+ left = sumAmounts(left, cut);
269
327
  }
328
+ let frontier = NO_EXPIRED_BACKING_PREFIX;
270
329
  for (const hold of placed) {
330
+ if (left <= 0) break;
331
+ if (shares.has(hold.id)) continue;
332
+ const share = Math.min(hold.amount, left);
333
+ frontier = { holdPlacedAt: hold.holdPlacedAt, id: hold.id, share };
334
+ left = sumAmounts(left, -share);
335
+ }
336
+ for (const hold of recorded) {
337
+ if (left <= 0) break;
271
338
  const share = shares.get(hold.id);
272
339
  const added = Math.min(sumAmounts(hold.amount, -share), left);
273
340
  shares.set(hold.id, sumAmounts(share, added));
274
341
  left = sumAmounts(left, -added);
275
342
  }
276
- return shares;
343
+ const rewrites = recorded.filter((hold) => shares.get(hold.id) !== hold.expiredBacking).map((hold) => ({ id: hold.id, expiredBacking: shares.get(hold.id) }));
344
+ return { total: wanted, frontier, ...rewrites.length > 0 ? { rewrites } : {} };
277
345
  }
278
- function expiredHoldBackingPatch(next, stored = 0) {
279
- return next > 0 || stored > 0 ? { expiredHoldBacking: next } : {};
346
+ function holdExpiredBacking(hold, frontier, rewrites) {
347
+ const rewritten = rewrites?.find((rewrite) => rewrite.id === hold.id);
348
+ const recorded = recordedExpiredBacking(
349
+ rewritten ? { ...hold, expiredBacking: rewritten.expiredBacking } : hold
350
+ );
351
+ if (recorded > 0) return recorded;
352
+ if (!frontier || !hold.holdPlacedAt) return 0;
353
+ const order = compareHolds({ holdPlacedAt: hold.holdPlacedAt, id: hold.id }, frontier);
354
+ if (order > 0) return 0;
355
+ return order < 0 ? hold.amount : Math.min(frontier.share, hold.amount);
356
+ }
357
+ function holdPlacedAtAfter(now, frontier) {
358
+ if (!frontier?.holdPlacedAt) return now;
359
+ const earliest = new Date(Date.parse(frontier.holdPlacedAt) + 1).toISOString();
360
+ return now > earliest ? now : earliest;
361
+ }
362
+ function expiredBackingPatch(next, stored) {
363
+ const total = Math.max(0, next.total);
364
+ const patch = {};
365
+ const storedTotal = stored.expiredHoldBacking;
366
+ if (total > 0 || storedTotal !== void 0 && storedTotal !== 0) {
367
+ patch.expiredHoldBacking = total;
368
+ }
369
+ if (total <= 0) {
370
+ if (stored.expiredBackingFrontier) patch.expiredBackingFrontier = null;
371
+ } else if (next.frontier) {
372
+ patch.expiredBackingFrontier = next.frontier;
373
+ }
374
+ return patch;
280
375
  }
281
376
  function assertValidBalanceTarget(value, field, context) {
282
377
  assertRepresentableAmount(value, field, context);
@@ -289,7 +384,10 @@ function assertValidBalanceTarget(value, field, context) {
289
384
  }
290
385
  }
291
386
  function assertValidTierBalanceInput(input, context) {
292
- if (input.balanceCap === void 0) return;
387
+ if (input.balanceCap === void 0) {
388
+ if (input.balance !== void 0) assertValidBalanceTarget(input.balance, "balance", context);
389
+ return;
390
+ }
293
391
  if (input.balance !== void 0) {
294
392
  throw new CreditError(
295
393
  "A tier update cannot set both balance and balanceCap",
@@ -1255,7 +1353,7 @@ function validateBalanceUpdate(current, updates) {
1255
1353
  const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
1256
1354
  const errors = [];
1257
1355
  if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
1258
- const marked = updates.balance === void 0 ? current.expiredHoldBacking ?? 0 : 0;
1356
+ const marked = updates.balance === void 0 ? heldExpiredBacking(current.balance, current.expiredHoldBacking) : 0;
1259
1357
  if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
1260
1358
  errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
1261
1359
  }
@@ -1407,6 +1505,7 @@ function commitTransitionJournal(store, input, existingId) {
1407
1505
  userId: input.userId,
1408
1506
  entryType: input.entryType,
1409
1507
  amount: input.amount,
1508
+ balanceBefore: input.balanceBefore,
1410
1509
  balanceAfter: input.balanceAfter,
1411
1510
  source: input.source,
1412
1511
  referenceId: input.reservationId,
@@ -1456,16 +1555,28 @@ function openHolds(store, userId) {
1456
1555
  (reservation) => reservation.status === "reserved"
1457
1556
  );
1458
1557
  }
1558
+ function expiredBackingInForce(store, credits) {
1559
+ const total = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
1560
+ if (total > 0 && (!credits.expiredBackingFrontier || credits.expiredHoldBacking !== total)) {
1561
+ return allocateExpiredBacking(openHolds(store, credits.userId), total);
1562
+ }
1563
+ return { total, frontier: credits.expiredBackingFrontier ?? null };
1564
+ }
1565
+ function writeRecordedShares(store, userId, allocation) {
1566
+ for (const { id, expiredBacking } of allocation.rewrites ?? []) {
1567
+ const hold = getReservation(store, userId, id);
1568
+ if (hold) hold.expiredBacking = expiredBacking;
1569
+ }
1570
+ }
1459
1571
  function writeExpiredHoldBacking(store, credits, next) {
1460
- const stored = credits.expiredHoldBacking;
1461
- if (next <= 0 && !stored) return;
1462
- const holds = openHolds(store, credits.userId);
1463
- const shares = attributeExpiredBacking(holds, next);
1464
- for (const hold of holds) {
1465
- const share = shares.get(hold.id) ?? 0;
1466
- if (share !== (hold.expiredBacking ?? 0)) hold.expiredBacking = share;
1572
+ if (next <= 0) {
1573
+ Object.assign(credits, expiredBackingPatch({ total: 0 }, credits));
1574
+ return;
1467
1575
  }
1468
- Object.assign(credits, expiredHoldBackingPatch(sumAmounts(...shares.values()), stored));
1576
+ if (next === credits.expiredHoldBacking) return;
1577
+ const allocation = allocateExpiredBacking(openHolds(store, credits.userId), next);
1578
+ writeRecordedShares(store, credits.userId, allocation);
1579
+ Object.assign(credits, expiredBackingPatch(allocation, credits));
1469
1580
  }
1470
1581
  function getReservation(store, userId, reservationId) {
1471
1582
  return store.reservations.get(userId)?.get(reservationId);
@@ -1514,6 +1625,7 @@ async function reserveCreditsV2(store, input) {
1514
1625
  };
1515
1626
  }
1516
1627
  const now = (/* @__PURE__ */ new Date()).toISOString();
1628
+ const backing = expiredBackingInForce(store, credits);
1517
1629
  const reservation = {
1518
1630
  id: generateId(),
1519
1631
  userId: input.userId,
@@ -1526,7 +1638,7 @@ async function reserveCreditsV2(store, input) {
1526
1638
  // The hold-origin fact. Written here and nowhere else, in the same
1527
1639
  // critical section that raises `reserved` below, so the row and the hold
1528
1640
  // become visible together. `createReservation` cannot set it.
1529
- holdPlacedAt: now
1641
+ holdPlacedAt: holdPlacedAtAfter(now, backing.total > 0 ? backing.frontier : null)
1530
1642
  };
1531
1643
  if (!store.reservations.has(input.userId)) {
1532
1644
  store.reservations.set(input.userId, /* @__PURE__ */ new Map());
@@ -1544,6 +1656,8 @@ async function reserveCreditsV2(store, input) {
1544
1656
  );
1545
1657
  }
1546
1658
  credits.reserved = nextReserved;
1659
+ writeRecordedShares(store, input.userId, backing);
1660
+ Object.assign(credits, expiredBackingPatch(backing, credits));
1547
1661
  credits.updatedAt = now;
1548
1662
  return { outcome: "created", reservation: copyRecord(reservation) };
1549
1663
  });
@@ -1564,11 +1678,12 @@ async function commitReservationV2(store, userId, reservationId, options) {
1564
1678
  const amount = reservation.amount;
1565
1679
  await yieldPoint(store);
1566
1680
  assertReservedCoversHold(credits, userId, reservationId, amount);
1681
+ const backing = expiredBackingInForce(store, credits);
1567
1682
  const paid = planHoldCommit(
1568
1683
  credits.balance,
1569
1684
  credits.bonusCredits,
1570
- credits.expiredHoldBacking ?? 0,
1571
- reservation.expiredBacking ?? 0,
1685
+ backing.total,
1686
+ holdExpiredBacking(reservation, backing.frontier, backing.rewrites),
1572
1687
  amount
1573
1688
  );
1574
1689
  if (!paid.covered) {
@@ -1586,6 +1701,7 @@ async function commitReservationV2(store, userId, reservationId, options) {
1586
1701
  operation: "commitReservation",
1587
1702
  entryType: "debit",
1588
1703
  amount,
1704
+ balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
1589
1705
  balanceAfter,
1590
1706
  source: "operation_commit",
1591
1707
  reservationId,
@@ -1613,9 +1729,10 @@ async function commitReservationV2(store, userId, reservationId, options) {
1613
1729
  credits.bonusCredits = nextBonusCredits;
1614
1730
  credits.reserved = nextReserved;
1615
1731
  credits.monthlyUsed = nextMonthlyUsed;
1732
+ writeRecordedShares(store, userId, backing);
1616
1733
  Object.assign(
1617
1734
  credits,
1618
- expiredHoldBackingPatch(paid.expiredHoldBacking, credits.expiredHoldBacking)
1735
+ expiredBackingPatch({ total: paid.expiredHoldBacking, frontier: backing.frontier }, credits)
1619
1736
  );
1620
1737
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1621
1738
  const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
@@ -1696,10 +1813,11 @@ function finishUnspent(store, reservation, transition) {
1696
1813
  reservation.id,
1697
1814
  reservation.amount
1698
1815
  );
1816
+ const inForce = expiredBackingInForce(store, credits);
1699
1817
  const backing = settleExpiredHoldBacking(
1700
1818
  credits.balance,
1701
- credits.expiredHoldBacking ?? 0,
1702
- reservation.expiredBacking ?? 0
1819
+ inForce.total,
1820
+ holdExpiredBacking(reservation, inForce.frontier, inForce.rewrites)
1703
1821
  );
1704
1822
  const nextBalance = sumAmounts(credits.balance, -backing.used);
1705
1823
  const journal = {
@@ -1707,6 +1825,7 @@ function finishUnspent(store, reservation, transition) {
1707
1825
  operation: `${transition.transition}Reservation`,
1708
1826
  entryType: "credit",
1709
1827
  amount: 0,
1828
+ balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
1710
1829
  balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
1711
1830
  source: transition.source,
1712
1831
  reservationId: reservation.id,
@@ -1729,7 +1848,11 @@ function finishUnspent(store, reservation, transition) {
1729
1848
  reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1730
1849
  credits.reserved = nextReserved;
1731
1850
  credits.balance = nextBalance;
1732
- Object.assign(credits, expiredHoldBackingPatch(backing.remaining, credits.expiredHoldBacking));
1851
+ writeRecordedShares(store, reservation.userId, inForce);
1852
+ Object.assign(
1853
+ credits,
1854
+ expiredBackingPatch({ total: backing.remaining, frontier: inForce.frontier }, credits)
1855
+ );
1733
1856
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1734
1857
  return commitTransitionJournal(store, journal, existingEntryId);
1735
1858
  }
@@ -1772,7 +1895,7 @@ var InMemoryCreditRepository = class {
1772
1895
  * simultaneous first deliveries of the same reference would then both credit.
1773
1896
  */
1774
1897
  seedUser(userId, tier, initialBalance) {
1775
- assertRepresentableAmount(initialBalance, "initialBalance", { userId });
1898
+ assertValidBalanceTarget(initialBalance, "initialBalance", { userId });
1776
1899
  const configured = getConfigMonthlyLimit(tier);
1777
1900
  assertRepresentableTierAmount(configured, "monthlyLimit", { userId, tier });
1778
1901
  const monthlyLimit = storedMonthlyLimit(configured);
@@ -1853,9 +1976,11 @@ var InMemoryCreditRepository = class {
1853
1976
  userId,
1854
1977
  operation: "updateUserCredits"
1855
1978
  });
1856
- if ((credits.expiredHoldBacking ?? 0) > 0 && updates.balance !== void 0) {
1857
- writeExpiredHoldBacking(this.store, next, 0);
1858
- }
1979
+ writeExpiredHoldBacking(
1980
+ this.store,
1981
+ next,
1982
+ updates.balance !== void 0 ? 0 : Math.min(heldExpiredBacking(credits.balance, credits.expiredHoldBacking), next.balance)
1983
+ );
1859
1984
  next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1860
1985
  this.store.users.set(userId, next);
1861
1986
  }
@@ -1887,6 +2012,11 @@ var InMemoryCreditRepository = class {
1887
2012
  credits.balance = planned.balance;
1888
2013
  writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
1889
2014
  }
2015
+ writeExpiredHoldBacking(
2016
+ this.store,
2017
+ credits,
2018
+ heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
2019
+ );
1890
2020
  if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
1891
2021
  if (input.subscriptionExpiresAt !== void 0) {
1892
2022
  if (input.subscriptionExpiresAt === null) {
@@ -2048,6 +2178,11 @@ var InMemoryCreditRepository = class {
2048
2178
  { userId, operation: "addCredits" }
2049
2179
  );
2050
2180
  credits.bonusCredits = nextBonusCredits;
2181
+ writeExpiredHoldBacking(
2182
+ this.store,
2183
+ credits,
2184
+ heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
2185
+ );
2051
2186
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2052
2187
  this.store.users.set(userId, credits);
2053
2188
  const transaction = this.recordTransaction({
@@ -2067,6 +2202,7 @@ var InMemoryCreditRepository = class {
2067
2202
  userId,
2068
2203
  entryType: "credit",
2069
2204
  amount,
2205
+ balanceBefore: previousTotal,
2070
2206
  balanceAfter: newTotal,
2071
2207
  source: payload.source,
2072
2208
  referenceId: transaction.id ?? paymentRef ?? "unknown",
@@ -2100,7 +2236,7 @@ var InMemoryCreditRepository = class {
2100
2236
  });
2101
2237
  }
2102
2238
  }
2103
- async deductCreditsAtomic(userId, amount) {
2239
+ async deductCreditsAtomic(userId, amount, journal) {
2104
2240
  assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
2105
2241
  const credits = this.store.users.get(userId);
2106
2242
  if (!credits) {
@@ -2126,11 +2262,26 @@ var InMemoryCreditRepository = class {
2126
2262
  { previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },
2127
2263
  { userId, operation: "deductCredits" }
2128
2264
  );
2265
+ const held = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
2129
2266
  credits.balance = nextBalance;
2130
2267
  credits.bonusCredits = nextBonusCredits;
2268
+ writeExpiredHoldBacking(this.store, credits, held);
2131
2269
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2132
2270
  this.store.users.set(userId, credits);
2133
- return { previousBalance, newBalance };
2271
+ if (!journal) return { previousBalance, newBalance };
2272
+ const entry = this.recordJournalEntry({
2273
+ userId,
2274
+ entryType: "debit",
2275
+ amount,
2276
+ balanceBefore: previousBalance,
2277
+ balanceAfter: newBalance,
2278
+ source: journal.source ?? "operation_commit",
2279
+ referenceId: journal.referenceId,
2280
+ referenceType: journal.referenceType ?? "adjustment",
2281
+ description: journal.description,
2282
+ metadata: journal.metadata
2283
+ });
2284
+ return { previousBalance, newBalance, journalEntryId: entry.id };
2134
2285
  }
2135
2286
  // ==================== Transactions ====================
2136
2287
  async createTransaction(input) {
@@ -2244,6 +2395,11 @@ var InMemoryCreditRepository = class {
2244
2395
  assertRepresentableAmount(input.balanceAfter, "journal balanceAfter", {
2245
2396
  userId: input.userId
2246
2397
  });
2398
+ if (input.balanceBefore !== void 0) {
2399
+ assertRepresentableAmount(input.balanceBefore, "journal balanceBefore", {
2400
+ userId: input.userId
2401
+ });
2402
+ }
2247
2403
  assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });
2248
2404
  assertPublicJournalKey(input.idempotencyKey, input.userId);
2249
2405
  const key = input.idempotencyKey !== void 0 ? scopedKey(input.userId, input.idempotencyKey) : void 0;
@@ -2259,6 +2415,7 @@ var InMemoryCreditRepository = class {
2259
2415
  userId: input.userId,
2260
2416
  entryType: input.entryType,
2261
2417
  amount: input.amount,
2418
+ ...input.balanceBefore !== void 0 ? { balanceBefore: input.balanceBefore } : {},
2262
2419
  balanceAfter: input.balanceAfter,
2263
2420
  source: input.source,
2264
2421
  referenceId: input.referenceId,
@@ -2411,6 +2568,7 @@ var InMemoryCreditRepository = class {
2411
2568
  userId,
2412
2569
  entryType: change > 0 ? "credit" : "debit",
2413
2570
  amount: Math.abs(change),
2571
+ balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
2414
2572
  balanceAfter,
2415
2573
  source: "monthly_reset",
2416
2574
  referenceId: `reset-${Date.now()}`,
@@ -2491,6 +2649,7 @@ var InMemoryCreditRepository = class {
2491
2649
  entryType: "debit",
2492
2650
  amount: 0,
2493
2651
  // No credits deducted, just tier change
2652
+ balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
2494
2653
  balanceAfter,
2495
2654
  source: "subscription_downgrade",
2496
2655
  referenceId: `downgrade-${Date.now()}`,
@@ -2654,6 +2813,7 @@ var CreditsService = class {
2654
2813
  entryType: "debit",
2655
2814
  amount: 0,
2656
2815
  // No credits deducted, just tier change
2816
+ balanceBefore: sumAmounts(data.balance, data.bonusCredits),
2657
2817
  // The ledger balance, matching every other journal writer: the V2
2658
2818
  // transitions and add/deduct all record `balance + bonusCredits`, so
2659
2819
  // recording `balance` alone made the audit trail disagree with itself
@@ -2697,6 +2857,7 @@ var CreditsService = class {
2697
2857
  userId,
2698
2858
  entryType: balanceChange > 0 ? "credit" : "debit",
2699
2859
  amount: Math.abs(balanceChange),
2860
+ balanceBefore: sumAmounts(data.balance, data.bonusCredits),
2700
2861
  balanceAfter: sumAmounts(
2701
2862
  resetResult.credits.balance,
2702
2863
  resetResult.credits.bonusCredits
@@ -2931,9 +3092,18 @@ var CreditsService = class {
2931
3092
  */
2932
3093
  async deductCredits(userId, amount, options) {
2933
3094
  assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
3095
+ const operationType = options?.operationType;
3096
+ const journal = {
3097
+ referenceId: options?.requestId ?? `deduct-${Date.now()}`,
3098
+ description: operationType ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}` : `Deducted ${amount} credits`,
3099
+ metadata: {
3100
+ ...options?.metadata,
3101
+ ...operationType && { operationType }
3102
+ }
3103
+ };
2934
3104
  let deduction;
2935
3105
  try {
2936
- deduction = await this.repository.deductCreditsAtomic(userId, amount);
3106
+ deduction = await this.repository.deductCreditsAtomic(userId, amount, journal);
2937
3107
  } catch (error) {
2938
3108
  if (isCreditError(error) && error.code === CreditErrorCode.INVALID_AMOUNT) throw error;
2939
3109
  const credits = await this.repository.getUserCredits(userId);
@@ -2949,21 +3119,18 @@ var CreditsService = class {
2949
3119
  shortfall: sumAmounts(amount, -available)
2950
3120
  };
2951
3121
  }
2952
- const operationType = options?.operationType;
2953
- await this.repository.createJournalEntry({
2954
- userId,
2955
- entryType: "debit",
2956
- amount,
2957
- balanceAfter: deduction.newBalance,
2958
- source: "operation_commit",
2959
- referenceId: options?.requestId ?? `deduct-${Date.now()}`,
2960
- referenceType: "adjustment",
2961
- description: operationType ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}` : `Deducted ${amount} credits`,
2962
- metadata: {
2963
- ...options?.metadata,
2964
- ...operationType && { operationType }
2965
- }
2966
- });
3122
+ if (deduction.journalEntryId === void 0) {
3123
+ await this.repository.createJournalEntry({
3124
+ userId,
3125
+ entryType: "debit",
3126
+ amount,
3127
+ balanceBefore: deduction.previousBalance,
3128
+ balanceAfter: deduction.newBalance,
3129
+ source: "operation_commit",
3130
+ referenceType: "adjustment",
3131
+ ...journal
3132
+ });
3133
+ }
2967
3134
  if (operationType) {
2968
3135
  await this.logUsage({
2969
3136
  userId,
@@ -3973,6 +4140,7 @@ export {
3973
4140
  CreditsSDKError,
3974
4141
  CreditsService,
3975
4142
  InMemoryCreditRepository,
4143
+ NO_EXPIRED_BACKING_PREFIX,
3976
4144
  NetworkError,
3977
4145
  RESERVATION_STATUSES,
3978
4146
  RESERVED_JOURNAL_KEY_PREFIX,
@@ -3985,6 +4153,7 @@ export {
3985
4153
  UNLIMITED_BALANCE_SENTINEL,
3986
4154
  ValidationError,
3987
4155
  addCreditsThroughRepository,
4156
+ allocateExpiredBacking,
3988
4157
  assertDirectStatusWriteAllowed,
3989
4158
  assertHoldPlaced,
3990
4159
  assertKnownReservationStatus,
@@ -4001,9 +4170,9 @@ export {
4001
4170
  assertValidStoredAmount,
4002
4171
  assertValidStoredAmountRaw,
4003
4172
  assertValidTierBalanceInput,
4004
- attributeExpiredBacking,
4005
4173
  backedBalanceFloor,
4006
4174
  calculateAvailableCredits,
4175
+ canonicalAmount,
4007
4176
  cappedBalance,
4008
4177
  checkAndNotifyLowBalance,
4009
4178
  classifyDatabaseError,
@@ -4030,7 +4199,7 @@ export {
4030
4199
  createUnsupportedOperationError,
4031
4200
  createUserNotFoundError,
4032
4201
  describePaymentMismatch,
4033
- expiredHoldBackingPatch,
4202
+ expiredBackingPatch,
4034
4203
  generateId,
4035
4204
  generateRequestId2 as generateRequestId,
4036
4205
  genericDeferred,
@@ -4056,6 +4225,9 @@ export {
4056
4225
  getValidOperationTypes,
4057
4226
  getValidTiers,
4058
4227
  hasPlacedHold,
4228
+ heldExpiredBacking,
4229
+ holdExpiredBacking,
4230
+ holdPlacedAtAfter,
4059
4231
  initializeConfig,
4060
4232
  isCreditError,
4061
4233
  isCreditErrorMessage,
@@ -4081,6 +4253,7 @@ export {
4081
4253
  isWinningOutcome,
4082
4254
  loadConfigFromEnv,
4083
4255
  monthlyResetBalance,
4256
+ multiplyAmount,
4084
4257
  noopDeferred,
4085
4258
  normalizePaymentRef,
4086
4259
  notifySubscriptionExpired,
@@ -4092,6 +4265,7 @@ export {
4092
4265
  planBalanceCap,
4093
4266
  planBalanceTarget,
4094
4267
  planHoldCommit,
4268
+ recordedExpiredBacking,
4095
4269
  registerConfigProvider,
4096
4270
  registerNotificationHandler,
4097
4271
  releaseReservationWithJournal,
@@ -4103,6 +4277,8 @@ export {
4103
4277
  sameAmount,
4104
4278
  settleExpiredHoldBacking,
4105
4279
  spendableBalance,
4280
+ storedAmountCeil,
4281
+ storedAmountFloor,
4106
4282
  storedMonthlyLimit,
4107
4283
  sumAmounts,
4108
4284
  supportsCreditsV2,