@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/core/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
329
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
|
382
|
-
|
|
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)
|
|
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
|
-
|
|
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,
|