@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
@@ -24,9 +24,11 @@ __export(core_exports, {
24
24
  CREDIT_AMOUNT_SCALE: () => CREDIT_AMOUNT_SCALE,
25
25
  CreditError: () => CreditError,
26
26
  CreditErrorCode: () => CreditErrorCode,
27
+ NO_EXPIRED_BACKING_PREFIX: () => NO_EXPIRED_BACKING_PREFIX,
27
28
  RESERVATION_STATUSES: () => RESERVATION_STATUSES,
28
29
  RESERVED_JOURNAL_KEY_PREFIX: () => RESERVED_JOURNAL_KEY_PREFIX,
29
30
  TERMINAL_RESERVATION_STATUSES: () => TERMINAL_RESERVATION_STATUSES,
31
+ allocateExpiredBacking: () => allocateExpiredBacking,
30
32
  assertDirectStatusWriteAllowed: () => assertDirectStatusWriteAllowed,
31
33
  assertHoldPlaced: () => assertHoldPlaced,
32
34
  assertKnownReservationStatus: () => assertKnownReservationStatus,
@@ -42,9 +44,9 @@ __export(core_exports, {
42
44
  assertValidStoredAmount: () => assertValidStoredAmount,
43
45
  assertValidStoredAmountRaw: () => assertValidStoredAmountRaw,
44
46
  assertValidTierBalanceInput: () => assertValidTierBalanceInput,
45
- attributeExpiredBacking: () => attributeExpiredBacking,
46
47
  backedBalanceFloor: () => backedBalanceFloor,
47
48
  calculateAvailableCredits: () => calculateAvailableCredits,
49
+ canonicalAmount: () => canonicalAmount,
48
50
  cappedBalance: () => cappedBalance,
49
51
  classifyDatabaseError: () => classifyDatabaseError,
50
52
  commitReservationWithJournal: () => commitReservationWithJournal,
@@ -61,10 +63,13 @@ __export(core_exports, {
61
63
  createUnsupportedOperationError: () => createUnsupportedOperationError,
62
64
  createUserNotFoundError: () => createUserNotFoundError,
63
65
  describePaymentMismatch: () => describePaymentMismatch,
64
- expiredHoldBackingPatch: () => expiredHoldBackingPatch,
66
+ expiredBackingPatch: () => expiredBackingPatch,
65
67
  genericDeferred: () => genericDeferred,
66
68
  getSqlState: () => getSqlState,
67
69
  hasPlacedHold: () => hasPlacedHold,
70
+ heldExpiredBacking: () => heldExpiredBacking,
71
+ holdExpiredBacking: () => holdExpiredBacking,
72
+ holdPlacedAtAfter: () => holdPlacedAtAfter,
68
73
  isCreditError: () => isCreditError,
69
74
  isCreditedOutcome: () => isCreditedOutcome,
70
75
  isIdempotencyConflictError: () => isIdempotencyConflictError,
@@ -80,17 +85,21 @@ __export(core_exports, {
80
85
  isValidCreditAmount: () => isValidCreditAmount,
81
86
  isValidIdempotencyKey: () => isValidIdempotencyKey,
82
87
  isWinningOutcome: () => isWinningOutcome,
88
+ multiplyAmount: () => multiplyAmount,
83
89
  noopDeferred: () => noopDeferred,
84
90
  normalizePaymentRef: () => normalizePaymentRef,
85
91
  numericToCents: () => numericToCents,
86
92
  planBalanceCap: () => planBalanceCap,
87
93
  planBalanceTarget: () => planBalanceTarget,
88
94
  planHoldCommit: () => planHoldCommit,
95
+ recordedExpiredBacking: () => recordedExpiredBacking,
89
96
  releaseReservationWithJournal: () => releaseReservationWithJournal,
90
97
  reserveCreditsForOperation: () => reserveCreditsForOperation,
91
98
  sameAmount: () => sameAmount,
92
99
  settleExpiredHoldBacking: () => settleExpiredHoldBacking,
93
100
  spendableBalance: () => spendableBalance,
101
+ storedAmountCeil: () => storedAmountCeil,
102
+ storedAmountFloor: () => storedAmountFloor,
94
103
  storedMonthlyLimit: () => storedMonthlyLimit,
95
104
  sumAmounts: () => sumAmounts,
96
105
  synchronousDeferred: () => synchronousDeferred,
@@ -270,6 +279,40 @@ function floatSum(values) {
270
279
  for (const value of values) total += value;
271
280
  return total;
272
281
  }
282
+ function multiplyAmount(unit, quantity) {
283
+ const cents = toCents(unit);
284
+ const product = cents === null ? Number.NaN : cents * quantity;
285
+ if (cents === null || cents < 0 || !Number.isSafeInteger(quantity) || quantity < 0 || !Number.isSafeInteger(product) || product > MAX_CENTS) {
286
+ throw new CreditError(
287
+ `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}`,
288
+ CreditErrorCode.INVALID_AMOUNT,
289
+ { unit, quantity, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE }
290
+ );
291
+ }
292
+ return product / 100;
293
+ }
294
+ var FLOAT_NOISE_FLOOR = 1e-6;
295
+ var FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;
296
+ function floatNoise(value) {
297
+ return Math.max(FLOAT_NOISE_FLOOR, Math.abs(value) * FLOAT_NOISE_RELATIVE);
298
+ }
299
+ function canonicalAmount(value) {
300
+ if (typeof value !== "number" || !Number.isFinite(value)) return value;
301
+ const cents = Math.round(value * 100);
302
+ if (!Number.isSafeInteger(cents)) return value;
303
+ const onGrid = cents / 100 + 0;
304
+ return Math.abs(value - onGrid) < floatNoise(value) ? onGrid : value;
305
+ }
306
+ function storedAmountFloor(value) {
307
+ const canonical = canonicalAmount(value);
308
+ if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
309
+ return Math.floor(canonical * 100) / 100;
310
+ }
311
+ function storedAmountCeil(value) {
312
+ const canonical = canonicalAmount(value);
313
+ if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
314
+ return Math.ceil(canonical * 100) / 100;
315
+ }
273
316
  function isValidCreditAmount(value) {
274
317
  if (typeof value !== "number") return false;
275
318
  const cents = toCents(value);
@@ -325,14 +368,19 @@ function backedBalanceFloor(reserved, bonusCredits) {
325
368
  function cappedBalance(balance, cap, reserved, bonusCredits) {
326
369
  return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
327
370
  }
328
- function spendableBalance(balance, expiredHoldBacking = 0) {
329
- return sumAmounts(balance, -Math.min(expiredHoldBacking, balance));
371
+ function heldExpiredBacking(balance, stored) {
372
+ if (!(stored !== void 0 && stored > 0)) return 0;
373
+ return Math.max(0, Math.min(stored, balance));
374
+ }
375
+ function spendableBalance(balance, expiredHoldBacking) {
376
+ return sumAmounts(balance, -heldExpiredBacking(balance, expiredHoldBacking));
330
377
  }
331
378
  function planBalanceTarget(target, reserved, bonusCredits) {
379
+ assertValidBalanceTarget(target, "balance target", { reserved, bonusCredits });
332
380
  const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
333
381
  return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
334
382
  }
335
- function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking = 0) {
383
+ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking) {
336
384
  const spendable = spendableBalance(balance, expiredHoldBacking);
337
385
  const target = Math.min(spendable, cap);
338
386
  const kept = Math.max(
@@ -343,8 +391,8 @@ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
343
391
  return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
344
392
  }
345
393
  function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
346
- const held = Math.min(expiredHoldBacking, balance);
347
- const used = Math.min(held, holdBacking);
394
+ const held = heldExpiredBacking(balance, expiredHoldBacking);
395
+ const used = Math.max(0, Math.min(held, holdBacking));
348
396
  return { used, remaining: sumAmounts(held, -used) };
349
397
  }
350
398
  function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
@@ -359,27 +407,83 @@ function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking,
359
407
  expiredHoldBacking: backing.remaining
360
408
  };
361
409
  }
362
- function attributeExpiredBacking(holds, total) {
363
- const placed = holds.filter((hold) => hold.holdPlacedAt).sort(
364
- (a, b) => a.holdPlacedAt === b.holdPlacedAt ? a.id < b.id ? -1 : 1 : a.holdPlacedAt < b.holdPlacedAt ? -1 : 1
365
- );
366
- const shares = /* @__PURE__ */ new Map();
367
- let left = total;
368
- for (const hold of placed) {
369
- const kept = Math.min(hold.expiredBacking ?? 0, hold.amount, left);
370
- shares.set(hold.id, kept);
371
- left = sumAmounts(left, -kept);
410
+ var NO_EXPIRED_BACKING_PREFIX = {
411
+ holdPlacedAt: "",
412
+ id: "",
413
+ share: 0
414
+ };
415
+ function compareHolds(a, b) {
416
+ if (a.holdPlacedAt !== b.holdPlacedAt) return a.holdPlacedAt < b.holdPlacedAt ? -1 : 1;
417
+ if (a.id === b.id) return 0;
418
+ return a.id < b.id ? -1 : 1;
419
+ }
420
+ function recordedExpiredBacking(hold) {
421
+ const stored = hold.expiredBacking;
422
+ if (typeof stored !== "number" || !Number.isFinite(stored) || stored <= 0) return 0;
423
+ if (!isValidCreditAmount(hold.amount)) return 0;
424
+ return Math.min(storedAmountCeil(stored), hold.amount);
425
+ }
426
+ function allocateExpiredBacking(holds, total) {
427
+ const placed = holds.filter((hold) => hold.holdPlacedAt && isValidCreditAmount(hold.amount)).map((hold) => ({ ...hold, holdPlacedAt: hold.holdPlacedAt })).sort(compareHolds);
428
+ const wanted = Math.max(0, total);
429
+ if (wanted === 0) return { total: 0, frontier: null };
430
+ const recorded = placed.filter((hold) => recordedExpiredBacking(hold) > 0);
431
+ const shares = new Map(recorded.map((hold) => [hold.id, recordedExpiredBacking(hold)]));
432
+ let left = sumAmounts(wanted, ...[...shares.values()].map((share) => -share));
433
+ for (const hold of [...recorded].reverse()) {
434
+ if (left >= 0) break;
435
+ const share = shares.get(hold.id);
436
+ const cut = Math.min(share, -left);
437
+ shares.set(hold.id, sumAmounts(share, -cut));
438
+ left = sumAmounts(left, cut);
372
439
  }
440
+ let frontier = NO_EXPIRED_BACKING_PREFIX;
373
441
  for (const hold of placed) {
442
+ if (left <= 0) break;
443
+ if (shares.has(hold.id)) continue;
444
+ const share = Math.min(hold.amount, left);
445
+ frontier = { holdPlacedAt: hold.holdPlacedAt, id: hold.id, share };
446
+ left = sumAmounts(left, -share);
447
+ }
448
+ for (const hold of recorded) {
449
+ if (left <= 0) break;
374
450
  const share = shares.get(hold.id);
375
451
  const added = Math.min(sumAmounts(hold.amount, -share), left);
376
452
  shares.set(hold.id, sumAmounts(share, added));
377
453
  left = sumAmounts(left, -added);
378
454
  }
379
- return shares;
455
+ const rewrites = recorded.filter((hold) => shares.get(hold.id) !== hold.expiredBacking).map((hold) => ({ id: hold.id, expiredBacking: shares.get(hold.id) }));
456
+ return { total: wanted, frontier, ...rewrites.length > 0 ? { rewrites } : {} };
380
457
  }
381
- function expiredHoldBackingPatch(next, stored = 0) {
382
- return next > 0 || stored > 0 ? { expiredHoldBacking: next } : {};
458
+ function holdExpiredBacking(hold, frontier, rewrites) {
459
+ const rewritten = rewrites?.find((rewrite) => rewrite.id === hold.id);
460
+ const recorded = recordedExpiredBacking(
461
+ rewritten ? { ...hold, expiredBacking: rewritten.expiredBacking } : hold
462
+ );
463
+ if (recorded > 0) return recorded;
464
+ if (!frontier || !hold.holdPlacedAt) return 0;
465
+ const order = compareHolds({ holdPlacedAt: hold.holdPlacedAt, id: hold.id }, frontier);
466
+ if (order > 0) return 0;
467
+ return order < 0 ? hold.amount : Math.min(frontier.share, hold.amount);
468
+ }
469
+ function holdPlacedAtAfter(now, frontier) {
470
+ if (!frontier?.holdPlacedAt) return now;
471
+ const earliest = new Date(Date.parse(frontier.holdPlacedAt) + 1).toISOString();
472
+ return now > earliest ? now : earliest;
473
+ }
474
+ function expiredBackingPatch(next, stored) {
475
+ const total = Math.max(0, next.total);
476
+ const patch = {};
477
+ const storedTotal = stored.expiredHoldBacking;
478
+ if (total > 0 || storedTotal !== void 0 && storedTotal !== 0) {
479
+ patch.expiredHoldBacking = total;
480
+ }
481
+ if (total <= 0) {
482
+ if (stored.expiredBackingFrontier) patch.expiredBackingFrontier = null;
483
+ } else if (next.frontier) {
484
+ patch.expiredBackingFrontier = next.frontier;
485
+ }
486
+ return patch;
383
487
  }
384
488
  function assertValidBalanceTarget(value, field, context) {
385
489
  assertRepresentableAmount(value, field, context);
@@ -392,7 +496,10 @@ function assertValidBalanceTarget(value, field, context) {
392
496
  }
393
497
  }
394
498
  function assertValidTierBalanceInput(input, context) {
395
- if (input.balanceCap === void 0) return;
499
+ if (input.balanceCap === void 0) {
500
+ if (input.balance !== void 0) assertValidBalanceTarget(input.balance, "balance", context);
501
+ return;
502
+ }
396
503
  if (input.balance !== void 0) {
397
504
  throw new CreditError(
398
505
  "A tier update cannot set both balance and balanceCap",
@@ -1178,9 +1285,11 @@ async function reserveCreditsForOperation(repository, userId, amount, operationT
1178
1285
  CREDIT_AMOUNT_SCALE,
1179
1286
  CreditError,
1180
1287
  CreditErrorCode,
1288
+ NO_EXPIRED_BACKING_PREFIX,
1181
1289
  RESERVATION_STATUSES,
1182
1290
  RESERVED_JOURNAL_KEY_PREFIX,
1183
1291
  TERMINAL_RESERVATION_STATUSES,
1292
+ allocateExpiredBacking,
1184
1293
  assertDirectStatusWriteAllowed,
1185
1294
  assertHoldPlaced,
1186
1295
  assertKnownReservationStatus,
@@ -1196,9 +1305,9 @@ async function reserveCreditsForOperation(repository, userId, amount, operationT
1196
1305
  assertValidStoredAmount,
1197
1306
  assertValidStoredAmountRaw,
1198
1307
  assertValidTierBalanceInput,
1199
- attributeExpiredBacking,
1200
1308
  backedBalanceFloor,
1201
1309
  calculateAvailableCredits,
1310
+ canonicalAmount,
1202
1311
  cappedBalance,
1203
1312
  classifyDatabaseError,
1204
1313
  commitReservationWithJournal,
@@ -1215,10 +1324,13 @@ async function reserveCreditsForOperation(repository, userId, amount, operationT
1215
1324
  createUnsupportedOperationError,
1216
1325
  createUserNotFoundError,
1217
1326
  describePaymentMismatch,
1218
- expiredHoldBackingPatch,
1327
+ expiredBackingPatch,
1219
1328
  genericDeferred,
1220
1329
  getSqlState,
1221
1330
  hasPlacedHold,
1331
+ heldExpiredBacking,
1332
+ holdExpiredBacking,
1333
+ holdPlacedAtAfter,
1222
1334
  isCreditError,
1223
1335
  isCreditedOutcome,
1224
1336
  isIdempotencyConflictError,
@@ -1234,17 +1346,21 @@ async function reserveCreditsForOperation(repository, userId, amount, operationT
1234
1346
  isValidCreditAmount,
1235
1347
  isValidIdempotencyKey,
1236
1348
  isWinningOutcome,
1349
+ multiplyAmount,
1237
1350
  noopDeferred,
1238
1351
  normalizePaymentRef,
1239
1352
  numericToCents,
1240
1353
  planBalanceCap,
1241
1354
  planBalanceTarget,
1242
1355
  planHoldCommit,
1356
+ recordedExpiredBacking,
1243
1357
  releaseReservationWithJournal,
1244
1358
  reserveCreditsForOperation,
1245
1359
  sameAmount,
1246
1360
  settleExpiredHoldBacking,
1247
1361
  spendableBalance,
1362
+ storedAmountCeil,
1363
+ storedAmountFloor,
1248
1364
  storedMonthlyLimit,
1249
1365
  sumAmounts,
1250
1366
  synchronousDeferred,