@nehorai/credits 2.2.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/adapters/index.cjs +169 -155
  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 +169 -155
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/core/index.cjs +148 -24
  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 +137 -22
  12. package/dist/core/index.js.map +1 -1
  13. package/dist/index-BrogG0Gc.d.cts +277 -0
  14. package/dist/index-DOvJES2m.d.ts +277 -0
  15. package/dist/index.cjs +324 -71
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +5 -5
  18. package/dist/index.d.ts +5 -5
  19. package/dist/index.js +311 -69
  20. package/dist/index.js.map +1 -1
  21. package/dist/repository/index.cjs +254 -49
  22. package/dist/repository/index.cjs.map +1 -1
  23. package/dist/repository/index.d.cts +3 -254
  24. package/dist/repository/index.d.ts +3 -254
  25. package/dist/repository/index.js +254 -49
  26. package/dist/repository/index.js.map +1 -1
  27. package/dist/sdk/index.cjs +1 -0
  28. package/dist/sdk/index.cjs.map +1 -1
  29. package/dist/sdk/index.d.cts +2 -2
  30. package/dist/sdk/index.d.ts +2 -2
  31. package/dist/sdk/index.js +1 -0
  32. package/dist/sdk/index.js.map +1 -1
  33. package/dist/service/index.cjs +42 -19
  34. package/dist/service/index.cjs.map +1 -1
  35. package/dist/service/index.d.cts +2 -2
  36. package/dist/service/index.d.ts +2 -2
  37. package/dist/service/index.js +42 -19
  38. package/dist/service/index.js.map +1 -1
  39. package/dist/{types-3c-IAwZI.d.ts → types-BL3YqxUD.d.ts} +28 -6
  40. package/dist/{types-MaViK5tD.d.cts → types-C3sxDvNb.d.cts} +29 -6
  41. package/dist/{types-MaViK5tD.d.ts → types-C3sxDvNb.d.ts} +29 -6
  42. package/dist/{types-XSjNXO5l.d.cts → types-YLAB8bFk.d.cts} +28 -6
  43. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
- import { R as ReservationStatus, a as PortableReservation, P as PortableTransaction, b as CreditSource, J as JournalReferenceType } from '../types-MaViK5tD.cjs';
2
- export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, D as DeductCreditsResult, M as MonthlyResetResult, f as PortableJournalEntry, d as PortableUsageLog, c as PortableUserCredits, h as ResourceType, e as SubscriptionExpiryResult, S as SubscriptionTier, T as TierConfig, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from '../types-MaViK5tD.cjs';
3
- import { T as TerminalReservationStatus, I as ICreditRepository } from '../types-XSjNXO5l.cjs';
4
- export { a as AddCreditsOutcome, C as CommitOutcome, E as ExpireOutcome, R as ReleaseOutcome, o as ReserveOutcome, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome } from '../types-XSjNXO5l.cjs';
1
+ import { R as ReservationStatus, a as PortableReservation, P as PortableTransaction, b as CreditSource, J as JournalReferenceType, E as ExpiredBackingFrontier } from '../types-C3sxDvNb.cjs';
2
+ export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, D as DeductCreditsResult, M as MonthlyResetResult, f as PortableJournalEntry, d as PortableUsageLog, c as PortableUserCredits, h as ResourceType, e as SubscriptionExpiryResult, S as SubscriptionTier, T as TierConfig, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from '../types-C3sxDvNb.cjs';
3
+ import { T as TerminalReservationStatus, I as ICreditRepository } from '../types-YLAB8bFk.cjs';
4
+ export { a as AddCreditsOutcome, C as CommitOutcome, E as ExpireOutcome, R as ReleaseOutcome, o as ReserveOutcome, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome } from '../types-YLAB8bFk.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
 
7
7
  /**
@@ -432,6 +432,40 @@ declare function toCents(value: number): number | null;
432
432
  * derived result is not enough and never was.
433
433
  */
434
434
  declare function sumAmounts(...values: number[]): number;
435
+ /**
436
+ * Multiply a price by a whole quantity, in cents.
437
+ *
438
+ * `1.1 * 3` is `3.3000000000000003`, which the amount validators refuse: the price
439
+ * was legal, and the float product blames the caller for a rounding artefact. A
440
+ * fractional quantity has no exact result on the grid and is refused rather than
441
+ * rounded, so the rounding policy stays with whoever prices fractions.
442
+ */
443
+ declare function multiplyAmount(unit: number, quantity: number): number;
444
+ /**
445
+ * The cent-grid value a float landed next to, or the value unchanged.
446
+ *
447
+ * Documents written by older versions can hold the result of IEEE 754 additions
448
+ * (`FieldValue.increment` stored `0.30000000000000004`), and an application can pass
449
+ * a raw float difference to a ledger record. That is noise, not an amount. A value
450
+ * genuinely off the grid, such as `0.005`, is not noise and comes back as it went in.
451
+ */
452
+ declare function canonicalAmount(value: number): number;
453
+ /**
454
+ * Read a stored amount the account owns as a cent-grid value, rounding down.
455
+ *
456
+ * A stored `24.999` makes every later sum off-grid, the validators then refuse the
457
+ * commit or release that needs it, and the holds on that account lock. Rounding down
458
+ * unlocks them. A value within {@link canonicalAmount}'s noise of the next cent reads as
459
+ * that cent, because a float cannot tell the two apart, so this can add at most that noise:
460
+ * 1e-6 up to 7e7, and about 1.4e-4 at the maximum. A non-finite value is left for the
461
+ * validators to refuse.
462
+ */
463
+ declare function storedAmountFloor(value: number): number;
464
+ /**
465
+ * Read a stored amount the account owes as a cent-grid value, rounding up, so what
466
+ * stands against the balance is never understated. See {@link storedAmountFloor}.
467
+ */
468
+ declare function storedAmountCeil(value: number): number;
435
469
  /** Amounts that may be *moved*: strictly positive and on the cent grid. */
436
470
  declare function isValidCreditAmount(value: unknown): value is number;
437
471
  /**
@@ -493,6 +527,13 @@ interface PlannedBalance {
493
527
  balance: number;
494
528
  expiredHoldBacking: number;
495
529
  }
530
+ /**
531
+ * The expired credits actually held in a balance: `0 <= held <= balance`, whatever
532
+ * is stored. Every read of `expiredHoldBacking` goes through this. Trusting the stored
533
+ * value made a negative one mint credits: `balance - (-10)` read as spendable, and a
534
+ * close that "used" -10 of backing added 10 to the balance.
535
+ */
536
+ declare function heldExpiredBacking(balance: number, stored?: number): number;
496
537
  /** The part of a balance not kept only for holds. A missing field reads as 0. */
497
538
  declare function spendableBalance(balance: number, expiredHoldBacking?: number): number;
498
539
  /**
@@ -509,8 +550,7 @@ declare function planBalanceTarget(target: number, reserved: number, bonusCredit
509
550
  declare function planBalanceCap(balance: number, cap: number, reserved: number, bonusCredits: number, expiredHoldBacking?: number): PlannedBalance;
510
551
  /**
511
552
  * What a closing hold takes out of the credits kept only for holds: its own share
512
- * and nothing of another hold's. `holdBacking` comes from
513
- * {@link attributeExpiredBacking}.
553
+ * and nothing of another hold's. `holdBacking` comes from {@link holdExpiredBacking}.
514
554
  */
515
555
  declare function settleExpiredHoldBacking(balance: number, expiredHoldBacking: number, holdBacking: number): {
516
556
  used: number;
@@ -530,28 +570,79 @@ interface PlannedHoldCommit {
530
570
  */
531
571
  declare function planHoldCommit(balance: number, bonusCredits: number, expiredHoldBacking: number, holdBacking: number, amount: number): PlannedHoldCommit;
532
572
  /** The fields of an open hold that decide its share of the marked credits. */
533
- type ExpiredBackingHold = Pick<PortableReservation, "id" | "amount" | "expiredBacking" | "holdPlacedAt">;
573
+ type ExpiredBackingHold = Pick<PortableReservation, "id" | "amount" | "holdPlacedAt" | "expiredBacking">;
574
+ /** A hold whose stored share has to change for a new total. */
575
+ interface ExpiredBackingRewrite {
576
+ id: string;
577
+ expiredBacking: number;
578
+ }
579
+ /** The marked credits on an account and where they end among its open holds. */
580
+ interface ExpiredBackingAllocation {
581
+ total: number;
582
+ frontier: ExpiredBackingFrontier | null;
583
+ /** Stored shares to write with this total. Absent when none changes. */
584
+ rewrites?: ExpiredBackingRewrite[];
585
+ }
586
+ /**
587
+ * A frontier before every hold: no hold shares through it. It records that the account's
588
+ * marks are placed, so the next close does not walk the holds again.
589
+ */
590
+ declare const NO_EXPIRED_BACKING_PREFIX: ExpiredBackingFrontier;
534
591
  /**
535
- * Split `total` marked credits across open holds, returning each hold's share.
592
+ * The share a 2.2.0 hold document recorded for itself: the order of record for that hold.
593
+ * Clocks are not, since the server that placed a hold may lag the one before it. Rounded
594
+ * up, like every amount that stands against the balance, and never above the hold. Zero
595
+ * means no record, which leaves the hold to the frontier.
596
+ */
597
+ declare function recordedExpiredBacking(hold: ExpiredBackingHold): number;
598
+ /**
599
+ * Place `total` marked credits on the open holds, so that what each hold takes does not
600
+ * depend on the order they close in.
601
+ *
602
+ * Shares a 2.2.0 hold document stored stand as recorded: they need not be a prefix of
603
+ * anything, and they sit on documents that already exist. The rest goes to the other
604
+ * holds oldest first, each up to its amount. That part is a prefix, so one
605
+ * {@link ExpiredBackingFrontier} on the account describes it and no hold document changes
606
+ * however many there are. A new hold carries no share, so no record is ever added.
536
607
  *
537
- * Shares a hold already has are kept first, oldest hold first, so lowering `total`
538
- * trims the newest and raising it never moves credits off a hold. Only what is left
539
- * goes to holds with room, again oldest first, which makes the split a function of
540
- * the holds and not of the order they later close in. A hold never carries more than
541
- * its amount. A row without `holdPlacedAt` is a record, not a hold, and gets nothing.
608
+ * A total below the records trims them newest first, and only when every other hold has
609
+ * nothing, and a total the other holds cannot take fills what the records left free. Those
610
+ * are the only hold writes: `rewrites`, at most one per record, each record being an open
611
+ * hold from before the upgrade.
542
612
  *
543
- * The account's `expiredHoldBacking` is written as the sum of these shares, in the
544
- * same write, so what `total` could not attribute is not marked. Such credits stay out
545
- * of reach anyway, because the `reserved` that kept them still counts against them.
613
+ * Marks no hold can carry stay marked. They were out of reach before this ran, and
614
+ * dropping them would make them spendable; the next absolute balance write replaces
615
+ * them. A row without `holdPlacedAt` is a record, not a hold, and a row with an unusable
616
+ * amount cannot close, so neither takes a share.
546
617
  */
547
- declare function attributeExpiredBacking(holds: readonly ExpiredBackingHold[], total: number): Map<string, number>;
618
+ declare function allocateExpiredBacking(holds: readonly ExpiredBackingHold[], total: number): ExpiredBackingAllocation;
548
619
  /**
549
- * The field to write for a new backing value: nothing when it was and stays 0, so
550
- * an account that never had any keeps a document without the field.
620
+ * This hold's share of the marked credits: its own record if it has one, otherwise where
621
+ * it falls against the account's frontier. Pass the `rewrites` of an allocation made in
622
+ * this same step, since the document does not hold them yet.
551
623
  */
552
- declare function expiredHoldBackingPatch(next: number, stored?: number): {
624
+ declare function holdExpiredBacking(hold: ExpiredBackingHold, frontier?: ExpiredBackingFrontier | null, rewrites?: readonly ExpiredBackingRewrite[]): number;
625
+ /**
626
+ * When a new hold counts as placed: `now`, or just after the frontier if this server's
627
+ * clock is behind the server that placed it. A hold that sorted before the frontier
628
+ * would read as fully marked, taking expired credits that belong to older holds.
629
+ */
630
+ declare function holdPlacedAtAfter(now: string, frontier?: ExpiredBackingFrontier | null): string;
631
+ /** The account fields that hold the marked credits. */
632
+ interface ExpiredBackingFields {
553
633
  expiredHoldBacking?: number;
554
- };
634
+ expiredBackingFrontier?: ExpiredBackingFrontier | null;
635
+ }
636
+ /**
637
+ * The fields to write for a new marked total: nothing when it was and stays 0, so an
638
+ * account that never had any keeps a document without the fields. A stored value that
639
+ * is not the new one is overwritten, which is what repairs a bad one. A `frontier` left
640
+ * undefined keeps the stored one.
641
+ */
642
+ declare function expiredBackingPatch(next: {
643
+ total: number;
644
+ frontier?: ExpiredBackingFrontier | null;
645
+ }, stored: ExpiredBackingFields): ExpiredBackingFields;
555
646
  /**
556
647
  * Validate a balance target supplied by the application: a cap or the result of
557
648
  * a monthly-reset decision. It must be storable and cannot be negative.
@@ -781,4 +872,4 @@ declare function releaseReservationWithJournal(repository: ICreditRepository, us
781
872
  */
782
873
  declare function reserveCreditsForOperation(repository: ICreditRepository, userId: string, amount: number, operationType: string, expiryMs?: number, idempotencyKey?: string): Promise<PortableReservation>;
783
874
 
784
- export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, type CreditErrorCodeType, CreditSource, JournalReferenceType, type PaymentEventPayload, PortableReservation, PortableTransaction, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, type ReservationIntegrityView, ReservationStatus, type StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, TerminalReservationStatus, 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 };
875
+ export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, type CreditErrorCodeType, CreditSource, type ExpiredBackingAllocation, type ExpiredBackingFields, ExpiredBackingFrontier, type ExpiredBackingHold, type ExpiredBackingRewrite, JournalReferenceType, NO_EXPIRED_BACKING_PREFIX, type PaymentEventPayload, PortableReservation, PortableTransaction, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, type ReservationIntegrityView, ReservationStatus, type StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, TerminalReservationStatus, 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 };
@@ -1,7 +1,7 @@
1
- import { R as ReservationStatus, a as PortableReservation, P as PortableTransaction, b as CreditSource, J as JournalReferenceType } from '../types-MaViK5tD.js';
2
- export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, D as DeductCreditsResult, M as MonthlyResetResult, f as PortableJournalEntry, d as PortableUsageLog, c as PortableUserCredits, h as ResourceType, e as SubscriptionExpiryResult, S as SubscriptionTier, T as TierConfig, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from '../types-MaViK5tD.js';
3
- import { T as TerminalReservationStatus, I as ICreditRepository } from '../types-3c-IAwZI.js';
4
- export { a as AddCreditsOutcome, C as CommitOutcome, E as ExpireOutcome, R as ReleaseOutcome, o as ReserveOutcome, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome } from '../types-3c-IAwZI.js';
1
+ import { R as ReservationStatus, a as PortableReservation, P as PortableTransaction, b as CreditSource, J as JournalReferenceType, E as ExpiredBackingFrontier } from '../types-C3sxDvNb.js';
2
+ export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, D as DeductCreditsResult, M as MonthlyResetResult, f as PortableJournalEntry, d as PortableUsageLog, c as PortableUserCredits, h as ResourceType, e as SubscriptionExpiryResult, S as SubscriptionTier, T as TierConfig, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from '../types-C3sxDvNb.js';
3
+ import { T as TerminalReservationStatus, I as ICreditRepository } from '../types-BL3YqxUD.js';
4
+ export { a as AddCreditsOutcome, C as CommitOutcome, E as ExpireOutcome, R as ReleaseOutcome, o as ReserveOutcome, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome } from '../types-BL3YqxUD.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
 
7
7
  /**
@@ -432,6 +432,40 @@ declare function toCents(value: number): number | null;
432
432
  * derived result is not enough and never was.
433
433
  */
434
434
  declare function sumAmounts(...values: number[]): number;
435
+ /**
436
+ * Multiply a price by a whole quantity, in cents.
437
+ *
438
+ * `1.1 * 3` is `3.3000000000000003`, which the amount validators refuse: the price
439
+ * was legal, and the float product blames the caller for a rounding artefact. A
440
+ * fractional quantity has no exact result on the grid and is refused rather than
441
+ * rounded, so the rounding policy stays with whoever prices fractions.
442
+ */
443
+ declare function multiplyAmount(unit: number, quantity: number): number;
444
+ /**
445
+ * The cent-grid value a float landed next to, or the value unchanged.
446
+ *
447
+ * Documents written by older versions can hold the result of IEEE 754 additions
448
+ * (`FieldValue.increment` stored `0.30000000000000004`), and an application can pass
449
+ * a raw float difference to a ledger record. That is noise, not an amount. A value
450
+ * genuinely off the grid, such as `0.005`, is not noise and comes back as it went in.
451
+ */
452
+ declare function canonicalAmount(value: number): number;
453
+ /**
454
+ * Read a stored amount the account owns as a cent-grid value, rounding down.
455
+ *
456
+ * A stored `24.999` makes every later sum off-grid, the validators then refuse the
457
+ * commit or release that needs it, and the holds on that account lock. Rounding down
458
+ * unlocks them. A value within {@link canonicalAmount}'s noise of the next cent reads as
459
+ * that cent, because a float cannot tell the two apart, so this can add at most that noise:
460
+ * 1e-6 up to 7e7, and about 1.4e-4 at the maximum. A non-finite value is left for the
461
+ * validators to refuse.
462
+ */
463
+ declare function storedAmountFloor(value: number): number;
464
+ /**
465
+ * Read a stored amount the account owes as a cent-grid value, rounding up, so what
466
+ * stands against the balance is never understated. See {@link storedAmountFloor}.
467
+ */
468
+ declare function storedAmountCeil(value: number): number;
435
469
  /** Amounts that may be *moved*: strictly positive and on the cent grid. */
436
470
  declare function isValidCreditAmount(value: unknown): value is number;
437
471
  /**
@@ -493,6 +527,13 @@ interface PlannedBalance {
493
527
  balance: number;
494
528
  expiredHoldBacking: number;
495
529
  }
530
+ /**
531
+ * The expired credits actually held in a balance: `0 <= held <= balance`, whatever
532
+ * is stored. Every read of `expiredHoldBacking` goes through this. Trusting the stored
533
+ * value made a negative one mint credits: `balance - (-10)` read as spendable, and a
534
+ * close that "used" -10 of backing added 10 to the balance.
535
+ */
536
+ declare function heldExpiredBacking(balance: number, stored?: number): number;
496
537
  /** The part of a balance not kept only for holds. A missing field reads as 0. */
497
538
  declare function spendableBalance(balance: number, expiredHoldBacking?: number): number;
498
539
  /**
@@ -509,8 +550,7 @@ declare function planBalanceTarget(target: number, reserved: number, bonusCredit
509
550
  declare function planBalanceCap(balance: number, cap: number, reserved: number, bonusCredits: number, expiredHoldBacking?: number): PlannedBalance;
510
551
  /**
511
552
  * What a closing hold takes out of the credits kept only for holds: its own share
512
- * and nothing of another hold's. `holdBacking` comes from
513
- * {@link attributeExpiredBacking}.
553
+ * and nothing of another hold's. `holdBacking` comes from {@link holdExpiredBacking}.
514
554
  */
515
555
  declare function settleExpiredHoldBacking(balance: number, expiredHoldBacking: number, holdBacking: number): {
516
556
  used: number;
@@ -530,28 +570,79 @@ interface PlannedHoldCommit {
530
570
  */
531
571
  declare function planHoldCommit(balance: number, bonusCredits: number, expiredHoldBacking: number, holdBacking: number, amount: number): PlannedHoldCommit;
532
572
  /** The fields of an open hold that decide its share of the marked credits. */
533
- type ExpiredBackingHold = Pick<PortableReservation, "id" | "amount" | "expiredBacking" | "holdPlacedAt">;
573
+ type ExpiredBackingHold = Pick<PortableReservation, "id" | "amount" | "holdPlacedAt" | "expiredBacking">;
574
+ /** A hold whose stored share has to change for a new total. */
575
+ interface ExpiredBackingRewrite {
576
+ id: string;
577
+ expiredBacking: number;
578
+ }
579
+ /** The marked credits on an account and where they end among its open holds. */
580
+ interface ExpiredBackingAllocation {
581
+ total: number;
582
+ frontier: ExpiredBackingFrontier | null;
583
+ /** Stored shares to write with this total. Absent when none changes. */
584
+ rewrites?: ExpiredBackingRewrite[];
585
+ }
586
+ /**
587
+ * A frontier before every hold: no hold shares through it. It records that the account's
588
+ * marks are placed, so the next close does not walk the holds again.
589
+ */
590
+ declare const NO_EXPIRED_BACKING_PREFIX: ExpiredBackingFrontier;
534
591
  /**
535
- * Split `total` marked credits across open holds, returning each hold's share.
592
+ * The share a 2.2.0 hold document recorded for itself: the order of record for that hold.
593
+ * Clocks are not, since the server that placed a hold may lag the one before it. Rounded
594
+ * up, like every amount that stands against the balance, and never above the hold. Zero
595
+ * means no record, which leaves the hold to the frontier.
596
+ */
597
+ declare function recordedExpiredBacking(hold: ExpiredBackingHold): number;
598
+ /**
599
+ * Place `total` marked credits on the open holds, so that what each hold takes does not
600
+ * depend on the order they close in.
601
+ *
602
+ * Shares a 2.2.0 hold document stored stand as recorded: they need not be a prefix of
603
+ * anything, and they sit on documents that already exist. The rest goes to the other
604
+ * holds oldest first, each up to its amount. That part is a prefix, so one
605
+ * {@link ExpiredBackingFrontier} on the account describes it and no hold document changes
606
+ * however many there are. A new hold carries no share, so no record is ever added.
536
607
  *
537
- * Shares a hold already has are kept first, oldest hold first, so lowering `total`
538
- * trims the newest and raising it never moves credits off a hold. Only what is left
539
- * goes to holds with room, again oldest first, which makes the split a function of
540
- * the holds and not of the order they later close in. A hold never carries more than
541
- * its amount. A row without `holdPlacedAt` is a record, not a hold, and gets nothing.
608
+ * A total below the records trims them newest first, and only when every other hold has
609
+ * nothing, and a total the other holds cannot take fills what the records left free. Those
610
+ * are the only hold writes: `rewrites`, at most one per record, each record being an open
611
+ * hold from before the upgrade.
542
612
  *
543
- * The account's `expiredHoldBacking` is written as the sum of these shares, in the
544
- * same write, so what `total` could not attribute is not marked. Such credits stay out
545
- * of reach anyway, because the `reserved` that kept them still counts against them.
613
+ * Marks no hold can carry stay marked. They were out of reach before this ran, and
614
+ * dropping them would make them spendable; the next absolute balance write replaces
615
+ * them. A row without `holdPlacedAt` is a record, not a hold, and a row with an unusable
616
+ * amount cannot close, so neither takes a share.
546
617
  */
547
- declare function attributeExpiredBacking(holds: readonly ExpiredBackingHold[], total: number): Map<string, number>;
618
+ declare function allocateExpiredBacking(holds: readonly ExpiredBackingHold[], total: number): ExpiredBackingAllocation;
548
619
  /**
549
- * The field to write for a new backing value: nothing when it was and stays 0, so
550
- * an account that never had any keeps a document without the field.
620
+ * This hold's share of the marked credits: its own record if it has one, otherwise where
621
+ * it falls against the account's frontier. Pass the `rewrites` of an allocation made in
622
+ * this same step, since the document does not hold them yet.
551
623
  */
552
- declare function expiredHoldBackingPatch(next: number, stored?: number): {
624
+ declare function holdExpiredBacking(hold: ExpiredBackingHold, frontier?: ExpiredBackingFrontier | null, rewrites?: readonly ExpiredBackingRewrite[]): number;
625
+ /**
626
+ * When a new hold counts as placed: `now`, or just after the frontier if this server's
627
+ * clock is behind the server that placed it. A hold that sorted before the frontier
628
+ * would read as fully marked, taking expired credits that belong to older holds.
629
+ */
630
+ declare function holdPlacedAtAfter(now: string, frontier?: ExpiredBackingFrontier | null): string;
631
+ /** The account fields that hold the marked credits. */
632
+ interface ExpiredBackingFields {
553
633
  expiredHoldBacking?: number;
554
- };
634
+ expiredBackingFrontier?: ExpiredBackingFrontier | null;
635
+ }
636
+ /**
637
+ * The fields to write for a new marked total: nothing when it was and stays 0, so an
638
+ * account that never had any keeps a document without the fields. A stored value that
639
+ * is not the new one is overwritten, which is what repairs a bad one. A `frontier` left
640
+ * undefined keeps the stored one.
641
+ */
642
+ declare function expiredBackingPatch(next: {
643
+ total: number;
644
+ frontier?: ExpiredBackingFrontier | null;
645
+ }, stored: ExpiredBackingFields): ExpiredBackingFields;
555
646
  /**
556
647
  * Validate a balance target supplied by the application: a cap or the result of
557
648
  * a monthly-reset decision. It must be storable and cannot be negative.
@@ -781,4 +872,4 @@ declare function releaseReservationWithJournal(repository: ICreditRepository, us
781
872
  */
782
873
  declare function reserveCreditsForOperation(repository: ICreditRepository, userId: string, amount: number, operationType: string, expiryMs?: number, idempotencyKey?: string): Promise<PortableReservation>;
783
874
 
784
- export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, type CreditErrorCodeType, CreditSource, JournalReferenceType, type PaymentEventPayload, PortableReservation, PortableTransaction, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, type ReservationIntegrityView, ReservationStatus, type StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, TerminalReservationStatus, 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 };
875
+ export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, type CreditErrorCodeType, CreditSource, type ExpiredBackingAllocation, type ExpiredBackingFields, ExpiredBackingFrontier, type ExpiredBackingHold, type ExpiredBackingRewrite, JournalReferenceType, NO_EXPIRED_BACKING_PREFIX, type PaymentEventPayload, PortableReservation, PortableTransaction, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, type ReservationIntegrityView, ReservationStatus, type StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, TerminalReservationStatus, 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 };
@@ -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",
@@ -809,17 +907,25 @@ var DEFAULT_CONFIG = {
809
907
  }
810
908
  };
811
909
  var currentConfig = DEFAULT_CONFIG;
910
+ function isOperationCostMap(value) {
911
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
912
+ return Object.values(value).every((cost) => isRepresentableAmount(cost) && cost >= 0);
913
+ }
812
914
  function loadConfigFromEnv() {
813
915
  const envConfig = {};
814
916
  const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;
815
917
  if (operationCostsEnv) {
816
918
  try {
817
919
  const parsed = JSON.parse(operationCostsEnv);
818
- if (typeof parsed === "object" && parsed !== null) {
920
+ if (isOperationCostMap(parsed)) {
819
921
  envConfig.operationCosts = {
820
922
  ...DEFAULT_CONFIG.operationCosts,
821
923
  ...parsed
822
924
  };
925
+ } else {
926
+ console.warn(
927
+ "[Credits Config] Ignoring CREDITS_OPERATION_COSTS: it must be a JSON object whose costs are numbers >= 0, exact to the cent"
928
+ );
823
929
  }
824
930
  } catch {
825
931
  console.warn("[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON");
@@ -1074,9 +1180,11 @@ export {
1074
1180
  CREDIT_AMOUNT_SCALE,
1075
1181
  CreditError,
1076
1182
  CreditErrorCode,
1183
+ NO_EXPIRED_BACKING_PREFIX,
1077
1184
  RESERVATION_STATUSES,
1078
1185
  RESERVED_JOURNAL_KEY_PREFIX,
1079
1186
  TERMINAL_RESERVATION_STATUSES,
1187
+ allocateExpiredBacking,
1080
1188
  assertDirectStatusWriteAllowed,
1081
1189
  assertHoldPlaced,
1082
1190
  assertKnownReservationStatus,
@@ -1092,9 +1200,9 @@ export {
1092
1200
  assertValidStoredAmount,
1093
1201
  assertValidStoredAmountRaw,
1094
1202
  assertValidTierBalanceInput,
1095
- attributeExpiredBacking,
1096
1203
  backedBalanceFloor,
1097
1204
  calculateAvailableCredits,
1205
+ canonicalAmount,
1098
1206
  cappedBalance,
1099
1207
  classifyDatabaseError,
1100
1208
  commitReservationWithJournal,
@@ -1111,10 +1219,13 @@ export {
1111
1219
  createUnsupportedOperationError,
1112
1220
  createUserNotFoundError,
1113
1221
  describePaymentMismatch,
1114
- expiredHoldBackingPatch,
1222
+ expiredBackingPatch,
1115
1223
  genericDeferred,
1116
1224
  getSqlState,
1117
1225
  hasPlacedHold,
1226
+ heldExpiredBacking,
1227
+ holdExpiredBacking,
1228
+ holdPlacedAtAfter,
1118
1229
  isCreditError,
1119
1230
  isCreditedOutcome,
1120
1231
  isIdempotencyConflictError,
@@ -1130,17 +1241,21 @@ export {
1130
1241
  isValidCreditAmount,
1131
1242
  isValidIdempotencyKey,
1132
1243
  isWinningOutcome,
1244
+ multiplyAmount,
1133
1245
  noopDeferred,
1134
1246
  normalizePaymentRef,
1135
1247
  numericToCents,
1136
1248
  planBalanceCap,
1137
1249
  planBalanceTarget,
1138
1250
  planHoldCommit,
1251
+ recordedExpiredBacking,
1139
1252
  releaseReservationWithJournal,
1140
1253
  reserveCreditsForOperation,
1141
1254
  sameAmount,
1142
1255
  settleExpiredHoldBacking,
1143
1256
  spendableBalance,
1257
+ storedAmountCeil,
1258
+ storedAmountFloor,
1144
1259
  storedMonthlyLimit,
1145
1260
  sumAmounts,
1146
1261
  synchronousDeferred,