@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
|
@@ -96,6 +96,23 @@ function floatSum(values) {
|
|
|
96
96
|
for (const value of values) total += value;
|
|
97
97
|
return total;
|
|
98
98
|
}
|
|
99
|
+
var FLOAT_NOISE_FLOOR = 1e-6;
|
|
100
|
+
var FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;
|
|
101
|
+
function floatNoise(value) {
|
|
102
|
+
return Math.max(FLOAT_NOISE_FLOOR, Math.abs(value) * FLOAT_NOISE_RELATIVE);
|
|
103
|
+
}
|
|
104
|
+
function canonicalAmount(value) {
|
|
105
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return value;
|
|
106
|
+
const cents = Math.round(value * 100);
|
|
107
|
+
if (!Number.isSafeInteger(cents)) return value;
|
|
108
|
+
const onGrid = cents / 100 + 0;
|
|
109
|
+
return Math.abs(value - onGrid) < floatNoise(value) ? onGrid : value;
|
|
110
|
+
}
|
|
111
|
+
function storedAmountCeil(value) {
|
|
112
|
+
const canonical = canonicalAmount(value);
|
|
113
|
+
if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
|
|
114
|
+
return Math.ceil(canonical * 100) / 100;
|
|
115
|
+
}
|
|
99
116
|
function isValidCreditAmount(value) {
|
|
100
117
|
if (typeof value !== "number") return false;
|
|
101
118
|
const cents = toCents(value);
|
|
@@ -148,14 +165,19 @@ function backedBalanceFloor(reserved, bonusCredits) {
|
|
|
148
165
|
assertRepresentableAmount(floor, "backedBalanceFloor", { reserved, bonusCredits });
|
|
149
166
|
return floor;
|
|
150
167
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
168
|
+
function heldExpiredBacking(balance, stored) {
|
|
169
|
+
if (!(stored !== void 0 && stored > 0)) return 0;
|
|
170
|
+
return Math.max(0, Math.min(stored, balance));
|
|
171
|
+
}
|
|
172
|
+
function spendableBalance(balance, expiredHoldBacking) {
|
|
173
|
+
return sumAmounts(balance, -heldExpiredBacking(balance, expiredHoldBacking));
|
|
153
174
|
}
|
|
154
175
|
function planBalanceTarget(target, reserved, bonusCredits) {
|
|
176
|
+
assertValidBalanceTarget(target, "balance target", { reserved, bonusCredits });
|
|
155
177
|
const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
|
|
156
178
|
return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
|
|
157
179
|
}
|
|
158
|
-
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
|
|
180
|
+
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking) {
|
|
159
181
|
const spendable = spendableBalance(balance, expiredHoldBacking);
|
|
160
182
|
const target = Math.min(spendable, cap);
|
|
161
183
|
const kept = Math.max(
|
|
@@ -166,8 +188,8 @@ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
|
|
|
166
188
|
return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
|
|
167
189
|
}
|
|
168
190
|
function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
|
|
169
|
-
const held =
|
|
170
|
-
const used = Math.min(held, holdBacking);
|
|
191
|
+
const held = heldExpiredBacking(balance, expiredHoldBacking);
|
|
192
|
+
const used = Math.max(0, Math.min(held, holdBacking));
|
|
171
193
|
return { used, remaining: sumAmounts(held, -used) };
|
|
172
194
|
}
|
|
173
195
|
function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
|
|
@@ -182,27 +204,83 @@ function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking,
|
|
|
182
204
|
expiredHoldBacking: backing.remaining
|
|
183
205
|
};
|
|
184
206
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
207
|
+
var NO_EXPIRED_BACKING_PREFIX = {
|
|
208
|
+
holdPlacedAt: "",
|
|
209
|
+
id: "",
|
|
210
|
+
share: 0
|
|
211
|
+
};
|
|
212
|
+
function compareHolds(a, b) {
|
|
213
|
+
if (a.holdPlacedAt !== b.holdPlacedAt) return a.holdPlacedAt < b.holdPlacedAt ? -1 : 1;
|
|
214
|
+
if (a.id === b.id) return 0;
|
|
215
|
+
return a.id < b.id ? -1 : 1;
|
|
216
|
+
}
|
|
217
|
+
function recordedExpiredBacking(hold) {
|
|
218
|
+
const stored = hold.expiredBacking;
|
|
219
|
+
if (typeof stored !== "number" || !Number.isFinite(stored) || stored <= 0) return 0;
|
|
220
|
+
if (!isValidCreditAmount(hold.amount)) return 0;
|
|
221
|
+
return Math.min(storedAmountCeil(stored), hold.amount);
|
|
222
|
+
}
|
|
223
|
+
function allocateExpiredBacking(holds, total) {
|
|
224
|
+
const placed = holds.filter((hold) => hold.holdPlacedAt && isValidCreditAmount(hold.amount)).map((hold) => ({ ...hold, holdPlacedAt: hold.holdPlacedAt })).sort(compareHolds);
|
|
225
|
+
const wanted = Math.max(0, total);
|
|
226
|
+
if (wanted === 0) return { total: 0, frontier: null };
|
|
227
|
+
const recorded = placed.filter((hold) => recordedExpiredBacking(hold) > 0);
|
|
228
|
+
const shares = new Map(recorded.map((hold) => [hold.id, recordedExpiredBacking(hold)]));
|
|
229
|
+
let left = sumAmounts(wanted, ...[...shares.values()].map((share) => -share));
|
|
230
|
+
for (const hold of [...recorded].reverse()) {
|
|
231
|
+
if (left >= 0) break;
|
|
232
|
+
const share = shares.get(hold.id);
|
|
233
|
+
const cut = Math.min(share, -left);
|
|
234
|
+
shares.set(hold.id, sumAmounts(share, -cut));
|
|
235
|
+
left = sumAmounts(left, cut);
|
|
195
236
|
}
|
|
237
|
+
let frontier = NO_EXPIRED_BACKING_PREFIX;
|
|
196
238
|
for (const hold of placed) {
|
|
239
|
+
if (left <= 0) break;
|
|
240
|
+
if (shares.has(hold.id)) continue;
|
|
241
|
+
const share = Math.min(hold.amount, left);
|
|
242
|
+
frontier = { holdPlacedAt: hold.holdPlacedAt, id: hold.id, share };
|
|
243
|
+
left = sumAmounts(left, -share);
|
|
244
|
+
}
|
|
245
|
+
for (const hold of recorded) {
|
|
246
|
+
if (left <= 0) break;
|
|
197
247
|
const share = shares.get(hold.id);
|
|
198
248
|
const added = Math.min(sumAmounts(hold.amount, -share), left);
|
|
199
249
|
shares.set(hold.id, sumAmounts(share, added));
|
|
200
250
|
left = sumAmounts(left, -added);
|
|
201
251
|
}
|
|
202
|
-
|
|
252
|
+
const rewrites = recorded.filter((hold) => shares.get(hold.id) !== hold.expiredBacking).map((hold) => ({ id: hold.id, expiredBacking: shares.get(hold.id) }));
|
|
253
|
+
return { total: wanted, frontier, ...rewrites.length > 0 ? { rewrites } : {} };
|
|
203
254
|
}
|
|
204
|
-
function
|
|
205
|
-
|
|
255
|
+
function holdExpiredBacking(hold, frontier, rewrites) {
|
|
256
|
+
const rewritten = rewrites?.find((rewrite) => rewrite.id === hold.id);
|
|
257
|
+
const recorded = recordedExpiredBacking(
|
|
258
|
+
rewritten ? { ...hold, expiredBacking: rewritten.expiredBacking } : hold
|
|
259
|
+
);
|
|
260
|
+
if (recorded > 0) return recorded;
|
|
261
|
+
if (!frontier || !hold.holdPlacedAt) return 0;
|
|
262
|
+
const order = compareHolds({ holdPlacedAt: hold.holdPlacedAt, id: hold.id }, frontier);
|
|
263
|
+
if (order > 0) return 0;
|
|
264
|
+
return order < 0 ? hold.amount : Math.min(frontier.share, hold.amount);
|
|
265
|
+
}
|
|
266
|
+
function holdPlacedAtAfter(now, frontier) {
|
|
267
|
+
if (!frontier?.holdPlacedAt) return now;
|
|
268
|
+
const earliest = new Date(Date.parse(frontier.holdPlacedAt) + 1).toISOString();
|
|
269
|
+
return now > earliest ? now : earliest;
|
|
270
|
+
}
|
|
271
|
+
function expiredBackingPatch(next, stored) {
|
|
272
|
+
const total = Math.max(0, next.total);
|
|
273
|
+
const patch = {};
|
|
274
|
+
const storedTotal = stored.expiredHoldBacking;
|
|
275
|
+
if (total > 0 || storedTotal !== void 0 && storedTotal !== 0) {
|
|
276
|
+
patch.expiredHoldBacking = total;
|
|
277
|
+
}
|
|
278
|
+
if (total <= 0) {
|
|
279
|
+
if (stored.expiredBackingFrontier) patch.expiredBackingFrontier = null;
|
|
280
|
+
} else if (next.frontier) {
|
|
281
|
+
patch.expiredBackingFrontier = next.frontier;
|
|
282
|
+
}
|
|
283
|
+
return patch;
|
|
206
284
|
}
|
|
207
285
|
function assertValidBalanceTarget(value, field, context) {
|
|
208
286
|
assertRepresentableAmount(value, field, context);
|
|
@@ -215,7 +293,10 @@ function assertValidBalanceTarget(value, field, context) {
|
|
|
215
293
|
}
|
|
216
294
|
}
|
|
217
295
|
function assertValidTierBalanceInput(input, context) {
|
|
218
|
-
if (input.balanceCap === void 0)
|
|
296
|
+
if (input.balanceCap === void 0) {
|
|
297
|
+
if (input.balance !== void 0) assertValidBalanceTarget(input.balance, "balance", context);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
219
300
|
if (input.balance !== void 0) {
|
|
220
301
|
throw new CreditError(
|
|
221
302
|
"A tier update cannot set both balance and balanceCap",
|
|
@@ -887,7 +968,7 @@ function validateBalanceUpdate(current, updates) {
|
|
|
887
968
|
const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
|
|
888
969
|
const errors = [];
|
|
889
970
|
if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
|
|
890
|
-
const marked = updates.balance === void 0 ? current.expiredHoldBacking
|
|
971
|
+
const marked = updates.balance === void 0 ? heldExpiredBacking(current.balance, current.expiredHoldBacking) : 0;
|
|
891
972
|
if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
|
|
892
973
|
errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
|
|
893
974
|
}
|
|
@@ -1140,6 +1221,7 @@ function commitTransitionJournal(store, input, existingId) {
|
|
|
1140
1221
|
userId: input.userId,
|
|
1141
1222
|
entryType: input.entryType,
|
|
1142
1223
|
amount: input.amount,
|
|
1224
|
+
balanceBefore: input.balanceBefore,
|
|
1143
1225
|
balanceAfter: input.balanceAfter,
|
|
1144
1226
|
source: input.source,
|
|
1145
1227
|
referenceId: input.reservationId,
|
|
@@ -1189,16 +1271,28 @@ function openHolds(store, userId) {
|
|
|
1189
1271
|
(reservation) => reservation.status === "reserved"
|
|
1190
1272
|
);
|
|
1191
1273
|
}
|
|
1274
|
+
function expiredBackingInForce(store, credits) {
|
|
1275
|
+
const total = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
1276
|
+
if (total > 0 && (!credits.expiredBackingFrontier || credits.expiredHoldBacking !== total)) {
|
|
1277
|
+
return allocateExpiredBacking(openHolds(store, credits.userId), total);
|
|
1278
|
+
}
|
|
1279
|
+
return { total, frontier: credits.expiredBackingFrontier ?? null };
|
|
1280
|
+
}
|
|
1281
|
+
function writeRecordedShares(store, userId, allocation) {
|
|
1282
|
+
for (const { id, expiredBacking } of allocation.rewrites ?? []) {
|
|
1283
|
+
const hold = getReservation(store, userId, id);
|
|
1284
|
+
if (hold) hold.expiredBacking = expiredBacking;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1192
1287
|
function writeExpiredHoldBacking(store, credits, next) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const shares = attributeExpiredBacking(holds, next);
|
|
1197
|
-
for (const hold of holds) {
|
|
1198
|
-
const share = shares.get(hold.id) ?? 0;
|
|
1199
|
-
if (share !== (hold.expiredBacking ?? 0)) hold.expiredBacking = share;
|
|
1288
|
+
if (next <= 0) {
|
|
1289
|
+
Object.assign(credits, expiredBackingPatch({ total: 0 }, credits));
|
|
1290
|
+
return;
|
|
1200
1291
|
}
|
|
1201
|
-
|
|
1292
|
+
if (next === credits.expiredHoldBacking) return;
|
|
1293
|
+
const allocation = allocateExpiredBacking(openHolds(store, credits.userId), next);
|
|
1294
|
+
writeRecordedShares(store, credits.userId, allocation);
|
|
1295
|
+
Object.assign(credits, expiredBackingPatch(allocation, credits));
|
|
1202
1296
|
}
|
|
1203
1297
|
function getReservation(store, userId, reservationId) {
|
|
1204
1298
|
return store.reservations.get(userId)?.get(reservationId);
|
|
@@ -1247,6 +1341,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1247
1341
|
};
|
|
1248
1342
|
}
|
|
1249
1343
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1344
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1250
1345
|
const reservation = {
|
|
1251
1346
|
id: generateId(),
|
|
1252
1347
|
userId: input.userId,
|
|
@@ -1259,7 +1354,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1259
1354
|
// The hold-origin fact. Written here and nowhere else, in the same
|
|
1260
1355
|
// critical section that raises `reserved` below, so the row and the hold
|
|
1261
1356
|
// become visible together. `createReservation` cannot set it.
|
|
1262
|
-
holdPlacedAt: now
|
|
1357
|
+
holdPlacedAt: holdPlacedAtAfter(now, backing.total > 0 ? backing.frontier : null)
|
|
1263
1358
|
};
|
|
1264
1359
|
if (!store.reservations.has(input.userId)) {
|
|
1265
1360
|
store.reservations.set(input.userId, /* @__PURE__ */ new Map());
|
|
@@ -1277,6 +1372,8 @@ async function reserveCreditsV2(store, input) {
|
|
|
1277
1372
|
);
|
|
1278
1373
|
}
|
|
1279
1374
|
credits.reserved = nextReserved;
|
|
1375
|
+
writeRecordedShares(store, input.userId, backing);
|
|
1376
|
+
Object.assign(credits, expiredBackingPatch(backing, credits));
|
|
1280
1377
|
credits.updatedAt = now;
|
|
1281
1378
|
return { outcome: "created", reservation: copyRecord(reservation) };
|
|
1282
1379
|
});
|
|
@@ -1297,11 +1394,12 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1297
1394
|
const amount = reservation.amount;
|
|
1298
1395
|
await yieldPoint(store);
|
|
1299
1396
|
assertReservedCoversHold(credits, userId, reservationId, amount);
|
|
1397
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1300
1398
|
const paid = planHoldCommit(
|
|
1301
1399
|
credits.balance,
|
|
1302
1400
|
credits.bonusCredits,
|
|
1303
|
-
|
|
1304
|
-
reservation.
|
|
1401
|
+
backing.total,
|
|
1402
|
+
holdExpiredBacking(reservation, backing.frontier, backing.rewrites),
|
|
1305
1403
|
amount
|
|
1306
1404
|
);
|
|
1307
1405
|
if (!paid.covered) {
|
|
@@ -1319,6 +1417,7 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1319
1417
|
operation: "commitReservation",
|
|
1320
1418
|
entryType: "debit",
|
|
1321
1419
|
amount,
|
|
1420
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1322
1421
|
balanceAfter,
|
|
1323
1422
|
source: "operation_commit",
|
|
1324
1423
|
reservationId,
|
|
@@ -1346,9 +1445,10 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1346
1445
|
credits.bonusCredits = nextBonusCredits;
|
|
1347
1446
|
credits.reserved = nextReserved;
|
|
1348
1447
|
credits.monthlyUsed = nextMonthlyUsed;
|
|
1448
|
+
writeRecordedShares(store, userId, backing);
|
|
1349
1449
|
Object.assign(
|
|
1350
1450
|
credits,
|
|
1351
|
-
|
|
1451
|
+
expiredBackingPatch({ total: paid.expiredHoldBacking, frontier: backing.frontier }, credits)
|
|
1352
1452
|
);
|
|
1353
1453
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1354
1454
|
const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
|
|
@@ -1429,10 +1529,11 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1429
1529
|
reservation.id,
|
|
1430
1530
|
reservation.amount
|
|
1431
1531
|
);
|
|
1532
|
+
const inForce = expiredBackingInForce(store, credits);
|
|
1432
1533
|
const backing = settleExpiredHoldBacking(
|
|
1433
1534
|
credits.balance,
|
|
1434
|
-
|
|
1435
|
-
reservation.
|
|
1535
|
+
inForce.total,
|
|
1536
|
+
holdExpiredBacking(reservation, inForce.frontier, inForce.rewrites)
|
|
1436
1537
|
);
|
|
1437
1538
|
const nextBalance = sumAmounts(credits.balance, -backing.used);
|
|
1438
1539
|
const journal = {
|
|
@@ -1440,6 +1541,7 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1440
1541
|
operation: `${transition.transition}Reservation`,
|
|
1441
1542
|
entryType: "credit",
|
|
1442
1543
|
amount: 0,
|
|
1544
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1443
1545
|
balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
|
|
1444
1546
|
source: transition.source,
|
|
1445
1547
|
reservationId: reservation.id,
|
|
@@ -1462,7 +1564,11 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1462
1564
|
reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1463
1565
|
credits.reserved = nextReserved;
|
|
1464
1566
|
credits.balance = nextBalance;
|
|
1465
|
-
|
|
1567
|
+
writeRecordedShares(store, reservation.userId, inForce);
|
|
1568
|
+
Object.assign(
|
|
1569
|
+
credits,
|
|
1570
|
+
expiredBackingPatch({ total: backing.remaining, frontier: inForce.frontier }, credits)
|
|
1571
|
+
);
|
|
1466
1572
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1467
1573
|
return commitTransitionJournal(store, journal, existingEntryId);
|
|
1468
1574
|
}
|
|
@@ -1505,7 +1611,7 @@ var InMemoryCreditRepository = class {
|
|
|
1505
1611
|
* simultaneous first deliveries of the same reference would then both credit.
|
|
1506
1612
|
*/
|
|
1507
1613
|
seedUser(userId, tier, initialBalance) {
|
|
1508
|
-
|
|
1614
|
+
assertValidBalanceTarget(initialBalance, "initialBalance", { userId });
|
|
1509
1615
|
const configured = getConfigMonthlyLimit(tier);
|
|
1510
1616
|
assertRepresentableTierAmount(configured, "monthlyLimit", { userId, tier });
|
|
1511
1617
|
const monthlyLimit = storedMonthlyLimit(configured);
|
|
@@ -1586,9 +1692,11 @@ var InMemoryCreditRepository = class {
|
|
|
1586
1692
|
userId,
|
|
1587
1693
|
operation: "updateUserCredits"
|
|
1588
1694
|
});
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1695
|
+
writeExpiredHoldBacking(
|
|
1696
|
+
this.store,
|
|
1697
|
+
next,
|
|
1698
|
+
updates.balance !== void 0 ? 0 : Math.min(heldExpiredBacking(credits.balance, credits.expiredHoldBacking), next.balance)
|
|
1699
|
+
);
|
|
1592
1700
|
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1593
1701
|
this.store.users.set(userId, next);
|
|
1594
1702
|
}
|
|
@@ -1620,6 +1728,11 @@ var InMemoryCreditRepository = class {
|
|
|
1620
1728
|
credits.balance = planned.balance;
|
|
1621
1729
|
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
1622
1730
|
}
|
|
1731
|
+
writeExpiredHoldBacking(
|
|
1732
|
+
this.store,
|
|
1733
|
+
credits,
|
|
1734
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
1735
|
+
);
|
|
1623
1736
|
if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
|
|
1624
1737
|
if (input.subscriptionExpiresAt !== void 0) {
|
|
1625
1738
|
if (input.subscriptionExpiresAt === null) {
|
|
@@ -1781,6 +1894,11 @@ var InMemoryCreditRepository = class {
|
|
|
1781
1894
|
{ userId, operation: "addCredits" }
|
|
1782
1895
|
);
|
|
1783
1896
|
credits.bonusCredits = nextBonusCredits;
|
|
1897
|
+
writeExpiredHoldBacking(
|
|
1898
|
+
this.store,
|
|
1899
|
+
credits,
|
|
1900
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
1901
|
+
);
|
|
1784
1902
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1785
1903
|
this.store.users.set(userId, credits);
|
|
1786
1904
|
const transaction = this.recordTransaction({
|
|
@@ -1800,6 +1918,7 @@ var InMemoryCreditRepository = class {
|
|
|
1800
1918
|
userId,
|
|
1801
1919
|
entryType: "credit",
|
|
1802
1920
|
amount,
|
|
1921
|
+
balanceBefore: previousTotal,
|
|
1803
1922
|
balanceAfter: newTotal,
|
|
1804
1923
|
source: payload.source,
|
|
1805
1924
|
referenceId: transaction.id ?? paymentRef ?? "unknown",
|
|
@@ -1833,7 +1952,7 @@ var InMemoryCreditRepository = class {
|
|
|
1833
1952
|
});
|
|
1834
1953
|
}
|
|
1835
1954
|
}
|
|
1836
|
-
async deductCreditsAtomic(userId, amount) {
|
|
1955
|
+
async deductCreditsAtomic(userId, amount, journal) {
|
|
1837
1956
|
assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
|
|
1838
1957
|
const credits = this.store.users.get(userId);
|
|
1839
1958
|
if (!credits) {
|
|
@@ -1859,11 +1978,26 @@ var InMemoryCreditRepository = class {
|
|
|
1859
1978
|
{ previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },
|
|
1860
1979
|
{ userId, operation: "deductCredits" }
|
|
1861
1980
|
);
|
|
1981
|
+
const held = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
1862
1982
|
credits.balance = nextBalance;
|
|
1863
1983
|
credits.bonusCredits = nextBonusCredits;
|
|
1984
|
+
writeExpiredHoldBacking(this.store, credits, held);
|
|
1864
1985
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1865
1986
|
this.store.users.set(userId, credits);
|
|
1866
|
-
return { previousBalance, newBalance };
|
|
1987
|
+
if (!journal) return { previousBalance, newBalance };
|
|
1988
|
+
const entry = this.recordJournalEntry({
|
|
1989
|
+
userId,
|
|
1990
|
+
entryType: "debit",
|
|
1991
|
+
amount,
|
|
1992
|
+
balanceBefore: previousBalance,
|
|
1993
|
+
balanceAfter: newBalance,
|
|
1994
|
+
source: journal.source ?? "operation_commit",
|
|
1995
|
+
referenceId: journal.referenceId,
|
|
1996
|
+
referenceType: journal.referenceType ?? "adjustment",
|
|
1997
|
+
description: journal.description,
|
|
1998
|
+
metadata: journal.metadata
|
|
1999
|
+
});
|
|
2000
|
+
return { previousBalance, newBalance, journalEntryId: entry.id };
|
|
1867
2001
|
}
|
|
1868
2002
|
// ==================== Transactions ====================
|
|
1869
2003
|
async createTransaction(input) {
|
|
@@ -1977,6 +2111,11 @@ var InMemoryCreditRepository = class {
|
|
|
1977
2111
|
assertRepresentableAmount(input.balanceAfter, "journal balanceAfter", {
|
|
1978
2112
|
userId: input.userId
|
|
1979
2113
|
});
|
|
2114
|
+
if (input.balanceBefore !== void 0) {
|
|
2115
|
+
assertRepresentableAmount(input.balanceBefore, "journal balanceBefore", {
|
|
2116
|
+
userId: input.userId
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
1980
2119
|
assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });
|
|
1981
2120
|
assertPublicJournalKey(input.idempotencyKey, input.userId);
|
|
1982
2121
|
const key = input.idempotencyKey !== void 0 ? scopedKey(input.userId, input.idempotencyKey) : void 0;
|
|
@@ -1992,6 +2131,7 @@ var InMemoryCreditRepository = class {
|
|
|
1992
2131
|
userId: input.userId,
|
|
1993
2132
|
entryType: input.entryType,
|
|
1994
2133
|
amount: input.amount,
|
|
2134
|
+
...input.balanceBefore !== void 0 ? { balanceBefore: input.balanceBefore } : {},
|
|
1995
2135
|
balanceAfter: input.balanceAfter,
|
|
1996
2136
|
source: input.source,
|
|
1997
2137
|
referenceId: input.referenceId,
|
|
@@ -2144,6 +2284,7 @@ var InMemoryCreditRepository = class {
|
|
|
2144
2284
|
userId,
|
|
2145
2285
|
entryType: change > 0 ? "credit" : "debit",
|
|
2146
2286
|
amount: Math.abs(change),
|
|
2287
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
2147
2288
|
balanceAfter,
|
|
2148
2289
|
source: "monthly_reset",
|
|
2149
2290
|
referenceId: `reset-${Date.now()}`,
|
|
@@ -2224,6 +2365,7 @@ var InMemoryCreditRepository = class {
|
|
|
2224
2365
|
entryType: "debit",
|
|
2225
2366
|
amount: 0,
|
|
2226
2367
|
// No credits deducted, just tier change
|
|
2368
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
2227
2369
|
balanceAfter,
|
|
2228
2370
|
source: "subscription_downgrade",
|
|
2229
2371
|
referenceId: `downgrade-${Date.now()}`,
|