@nehorai/credits 2.1.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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/index.cjs +3 -2
  3. package/dist/adapters/index.cjs.map +1 -1
  4. package/dist/adapters/index.d.cts +2 -2
  5. package/dist/adapters/index.d.ts +2 -2
  6. package/dist/adapters/index.js +3 -2
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/core/index.cjs +218 -2
  9. package/dist/core/index.cjs.map +1 -1
  10. package/dist/core/index.d.cts +183 -5
  11. package/dist/core/index.d.ts +183 -5
  12. package/dist/core/index.js +199 -2
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/index.cjs +477 -59
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +6 -6
  17. package/dist/index.d.ts +6 -6
  18. package/dist/index.js +456 -59
  19. package/dist/index.js.map +1 -1
  20. package/dist/repository/index.cjs +394 -44
  21. package/dist/repository/index.cjs.map +1 -1
  22. package/dist/repository/index.d.cts +36 -7
  23. package/dist/repository/index.d.ts +36 -7
  24. package/dist/repository/index.js +391 -43
  25. package/dist/repository/index.js.map +1 -1
  26. package/dist/sdk/index.cjs +1 -0
  27. package/dist/sdk/index.cjs.map +1 -1
  28. package/dist/sdk/index.d.cts +2 -2
  29. package/dist/sdk/index.d.ts +2 -2
  30. package/dist/sdk/index.js +1 -0
  31. package/dist/sdk/index.js.map +1 -1
  32. package/dist/service/index.cjs +33 -20
  33. package/dist/service/index.cjs.map +1 -1
  34. package/dist/service/index.d.cts +2 -2
  35. package/dist/service/index.d.ts +2 -2
  36. package/dist/service/index.js +33 -20
  37. package/dist/service/index.js.map +1 -1
  38. package/dist/{types-Dzc443vy.d.cts → types-C3sxDvNb.d.cts} +43 -2
  39. package/dist/{types-Dzc443vy.d.ts → types-C3sxDvNb.d.ts} +43 -2
  40. package/dist/{types-BhCdep_t.d.ts → types-CY-oHIMA.d.ts} +55 -4
  41. package/dist/{types-wtksMa4n.d.cts → types-DOjguhEX.d.cts} +55 -4
  42. package/package.json +70 -71
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);
@@ -219,6 +253,150 @@ function backedBalanceFloor(reserved, bonusCredits) {
219
253
  assertRepresentableAmount(floor, "backedBalanceFloor", { reserved, bonusCredits });
220
254
  return floor;
221
255
  }
256
+ function cappedBalance(balance, cap, reserved, bonusCredits) {
257
+ return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
258
+ }
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));
265
+ }
266
+ function planBalanceTarget(target, reserved, bonusCredits) {
267
+ assertValidBalanceTarget(target, "balance target", { reserved, bonusCredits });
268
+ const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
269
+ return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
270
+ }
271
+ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking) {
272
+ const spendable = spendableBalance(balance, expiredHoldBacking);
273
+ const target = Math.min(spendable, cap);
274
+ const kept = Math.max(
275
+ sumAmounts(balance, -spendable),
276
+ sumAmounts(backedBalanceFloor(reserved, bonusCredits), -target)
277
+ );
278
+ const next = Math.min(balance, sumAmounts(target, kept));
279
+ return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
280
+ }
281
+ function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
282
+ const held = heldExpiredBacking(balance, expiredHoldBacking);
283
+ const used = Math.max(0, Math.min(held, holdBacking));
284
+ return { used, remaining: sumAmounts(held, -used) };
285
+ }
286
+ function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
287
+ const backing = settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking);
288
+ const rest = sumAmounts(amount, -backing.used);
289
+ const fromSpendable = Math.min(spendableBalance(balance, expiredHoldBacking), rest);
290
+ const fromBonus = sumAmounts(rest, -fromSpendable);
291
+ return {
292
+ covered: fromBonus <= bonusCredits,
293
+ balance: sumAmounts(balance, -backing.used, -fromSpendable),
294
+ bonusCredits: sumAmounts(bonusCredits, -fromBonus),
295
+ expiredHoldBacking: backing.remaining
296
+ };
297
+ }
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);
327
+ }
328
+ let frontier = NO_EXPIRED_BACKING_PREFIX;
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;
338
+ const share = shares.get(hold.id);
339
+ const added = Math.min(sumAmounts(hold.amount, -share), left);
340
+ shares.set(hold.id, sumAmounts(share, added));
341
+ left = sumAmounts(left, -added);
342
+ }
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 } : {} };
345
+ }
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;
375
+ }
376
+ function assertValidBalanceTarget(value, field, context) {
377
+ assertRepresentableAmount(value, field, context);
378
+ if (value < 0) {
379
+ throw new CreditError(
380
+ `${field} cannot be negative (got ${value})`,
381
+ CreditErrorCode.INVALID_AMOUNT,
382
+ { field, value, ...context }
383
+ );
384
+ }
385
+ }
386
+ function assertValidTierBalanceInput(input, context) {
387
+ if (input.balanceCap === void 0) {
388
+ if (input.balance !== void 0) assertValidBalanceTarget(input.balance, "balance", context);
389
+ return;
390
+ }
391
+ if (input.balance !== void 0) {
392
+ throw new CreditError(
393
+ "A tier update cannot set both balance and balanceCap",
394
+ CreditErrorCode.INVALID_AMOUNT,
395
+ { balance: input.balance, balanceCap: input.balanceCap, ...context }
396
+ );
397
+ }
398
+ assertValidBalanceTarget(input.balanceCap, "balanceCap", context);
399
+ }
222
400
  function assertRepresentableTierAmount(value, field, context) {
223
401
  if (value === Number.POSITIVE_INFINITY) return;
224
402
  assertRepresentableAmount(value, field, context);
@@ -627,9 +805,9 @@ var creditSystemConfigSchema = z.object({
627
805
  */
628
806
  reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
629
807
  /**
630
- * Default credits for new users (free tier)
808
+ * Default credits for new users (free tier). 0 means no signup grant.
631
809
  */
632
- defaultFreeCredits: z.number().positive().default(25),
810
+ defaultFreeCredits: z.number().min(0).default(25),
633
811
  /**
634
812
  * Grace period in days after subscription expires before downgrade
635
813
  */
@@ -929,6 +1107,8 @@ function toClientUserCredits(credits) {
929
1107
  monthlyUsed: credits.monthlyUsed,
930
1108
  monthlyResetAt: toISOString(credits.monthlyResetAt),
931
1109
  subscriptionExpiresAt: credits.subscriptionExpiresAt ? toISOString(credits.subscriptionExpiresAt) : null,
1110
+ ...credits.expiredHoldBacking !== void 0 ? { expiredHoldBacking: credits.expiredHoldBacking } : {},
1111
+ ...credits.signupGrant !== void 0 ? { signupGrant: credits.signupGrant } : {},
932
1112
  createdAt: toISOString(credits.createdAt),
933
1113
  updatedAt: toISOString(credits.updatedAt)
934
1114
  };
@@ -1160,11 +1340,36 @@ function toDate2(value) {
1160
1340
  return /* @__PURE__ */ new Date();
1161
1341
  }
1162
1342
  function getNextMonthlyReset(from = /* @__PURE__ */ new Date()) {
1163
- const next = new Date(from);
1164
- next.setMonth(next.getMonth() + 1);
1165
- next.setDate(1);
1166
- next.setHours(0, 0, 0, 0);
1167
- return next;
1343
+ return new Date(from.getFullYear(), from.getMonth() + 1, 1);
1344
+ }
1345
+ function validateBalanceUpdate(current, updates) {
1346
+ const next = (base, absolute, increment) => sumAmounts(absolute ?? base, increment ?? 0);
1347
+ const balance = next(current.balance, updates.balance, updates.balanceIncrement);
1348
+ const bonusCredits = next(
1349
+ current.bonusCredits ?? 0,
1350
+ updates.bonusCredits,
1351
+ updates.bonusCreditsIncrement
1352
+ );
1353
+ const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
1354
+ const errors = [];
1355
+ if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
1356
+ const marked = updates.balance === void 0 ? heldExpiredBacking(current.balance, current.expiredHoldBacking) : 0;
1357
+ if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
1358
+ errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
1359
+ }
1360
+ if (bonusCredits < 0) errors.push(`BonusCredits cannot be negative: ${bonusCredits}`);
1361
+ if (reserved < 0) errors.push(`Reserved cannot be negative: ${reserved}`);
1362
+ const covering = sumAmounts(balance, bonusCredits);
1363
+ if (reserved > covering) {
1364
+ errors.push(`Reserved (${reserved}) exceeds balance + bonusCredits (${covering})`);
1365
+ }
1366
+ return { valid: errors.length === 0, errors };
1367
+ }
1368
+ function assertValidBalanceUpdate(current, updates) {
1369
+ const result = validateBalanceUpdate(current, updates);
1370
+ if (!result.valid) {
1371
+ throw new Error(`Invalid balance update: ${result.errors.join("; ")}`);
1372
+ }
1168
1373
  }
1169
1374
 
1170
1375
  // src/repository/memory/snapshot.ts
@@ -1300,6 +1505,7 @@ function commitTransitionJournal(store, input, existingId) {
1300
1505
  userId: input.userId,
1301
1506
  entryType: input.entryType,
1302
1507
  amount: input.amount,
1508
+ balanceBefore: input.balanceBefore,
1303
1509
  balanceAfter: input.balanceAfter,
1304
1510
  source: input.source,
1305
1511
  referenceId: input.reservationId,
@@ -1344,6 +1550,34 @@ function assertTrustworthy(reservation, transition) {
1344
1550
  assertLockedAmount(reservation, transition);
1345
1551
  assertHoldPlaced(reservation, transition);
1346
1552
  }
1553
+ function openHolds(store, userId) {
1554
+ return [...store.reservations.get(userId)?.values() ?? []].filter(
1555
+ (reservation) => reservation.status === "reserved"
1556
+ );
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
+ }
1571
+ function writeExpiredHoldBacking(store, credits, next) {
1572
+ if (next <= 0) {
1573
+ Object.assign(credits, expiredBackingPatch({ total: 0 }, credits));
1574
+ return;
1575
+ }
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));
1580
+ }
1347
1581
  function getReservation(store, userId, reservationId) {
1348
1582
  return store.reservations.get(userId)?.get(reservationId);
1349
1583
  }
@@ -1391,6 +1625,7 @@ async function reserveCreditsV2(store, input) {
1391
1625
  };
1392
1626
  }
1393
1627
  const now = (/* @__PURE__ */ new Date()).toISOString();
1628
+ const backing = expiredBackingInForce(store, credits);
1394
1629
  const reservation = {
1395
1630
  id: generateId(),
1396
1631
  userId: input.userId,
@@ -1403,7 +1638,7 @@ async function reserveCreditsV2(store, input) {
1403
1638
  // The hold-origin fact. Written here and nowhere else, in the same
1404
1639
  // critical section that raises `reserved` below, so the row and the hold
1405
1640
  // become visible together. `createReservation` cannot set it.
1406
- holdPlacedAt: now
1641
+ holdPlacedAt: holdPlacedAtAfter(now, backing.total > 0 ? backing.frontier : null)
1407
1642
  };
1408
1643
  if (!store.reservations.has(input.userId)) {
1409
1644
  store.reservations.set(input.userId, /* @__PURE__ */ new Map());
@@ -1421,6 +1656,8 @@ async function reserveCreditsV2(store, input) {
1421
1656
  );
1422
1657
  }
1423
1658
  credits.reserved = nextReserved;
1659
+ writeRecordedShares(store, input.userId, backing);
1660
+ Object.assign(credits, expiredBackingPatch(backing, credits));
1424
1661
  credits.updatedAt = now;
1425
1662
  return { outcome: "created", reservation: copyRecord(reservation) };
1426
1663
  });
@@ -1441,22 +1678,30 @@ async function commitReservationV2(store, userId, reservationId, options) {
1441
1678
  const amount = reservation.amount;
1442
1679
  await yieldPoint(store);
1443
1680
  assertReservedCoversHold(credits, userId, reservationId, amount);
1444
- if (sumAmounts(credits.balance, credits.bonusCredits) < amount) {
1681
+ const backing = expiredBackingInForce(store, credits);
1682
+ const paid = planHoldCommit(
1683
+ credits.balance,
1684
+ credits.bonusCredits,
1685
+ backing.total,
1686
+ holdExpiredBacking(reservation, backing.frontier, backing.rewrites),
1687
+ amount
1688
+ );
1689
+ if (!paid.covered) {
1445
1690
  throw new CreditError(
1446
1691
  `Insufficient credits to commit reservation ${reservationId}`,
1447
1692
  CreditErrorCode.INSUFFICIENT_CREDITS,
1448
1693
  { userId, reservationId, required: amount }
1449
1694
  );
1450
1695
  }
1451
- const fromBalance = Math.min(credits.balance, amount);
1452
- const nextBalance = sumAmounts(credits.balance, -fromBalance);
1453
- const nextBonusCredits = sumAmounts(credits.bonusCredits, -sumAmounts(amount, -fromBalance));
1696
+ const nextBalance = paid.balance;
1697
+ const nextBonusCredits = paid.bonusCredits;
1454
1698
  const balanceAfter = sumAmounts(nextBalance, nextBonusCredits);
1455
1699
  const journal = {
1456
1700
  userId,
1457
1701
  operation: "commitReservation",
1458
1702
  entryType: "debit",
1459
1703
  amount,
1704
+ balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
1460
1705
  balanceAfter,
1461
1706
  source: "operation_commit",
1462
1707
  reservationId,
@@ -1484,6 +1729,11 @@ async function commitReservationV2(store, userId, reservationId, options) {
1484
1729
  credits.bonusCredits = nextBonusCredits;
1485
1730
  credits.reserved = nextReserved;
1486
1731
  credits.monthlyUsed = nextMonthlyUsed;
1732
+ writeRecordedShares(store, userId, backing);
1733
+ Object.assign(
1734
+ credits,
1735
+ expiredBackingPatch({ total: paid.expiredHoldBacking, frontier: backing.frontier }, credits)
1736
+ );
1487
1737
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1488
1738
  const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
1489
1739
  return {
@@ -1563,19 +1813,28 @@ function finishUnspent(store, reservation, transition) {
1563
1813
  reservation.id,
1564
1814
  reservation.amount
1565
1815
  );
1816
+ const inForce = expiredBackingInForce(store, credits);
1817
+ const backing = settleExpiredHoldBacking(
1818
+ credits.balance,
1819
+ inForce.total,
1820
+ holdExpiredBacking(reservation, inForce.frontier, inForce.rewrites)
1821
+ );
1822
+ const nextBalance = sumAmounts(credits.balance, -backing.used);
1566
1823
  const journal = {
1567
1824
  userId: reservation.userId,
1568
1825
  operation: `${transition.transition}Reservation`,
1569
1826
  entryType: "credit",
1570
1827
  amount: 0,
1571
- balanceAfter: sumAmounts(credits.balance, credits.bonusCredits),
1828
+ balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
1829
+ balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
1572
1830
  source: transition.source,
1573
1831
  reservationId: reservation.id,
1574
1832
  description: transition.options?.description ?? `${transition.verb} ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
1575
1833
  metadata: {
1576
1834
  ...transition.options?.metadata,
1577
1835
  operationType: reservation.operationType,
1578
- amount: reservation.amount
1836
+ amount: reservation.amount,
1837
+ ...backing.used > 0 ? { expiredCreditsRemoved: backing.used } : {}
1579
1838
  },
1580
1839
  idempotencyKey: reservationJournalKey(reservation.id, transition.transition)
1581
1840
  };
@@ -1588,6 +1847,12 @@ function finishUnspent(store, reservation, transition) {
1588
1847
  reservation.status = transition.status;
1589
1848
  reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1590
1849
  credits.reserved = nextReserved;
1850
+ credits.balance = nextBalance;
1851
+ writeRecordedShares(store, reservation.userId, inForce);
1852
+ Object.assign(
1853
+ credits,
1854
+ expiredBackingPatch({ total: backing.remaining, frontier: inForce.frontier }, credits)
1855
+ );
1591
1856
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1592
1857
  return commitTransitionJournal(store, journal, existingEntryId);
1593
1858
  }
@@ -1598,6 +1863,10 @@ function isoOf(value) {
1598
1863
  }
1599
1864
  var InMemoryCreditRepository = class {
1600
1865
  store = new MemoryStore();
1866
+ options;
1867
+ constructor(options = {}) {
1868
+ this.options = options;
1869
+ }
1601
1870
  // ==================== User Credits ====================
1602
1871
  /**
1603
1872
  * A copy, never the live record.
@@ -1614,7 +1883,8 @@ var InMemoryCreditRepository = class {
1614
1883
  return copyRecord(credits) ?? null;
1615
1884
  }
1616
1885
  async initializeUserCredits(userId, tier, initialBalance) {
1617
- return copyRecord(this.seedUser(userId, tier, initialBalance));
1886
+ const existing = this.store.users.get(userId);
1887
+ return copyRecord(existing ?? this.seedUser(userId, tier, initialBalance));
1618
1888
  }
1619
1889
  /**
1620
1890
  * The synchronous body of `initializeUserCredits`.
@@ -1625,7 +1895,7 @@ var InMemoryCreditRepository = class {
1625
1895
  * simultaneous first deliveries of the same reference would then both credit.
1626
1896
  */
1627
1897
  seedUser(userId, tier, initialBalance) {
1628
- assertRepresentableAmount(initialBalance, "initialBalance", { userId });
1898
+ assertValidBalanceTarget(initialBalance, "initialBalance", { userId });
1629
1899
  const configured = getConfigMonthlyLimit(tier);
1630
1900
  assertRepresentableTierAmount(configured, "monthlyLimit", { userId, tier });
1631
1901
  const monthlyLimit = storedMonthlyLimit(configured);
@@ -1665,8 +1935,11 @@ var InMemoryCreditRepository = class {
1665
1935
  if (!credits) {
1666
1936
  throw new Error(`User ${userId} not found`);
1667
1937
  }
1938
+ assertValidBalanceUpdate(credits, updates);
1668
1939
  const next = { ...credits };
1669
- if (updates.balance !== void 0) next.balance = updates.balance;
1940
+ if (updates.balance !== void 0) {
1941
+ next.balance = updates.balance;
1942
+ }
1670
1943
  if (updates.bonusCredits !== void 0) next.bonusCredits = updates.bonusCredits;
1671
1944
  if (updates.reserved !== void 0) next.reserved = updates.reserved;
1672
1945
  if (updates.tier !== void 0) next.tier = updates.tier;
@@ -1703,6 +1976,11 @@ var InMemoryCreditRepository = class {
1703
1976
  userId,
1704
1977
  operation: "updateUserCredits"
1705
1978
  });
1979
+ writeExpiredHoldBacking(
1980
+ this.store,
1981
+ next,
1982
+ updates.balance !== void 0 ? 0 : Math.min(heldExpiredBacking(credits.balance, credits.expiredHoldBacking), next.balance)
1983
+ );
1706
1984
  next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1707
1985
  this.store.users.set(userId, next);
1708
1986
  }
@@ -1711,18 +1989,34 @@ var InMemoryCreditRepository = class {
1711
1989
  { monthlyLimit: input.monthlyLimit, balance: input.balance, monthlyUsed: input.monthlyUsed },
1712
1990
  { userId, operation: "updateUserTier" }
1713
1991
  );
1992
+ assertValidTierBalanceInput(input, { userId, operation: "updateUserTier" });
1714
1993
  const credits = this.store.users.get(userId);
1715
1994
  if (!credits) {
1716
1995
  throw new Error(`User ${userId} not found`);
1717
1996
  }
1997
+ if (input.balanceCap !== void 0 && !isFreeTier(credits.tier)) {
1998
+ const capped = planBalanceCap(
1999
+ credits.balance,
2000
+ input.balanceCap,
2001
+ credits.reserved,
2002
+ credits.bonusCredits,
2003
+ credits.expiredHoldBacking
2004
+ );
2005
+ credits.balance = capped.balance;
2006
+ writeExpiredHoldBacking(this.store, credits, capped.expiredHoldBacking);
2007
+ }
1718
2008
  credits.tier = input.tier;
1719
2009
  credits.monthlyLimit = input.monthlyLimit;
1720
2010
  if (input.balance !== void 0) {
1721
- credits.balance = Math.max(
1722
- input.balance,
1723
- backedBalanceFloor(credits.reserved, credits.bonusCredits)
1724
- );
2011
+ const planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);
2012
+ credits.balance = planned.balance;
2013
+ writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
1725
2014
  }
2015
+ writeExpiredHoldBacking(
2016
+ this.store,
2017
+ credits,
2018
+ heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
2019
+ );
1726
2020
  if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
1727
2021
  if (input.subscriptionExpiresAt !== void 0) {
1728
2022
  if (input.subscriptionExpiresAt === null) {
@@ -1884,6 +2178,11 @@ var InMemoryCreditRepository = class {
1884
2178
  { userId, operation: "addCredits" }
1885
2179
  );
1886
2180
  credits.bonusCredits = nextBonusCredits;
2181
+ writeExpiredHoldBacking(
2182
+ this.store,
2183
+ credits,
2184
+ heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
2185
+ );
1887
2186
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1888
2187
  this.store.users.set(userId, credits);
1889
2188
  const transaction = this.recordTransaction({
@@ -1903,6 +2202,7 @@ var InMemoryCreditRepository = class {
1903
2202
  userId,
1904
2203
  entryType: "credit",
1905
2204
  amount,
2205
+ balanceBefore: previousTotal,
1906
2206
  balanceAfter: newTotal,
1907
2207
  source: payload.source,
1908
2208
  referenceId: transaction.id ?? paymentRef ?? "unknown",
@@ -1936,19 +2236,23 @@ var InMemoryCreditRepository = class {
1936
2236
  });
1937
2237
  }
1938
2238
  }
1939
- async deductCreditsAtomic(userId, amount) {
2239
+ async deductCreditsAtomic(userId, amount, journal) {
1940
2240
  assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
1941
2241
  const credits = this.store.users.get(userId);
1942
2242
  if (!credits) {
1943
2243
  throw new Error(`User credits not found for userId: ${userId}`);
1944
2244
  }
1945
- const available = sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved);
2245
+ const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
2246
+ const available = Math.min(
2247
+ sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved),
2248
+ sumAmounts(spendable, credits.bonusCredits)
2249
+ );
1946
2250
  if (available < amount) {
1947
2251
  throw new Error(
1948
2252
  `Insufficient credits. Available: ${available}, requested: ${amount}`
1949
2253
  );
1950
2254
  }
1951
- const balanceDeduction = Math.min(credits.balance, amount);
2255
+ const balanceDeduction = Math.min(spendable, amount);
1952
2256
  const bonusDeduction = sumAmounts(amount, -balanceDeduction);
1953
2257
  const previousBalance = sumAmounts(credits.balance, credits.bonusCredits);
1954
2258
  const nextBalance = sumAmounts(credits.balance, -balanceDeduction);
@@ -1958,11 +2262,26 @@ var InMemoryCreditRepository = class {
1958
2262
  { previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },
1959
2263
  { userId, operation: "deductCredits" }
1960
2264
  );
2265
+ const held = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
1961
2266
  credits.balance = nextBalance;
1962
2267
  credits.bonusCredits = nextBonusCredits;
2268
+ writeExpiredHoldBacking(this.store, credits, held);
1963
2269
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1964
2270
  this.store.users.set(userId, credits);
1965
- 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 };
1966
2285
  }
1967
2286
  // ==================== Transactions ====================
1968
2287
  async createTransaction(input) {
@@ -2076,6 +2395,11 @@ var InMemoryCreditRepository = class {
2076
2395
  assertRepresentableAmount(input.balanceAfter, "journal balanceAfter", {
2077
2396
  userId: input.userId
2078
2397
  });
2398
+ if (input.balanceBefore !== void 0) {
2399
+ assertRepresentableAmount(input.balanceBefore, "journal balanceBefore", {
2400
+ userId: input.userId
2401
+ });
2402
+ }
2079
2403
  assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });
2080
2404
  assertPublicJournalKey(input.idempotencyKey, input.userId);
2081
2405
  const key = input.idempotencyKey !== void 0 ? scopedKey(input.userId, input.idempotencyKey) : void 0;
@@ -2091,6 +2415,7 @@ var InMemoryCreditRepository = class {
2091
2415
  userId: input.userId,
2092
2416
  entryType: input.entryType,
2093
2417
  amount: input.amount,
2418
+ ...input.balanceBefore !== void 0 ? { balanceBefore: input.balanceBefore } : {},
2094
2419
  balanceAfter: input.balanceAfter,
2095
2420
  source: input.source,
2096
2421
  referenceId: input.referenceId,
@@ -2185,7 +2510,11 @@ var InMemoryCreditRepository = class {
2185
2510
  return { expiredCount, creditsReleased, errors };
2186
2511
  }
2187
2512
  // ==================== Atomic Monthly Reset ====================
2188
- async atomicMonthlyReset(userId, tier, expectedResetAt) {
2513
+ /**
2514
+ * @param _tier - Not trusted. The caller read it earlier and the account may
2515
+ * have changed tier since, so the reset uses the tier stored on the account.
2516
+ */
2517
+ async atomicMonthlyReset(userId, _tier, expectedResetAt) {
2189
2518
  const credits = this.store.users.get(userId);
2190
2519
  if (!credits) {
2191
2520
  throw new Error(`User ${userId} not found`);
@@ -2195,17 +2524,41 @@ var InMemoryCreditRepository = class {
2195
2524
  if (currentResetAt !== expected) {
2196
2525
  return { wasReset: false, credits: { ...credits } };
2197
2526
  }
2198
- assertRepresentableTierAmount(getConfigMonthlyLimit(tier), "monthlyLimit", { userId, tier });
2527
+ const tier = credits.tier;
2528
+ const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
2529
+ const decision = this.options.resolveMonthlyReset?.({ tier, balance: spendable });
2530
+ if (decision === null) {
2531
+ return { wasReset: false, credits: { ...credits } };
2532
+ }
2533
+ if (decision) {
2534
+ assertValidBalanceTarget(decision.balance, "resolveMonthlyReset balance", { userId, tier });
2535
+ if (decision.monthlyLimit !== void 0) {
2536
+ assertValidBalanceTarget(decision.monthlyLimit, "resolveMonthlyReset monthlyLimit", {
2537
+ userId,
2538
+ tier
2539
+ });
2540
+ }
2541
+ } else {
2542
+ assertRepresentableTierAmount(getConfigMonthlyLimit(tier), "monthlyLimit", { userId, tier });
2543
+ }
2199
2544
  const nextReset = getNextMonthlyReset();
2200
- const target = monthlyResetBalance(tier);
2201
- const floor = backedBalanceFloor(credits.reserved, credits.bonusCredits);
2202
2545
  const previousBalance = credits.balance;
2203
- const newBalance = target.kind === "atLeast" ? Math.max(credits.balance, target.value, floor) : Math.max(target.value, floor);
2546
+ let target;
2547
+ if (decision) {
2548
+ target = decision.balance;
2549
+ } else {
2550
+ const configured = monthlyResetBalance(tier);
2551
+ target = configured.kind === "atLeast" ? Math.max(spendable, configured.value) : configured.value;
2552
+ }
2553
+ const planned = planBalanceTarget(target, credits.reserved, credits.bonusCredits);
2554
+ const newBalance = planned.balance;
2204
2555
  const change = sumAmounts(newBalance, -previousBalance);
2205
2556
  const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
2206
2557
  assertRepresentableAmount(Math.abs(change), "journal amount", { userId });
2207
2558
  assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
2208
2559
  credits.balance = newBalance;
2560
+ writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
2561
+ if (decision?.monthlyLimit !== void 0) credits.monthlyLimit = decision.monthlyLimit;
2209
2562
  credits.monthlyUsed = 0;
2210
2563
  credits.monthlyResetAt = nextReset.toISOString();
2211
2564
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -2215,6 +2568,7 @@ var InMemoryCreditRepository = class {
2215
2568
  userId,
2216
2569
  entryType: change > 0 ? "credit" : "debit",
2217
2570
  amount: Math.abs(change),
2571
+ balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
2218
2572
  balanceAfter,
2219
2573
  source: "monthly_reset",
2220
2574
  referenceId: `reset-${Date.now()}`,
@@ -2259,23 +2613,34 @@ var InMemoryCreditRepository = class {
2259
2613
  };
2260
2614
  }
2261
2615
  const defaultTier = getDefaultTier();
2262
- const configured = getConfigMonthlyLimit(defaultTier);
2263
- assertRepresentableTierAmount(configured, "monthlyLimit", {
2264
- userId,
2265
- tier: defaultTier
2266
- });
2267
- const limit = storedMonthlyLimit(configured);
2616
+ let limit;
2617
+ if (this.options.getDowngradeBalanceCap) {
2618
+ limit = this.options.getDowngradeBalanceCap();
2619
+ assertValidBalanceTarget(limit, "getDowngradeBalanceCap", { userId, tier: defaultTier });
2620
+ } else {
2621
+ const configured = getConfigMonthlyLimit(defaultTier);
2622
+ assertRepresentableTierAmount(configured, "monthlyLimit", {
2623
+ userId,
2624
+ tier: defaultTier
2625
+ });
2626
+ limit = storedMonthlyLimit(configured);
2627
+ }
2268
2628
  const previousTier = credits.tier;
2269
2629
  const previousBalance = credits.balance;
2270
- const newBalance = Math.max(
2271
- Math.min(credits.balance, limit),
2272
- backedBalanceFloor(credits.reserved, credits.bonusCredits)
2630
+ const planned = planBalanceCap(
2631
+ credits.balance,
2632
+ limit,
2633
+ credits.reserved,
2634
+ credits.bonusCredits,
2635
+ credits.expiredHoldBacking
2273
2636
  );
2637
+ const newBalance = planned.balance;
2274
2638
  const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
2275
2639
  assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
2276
2640
  credits.tier = defaultTier;
2277
2641
  credits.monthlyLimit = limit;
2278
2642
  credits.balance = newBalance;
2643
+ writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
2279
2644
  credits.subscriptionExpiresAt = null;
2280
2645
  credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2281
2646
  this.store.users.set(userId, credits);
@@ -2284,6 +2649,7 @@ var InMemoryCreditRepository = class {
2284
2649
  entryType: "debit",
2285
2650
  amount: 0,
2286
2651
  // No credits deducted, just tier change
2652
+ balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
2287
2653
  balanceAfter,
2288
2654
  source: "subscription_downgrade",
2289
2655
  referenceId: `downgrade-${Date.now()}`,
@@ -2333,8 +2699,8 @@ var InMemoryCreditRepository = class {
2333
2699
  return copyRecords(Array.from(userReservations.values()));
2334
2700
  }
2335
2701
  };
2336
- function createInMemoryCreditRepository() {
2337
- return new InMemoryCreditRepository();
2702
+ function createInMemoryCreditRepository(options = {}) {
2703
+ return new InMemoryCreditRepository(options);
2338
2704
  }
2339
2705
 
2340
2706
  // src/auth/api-key.ts
@@ -2447,6 +2813,7 @@ var CreditsService = class {
2447
2813
  entryType: "debit",
2448
2814
  amount: 0,
2449
2815
  // No credits deducted, just tier change
2816
+ balanceBefore: sumAmounts(data.balance, data.bonusCredits),
2450
2817
  // The ledger balance, matching every other journal writer: the V2
2451
2818
  // transitions and add/deduct all record `balance + bonusCredits`, so
2452
2819
  // recording `balance` alone made the audit trail disagree with itself
@@ -2490,6 +2857,7 @@ var CreditsService = class {
2490
2857
  userId,
2491
2858
  entryType: balanceChange > 0 ? "credit" : "debit",
2492
2859
  amount: Math.abs(balanceChange),
2860
+ balanceBefore: sumAmounts(data.balance, data.bonusCredits),
2493
2861
  balanceAfter: sumAmounts(
2494
2862
  resetResult.credits.balance,
2495
2863
  resetResult.credits.bonusCredits
@@ -2497,9 +2865,11 @@ var CreditsService = class {
2497
2865
  source: "monthly_reset",
2498
2866
  referenceId: `reset-${Date.now()}`,
2499
2867
  referenceType: "reset",
2500
- description: `Monthly credit reset for ${data.tier} tier.`,
2868
+ // The tier the reset ran on, which a concurrent downgrade may have
2869
+ // changed since `data` was read.
2870
+ description: `Monthly credit reset for ${resetResult.credits.tier} tier.`,
2501
2871
  metadata: {
2502
- tier: data.tier,
2872
+ tier: resetResult.credits.tier,
2503
2873
  previousBalance: data.balance,
2504
2874
  newBalance: resetResult.credits.balance
2505
2875
  }
@@ -2722,9 +3092,18 @@ var CreditsService = class {
2722
3092
  */
2723
3093
  async deductCredits(userId, amount, options) {
2724
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
+ };
2725
3104
  let deduction;
2726
3105
  try {
2727
- deduction = await this.repository.deductCreditsAtomic(userId, amount);
3106
+ deduction = await this.repository.deductCreditsAtomic(userId, amount, journal);
2728
3107
  } catch (error) {
2729
3108
  if (isCreditError(error) && error.code === CreditErrorCode.INVALID_AMOUNT) throw error;
2730
3109
  const credits = await this.repository.getUserCredits(userId);
@@ -2740,21 +3119,18 @@ var CreditsService = class {
2740
3119
  shortfall: sumAmounts(amount, -available)
2741
3120
  };
2742
3121
  }
2743
- const operationType = options?.operationType;
2744
- await this.repository.createJournalEntry({
2745
- userId,
2746
- entryType: "debit",
2747
- amount,
2748
- balanceAfter: deduction.newBalance,
2749
- source: "operation_commit",
2750
- referenceId: options?.requestId ?? `deduct-${Date.now()}`,
2751
- referenceType: "adjustment",
2752
- description: operationType ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}` : `Deducted ${amount} credits`,
2753
- metadata: {
2754
- ...options?.metadata,
2755
- ...operationType && { operationType }
2756
- }
2757
- });
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
+ }
2758
3134
  if (operationType) {
2759
3135
  await this.logUsage({
2760
3136
  userId,
@@ -3764,6 +4140,7 @@ export {
3764
4140
  CreditsSDKError,
3765
4141
  CreditsService,
3766
4142
  InMemoryCreditRepository,
4143
+ NO_EXPIRED_BACKING_PREFIX,
3767
4144
  NetworkError,
3768
4145
  RESERVATION_STATUSES,
3769
4146
  RESERVED_JOURNAL_KEY_PREFIX,
@@ -3776,6 +4153,7 @@ export {
3776
4153
  UNLIMITED_BALANCE_SENTINEL,
3777
4154
  ValidationError,
3778
4155
  addCreditsThroughRepository,
4156
+ allocateExpiredBacking,
3779
4157
  assertDirectStatusWriteAllowed,
3780
4158
  assertHoldPlaced,
3781
4159
  assertKnownReservationStatus,
@@ -3785,12 +4163,17 @@ export {
3785
4163
  assertRepresentableTierAmount,
3786
4164
  assertUnkeyedDirectReservation,
3787
4165
  assertUnreferencedDirectTransaction,
4166
+ assertValidBalanceTarget,
4167
+ assertValidBalanceUpdate,
3788
4168
  assertValidCreditAmount,
3789
4169
  assertValidIdempotencyKey,
3790
4170
  assertValidStoredAmount,
3791
4171
  assertValidStoredAmountRaw,
4172
+ assertValidTierBalanceInput,
3792
4173
  backedBalanceFloor,
3793
4174
  calculateAvailableCredits,
4175
+ canonicalAmount,
4176
+ cappedBalance,
3794
4177
  checkAndNotifyLowBalance,
3795
4178
  classifyDatabaseError,
3796
4179
  clearConfigProvider,
@@ -3816,6 +4199,7 @@ export {
3816
4199
  createUnsupportedOperationError,
3817
4200
  createUserNotFoundError,
3818
4201
  describePaymentMismatch,
4202
+ expiredBackingPatch,
3819
4203
  generateId,
3820
4204
  generateRequestId2 as generateRequestId,
3821
4205
  genericDeferred,
@@ -3841,6 +4225,9 @@ export {
3841
4225
  getValidOperationTypes,
3842
4226
  getValidTiers,
3843
4227
  hasPlacedHold,
4228
+ heldExpiredBacking,
4229
+ holdExpiredBacking,
4230
+ holdPlacedAtAfter,
3844
4231
  initializeConfig,
3845
4232
  isCreditError,
3846
4233
  isCreditErrorMessage,
@@ -3866,6 +4253,7 @@ export {
3866
4253
  isWinningOutcome,
3867
4254
  loadConfigFromEnv,
3868
4255
  monthlyResetBalance,
4256
+ multiplyAmount,
3869
4257
  noopDeferred,
3870
4258
  normalizePaymentRef,
3871
4259
  notifySubscriptionExpired,
@@ -3874,6 +4262,10 @@ export {
3874
4262
  parseApiError,
3875
4263
  parseCreditError,
3876
4264
  parseTier,
4265
+ planBalanceCap,
4266
+ planBalanceTarget,
4267
+ planHoldCommit,
4268
+ recordedExpiredBacking,
3877
4269
  registerConfigProvider,
3878
4270
  registerNotificationHandler,
3879
4271
  releaseReservationWithJournal,
@@ -3883,6 +4275,10 @@ export {
3883
4275
  reserveThroughRepository,
3884
4276
  resetConfig,
3885
4277
  sameAmount,
4278
+ settleExpiredHoldBacking,
4279
+ spendableBalance,
4280
+ storedAmountCeil,
4281
+ storedAmountFloor,
3886
4282
  storedMonthlyLimit,
3887
4283
  sumAmounts,
3888
4284
  supportsCreditsV2,
@@ -3894,6 +4290,7 @@ export {
3894
4290
  toClientUserCredits,
3895
4291
  toDate,
3896
4292
  toPortableTimestamp,
4293
+ validateBalanceUpdate,
3897
4294
  verifyAdminApiKey,
3898
4295
  verifyBearerToken
3899
4296
  };