@nehorai/credits 2.2.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/index.cjs +169 -155
- 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 +169 -155
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +148 -24
- 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 +137 -22
- package/dist/core/index.js.map +1 -1
- package/dist/index-BrogG0Gc.d.cts +277 -0
- package/dist/index-DOvJES2m.d.ts +277 -0
- package/dist/index.cjs +324 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +311 -69
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +254 -49
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +3 -254
- package/dist/repository/index.d.ts +3 -254
- package/dist/repository/index.js +254 -49
- 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 +42 -19
- 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 +42 -19
- package/dist/service/index.js.map +1 -1
- package/dist/{types-3c-IAwZI.d.ts → types-BL3YqxUD.d.ts} +28 -6
- 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-XSjNXO5l.d.cts → types-YLAB8bFk.d.cts} +28 -6
- 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,
|
|
@@ -162,13 +169,17 @@ __export(src_exports, {
|
|
|
162
169
|
resetConfig: () => resetConfig,
|
|
163
170
|
sameAmount: () => sameAmount,
|
|
164
171
|
settleExpiredHoldBacking: () => settleExpiredHoldBacking,
|
|
172
|
+
signupGrantJournalInput: () => signupGrantJournalInput,
|
|
165
173
|
spendableBalance: () => spendableBalance,
|
|
174
|
+
storedAmountCeil: () => storedAmountCeil,
|
|
175
|
+
storedAmountFloor: () => storedAmountFloor,
|
|
166
176
|
storedMonthlyLimit: () => storedMonthlyLimit,
|
|
167
177
|
sumAmounts: () => sumAmounts,
|
|
168
178
|
supportsCreditsV2: () => supportsCreditsV2,
|
|
169
179
|
supportsIdempotentCredit: () => supportsIdempotentCredit,
|
|
170
180
|
synchronousDeferred: () => synchronousDeferred,
|
|
171
181
|
terminalStatusOf: () => terminalStatusOf,
|
|
182
|
+
tierChangeJournalInput: () => tierChangeJournalInput,
|
|
172
183
|
tierSchema: () => tierSchema,
|
|
173
184
|
toCents: () => toCents,
|
|
174
185
|
toClientUserCredits: () => toClientUserCredits,
|
|
@@ -349,6 +360,40 @@ function floatSum(values) {
|
|
|
349
360
|
for (const value of values) total += value;
|
|
350
361
|
return total;
|
|
351
362
|
}
|
|
363
|
+
function multiplyAmount(unit, quantity) {
|
|
364
|
+
const cents = toCents(unit);
|
|
365
|
+
const product = cents === null ? Number.NaN : cents * quantity;
|
|
366
|
+
if (cents === null || cents < 0 || !Number.isSafeInteger(quantity) || quantity < 0 || !Number.isSafeInteger(product) || product > MAX_CENTS) {
|
|
367
|
+
throw new CreditError(
|
|
368
|
+
`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}`,
|
|
369
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
370
|
+
{ unit, quantity, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE }
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return product / 100;
|
|
374
|
+
}
|
|
375
|
+
var FLOAT_NOISE_FLOOR = 1e-6;
|
|
376
|
+
var FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;
|
|
377
|
+
function floatNoise(value) {
|
|
378
|
+
return Math.max(FLOAT_NOISE_FLOOR, Math.abs(value) * FLOAT_NOISE_RELATIVE);
|
|
379
|
+
}
|
|
380
|
+
function canonicalAmount(value) {
|
|
381
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return value;
|
|
382
|
+
const cents = Math.round(value * 100);
|
|
383
|
+
if (!Number.isSafeInteger(cents)) return value;
|
|
384
|
+
const onGrid = cents / 100 + 0;
|
|
385
|
+
return Math.abs(value - onGrid) < floatNoise(value) ? onGrid : value;
|
|
386
|
+
}
|
|
387
|
+
function storedAmountFloor(value) {
|
|
388
|
+
const canonical = canonicalAmount(value);
|
|
389
|
+
if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
|
|
390
|
+
return Math.floor(canonical * 100) / 100;
|
|
391
|
+
}
|
|
392
|
+
function storedAmountCeil(value) {
|
|
393
|
+
const canonical = canonicalAmount(value);
|
|
394
|
+
if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
|
|
395
|
+
return Math.ceil(canonical * 100) / 100;
|
|
396
|
+
}
|
|
352
397
|
function isValidCreditAmount(value) {
|
|
353
398
|
if (typeof value !== "number") return false;
|
|
354
399
|
const cents = toCents(value);
|
|
@@ -404,14 +449,19 @@ function backedBalanceFloor(reserved, bonusCredits) {
|
|
|
404
449
|
function cappedBalance(balance, cap, reserved, bonusCredits) {
|
|
405
450
|
return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
|
|
406
451
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
452
|
+
function heldExpiredBacking(balance, stored) {
|
|
453
|
+
if (!(stored !== void 0 && stored > 0)) return 0;
|
|
454
|
+
return Math.max(0, Math.min(stored, balance));
|
|
455
|
+
}
|
|
456
|
+
function spendableBalance(balance, expiredHoldBacking) {
|
|
457
|
+
return sumAmounts(balance, -heldExpiredBacking(balance, expiredHoldBacking));
|
|
409
458
|
}
|
|
410
459
|
function planBalanceTarget(target, reserved, bonusCredits) {
|
|
460
|
+
assertValidBalanceTarget(target, "balance target", { reserved, bonusCredits });
|
|
411
461
|
const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
|
|
412
462
|
return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
|
|
413
463
|
}
|
|
414
|
-
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
|
|
464
|
+
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking) {
|
|
415
465
|
const spendable = spendableBalance(balance, expiredHoldBacking);
|
|
416
466
|
const target = Math.min(spendable, cap);
|
|
417
467
|
const kept = Math.max(
|
|
@@ -422,8 +472,8 @@ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
|
|
|
422
472
|
return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
|
|
423
473
|
}
|
|
424
474
|
function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
|
|
425
|
-
const held =
|
|
426
|
-
const used = Math.min(held, holdBacking);
|
|
475
|
+
const held = heldExpiredBacking(balance, expiredHoldBacking);
|
|
476
|
+
const used = Math.max(0, Math.min(held, holdBacking));
|
|
427
477
|
return { used, remaining: sumAmounts(held, -used) };
|
|
428
478
|
}
|
|
429
479
|
function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
|
|
@@ -438,27 +488,83 @@ function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking,
|
|
|
438
488
|
expiredHoldBacking: backing.remaining
|
|
439
489
|
};
|
|
440
490
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
491
|
+
var NO_EXPIRED_BACKING_PREFIX = {
|
|
492
|
+
holdPlacedAt: "",
|
|
493
|
+
id: "",
|
|
494
|
+
share: 0
|
|
495
|
+
};
|
|
496
|
+
function compareHolds(a, b) {
|
|
497
|
+
if (a.holdPlacedAt !== b.holdPlacedAt) return a.holdPlacedAt < b.holdPlacedAt ? -1 : 1;
|
|
498
|
+
if (a.id === b.id) return 0;
|
|
499
|
+
return a.id < b.id ? -1 : 1;
|
|
500
|
+
}
|
|
501
|
+
function recordedExpiredBacking(hold) {
|
|
502
|
+
const stored = hold.expiredBacking;
|
|
503
|
+
if (typeof stored !== "number" || !Number.isFinite(stored) || stored <= 0) return 0;
|
|
504
|
+
if (!isValidCreditAmount(hold.amount)) return 0;
|
|
505
|
+
return Math.min(storedAmountCeil(stored), hold.amount);
|
|
506
|
+
}
|
|
507
|
+
function allocateExpiredBacking(holds, total) {
|
|
508
|
+
const placed = holds.filter((hold) => hold.holdPlacedAt && isValidCreditAmount(hold.amount)).map((hold) => ({ ...hold, holdPlacedAt: hold.holdPlacedAt })).sort(compareHolds);
|
|
509
|
+
const wanted = Math.max(0, total);
|
|
510
|
+
if (wanted === 0) return { total: 0, frontier: null };
|
|
511
|
+
const recorded = placed.filter((hold) => recordedExpiredBacking(hold) > 0);
|
|
512
|
+
const shares = new Map(recorded.map((hold) => [hold.id, recordedExpiredBacking(hold)]));
|
|
513
|
+
let left = sumAmounts(wanted, ...[...shares.values()].map((share) => -share));
|
|
514
|
+
for (const hold of [...recorded].reverse()) {
|
|
515
|
+
if (left >= 0) break;
|
|
516
|
+
const share = shares.get(hold.id);
|
|
517
|
+
const cut = Math.min(share, -left);
|
|
518
|
+
shares.set(hold.id, sumAmounts(share, -cut));
|
|
519
|
+
left = sumAmounts(left, cut);
|
|
451
520
|
}
|
|
521
|
+
let frontier = NO_EXPIRED_BACKING_PREFIX;
|
|
452
522
|
for (const hold of placed) {
|
|
523
|
+
if (left <= 0) break;
|
|
524
|
+
if (shares.has(hold.id)) continue;
|
|
525
|
+
const share = Math.min(hold.amount, left);
|
|
526
|
+
frontier = { holdPlacedAt: hold.holdPlacedAt, id: hold.id, share };
|
|
527
|
+
left = sumAmounts(left, -share);
|
|
528
|
+
}
|
|
529
|
+
for (const hold of recorded) {
|
|
530
|
+
if (left <= 0) break;
|
|
453
531
|
const share = shares.get(hold.id);
|
|
454
532
|
const added = Math.min(sumAmounts(hold.amount, -share), left);
|
|
455
533
|
shares.set(hold.id, sumAmounts(share, added));
|
|
456
534
|
left = sumAmounts(left, -added);
|
|
457
535
|
}
|
|
458
|
-
|
|
536
|
+
const rewrites = recorded.filter((hold) => shares.get(hold.id) !== hold.expiredBacking).map((hold) => ({ id: hold.id, expiredBacking: shares.get(hold.id) }));
|
|
537
|
+
return { total: wanted, frontier, ...rewrites.length > 0 ? { rewrites } : {} };
|
|
459
538
|
}
|
|
460
|
-
function
|
|
461
|
-
|
|
539
|
+
function holdExpiredBacking(hold, frontier, rewrites) {
|
|
540
|
+
const rewritten = rewrites?.find((rewrite) => rewrite.id === hold.id);
|
|
541
|
+
const recorded = recordedExpiredBacking(
|
|
542
|
+
rewritten ? { ...hold, expiredBacking: rewritten.expiredBacking } : hold
|
|
543
|
+
);
|
|
544
|
+
if (recorded > 0) return recorded;
|
|
545
|
+
if (!frontier || !hold.holdPlacedAt) return 0;
|
|
546
|
+
const order = compareHolds({ holdPlacedAt: hold.holdPlacedAt, id: hold.id }, frontier);
|
|
547
|
+
if (order > 0) return 0;
|
|
548
|
+
return order < 0 ? hold.amount : Math.min(frontier.share, hold.amount);
|
|
549
|
+
}
|
|
550
|
+
function holdPlacedAtAfter(now, frontier) {
|
|
551
|
+
if (!frontier?.holdPlacedAt) return now;
|
|
552
|
+
const earliest = new Date(Date.parse(frontier.holdPlacedAt) + 1).toISOString();
|
|
553
|
+
return now > earliest ? now : earliest;
|
|
554
|
+
}
|
|
555
|
+
function expiredBackingPatch(next, stored) {
|
|
556
|
+
const total = Math.max(0, next.total);
|
|
557
|
+
const patch = {};
|
|
558
|
+
const storedTotal = stored.expiredHoldBacking;
|
|
559
|
+
if (total > 0 || storedTotal !== void 0 && storedTotal !== 0) {
|
|
560
|
+
patch.expiredHoldBacking = total;
|
|
561
|
+
}
|
|
562
|
+
if (total <= 0) {
|
|
563
|
+
if (stored.expiredBackingFrontier) patch.expiredBackingFrontier = null;
|
|
564
|
+
} else if (next.frontier) {
|
|
565
|
+
patch.expiredBackingFrontier = next.frontier;
|
|
566
|
+
}
|
|
567
|
+
return patch;
|
|
462
568
|
}
|
|
463
569
|
function assertValidBalanceTarget(value, field, context) {
|
|
464
570
|
assertRepresentableAmount(value, field, context);
|
|
@@ -471,7 +577,10 @@ function assertValidBalanceTarget(value, field, context) {
|
|
|
471
577
|
}
|
|
472
578
|
}
|
|
473
579
|
function assertValidTierBalanceInput(input, context) {
|
|
474
|
-
if (input.balanceCap === void 0)
|
|
580
|
+
if (input.balanceCap === void 0) {
|
|
581
|
+
if (input.balance !== void 0) assertValidBalanceTarget(input.balance, "balance", context);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
475
584
|
if (input.balance !== void 0) {
|
|
476
585
|
throw new CreditError(
|
|
477
586
|
"A tier update cannot set both balance and balanceCap",
|
|
@@ -997,17 +1106,25 @@ var DEFAULT_CONFIG = {
|
|
|
997
1106
|
}
|
|
998
1107
|
};
|
|
999
1108
|
var currentConfig = DEFAULT_CONFIG;
|
|
1109
|
+
function isOperationCostMap(value) {
|
|
1110
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1111
|
+
return Object.values(value).every((cost) => isRepresentableAmount(cost) && cost >= 0);
|
|
1112
|
+
}
|
|
1000
1113
|
function loadConfigFromEnv() {
|
|
1001
1114
|
const envConfig = {};
|
|
1002
1115
|
const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;
|
|
1003
1116
|
if (operationCostsEnv) {
|
|
1004
1117
|
try {
|
|
1005
1118
|
const parsed = JSON.parse(operationCostsEnv);
|
|
1006
|
-
if (
|
|
1119
|
+
if (isOperationCostMap(parsed)) {
|
|
1007
1120
|
envConfig.operationCosts = {
|
|
1008
1121
|
...DEFAULT_CONFIG.operationCosts,
|
|
1009
1122
|
...parsed
|
|
1010
1123
|
};
|
|
1124
|
+
} else {
|
|
1125
|
+
console.warn(
|
|
1126
|
+
"[Credits Config] Ignoring CREDITS_OPERATION_COSTS: it must be a JSON object whose costs are numbers >= 0, exact to the cent"
|
|
1127
|
+
);
|
|
1011
1128
|
}
|
|
1012
1129
|
} catch {
|
|
1013
1130
|
console.warn("[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON");
|
|
@@ -1437,7 +1554,7 @@ function validateBalanceUpdate(current, updates) {
|
|
|
1437
1554
|
const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
|
|
1438
1555
|
const errors = [];
|
|
1439
1556
|
if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
|
|
1440
|
-
const marked = updates.balance === void 0 ? current.expiredHoldBacking
|
|
1557
|
+
const marked = updates.balance === void 0 ? heldExpiredBacking(current.balance, current.expiredHoldBacking) : 0;
|
|
1441
1558
|
if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
|
|
1442
1559
|
errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
|
|
1443
1560
|
}
|
|
@@ -1455,6 +1572,50 @@ function assertValidBalanceUpdate(current, updates) {
|
|
|
1455
1572
|
throw new Error(`Invalid balance update: ${result.errors.join("; ")}`);
|
|
1456
1573
|
}
|
|
1457
1574
|
}
|
|
1575
|
+
function signupGrantJournalInput(userId, tier, grant) {
|
|
1576
|
+
if (grant <= 0) return null;
|
|
1577
|
+
return {
|
|
1578
|
+
userId,
|
|
1579
|
+
entryType: "credit",
|
|
1580
|
+
amount: grant,
|
|
1581
|
+
balanceBefore: 0,
|
|
1582
|
+
balanceAfter: grant,
|
|
1583
|
+
source: "bonus",
|
|
1584
|
+
referenceId: `signup-${userId}`,
|
|
1585
|
+
referenceType: "adjustment",
|
|
1586
|
+
description: `Signup grant for ${tier} tier.`,
|
|
1587
|
+
metadata: { tier }
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function tierChangeJournalInput(input) {
|
|
1591
|
+
const { userId, previousTier, newTier, previousBalance, newBalance, bonusCredits } = input;
|
|
1592
|
+
const change = sumAmounts(newBalance, -previousBalance);
|
|
1593
|
+
if (change === 0) return null;
|
|
1594
|
+
const raised = change > 0;
|
|
1595
|
+
const amount = raised ? newBalance : 0;
|
|
1596
|
+
const balanceBefore = sumAmounts(previousBalance, bonusCredits);
|
|
1597
|
+
const balanceAfter = sumAmounts(newBalance, bonusCredits);
|
|
1598
|
+
assertRepresentableFields(
|
|
1599
|
+
{
|
|
1600
|
+
"journal amount": amount,
|
|
1601
|
+
"journal balanceBefore": balanceBefore,
|
|
1602
|
+
"journal balanceAfter": balanceAfter
|
|
1603
|
+
},
|
|
1604
|
+
{ userId, operation: "updateUserTier" }
|
|
1605
|
+
);
|
|
1606
|
+
return {
|
|
1607
|
+
userId,
|
|
1608
|
+
entryType: raised ? "credit" : "debit",
|
|
1609
|
+
amount,
|
|
1610
|
+
balanceBefore,
|
|
1611
|
+
balanceAfter,
|
|
1612
|
+
source: raised ? "subscription_upgrade" : "subscription_downgrade",
|
|
1613
|
+
referenceId: `tier-${Date.now()}`,
|
|
1614
|
+
referenceType: "subscription",
|
|
1615
|
+
description: `Tier changed from ${previousTier} to ${newTier}.`,
|
|
1616
|
+
metadata: { previousTier, newTier, previousBalance, newBalance }
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1458
1619
|
|
|
1459
1620
|
// src/repository/memory/snapshot.ts
|
|
1460
1621
|
function copyRecord(record) {
|
|
@@ -1589,6 +1750,7 @@ function commitTransitionJournal(store, input, existingId) {
|
|
|
1589
1750
|
userId: input.userId,
|
|
1590
1751
|
entryType: input.entryType,
|
|
1591
1752
|
amount: input.amount,
|
|
1753
|
+
balanceBefore: input.balanceBefore,
|
|
1592
1754
|
balanceAfter: input.balanceAfter,
|
|
1593
1755
|
source: input.source,
|
|
1594
1756
|
referenceId: input.reservationId,
|
|
@@ -1638,16 +1800,28 @@ function openHolds(store, userId) {
|
|
|
1638
1800
|
(reservation) => reservation.status === "reserved"
|
|
1639
1801
|
);
|
|
1640
1802
|
}
|
|
1803
|
+
function expiredBackingInForce(store, credits) {
|
|
1804
|
+
const total = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
1805
|
+
if (total > 0 && (!credits.expiredBackingFrontier || credits.expiredHoldBacking !== total)) {
|
|
1806
|
+
return allocateExpiredBacking(openHolds(store, credits.userId), total);
|
|
1807
|
+
}
|
|
1808
|
+
return { total, frontier: credits.expiredBackingFrontier ?? null };
|
|
1809
|
+
}
|
|
1810
|
+
function writeRecordedShares(store, userId, allocation) {
|
|
1811
|
+
for (const { id, expiredBacking } of allocation.rewrites ?? []) {
|
|
1812
|
+
const hold = getReservation(store, userId, id);
|
|
1813
|
+
if (hold) hold.expiredBacking = expiredBacking;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1641
1816
|
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;
|
|
1817
|
+
if (next <= 0) {
|
|
1818
|
+
Object.assign(credits, expiredBackingPatch({ total: 0 }, credits));
|
|
1819
|
+
return;
|
|
1649
1820
|
}
|
|
1650
|
-
|
|
1821
|
+
if (next === credits.expiredHoldBacking) return;
|
|
1822
|
+
const allocation = allocateExpiredBacking(openHolds(store, credits.userId), next);
|
|
1823
|
+
writeRecordedShares(store, credits.userId, allocation);
|
|
1824
|
+
Object.assign(credits, expiredBackingPatch(allocation, credits));
|
|
1651
1825
|
}
|
|
1652
1826
|
function getReservation(store, userId, reservationId) {
|
|
1653
1827
|
return store.reservations.get(userId)?.get(reservationId);
|
|
@@ -1696,6 +1870,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1696
1870
|
};
|
|
1697
1871
|
}
|
|
1698
1872
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1873
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1699
1874
|
const reservation = {
|
|
1700
1875
|
id: generateId(),
|
|
1701
1876
|
userId: input.userId,
|
|
@@ -1708,7 +1883,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1708
1883
|
// The hold-origin fact. Written here and nowhere else, in the same
|
|
1709
1884
|
// critical section that raises `reserved` below, so the row and the hold
|
|
1710
1885
|
// become visible together. `createReservation` cannot set it.
|
|
1711
|
-
holdPlacedAt: now
|
|
1886
|
+
holdPlacedAt: holdPlacedAtAfter(now, backing.total > 0 ? backing.frontier : null)
|
|
1712
1887
|
};
|
|
1713
1888
|
if (!store.reservations.has(input.userId)) {
|
|
1714
1889
|
store.reservations.set(input.userId, /* @__PURE__ */ new Map());
|
|
@@ -1726,6 +1901,8 @@ async function reserveCreditsV2(store, input) {
|
|
|
1726
1901
|
);
|
|
1727
1902
|
}
|
|
1728
1903
|
credits.reserved = nextReserved;
|
|
1904
|
+
writeRecordedShares(store, input.userId, backing);
|
|
1905
|
+
Object.assign(credits, expiredBackingPatch(backing, credits));
|
|
1729
1906
|
credits.updatedAt = now;
|
|
1730
1907
|
return { outcome: "created", reservation: copyRecord(reservation) };
|
|
1731
1908
|
});
|
|
@@ -1746,11 +1923,12 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1746
1923
|
const amount = reservation.amount;
|
|
1747
1924
|
await yieldPoint(store);
|
|
1748
1925
|
assertReservedCoversHold(credits, userId, reservationId, amount);
|
|
1926
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1749
1927
|
const paid = planHoldCommit(
|
|
1750
1928
|
credits.balance,
|
|
1751
1929
|
credits.bonusCredits,
|
|
1752
|
-
|
|
1753
|
-
reservation.
|
|
1930
|
+
backing.total,
|
|
1931
|
+
holdExpiredBacking(reservation, backing.frontier, backing.rewrites),
|
|
1754
1932
|
amount
|
|
1755
1933
|
);
|
|
1756
1934
|
if (!paid.covered) {
|
|
@@ -1768,6 +1946,7 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1768
1946
|
operation: "commitReservation",
|
|
1769
1947
|
entryType: "debit",
|
|
1770
1948
|
amount,
|
|
1949
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1771
1950
|
balanceAfter,
|
|
1772
1951
|
source: "operation_commit",
|
|
1773
1952
|
reservationId,
|
|
@@ -1795,9 +1974,10 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1795
1974
|
credits.bonusCredits = nextBonusCredits;
|
|
1796
1975
|
credits.reserved = nextReserved;
|
|
1797
1976
|
credits.monthlyUsed = nextMonthlyUsed;
|
|
1977
|
+
writeRecordedShares(store, userId, backing);
|
|
1798
1978
|
Object.assign(
|
|
1799
1979
|
credits,
|
|
1800
|
-
|
|
1980
|
+
expiredBackingPatch({ total: paid.expiredHoldBacking, frontier: backing.frontier }, credits)
|
|
1801
1981
|
);
|
|
1802
1982
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1803
1983
|
const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
|
|
@@ -1878,10 +2058,11 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1878
2058
|
reservation.id,
|
|
1879
2059
|
reservation.amount
|
|
1880
2060
|
);
|
|
2061
|
+
const inForce = expiredBackingInForce(store, credits);
|
|
1881
2062
|
const backing = settleExpiredHoldBacking(
|
|
1882
2063
|
credits.balance,
|
|
1883
|
-
|
|
1884
|
-
reservation.
|
|
2064
|
+
inForce.total,
|
|
2065
|
+
holdExpiredBacking(reservation, inForce.frontier, inForce.rewrites)
|
|
1885
2066
|
);
|
|
1886
2067
|
const nextBalance = sumAmounts(credits.balance, -backing.used);
|
|
1887
2068
|
const journal = {
|
|
@@ -1889,6 +2070,7 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1889
2070
|
operation: `${transition.transition}Reservation`,
|
|
1890
2071
|
entryType: "credit",
|
|
1891
2072
|
amount: 0,
|
|
2073
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1892
2074
|
balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
|
|
1893
2075
|
source: transition.source,
|
|
1894
2076
|
reservationId: reservation.id,
|
|
@@ -1911,7 +2093,11 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1911
2093
|
reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1912
2094
|
credits.reserved = nextReserved;
|
|
1913
2095
|
credits.balance = nextBalance;
|
|
1914
|
-
|
|
2096
|
+
writeRecordedShares(store, reservation.userId, inForce);
|
|
2097
|
+
Object.assign(
|
|
2098
|
+
credits,
|
|
2099
|
+
expiredBackingPatch({ total: backing.remaining, frontier: inForce.frontier }, credits)
|
|
2100
|
+
);
|
|
1915
2101
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1916
2102
|
return commitTransitionJournal(store, journal, existingEntryId);
|
|
1917
2103
|
}
|
|
@@ -1954,7 +2140,7 @@ var InMemoryCreditRepository = class {
|
|
|
1954
2140
|
* simultaneous first deliveries of the same reference would then both credit.
|
|
1955
2141
|
*/
|
|
1956
2142
|
seedUser(userId, tier, initialBalance) {
|
|
1957
|
-
|
|
2143
|
+
assertValidBalanceTarget(initialBalance, "initialBalance", { userId });
|
|
1958
2144
|
const configured = getConfigMonthlyLimit(tier);
|
|
1959
2145
|
assertRepresentableTierAmount(configured, "monthlyLimit", { userId, tier });
|
|
1960
2146
|
const monthlyLimit = storedMonthlyLimit(configured);
|
|
@@ -1973,6 +2159,8 @@ var InMemoryCreditRepository = class {
|
|
|
1973
2159
|
updatedAt: now
|
|
1974
2160
|
};
|
|
1975
2161
|
this.store.users.set(userId, credits);
|
|
2162
|
+
const grantLine = signupGrantJournalInput(userId, tier, initialBalance);
|
|
2163
|
+
if (grantLine) this.recordJournalEntry(grantLine);
|
|
1976
2164
|
return credits;
|
|
1977
2165
|
}
|
|
1978
2166
|
async updateUserCredits(userId, updates) {
|
|
@@ -2035,9 +2223,11 @@ var InMemoryCreditRepository = class {
|
|
|
2035
2223
|
userId,
|
|
2036
2224
|
operation: "updateUserCredits"
|
|
2037
2225
|
});
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2226
|
+
writeExpiredHoldBacking(
|
|
2227
|
+
this.store,
|
|
2228
|
+
next,
|
|
2229
|
+
updates.balance !== void 0 ? 0 : Math.min(heldExpiredBacking(credits.balance, credits.expiredHoldBacking), next.balance)
|
|
2230
|
+
);
|
|
2041
2231
|
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2042
2232
|
this.store.users.set(userId, next);
|
|
2043
2233
|
}
|
|
@@ -2051,24 +2241,37 @@ var InMemoryCreditRepository = class {
|
|
|
2051
2241
|
if (!credits) {
|
|
2052
2242
|
throw new Error(`User ${userId} not found`);
|
|
2053
2243
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2244
|
+
let planned;
|
|
2245
|
+
if (input.balance !== void 0) {
|
|
2246
|
+
planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);
|
|
2247
|
+
} else if (input.balanceCap !== void 0 && !isFreeTier(credits.tier)) {
|
|
2248
|
+
planned = planBalanceCap(
|
|
2056
2249
|
credits.balance,
|
|
2057
2250
|
input.balanceCap,
|
|
2058
2251
|
credits.reserved,
|
|
2059
2252
|
credits.bonusCredits,
|
|
2060
2253
|
credits.expiredHoldBacking
|
|
2061
2254
|
);
|
|
2062
|
-
credits.balance = capped.balance;
|
|
2063
|
-
writeExpiredHoldBacking(this.store, credits, capped.expiredHoldBacking);
|
|
2064
2255
|
}
|
|
2256
|
+
const journalLine = planned ? tierChangeJournalInput({
|
|
2257
|
+
userId,
|
|
2258
|
+
previousTier: credits.tier,
|
|
2259
|
+
newTier: input.tier,
|
|
2260
|
+
previousBalance: credits.balance,
|
|
2261
|
+
newBalance: planned.balance,
|
|
2262
|
+
bonusCredits: credits.bonusCredits
|
|
2263
|
+
}) : null;
|
|
2065
2264
|
credits.tier = input.tier;
|
|
2066
2265
|
credits.monthlyLimit = input.monthlyLimit;
|
|
2067
|
-
if (
|
|
2068
|
-
const planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);
|
|
2266
|
+
if (planned) {
|
|
2069
2267
|
credits.balance = planned.balance;
|
|
2070
2268
|
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
2071
2269
|
}
|
|
2270
|
+
writeExpiredHoldBacking(
|
|
2271
|
+
this.store,
|
|
2272
|
+
credits,
|
|
2273
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
2274
|
+
);
|
|
2072
2275
|
if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
|
|
2073
2276
|
if (input.subscriptionExpiresAt !== void 0) {
|
|
2074
2277
|
if (input.subscriptionExpiresAt === null) {
|
|
@@ -2079,6 +2282,7 @@ var InMemoryCreditRepository = class {
|
|
|
2079
2282
|
}
|
|
2080
2283
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2081
2284
|
this.store.users.set(userId, credits);
|
|
2285
|
+
if (journalLine) this.recordJournalEntry(journalLine);
|
|
2082
2286
|
}
|
|
2083
2287
|
// ==================== Reservations ====================
|
|
2084
2288
|
async createReservation(input) {
|
|
@@ -2230,6 +2434,11 @@ var InMemoryCreditRepository = class {
|
|
|
2230
2434
|
{ userId, operation: "addCredits" }
|
|
2231
2435
|
);
|
|
2232
2436
|
credits.bonusCredits = nextBonusCredits;
|
|
2437
|
+
writeExpiredHoldBacking(
|
|
2438
|
+
this.store,
|
|
2439
|
+
credits,
|
|
2440
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
2441
|
+
);
|
|
2233
2442
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2234
2443
|
this.store.users.set(userId, credits);
|
|
2235
2444
|
const transaction = this.recordTransaction({
|
|
@@ -2249,6 +2458,7 @@ var InMemoryCreditRepository = class {
|
|
|
2249
2458
|
userId,
|
|
2250
2459
|
entryType: "credit",
|
|
2251
2460
|
amount,
|
|
2461
|
+
balanceBefore: previousTotal,
|
|
2252
2462
|
balanceAfter: newTotal,
|
|
2253
2463
|
source: payload.source,
|
|
2254
2464
|
referenceId: transaction.id ?? paymentRef ?? "unknown",
|
|
@@ -2282,7 +2492,7 @@ var InMemoryCreditRepository = class {
|
|
|
2282
2492
|
});
|
|
2283
2493
|
}
|
|
2284
2494
|
}
|
|
2285
|
-
async deductCreditsAtomic(userId, amount) {
|
|
2495
|
+
async deductCreditsAtomic(userId, amount, journal) {
|
|
2286
2496
|
assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
|
|
2287
2497
|
const credits = this.store.users.get(userId);
|
|
2288
2498
|
if (!credits) {
|
|
@@ -2308,11 +2518,26 @@ var InMemoryCreditRepository = class {
|
|
|
2308
2518
|
{ previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },
|
|
2309
2519
|
{ userId, operation: "deductCredits" }
|
|
2310
2520
|
);
|
|
2521
|
+
const held = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
2311
2522
|
credits.balance = nextBalance;
|
|
2312
2523
|
credits.bonusCredits = nextBonusCredits;
|
|
2524
|
+
writeExpiredHoldBacking(this.store, credits, held);
|
|
2313
2525
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2314
2526
|
this.store.users.set(userId, credits);
|
|
2315
|
-
return { previousBalance, newBalance };
|
|
2527
|
+
if (!journal) return { previousBalance, newBalance };
|
|
2528
|
+
const entry = this.recordJournalEntry({
|
|
2529
|
+
userId,
|
|
2530
|
+
entryType: "debit",
|
|
2531
|
+
amount,
|
|
2532
|
+
balanceBefore: previousBalance,
|
|
2533
|
+
balanceAfter: newBalance,
|
|
2534
|
+
source: journal.source ?? "operation_commit",
|
|
2535
|
+
referenceId: journal.referenceId,
|
|
2536
|
+
referenceType: journal.referenceType ?? "adjustment",
|
|
2537
|
+
description: journal.description,
|
|
2538
|
+
metadata: journal.metadata
|
|
2539
|
+
});
|
|
2540
|
+
return { previousBalance, newBalance, journalEntryId: entry.id };
|
|
2316
2541
|
}
|
|
2317
2542
|
// ==================== Transactions ====================
|
|
2318
2543
|
async createTransaction(input) {
|
|
@@ -2426,6 +2651,11 @@ var InMemoryCreditRepository = class {
|
|
|
2426
2651
|
assertRepresentableAmount(input.balanceAfter, "journal balanceAfter", {
|
|
2427
2652
|
userId: input.userId
|
|
2428
2653
|
});
|
|
2654
|
+
if (input.balanceBefore !== void 0) {
|
|
2655
|
+
assertRepresentableAmount(input.balanceBefore, "journal balanceBefore", {
|
|
2656
|
+
userId: input.userId
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2429
2659
|
assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });
|
|
2430
2660
|
assertPublicJournalKey(input.idempotencyKey, input.userId);
|
|
2431
2661
|
const key = input.idempotencyKey !== void 0 ? scopedKey(input.userId, input.idempotencyKey) : void 0;
|
|
@@ -2441,6 +2671,7 @@ var InMemoryCreditRepository = class {
|
|
|
2441
2671
|
userId: input.userId,
|
|
2442
2672
|
entryType: input.entryType,
|
|
2443
2673
|
amount: input.amount,
|
|
2674
|
+
...input.balanceBefore !== void 0 ? { balanceBefore: input.balanceBefore } : {},
|
|
2444
2675
|
balanceAfter: input.balanceAfter,
|
|
2445
2676
|
source: input.source,
|
|
2446
2677
|
referenceId: input.referenceId,
|
|
@@ -2593,6 +2824,7 @@ var InMemoryCreditRepository = class {
|
|
|
2593
2824
|
userId,
|
|
2594
2825
|
entryType: change > 0 ? "credit" : "debit",
|
|
2595
2826
|
amount: Math.abs(change),
|
|
2827
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
2596
2828
|
balanceAfter,
|
|
2597
2829
|
source: "monthly_reset",
|
|
2598
2830
|
referenceId: `reset-${Date.now()}`,
|
|
@@ -2672,7 +2904,8 @@ var InMemoryCreditRepository = class {
|
|
|
2672
2904
|
userId,
|
|
2673
2905
|
entryType: "debit",
|
|
2674
2906
|
amount: 0,
|
|
2675
|
-
//
|
|
2907
|
+
// 0, however far the cap lowered the balance: the totals record the move
|
|
2908
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
2676
2909
|
balanceAfter,
|
|
2677
2910
|
source: "subscription_downgrade",
|
|
2678
2911
|
referenceId: `downgrade-${Date.now()}`,
|
|
@@ -2835,7 +3068,8 @@ var CreditsService = class {
|
|
|
2835
3068
|
userId,
|
|
2836
3069
|
entryType: "debit",
|
|
2837
3070
|
amount: 0,
|
|
2838
|
-
//
|
|
3071
|
+
// 0, however far the cap lowered the balance: the totals record the move
|
|
3072
|
+
balanceBefore: sumAmounts(data.balance, data.bonusCredits),
|
|
2839
3073
|
// The ledger balance, matching every other journal writer: the V2
|
|
2840
3074
|
// transitions and add/deduct all record `balance + bonusCredits`, so
|
|
2841
3075
|
// recording `balance` alone made the audit trail disagree with itself
|
|
@@ -2879,6 +3113,7 @@ var CreditsService = class {
|
|
|
2879
3113
|
userId,
|
|
2880
3114
|
entryType: balanceChange > 0 ? "credit" : "debit",
|
|
2881
3115
|
amount: Math.abs(balanceChange),
|
|
3116
|
+
balanceBefore: sumAmounts(data.balance, data.bonusCredits),
|
|
2882
3117
|
balanceAfter: sumAmounts(
|
|
2883
3118
|
resetResult.credits.balance,
|
|
2884
3119
|
resetResult.credits.bonusCredits
|
|
@@ -3113,9 +3348,18 @@ var CreditsService = class {
|
|
|
3113
3348
|
*/
|
|
3114
3349
|
async deductCredits(userId, amount, options) {
|
|
3115
3350
|
assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
|
|
3351
|
+
const operationType = options?.operationType;
|
|
3352
|
+
const journal = {
|
|
3353
|
+
referenceId: options?.requestId ?? `deduct-${Date.now()}`,
|
|
3354
|
+
description: operationType ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}` : `Deducted ${amount} credits`,
|
|
3355
|
+
metadata: {
|
|
3356
|
+
...options?.metadata,
|
|
3357
|
+
...operationType && { operationType }
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3116
3360
|
let deduction;
|
|
3117
3361
|
try {
|
|
3118
|
-
deduction = await this.repository.deductCreditsAtomic(userId, amount);
|
|
3362
|
+
deduction = await this.repository.deductCreditsAtomic(userId, amount, journal);
|
|
3119
3363
|
} catch (error) {
|
|
3120
3364
|
if (isCreditError(error) && error.code === CreditErrorCode.INVALID_AMOUNT) throw error;
|
|
3121
3365
|
const credits = await this.repository.getUserCredits(userId);
|
|
@@ -3131,21 +3375,18 @@ var CreditsService = class {
|
|
|
3131
3375
|
shortfall: sumAmounts(amount, -available)
|
|
3132
3376
|
};
|
|
3133
3377
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
...operationType && { operationType }
|
|
3147
|
-
}
|
|
3148
|
-
});
|
|
3378
|
+
if (deduction.journalEntryId === void 0) {
|
|
3379
|
+
await this.repository.createJournalEntry({
|
|
3380
|
+
userId,
|
|
3381
|
+
entryType: "debit",
|
|
3382
|
+
amount,
|
|
3383
|
+
balanceBefore: deduction.previousBalance,
|
|
3384
|
+
balanceAfter: deduction.newBalance,
|
|
3385
|
+
source: "operation_commit",
|
|
3386
|
+
referenceType: "adjustment",
|
|
3387
|
+
...journal
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3149
3390
|
if (operationType) {
|
|
3150
3391
|
await this.logUsage({
|
|
3151
3392
|
userId,
|
|
@@ -3219,7 +3460,8 @@ var CreditsService = class {
|
|
|
3219
3460
|
]);
|
|
3220
3461
|
const historyEntries = entries.map((entry) => {
|
|
3221
3462
|
const type = this.mapSourceToHistoryType(entry.source);
|
|
3222
|
-
const
|
|
3463
|
+
const signedAmount = entry.entryType === "credit" ? entry.amount : -entry.amount;
|
|
3464
|
+
const creditsChange = entry.balanceBefore !== void 0 ? sumAmounts(entry.balanceAfter, -entry.balanceBefore) : signedAmount;
|
|
3223
3465
|
return {
|
|
3224
3466
|
id: entry.id,
|
|
3225
3467
|
type,
|
|
@@ -4156,6 +4398,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4156
4398
|
CreditsSDKError,
|
|
4157
4399
|
CreditsService,
|
|
4158
4400
|
InMemoryCreditRepository,
|
|
4401
|
+
NO_EXPIRED_BACKING_PREFIX,
|
|
4159
4402
|
NetworkError,
|
|
4160
4403
|
RESERVATION_STATUSES,
|
|
4161
4404
|
RESERVED_JOURNAL_KEY_PREFIX,
|
|
@@ -4168,6 +4411,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4168
4411
|
UNLIMITED_BALANCE_SENTINEL,
|
|
4169
4412
|
ValidationError,
|
|
4170
4413
|
addCreditsThroughRepository,
|
|
4414
|
+
allocateExpiredBacking,
|
|
4171
4415
|
assertDirectStatusWriteAllowed,
|
|
4172
4416
|
assertHoldPlaced,
|
|
4173
4417
|
assertKnownReservationStatus,
|
|
@@ -4184,9 +4428,9 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4184
4428
|
assertValidStoredAmount,
|
|
4185
4429
|
assertValidStoredAmountRaw,
|
|
4186
4430
|
assertValidTierBalanceInput,
|
|
4187
|
-
attributeExpiredBacking,
|
|
4188
4431
|
backedBalanceFloor,
|
|
4189
4432
|
calculateAvailableCredits,
|
|
4433
|
+
canonicalAmount,
|
|
4190
4434
|
cappedBalance,
|
|
4191
4435
|
checkAndNotifyLowBalance,
|
|
4192
4436
|
classifyDatabaseError,
|
|
@@ -4213,7 +4457,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4213
4457
|
createUnsupportedOperationError,
|
|
4214
4458
|
createUserNotFoundError,
|
|
4215
4459
|
describePaymentMismatch,
|
|
4216
|
-
|
|
4460
|
+
expiredBackingPatch,
|
|
4217
4461
|
generateId,
|
|
4218
4462
|
generateRequestId,
|
|
4219
4463
|
genericDeferred,
|
|
@@ -4239,6 +4483,9 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4239
4483
|
getValidOperationTypes,
|
|
4240
4484
|
getValidTiers,
|
|
4241
4485
|
hasPlacedHold,
|
|
4486
|
+
heldExpiredBacking,
|
|
4487
|
+
holdExpiredBacking,
|
|
4488
|
+
holdPlacedAtAfter,
|
|
4242
4489
|
initializeConfig,
|
|
4243
4490
|
isCreditError,
|
|
4244
4491
|
isCreditErrorMessage,
|
|
@@ -4264,6 +4511,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4264
4511
|
isWinningOutcome,
|
|
4265
4512
|
loadConfigFromEnv,
|
|
4266
4513
|
monthlyResetBalance,
|
|
4514
|
+
multiplyAmount,
|
|
4267
4515
|
noopDeferred,
|
|
4268
4516
|
normalizePaymentRef,
|
|
4269
4517
|
notifySubscriptionExpired,
|
|
@@ -4275,6 +4523,7 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4275
4523
|
planBalanceCap,
|
|
4276
4524
|
planBalanceTarget,
|
|
4277
4525
|
planHoldCommit,
|
|
4526
|
+
recordedExpiredBacking,
|
|
4278
4527
|
registerConfigProvider,
|
|
4279
4528
|
registerNotificationHandler,
|
|
4280
4529
|
releaseReservationWithJournal,
|
|
@@ -4285,13 +4534,17 @@ function createDummyReservation2(userId, operationType) {
|
|
|
4285
4534
|
resetConfig,
|
|
4286
4535
|
sameAmount,
|
|
4287
4536
|
settleExpiredHoldBacking,
|
|
4537
|
+
signupGrantJournalInput,
|
|
4288
4538
|
spendableBalance,
|
|
4539
|
+
storedAmountCeil,
|
|
4540
|
+
storedAmountFloor,
|
|
4289
4541
|
storedMonthlyLimit,
|
|
4290
4542
|
sumAmounts,
|
|
4291
4543
|
supportsCreditsV2,
|
|
4292
4544
|
supportsIdempotentCredit,
|
|
4293
4545
|
synchronousDeferred,
|
|
4294
4546
|
terminalStatusOf,
|
|
4547
|
+
tierChangeJournalInput,
|
|
4295
4548
|
tierSchema,
|
|
4296
4549
|
toCents,
|
|
4297
4550
|
toClientUserCredits,
|