@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.
- package/dist/adapters/index.cjs +1 -0
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +139 -23
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +113 -22
- package/dist/core/index.d.ts +113 -22
- package/dist/core/index.js +128 -21
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +246 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +235 -59
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +183 -41
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +6 -5
- package/dist/repository/index.d.ts +6 -5
- package/dist/repository/index.js +183 -41
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.cjs +1 -0
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.d.cts +2 -2
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/service/index.cjs +25 -16
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +2 -2
- package/dist/service/index.d.ts +2 -2
- package/dist/service/index.js +25 -16
- package/dist/service/index.js.map +1 -1
- package/dist/{types-MaViK5tD.d.cts → types-C3sxDvNb.d.cts} +29 -6
- package/dist/{types-MaViK5tD.d.ts → types-C3sxDvNb.d.ts} +29 -6
- package/dist/{types-3c-IAwZI.d.ts → types-CY-oHIMA.d.ts} +19 -3
- package/dist/{types-XSjNXO5l.d.cts → types-DOjguhEX.d.cts} +19 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(src_exports, {
|
|
|
33
33
|
CreditsSDKError: () => CreditsSDKError,
|
|
34
34
|
CreditsService: () => CreditsService,
|
|
35
35
|
InMemoryCreditRepository: () => InMemoryCreditRepository,
|
|
36
|
+
NO_EXPIRED_BACKING_PREFIX: () => NO_EXPIRED_BACKING_PREFIX,
|
|
36
37
|
NetworkError: () => NetworkError,
|
|
37
38
|
RESERVATION_STATUSES: () => RESERVATION_STATUSES,
|
|
38
39
|
RESERVED_JOURNAL_KEY_PREFIX: () => RESERVED_JOURNAL_KEY_PREFIX,
|
|
@@ -45,6 +46,7 @@ __export(src_exports, {
|
|
|
45
46
|
UNLIMITED_BALANCE_SENTINEL: () => UNLIMITED_BALANCE_SENTINEL,
|
|
46
47
|
ValidationError: () => ValidationError,
|
|
47
48
|
addCreditsThroughRepository: () => addCreditsThroughRepository,
|
|
49
|
+
allocateExpiredBacking: () => allocateExpiredBacking,
|
|
48
50
|
assertDirectStatusWriteAllowed: () => assertDirectStatusWriteAllowed,
|
|
49
51
|
assertHoldPlaced: () => assertHoldPlaced,
|
|
50
52
|
assertKnownReservationStatus: () => assertKnownReservationStatus,
|
|
@@ -61,9 +63,9 @@ __export(src_exports, {
|
|
|
61
63
|
assertValidStoredAmount: () => assertValidStoredAmount,
|
|
62
64
|
assertValidStoredAmountRaw: () => assertValidStoredAmountRaw,
|
|
63
65
|
assertValidTierBalanceInput: () => assertValidTierBalanceInput,
|
|
64
|
-
attributeExpiredBacking: () => attributeExpiredBacking,
|
|
65
66
|
backedBalanceFloor: () => backedBalanceFloor,
|
|
66
67
|
calculateAvailableCredits: () => calculateAvailableCredits,
|
|
68
|
+
canonicalAmount: () => canonicalAmount,
|
|
67
69
|
cappedBalance: () => cappedBalance,
|
|
68
70
|
checkAndNotifyLowBalance: () => checkAndNotifyLowBalance,
|
|
69
71
|
classifyDatabaseError: () => classifyDatabaseError,
|
|
@@ -90,7 +92,7 @@ __export(src_exports, {
|
|
|
90
92
|
createUnsupportedOperationError: () => createUnsupportedOperationError,
|
|
91
93
|
createUserNotFoundError: () => createUserNotFoundError,
|
|
92
94
|
describePaymentMismatch: () => describePaymentMismatch,
|
|
93
|
-
|
|
95
|
+
expiredBackingPatch: () => expiredBackingPatch,
|
|
94
96
|
generateId: () => generateId,
|
|
95
97
|
generateRequestId: () => generateRequestId2,
|
|
96
98
|
genericDeferred: () => genericDeferred,
|
|
@@ -116,6 +118,9 @@ __export(src_exports, {
|
|
|
116
118
|
getValidOperationTypes: () => getValidOperationTypes,
|
|
117
119
|
getValidTiers: () => getValidTiers,
|
|
118
120
|
hasPlacedHold: () => hasPlacedHold,
|
|
121
|
+
heldExpiredBacking: () => heldExpiredBacking,
|
|
122
|
+
holdExpiredBacking: () => holdExpiredBacking,
|
|
123
|
+
holdPlacedAtAfter: () => holdPlacedAtAfter,
|
|
119
124
|
initializeConfig: () => initializeConfig,
|
|
120
125
|
isCreditError: () => isCreditError,
|
|
121
126
|
isCreditErrorMessage: () => isCreditErrorMessage,
|
|
@@ -141,6 +146,7 @@ __export(src_exports, {
|
|
|
141
146
|
isWinningOutcome: () => isWinningOutcome,
|
|
142
147
|
loadConfigFromEnv: () => loadConfigFromEnv,
|
|
143
148
|
monthlyResetBalance: () => monthlyResetBalance,
|
|
149
|
+
multiplyAmount: () => multiplyAmount,
|
|
144
150
|
noopDeferred: () => noopDeferred,
|
|
145
151
|
normalizePaymentRef: () => normalizePaymentRef,
|
|
146
152
|
notifySubscriptionExpired: () => notifySubscriptionExpired,
|
|
@@ -152,6 +158,7 @@ __export(src_exports, {
|
|
|
152
158
|
planBalanceCap: () => planBalanceCap,
|
|
153
159
|
planBalanceTarget: () => planBalanceTarget,
|
|
154
160
|
planHoldCommit: () => planHoldCommit,
|
|
161
|
+
recordedExpiredBacking: () => recordedExpiredBacking,
|
|
155
162
|
registerConfigProvider: () => registerConfigProvider,
|
|
156
163
|
registerNotificationHandler: () => registerNotificationHandler,
|
|
157
164
|
releaseReservationWithJournal: () => releaseReservationWithJournal,
|
|
@@ -163,6 +170,8 @@ __export(src_exports, {
|
|
|
163
170
|
sameAmount: () => sameAmount,
|
|
164
171
|
settleExpiredHoldBacking: () => settleExpiredHoldBacking,
|
|
165
172
|
spendableBalance: () => spendableBalance,
|
|
173
|
+
storedAmountCeil: () => storedAmountCeil,
|
|
174
|
+
storedAmountFloor: () => storedAmountFloor,
|
|
166
175
|
storedMonthlyLimit: () => storedMonthlyLimit,
|
|
167
176
|
sumAmounts: () => sumAmounts,
|
|
168
177
|
supportsCreditsV2: () => supportsCreditsV2,
|
|
@@ -349,6 +358,40 @@ function floatSum(values) {
|
|
|
349
358
|
for (const value of values) total += value;
|
|
350
359
|
return total;
|
|
351
360
|
}
|
|
361
|
+
function multiplyAmount(unit, quantity) {
|
|
362
|
+
const cents = toCents(unit);
|
|
363
|
+
const product = cents === null ? Number.NaN : cents * quantity;
|
|
364
|
+
if (cents === null || cents < 0 || !Number.isSafeInteger(quantity) || quantity < 0 || !Number.isSafeInteger(product) || product > MAX_CENTS) {
|
|
365
|
+
throw new CreditError(
|
|
366
|
+
`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}`,
|
|
367
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
368
|
+
{ unit, quantity, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE }
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return product / 100;
|
|
372
|
+
}
|
|
373
|
+
var FLOAT_NOISE_FLOOR = 1e-6;
|
|
374
|
+
var FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;
|
|
375
|
+
function floatNoise(value) {
|
|
376
|
+
return Math.max(FLOAT_NOISE_FLOOR, Math.abs(value) * FLOAT_NOISE_RELATIVE);
|
|
377
|
+
}
|
|
378
|
+
function canonicalAmount(value) {
|
|
379
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return value;
|
|
380
|
+
const cents = Math.round(value * 100);
|
|
381
|
+
if (!Number.isSafeInteger(cents)) return value;
|
|
382
|
+
const onGrid = cents / 100 + 0;
|
|
383
|
+
return Math.abs(value - onGrid) < floatNoise(value) ? onGrid : value;
|
|
384
|
+
}
|
|
385
|
+
function storedAmountFloor(value) {
|
|
386
|
+
const canonical = canonicalAmount(value);
|
|
387
|
+
if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
|
|
388
|
+
return Math.floor(canonical * 100) / 100;
|
|
389
|
+
}
|
|
390
|
+
function storedAmountCeil(value) {
|
|
391
|
+
const canonical = canonicalAmount(value);
|
|
392
|
+
if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
|
|
393
|
+
return Math.ceil(canonical * 100) / 100;
|
|
394
|
+
}
|
|
352
395
|
function isValidCreditAmount(value) {
|
|
353
396
|
if (typeof value !== "number") return false;
|
|
354
397
|
const cents = toCents(value);
|
|
@@ -404,14 +447,19 @@ function backedBalanceFloor(reserved, bonusCredits) {
|
|
|
404
447
|
function cappedBalance(balance, cap, reserved, bonusCredits) {
|
|
405
448
|
return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
|
|
406
449
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
450
|
+
function heldExpiredBacking(balance, stored) {
|
|
451
|
+
if (!(stored !== void 0 && stored > 0)) return 0;
|
|
452
|
+
return Math.max(0, Math.min(stored, balance));
|
|
453
|
+
}
|
|
454
|
+
function spendableBalance(balance, expiredHoldBacking) {
|
|
455
|
+
return sumAmounts(balance, -heldExpiredBacking(balance, expiredHoldBacking));
|
|
409
456
|
}
|
|
410
457
|
function planBalanceTarget(target, reserved, bonusCredits) {
|
|
458
|
+
assertValidBalanceTarget(target, "balance target", { reserved, bonusCredits });
|
|
411
459
|
const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
|
|
412
460
|
return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
|
|
413
461
|
}
|
|
414
|
-
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
|
|
462
|
+
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking) {
|
|
415
463
|
const spendable = spendableBalance(balance, expiredHoldBacking);
|
|
416
464
|
const target = Math.min(spendable, cap);
|
|
417
465
|
const kept = Math.max(
|
|
@@ -422,8 +470,8 @@ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
|
|
|
422
470
|
return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
|
|
423
471
|
}
|
|
424
472
|
function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
|
|
425
|
-
const held =
|
|
426
|
-
const used = Math.min(held, holdBacking);
|
|
473
|
+
const held = heldExpiredBacking(balance, expiredHoldBacking);
|
|
474
|
+
const used = Math.max(0, Math.min(held, holdBacking));
|
|
427
475
|
return { used, remaining: sumAmounts(held, -used) };
|
|
428
476
|
}
|
|
429
477
|
function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
|
|
@@ -438,27 +486,83 @@ function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking,
|
|
|
438
486
|
expiredHoldBacking: backing.remaining
|
|
439
487
|
};
|
|
440
488
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
489
|
+
var NO_EXPIRED_BACKING_PREFIX = {
|
|
490
|
+
holdPlacedAt: "",
|
|
491
|
+
id: "",
|
|
492
|
+
share: 0
|
|
493
|
+
};
|
|
494
|
+
function compareHolds(a, b) {
|
|
495
|
+
if (a.holdPlacedAt !== b.holdPlacedAt) return a.holdPlacedAt < b.holdPlacedAt ? -1 : 1;
|
|
496
|
+
if (a.id === b.id) return 0;
|
|
497
|
+
return a.id < b.id ? -1 : 1;
|
|
498
|
+
}
|
|
499
|
+
function recordedExpiredBacking(hold) {
|
|
500
|
+
const stored = hold.expiredBacking;
|
|
501
|
+
if (typeof stored !== "number" || !Number.isFinite(stored) || stored <= 0) return 0;
|
|
502
|
+
if (!isValidCreditAmount(hold.amount)) return 0;
|
|
503
|
+
return Math.min(storedAmountCeil(stored), hold.amount);
|
|
504
|
+
}
|
|
505
|
+
function allocateExpiredBacking(holds, total) {
|
|
506
|
+
const placed = holds.filter((hold) => hold.holdPlacedAt && isValidCreditAmount(hold.amount)).map((hold) => ({ ...hold, holdPlacedAt: hold.holdPlacedAt })).sort(compareHolds);
|
|
507
|
+
const wanted = Math.max(0, total);
|
|
508
|
+
if (wanted === 0) return { total: 0, frontier: null };
|
|
509
|
+
const recorded = placed.filter((hold) => recordedExpiredBacking(hold) > 0);
|
|
510
|
+
const shares = new Map(recorded.map((hold) => [hold.id, recordedExpiredBacking(hold)]));
|
|
511
|
+
let left = sumAmounts(wanted, ...[...shares.values()].map((share) => -share));
|
|
512
|
+
for (const hold of [...recorded].reverse()) {
|
|
513
|
+
if (left >= 0) break;
|
|
514
|
+
const share = shares.get(hold.id);
|
|
515
|
+
const cut = Math.min(share, -left);
|
|
516
|
+
shares.set(hold.id, sumAmounts(share, -cut));
|
|
517
|
+
left = sumAmounts(left, cut);
|
|
451
518
|
}
|
|
519
|
+
let frontier = NO_EXPIRED_BACKING_PREFIX;
|
|
452
520
|
for (const hold of placed) {
|
|
521
|
+
if (left <= 0) break;
|
|
522
|
+
if (shares.has(hold.id)) continue;
|
|
523
|
+
const share = Math.min(hold.amount, left);
|
|
524
|
+
frontier = { holdPlacedAt: hold.holdPlacedAt, id: hold.id, share };
|
|
525
|
+
left = sumAmounts(left, -share);
|
|
526
|
+
}
|
|
527
|
+
for (const hold of recorded) {
|
|
528
|
+
if (left <= 0) break;
|
|
453
529
|
const share = shares.get(hold.id);
|
|
454
530
|
const added = Math.min(sumAmounts(hold.amount, -share), left);
|
|
455
531
|
shares.set(hold.id, sumAmounts(share, added));
|
|
456
532
|
left = sumAmounts(left, -added);
|
|
457
533
|
}
|
|
458
|
-
|
|
534
|
+
const rewrites = recorded.filter((hold) => shares.get(hold.id) !== hold.expiredBacking).map((hold) => ({ id: hold.id, expiredBacking: shares.get(hold.id) }));
|
|
535
|
+
return { total: wanted, frontier, ...rewrites.length > 0 ? { rewrites } : {} };
|
|
459
536
|
}
|
|
460
|
-
function
|
|
461
|
-
|
|
537
|
+
function holdExpiredBacking(hold, frontier, rewrites) {
|
|
538
|
+
const rewritten = rewrites?.find((rewrite) => rewrite.id === hold.id);
|
|
539
|
+
const recorded = recordedExpiredBacking(
|
|
540
|
+
rewritten ? { ...hold, expiredBacking: rewritten.expiredBacking } : hold
|
|
541
|
+
);
|
|
542
|
+
if (recorded > 0) return recorded;
|
|
543
|
+
if (!frontier || !hold.holdPlacedAt) return 0;
|
|
544
|
+
const order = compareHolds({ holdPlacedAt: hold.holdPlacedAt, id: hold.id }, frontier);
|
|
545
|
+
if (order > 0) return 0;
|
|
546
|
+
return order < 0 ? hold.amount : Math.min(frontier.share, hold.amount);
|
|
547
|
+
}
|
|
548
|
+
function holdPlacedAtAfter(now, frontier) {
|
|
549
|
+
if (!frontier?.holdPlacedAt) return now;
|
|
550
|
+
const earliest = new Date(Date.parse(frontier.holdPlacedAt) + 1).toISOString();
|
|
551
|
+
return now > earliest ? now : earliest;
|
|
552
|
+
}
|
|
553
|
+
function expiredBackingPatch(next, stored) {
|
|
554
|
+
const total = Math.max(0, next.total);
|
|
555
|
+
const patch = {};
|
|
556
|
+
const storedTotal = stored.expiredHoldBacking;
|
|
557
|
+
if (total > 0 || storedTotal !== void 0 && storedTotal !== 0) {
|
|
558
|
+
patch.expiredHoldBacking = total;
|
|
559
|
+
}
|
|
560
|
+
if (total <= 0) {
|
|
561
|
+
if (stored.expiredBackingFrontier) patch.expiredBackingFrontier = null;
|
|
562
|
+
} else if (next.frontier) {
|
|
563
|
+
patch.expiredBackingFrontier = next.frontier;
|
|
564
|
+
}
|
|
565
|
+
return patch;
|
|
462
566
|
}
|
|
463
567
|
function assertValidBalanceTarget(value, field, context) {
|
|
464
568
|
assertRepresentableAmount(value, field, context);
|
|
@@ -471,7 +575,10 @@ function assertValidBalanceTarget(value, field, context) {
|
|
|
471
575
|
}
|
|
472
576
|
}
|
|
473
577
|
function assertValidTierBalanceInput(input, context) {
|
|
474
|
-
if (input.balanceCap === void 0)
|
|
578
|
+
if (input.balanceCap === void 0) {
|
|
579
|
+
if (input.balance !== void 0) assertValidBalanceTarget(input.balance, "balance", context);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
475
582
|
if (input.balance !== void 0) {
|
|
476
583
|
throw new CreditError(
|
|
477
584
|
"A tier update cannot set both balance and balanceCap",
|
|
@@ -1437,7 +1544,7 @@ function validateBalanceUpdate(current, updates) {
|
|
|
1437
1544
|
const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
|
|
1438
1545
|
const errors = [];
|
|
1439
1546
|
if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
|
|
1440
|
-
const marked = updates.balance === void 0 ? current.expiredHoldBacking
|
|
1547
|
+
const marked = updates.balance === void 0 ? heldExpiredBacking(current.balance, current.expiredHoldBacking) : 0;
|
|
1441
1548
|
if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
|
|
1442
1549
|
errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
|
|
1443
1550
|
}
|
|
@@ -1589,6 +1696,7 @@ function commitTransitionJournal(store, input, existingId) {
|
|
|
1589
1696
|
userId: input.userId,
|
|
1590
1697
|
entryType: input.entryType,
|
|
1591
1698
|
amount: input.amount,
|
|
1699
|
+
balanceBefore: input.balanceBefore,
|
|
1592
1700
|
balanceAfter: input.balanceAfter,
|
|
1593
1701
|
source: input.source,
|
|
1594
1702
|
referenceId: input.reservationId,
|
|
@@ -1638,16 +1746,28 @@ function openHolds(store, userId) {
|
|
|
1638
1746
|
(reservation) => reservation.status === "reserved"
|
|
1639
1747
|
);
|
|
1640
1748
|
}
|
|
1749
|
+
function expiredBackingInForce(store, credits) {
|
|
1750
|
+
const total = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
1751
|
+
if (total > 0 && (!credits.expiredBackingFrontier || credits.expiredHoldBacking !== total)) {
|
|
1752
|
+
return allocateExpiredBacking(openHolds(store, credits.userId), total);
|
|
1753
|
+
}
|
|
1754
|
+
return { total, frontier: credits.expiredBackingFrontier ?? null };
|
|
1755
|
+
}
|
|
1756
|
+
function writeRecordedShares(store, userId, allocation) {
|
|
1757
|
+
for (const { id, expiredBacking } of allocation.rewrites ?? []) {
|
|
1758
|
+
const hold = getReservation(store, userId, id);
|
|
1759
|
+
if (hold) hold.expiredBacking = expiredBacking;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1641
1762
|
function writeExpiredHoldBacking(store, credits, next) {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
const shares = attributeExpiredBacking(holds, next);
|
|
1646
|
-
for (const hold of holds) {
|
|
1647
|
-
const share = shares.get(hold.id) ?? 0;
|
|
1648
|
-
if (share !== (hold.expiredBacking ?? 0)) hold.expiredBacking = share;
|
|
1763
|
+
if (next <= 0) {
|
|
1764
|
+
Object.assign(credits, expiredBackingPatch({ total: 0 }, credits));
|
|
1765
|
+
return;
|
|
1649
1766
|
}
|
|
1650
|
-
|
|
1767
|
+
if (next === credits.expiredHoldBacking) return;
|
|
1768
|
+
const allocation = allocateExpiredBacking(openHolds(store, credits.userId), next);
|
|
1769
|
+
writeRecordedShares(store, credits.userId, allocation);
|
|
1770
|
+
Object.assign(credits, expiredBackingPatch(allocation, credits));
|
|
1651
1771
|
}
|
|
1652
1772
|
function getReservation(store, userId, reservationId) {
|
|
1653
1773
|
return store.reservations.get(userId)?.get(reservationId);
|
|
@@ -1696,6 +1816,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1696
1816
|
};
|
|
1697
1817
|
}
|
|
1698
1818
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1819
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1699
1820
|
const reservation = {
|
|
1700
1821
|
id: generateId(),
|
|
1701
1822
|
userId: input.userId,
|
|
@@ -1708,7 +1829,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1708
1829
|
// The hold-origin fact. Written here and nowhere else, in the same
|
|
1709
1830
|
// critical section that raises `reserved` below, so the row and the hold
|
|
1710
1831
|
// become visible together. `createReservation` cannot set it.
|
|
1711
|
-
holdPlacedAt: now
|
|
1832
|
+
holdPlacedAt: holdPlacedAtAfter(now, backing.total > 0 ? backing.frontier : null)
|
|
1712
1833
|
};
|
|
1713
1834
|
if (!store.reservations.has(input.userId)) {
|
|
1714
1835
|
store.reservations.set(input.userId, /* @__PURE__ */ new Map());
|
|
@@ -1726,6 +1847,8 @@ async function reserveCreditsV2(store, input) {
|
|
|
1726
1847
|
);
|
|
1727
1848
|
}
|
|
1728
1849
|
credits.reserved = nextReserved;
|
|
1850
|
+
writeRecordedShares(store, input.userId, backing);
|
|
1851
|
+
Object.assign(credits, expiredBackingPatch(backing, credits));
|
|
1729
1852
|
credits.updatedAt = now;
|
|
1730
1853
|
return { outcome: "created", reservation: copyRecord(reservation) };
|
|
1731
1854
|
});
|
|
@@ -1746,11 +1869,12 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1746
1869
|
const amount = reservation.amount;
|
|
1747
1870
|
await yieldPoint(store);
|
|
1748
1871
|
assertReservedCoversHold(credits, userId, reservationId, amount);
|
|
1872
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1749
1873
|
const paid = planHoldCommit(
|
|
1750
1874
|
credits.balance,
|
|
1751
1875
|
credits.bonusCredits,
|
|
1752
|
-
|
|
1753
|
-
reservation.
|
|
1876
|
+
backing.total,
|
|
1877
|
+
holdExpiredBacking(reservation, backing.frontier, backing.rewrites),
|
|
1754
1878
|
amount
|
|
1755
1879
|
);
|
|
1756
1880
|
if (!paid.covered) {
|
|
@@ -1768,6 +1892,7 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1768
1892
|
operation: "commitReservation",
|
|
1769
1893
|
entryType: "debit",
|
|
1770
1894
|
amount,
|
|
1895
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1771
1896
|
balanceAfter,
|
|
1772
1897
|
source: "operation_commit",
|
|
1773
1898
|
reservationId,
|
|
@@ -1795,9 +1920,10 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1795
1920
|
credits.bonusCredits = nextBonusCredits;
|
|
1796
1921
|
credits.reserved = nextReserved;
|
|
1797
1922
|
credits.monthlyUsed = nextMonthlyUsed;
|
|
1923
|
+
writeRecordedShares(store, userId, backing);
|
|
1798
1924
|
Object.assign(
|
|
1799
1925
|
credits,
|
|
1800
|
-
|
|
1926
|
+
expiredBackingPatch({ total: paid.expiredHoldBacking, frontier: backing.frontier }, credits)
|
|
1801
1927
|
);
|
|
1802
1928
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1803
1929
|
const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
|
|
@@ -1878,10 +2004,11 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1878
2004
|
reservation.id,
|
|
1879
2005
|
reservation.amount
|
|
1880
2006
|
);
|
|
2007
|
+
const inForce = expiredBackingInForce(store, credits);
|
|
1881
2008
|
const backing = settleExpiredHoldBacking(
|
|
1882
2009
|
credits.balance,
|
|
1883
|
-
|
|
1884
|
-
reservation.
|
|
2010
|
+
inForce.total,
|
|
2011
|
+
holdExpiredBacking(reservation, inForce.frontier, inForce.rewrites)
|
|
1885
2012
|
);
|
|
1886
2013
|
const nextBalance = sumAmounts(credits.balance, -backing.used);
|
|
1887
2014
|
const journal = {
|
|
@@ -1889,6 +2016,7 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1889
2016
|
operation: `${transition.transition}Reservation`,
|
|
1890
2017
|
entryType: "credit",
|
|
1891
2018
|
amount: 0,
|
|
2019
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1892
2020
|
balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
|
|
1893
2021
|
source: transition.source,
|
|
1894
2022
|
reservationId: reservation.id,
|
|
@@ -1911,7 +2039,11 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1911
2039
|
reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1912
2040
|
credits.reserved = nextReserved;
|
|
1913
2041
|
credits.balance = nextBalance;
|
|
1914
|
-
|
|
2042
|
+
writeRecordedShares(store, reservation.userId, inForce);
|
|
2043
|
+
Object.assign(
|
|
2044
|
+
credits,
|
|
2045
|
+
expiredBackingPatch({ total: backing.remaining, frontier: inForce.frontier }, credits)
|
|
2046
|
+
);
|
|
1915
2047
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1916
2048
|
return commitTransitionJournal(store, journal, existingEntryId);
|
|
1917
2049
|
}
|
|
@@ -1954,7 +2086,7 @@ var InMemoryCreditRepository = class {
|
|
|
1954
2086
|
* simultaneous first deliveries of the same reference would then both credit.
|
|
1955
2087
|
*/
|
|
1956
2088
|
seedUser(userId, tier, initialBalance) {
|
|
1957
|
-
|
|
2089
|
+
assertValidBalanceTarget(initialBalance, "initialBalance", { userId });
|
|
1958
2090
|
const configured = getConfigMonthlyLimit(tier);
|
|
1959
2091
|
assertRepresentableTierAmount(configured, "monthlyLimit", { userId, tier });
|
|
1960
2092
|
const monthlyLimit = storedMonthlyLimit(configured);
|
|
@@ -2035,9 +2167,11 @@ var InMemoryCreditRepository = class {
|
|
|
2035
2167
|
userId,
|
|
2036
2168
|
operation: "updateUserCredits"
|
|
2037
2169
|
});
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2170
|
+
writeExpiredHoldBacking(
|
|
2171
|
+
this.store,
|
|
2172
|
+
next,
|
|
2173
|
+
updates.balance !== void 0 ? 0 : Math.min(heldExpiredBacking(credits.balance, credits.expiredHoldBacking), next.balance)
|
|
2174
|
+
);
|
|
2041
2175
|
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2042
2176
|
this.store.users.set(userId, next);
|
|
2043
2177
|
}
|
|
@@ -2069,6 +2203,11 @@ var InMemoryCreditRepository = class {
|
|
|
2069
2203
|
credits.balance = planned.balance;
|
|
2070
2204
|
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
2071
2205
|
}
|
|
2206
|
+
writeExpiredHoldBacking(
|
|
2207
|
+
this.store,
|
|
2208
|
+
credits,
|
|
2209
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
2210
|
+
);
|
|
2072
2211
|
if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
|
|
2073
2212
|
if (input.subscriptionExpiresAt !== void 0) {
|
|
2074
2213
|
if (input.subscriptionExpiresAt === null) {
|
|
@@ -2230,6 +2369,11 @@ var InMemoryCreditRepository = class {
|
|
|
2230
2369
|
{ userId, operation: "addCredits" }
|
|
2231
2370
|
);
|
|
2232
2371
|
credits.bonusCredits = nextBonusCredits;
|
|
2372
|
+
writeExpiredHoldBacking(
|
|
2373
|
+
this.store,
|
|
2374
|
+
credits,
|
|
2375
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
2376
|
+
);
|
|
2233
2377
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2234
2378
|
this.store.users.set(userId, credits);
|
|
2235
2379
|
const transaction = this.recordTransaction({
|
|
@@ -2249,6 +2393,7 @@ var InMemoryCreditRepository = class {
|
|
|
2249
2393
|
userId,
|
|
2250
2394
|
entryType: "credit",
|
|
2251
2395
|
amount,
|
|
2396
|
+
balanceBefore: previousTotal,
|
|
2252
2397
|
balanceAfter: newTotal,
|
|
2253
2398
|
source: payload.source,
|
|
2254
2399
|
referenceId: transaction.id ?? paymentRef ?? "unknown",
|
|
@@ -2282,7 +2427,7 @@ var InMemoryCreditRepository = class {
|
|
|
2282
2427
|
});
|
|
2283
2428
|
}
|
|
2284
2429
|
}
|
|
2285
|
-
async deductCreditsAtomic(userId, amount) {
|
|
2430
|
+
async deductCreditsAtomic(userId, amount, journal) {
|
|
2286
2431
|
assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
|
|
2287
2432
|
const credits = this.store.users.get(userId);
|
|
2288
2433
|
if (!credits) {
|
|
@@ -2308,11 +2453,26 @@ var InMemoryCreditRepository = class {
|
|
|
2308
2453
|
{ previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },
|
|
2309
2454
|
{ userId, operation: "deductCredits" }
|
|
2310
2455
|
);
|
|
2456
|
+
const held = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
2311
2457
|
credits.balance = nextBalance;
|
|
2312
2458
|
credits.bonusCredits = nextBonusCredits;
|
|
2459
|
+
writeExpiredHoldBacking(this.store, credits, held);
|
|
2313
2460
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2314
2461
|
this.store.users.set(userId, credits);
|
|
2315
|
-
return { previousBalance, newBalance };
|
|
2462
|
+
if (!journal) return { previousBalance, newBalance };
|
|
2463
|
+
const entry = this.recordJournalEntry({
|
|
2464
|
+
userId,
|
|
2465
|
+
entryType: "debit",
|
|
2466
|
+
amount,
|
|
2467
|
+
balanceBefore: previousBalance,
|
|
2468
|
+
balanceAfter: newBalance,
|
|
2469
|
+
source: journal.source ?? "operation_commit",
|
|
2470
|
+
referenceId: journal.referenceId,
|
|
2471
|
+
referenceType: journal.referenceType ?? "adjustment",
|
|
2472
|
+
description: journal.description,
|
|
2473
|
+
metadata: journal.metadata
|
|
2474
|
+
});
|
|
2475
|
+
return { previousBalance, newBalance, journalEntryId: entry.id };
|
|
2316
2476
|
}
|
|
2317
2477
|
// ==================== Transactions ====================
|
|
2318
2478
|
async createTransaction(input) {
|
|
@@ -2426,6 +2586,11 @@ var InMemoryCreditRepository = class {
|
|
|
2426
2586
|
assertRepresentableAmount(input.balanceAfter, "journal balanceAfter", {
|
|
2427
2587
|
userId: input.userId
|
|
2428
2588
|
});
|
|
2589
|
+
if (input.balanceBefore !== void 0) {
|
|
2590
|
+
assertRepresentableAmount(input.balanceBefore, "journal balanceBefore", {
|
|
2591
|
+
userId: input.userId
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2429
2594
|
assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });
|
|
2430
2595
|
assertPublicJournalKey(input.idempotencyKey, input.userId);
|
|
2431
2596
|
const key = input.idempotencyKey !== void 0 ? scopedKey(input.userId, input.idempotencyKey) : void 0;
|
|
@@ -2441,6 +2606,7 @@ var InMemoryCreditRepository = class {
|
|
|
2441
2606
|
userId: input.userId,
|
|
2442
2607
|
entryType: input.entryType,
|
|
2443
2608
|
amount: input.amount,
|
|
2609
|
+
...input.balanceBefore !== void 0 ? { balanceBefore: input.balanceBefore } : {},
|
|
2444
2610
|
balanceAfter: input.balanceAfter,
|
|
2445
2611
|
source: input.source,
|
|
2446
2612
|
referenceId: input.referenceId,
|
|
@@ -2593,6 +2759,7 @@ var InMemoryCreditRepository = class {
|
|
|
2593
2759
|
userId,
|
|
2594
2760
|
entryType: change > 0 ? "credit" : "debit",
|
|
2595
2761
|
amount: Math.abs(change),
|
|
2762
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
2596
2763
|
balanceAfter,
|
|
2597
2764
|
source: "monthly_reset",
|
|
2598
2765
|
referenceId: `reset-${Date.now()}`,
|
|
@@ -2673,6 +2840,7 @@ var InMemoryCreditRepository = class {
|
|
|
2673
2840
|
entryType: "debit",
|
|
2674
2841
|
amount: 0,
|
|
2675
2842
|
// No credits deducted, just tier change
|
|
2843
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
2676
2844
|
balanceAfter,
|
|
2677
2845
|
source: "subscription_downgrade",
|
|
2678
2846
|
referenceId: `downgrade-${Date.now()}`,
|
|
@@ -2836,6 +3004,7 @@ var CreditsService = class {
|
|
|
2836
3004
|
entryType: "debit",
|
|
2837
3005
|
amount: 0,
|
|
2838
3006
|
// No credits deducted, just tier change
|
|
3007
|
+
balanceBefore: sumAmounts(data.balance, data.bonusCredits),
|
|
2839
3008
|
// The ledger balance, matching every other journal writer: the V2
|
|
2840
3009
|
// transitions and add/deduct all record `balance + bonusCredits`, so
|
|
2841
3010
|
// recording `balance` alone made the audit trail disagree with itself
|
|
@@ -2879,6 +3048,7 @@ var CreditsService = class {
|
|
|
2879
3048
|
userId,
|
|
2880
3049
|
entryType: balanceChange > 0 ? "credit" : "debit",
|
|
2881
3050
|
amount: Math.abs(balanceChange),
|
|
3051
|
+
balanceBefore: sumAmounts(data.balance, data.bonusCredits),
|
|
2882
3052
|
balanceAfter: sumAmounts(
|
|
2883
3053
|
resetResult.credits.balance,
|
|
2884
3054
|
resetResult.credits.bonusCredits
|
|
@@ -3113,9 +3283,18 @@ var CreditsService = class {
|
|
|
3113
3283
|
*/
|
|
3114
3284
|
async deductCredits(userId, amount, options) {
|
|
3115
3285
|
assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
|
|
3286
|
+
const operationType = options?.operationType;
|
|
3287
|
+
const journal = {
|
|
3288
|
+
referenceId: options?.requestId ?? `deduct-${Date.now()}`,
|
|
3289
|
+
description: operationType ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}` : `Deducted ${amount} credits`,
|
|
3290
|
+
metadata: {
|
|
3291
|
+
...options?.metadata,
|
|
3292
|
+
...operationType && { operationType }
|
|
3293
|
+
}
|
|
3294
|
+
};
|
|
3116
3295
|
let deduction;
|
|
3117
3296
|
try {
|
|
3118
|
-
deduction = await this.repository.deductCreditsAtomic(userId, amount);
|
|
3297
|
+
deduction = await this.repository.deductCreditsAtomic(userId, amount, journal);
|
|
3119
3298
|
} catch (error) {
|
|
3120
3299
|
if (isCreditError(error) && error.code === CreditErrorCode.INVALID_AMOUNT) throw error;
|
|
3121
3300
|
const credits = await this.repository.getUserCredits(userId);
|
|
@@ -3131,21 +3310,18 @@ var CreditsService = class {
|
|
|
3131
3310
|
shortfall: sumAmounts(amount, -available)
|
|
3132
3311
|
};
|
|
3133
3312
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
...operationType && { operationType }
|
|
3147
|
-
}
|
|
3148
|
-
});
|
|
3313
|
+
if (deduction.journalEntryId === void 0) {
|
|
3314
|
+
await this.repository.createJournalEntry({
|
|
3315
|
+
userId,
|
|
3316
|
+
entryType: "debit",
|
|
3317
|
+
amount,
|
|
3318
|
+
balanceBefore: deduction.previousBalance,
|
|
3319
|
+
balanceAfter: deduction.newBalance,
|
|
3320
|
+
source: "operation_commit",
|
|
3321
|
+
referenceType: "adjustment",
|
|
3322
|
+
...journal
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3149
3325
|
if (operationType) {
|
|
3150
3326
|
await this.logUsage({
|
|
3151
3327
|
userId,
|
|
@@ -4156,6 +4332,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4156
4332
|
CreditsSDKError,
|
|
4157
4333
|
CreditsService,
|
|
4158
4334
|
InMemoryCreditRepository,
|
|
4335
|
+
NO_EXPIRED_BACKING_PREFIX,
|
|
4159
4336
|
NetworkError,
|
|
4160
4337
|
RESERVATION_STATUSES,
|
|
4161
4338
|
RESERVED_JOURNAL_KEY_PREFIX,
|
|
@@ -4168,6 +4345,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4168
4345
|
UNLIMITED_BALANCE_SENTINEL,
|
|
4169
4346
|
ValidationError,
|
|
4170
4347
|
addCreditsThroughRepository,
|
|
4348
|
+
allocateExpiredBacking,
|
|
4171
4349
|
assertDirectStatusWriteAllowed,
|
|
4172
4350
|
assertHoldPlaced,
|
|
4173
4351
|
assertKnownReservationStatus,
|
|
@@ -4184,9 +4362,9 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4184
4362
|
assertValidStoredAmount,
|
|
4185
4363
|
assertValidStoredAmountRaw,
|
|
4186
4364
|
assertValidTierBalanceInput,
|
|
4187
|
-
attributeExpiredBacking,
|
|
4188
4365
|
backedBalanceFloor,
|
|
4189
4366
|
calculateAvailableCredits,
|
|
4367
|
+
canonicalAmount,
|
|
4190
4368
|
cappedBalance,
|
|
4191
4369
|
checkAndNotifyLowBalance,
|
|
4192
4370
|
classifyDatabaseError,
|
|
@@ -4213,7 +4391,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4213
4391
|
createUnsupportedOperationError,
|
|
4214
4392
|
createUserNotFoundError,
|
|
4215
4393
|
describePaymentMismatch,
|
|
4216
|
-
|
|
4394
|
+
expiredBackingPatch,
|
|
4217
4395
|
generateId,
|
|
4218
4396
|
generateRequestId,
|
|
4219
4397
|
genericDeferred,
|
|
@@ -4239,6 +4417,9 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4239
4417
|
getValidOperationTypes,
|
|
4240
4418
|
getValidTiers,
|
|
4241
4419
|
hasPlacedHold,
|
|
4420
|
+
heldExpiredBacking,
|
|
4421
|
+
holdExpiredBacking,
|
|
4422
|
+
holdPlacedAtAfter,
|
|
4242
4423
|
initializeConfig,
|
|
4243
4424
|
isCreditError,
|
|
4244
4425
|
isCreditErrorMessage,
|
|
@@ -4264,6 +4445,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4264
4445
|
isWinningOutcome,
|
|
4265
4446
|
loadConfigFromEnv,
|
|
4266
4447
|
monthlyResetBalance,
|
|
4448
|
+
multiplyAmount,
|
|
4267
4449
|
noopDeferred,
|
|
4268
4450
|
normalizePaymentRef,
|
|
4269
4451
|
notifySubscriptionExpired,
|
|
@@ -4275,6 +4457,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4275
4457
|
planBalanceCap,
|
|
4276
4458
|
planBalanceTarget,
|
|
4277
4459
|
planHoldCommit,
|
|
4460
|
+
recordedExpiredBacking,
|
|
4278
4461
|
registerConfigProvider,
|
|
4279
4462
|
registerNotificationHandler,
|
|
4280
4463
|
releaseReservationWithJournal,
|
|
@@ -4286,6 +4469,8 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4286
4469
|
sameAmount,
|
|
4287
4470
|
settleExpiredHoldBacking,
|
|
4288
4471
|
spendableBalance,
|
|
4472
|
+
storedAmountCeil,
|
|
4473
|
+
storedAmountFloor,
|
|
4289
4474
|
storedMonthlyLimit,
|
|
4290
4475
|
sumAmounts,
|
|
4291
4476
|
supportsCreditsV2,
|