@nehorai/credits 2.1.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/LICENSE +21 -0
- package/dist/adapters/index.cjs +3 -2
- 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 +3 -2
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +218 -2
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +183 -5
- package/dist/core/index.d.ts +183 -5
- package/dist/core/index.js +199 -2
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +477 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +456 -59
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +394 -44
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +36 -7
- package/dist/repository/index.d.ts +36 -7
- package/dist/repository/index.js +391 -43
- 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 +33 -20
- 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 +33 -20
- package/dist/service/index.js.map +1 -1
- package/dist/{types-Dzc443vy.d.cts → types-C3sxDvNb.d.cts} +43 -2
- package/dist/{types-Dzc443vy.d.ts → types-C3sxDvNb.d.ts} +43 -2
- package/dist/{types-BhCdep_t.d.ts → types-CY-oHIMA.d.ts} +55 -4
- package/dist/{types-wtksMa4n.d.cts → types-DOjguhEX.d.cts} +55 -4
- package/package.json +70 -71
package/dist/repository/index.js
CHANGED
|
@@ -16,6 +16,8 @@ function toClientUserCredits(credits) {
|
|
|
16
16
|
monthlyUsed: credits.monthlyUsed,
|
|
17
17
|
monthlyResetAt: toISOString(credits.monthlyResetAt),
|
|
18
18
|
subscriptionExpiresAt: credits.subscriptionExpiresAt ? toISOString(credits.subscriptionExpiresAt) : null,
|
|
19
|
+
...credits.expiredHoldBacking !== void 0 ? { expiredHoldBacking: credits.expiredHoldBacking } : {},
|
|
20
|
+
...credits.signupGrant !== void 0 ? { signupGrant: credits.signupGrant } : {},
|
|
19
21
|
createdAt: toISOString(credits.createdAt),
|
|
20
22
|
updatedAt: toISOString(credits.updatedAt)
|
|
21
23
|
};
|
|
@@ -54,6 +56,23 @@ function floatSum(values) {
|
|
|
54
56
|
for (const value of values) total += value;
|
|
55
57
|
return total;
|
|
56
58
|
}
|
|
59
|
+
var FLOAT_NOISE_FLOOR = 1e-6;
|
|
60
|
+
var FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;
|
|
61
|
+
function floatNoise(value) {
|
|
62
|
+
return Math.max(FLOAT_NOISE_FLOOR, Math.abs(value) * FLOAT_NOISE_RELATIVE);
|
|
63
|
+
}
|
|
64
|
+
function canonicalAmount(value) {
|
|
65
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return value;
|
|
66
|
+
const cents = Math.round(value * 100);
|
|
67
|
+
if (!Number.isSafeInteger(cents)) return value;
|
|
68
|
+
const onGrid = cents / 100 + 0;
|
|
69
|
+
return Math.abs(value - onGrid) < floatNoise(value) ? onGrid : value;
|
|
70
|
+
}
|
|
71
|
+
function storedAmountCeil(value) {
|
|
72
|
+
const canonical = canonicalAmount(value);
|
|
73
|
+
if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
|
|
74
|
+
return Math.ceil(canonical * 100) / 100;
|
|
75
|
+
}
|
|
57
76
|
function isValidCreditAmount(value) {
|
|
58
77
|
if (typeof value !== "number") return false;
|
|
59
78
|
const cents = toCents(value);
|
|
@@ -106,6 +125,147 @@ function backedBalanceFloor(reserved, bonusCredits) {
|
|
|
106
125
|
assertRepresentableAmount(floor, "backedBalanceFloor", { reserved, bonusCredits });
|
|
107
126
|
return floor;
|
|
108
127
|
}
|
|
128
|
+
function heldExpiredBacking(balance, stored) {
|
|
129
|
+
if (!(stored !== void 0 && stored > 0)) return 0;
|
|
130
|
+
return Math.max(0, Math.min(stored, balance));
|
|
131
|
+
}
|
|
132
|
+
function spendableBalance(balance, expiredHoldBacking) {
|
|
133
|
+
return sumAmounts(balance, -heldExpiredBacking(balance, expiredHoldBacking));
|
|
134
|
+
}
|
|
135
|
+
function planBalanceTarget(target, reserved, bonusCredits) {
|
|
136
|
+
assertValidBalanceTarget(target, "balance target", { reserved, bonusCredits });
|
|
137
|
+
const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
|
|
138
|
+
return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
|
|
139
|
+
}
|
|
140
|
+
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking) {
|
|
141
|
+
const spendable = spendableBalance(balance, expiredHoldBacking);
|
|
142
|
+
const target = Math.min(spendable, cap);
|
|
143
|
+
const kept = Math.max(
|
|
144
|
+
sumAmounts(balance, -spendable),
|
|
145
|
+
sumAmounts(backedBalanceFloor(reserved, bonusCredits), -target)
|
|
146
|
+
);
|
|
147
|
+
const next = Math.min(balance, sumAmounts(target, kept));
|
|
148
|
+
return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
|
|
149
|
+
}
|
|
150
|
+
function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
|
|
151
|
+
const held = heldExpiredBacking(balance, expiredHoldBacking);
|
|
152
|
+
const used = Math.max(0, Math.min(held, holdBacking));
|
|
153
|
+
return { used, remaining: sumAmounts(held, -used) };
|
|
154
|
+
}
|
|
155
|
+
function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
|
|
156
|
+
const backing = settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking);
|
|
157
|
+
const rest = sumAmounts(amount, -backing.used);
|
|
158
|
+
const fromSpendable = Math.min(spendableBalance(balance, expiredHoldBacking), rest);
|
|
159
|
+
const fromBonus = sumAmounts(rest, -fromSpendable);
|
|
160
|
+
return {
|
|
161
|
+
covered: fromBonus <= bonusCredits,
|
|
162
|
+
balance: sumAmounts(balance, -backing.used, -fromSpendable),
|
|
163
|
+
bonusCredits: sumAmounts(bonusCredits, -fromBonus),
|
|
164
|
+
expiredHoldBacking: backing.remaining
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
var NO_EXPIRED_BACKING_PREFIX = {
|
|
168
|
+
holdPlacedAt: "",
|
|
169
|
+
id: "",
|
|
170
|
+
share: 0
|
|
171
|
+
};
|
|
172
|
+
function compareHolds(a, b) {
|
|
173
|
+
if (a.holdPlacedAt !== b.holdPlacedAt) return a.holdPlacedAt < b.holdPlacedAt ? -1 : 1;
|
|
174
|
+
if (a.id === b.id) return 0;
|
|
175
|
+
return a.id < b.id ? -1 : 1;
|
|
176
|
+
}
|
|
177
|
+
function recordedExpiredBacking(hold) {
|
|
178
|
+
const stored = hold.expiredBacking;
|
|
179
|
+
if (typeof stored !== "number" || !Number.isFinite(stored) || stored <= 0) return 0;
|
|
180
|
+
if (!isValidCreditAmount(hold.amount)) return 0;
|
|
181
|
+
return Math.min(storedAmountCeil(stored), hold.amount);
|
|
182
|
+
}
|
|
183
|
+
function allocateExpiredBacking(holds, total) {
|
|
184
|
+
const placed = holds.filter((hold) => hold.holdPlacedAt && isValidCreditAmount(hold.amount)).map((hold) => ({ ...hold, holdPlacedAt: hold.holdPlacedAt })).sort(compareHolds);
|
|
185
|
+
const wanted = Math.max(0, total);
|
|
186
|
+
if (wanted === 0) return { total: 0, frontier: null };
|
|
187
|
+
const recorded = placed.filter((hold) => recordedExpiredBacking(hold) > 0);
|
|
188
|
+
const shares = new Map(recorded.map((hold) => [hold.id, recordedExpiredBacking(hold)]));
|
|
189
|
+
let left = sumAmounts(wanted, ...[...shares.values()].map((share) => -share));
|
|
190
|
+
for (const hold of [...recorded].reverse()) {
|
|
191
|
+
if (left >= 0) break;
|
|
192
|
+
const share = shares.get(hold.id);
|
|
193
|
+
const cut = Math.min(share, -left);
|
|
194
|
+
shares.set(hold.id, sumAmounts(share, -cut));
|
|
195
|
+
left = sumAmounts(left, cut);
|
|
196
|
+
}
|
|
197
|
+
let frontier = NO_EXPIRED_BACKING_PREFIX;
|
|
198
|
+
for (const hold of placed) {
|
|
199
|
+
if (left <= 0) break;
|
|
200
|
+
if (shares.has(hold.id)) continue;
|
|
201
|
+
const share = Math.min(hold.amount, left);
|
|
202
|
+
frontier = { holdPlacedAt: hold.holdPlacedAt, id: hold.id, share };
|
|
203
|
+
left = sumAmounts(left, -share);
|
|
204
|
+
}
|
|
205
|
+
for (const hold of recorded) {
|
|
206
|
+
if (left <= 0) break;
|
|
207
|
+
const share = shares.get(hold.id);
|
|
208
|
+
const added = Math.min(sumAmounts(hold.amount, -share), left);
|
|
209
|
+
shares.set(hold.id, sumAmounts(share, added));
|
|
210
|
+
left = sumAmounts(left, -added);
|
|
211
|
+
}
|
|
212
|
+
const rewrites = recorded.filter((hold) => shares.get(hold.id) !== hold.expiredBacking).map((hold) => ({ id: hold.id, expiredBacking: shares.get(hold.id) }));
|
|
213
|
+
return { total: wanted, frontier, ...rewrites.length > 0 ? { rewrites } : {} };
|
|
214
|
+
}
|
|
215
|
+
function holdExpiredBacking(hold, frontier, rewrites) {
|
|
216
|
+
const rewritten = rewrites?.find((rewrite) => rewrite.id === hold.id);
|
|
217
|
+
const recorded = recordedExpiredBacking(
|
|
218
|
+
rewritten ? { ...hold, expiredBacking: rewritten.expiredBacking } : hold
|
|
219
|
+
);
|
|
220
|
+
if (recorded > 0) return recorded;
|
|
221
|
+
if (!frontier || !hold.holdPlacedAt) return 0;
|
|
222
|
+
const order = compareHolds({ holdPlacedAt: hold.holdPlacedAt, id: hold.id }, frontier);
|
|
223
|
+
if (order > 0) return 0;
|
|
224
|
+
return order < 0 ? hold.amount : Math.min(frontier.share, hold.amount);
|
|
225
|
+
}
|
|
226
|
+
function holdPlacedAtAfter(now, frontier) {
|
|
227
|
+
if (!frontier?.holdPlacedAt) return now;
|
|
228
|
+
const earliest = new Date(Date.parse(frontier.holdPlacedAt) + 1).toISOString();
|
|
229
|
+
return now > earliest ? now : earliest;
|
|
230
|
+
}
|
|
231
|
+
function expiredBackingPatch(next, stored) {
|
|
232
|
+
const total = Math.max(0, next.total);
|
|
233
|
+
const patch = {};
|
|
234
|
+
const storedTotal = stored.expiredHoldBacking;
|
|
235
|
+
if (total > 0 || storedTotal !== void 0 && storedTotal !== 0) {
|
|
236
|
+
patch.expiredHoldBacking = total;
|
|
237
|
+
}
|
|
238
|
+
if (total <= 0) {
|
|
239
|
+
if (stored.expiredBackingFrontier) patch.expiredBackingFrontier = null;
|
|
240
|
+
} else if (next.frontier) {
|
|
241
|
+
patch.expiredBackingFrontier = next.frontier;
|
|
242
|
+
}
|
|
243
|
+
return patch;
|
|
244
|
+
}
|
|
245
|
+
function assertValidBalanceTarget(value, field, context) {
|
|
246
|
+
assertRepresentableAmount(value, field, context);
|
|
247
|
+
if (value < 0) {
|
|
248
|
+
throw new CreditError(
|
|
249
|
+
`${field} cannot be negative (got ${value})`,
|
|
250
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
251
|
+
{ field, value, ...context }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function assertValidTierBalanceInput(input, context) {
|
|
256
|
+
if (input.balanceCap === void 0) {
|
|
257
|
+
if (input.balance !== void 0) assertValidBalanceTarget(input.balance, "balance", context);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (input.balance !== void 0) {
|
|
261
|
+
throw new CreditError(
|
|
262
|
+
"A tier update cannot set both balance and balanceCap",
|
|
263
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
264
|
+
{ balance: input.balance, balanceCap: input.balanceCap, ...context }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
assertValidBalanceTarget(input.balanceCap, "balanceCap", context);
|
|
268
|
+
}
|
|
109
269
|
function assertRepresentableTierAmount(value, field, context) {
|
|
110
270
|
if (value === Number.POSITIVE_INFINITY) return;
|
|
111
271
|
assertRepresentableAmount(value, field, context);
|
|
@@ -323,9 +483,9 @@ var creditSystemConfigSchema = z.object({
|
|
|
323
483
|
*/
|
|
324
484
|
reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
|
|
325
485
|
/**
|
|
326
|
-
* Default credits for new users (free tier)
|
|
486
|
+
* Default credits for new users (free tier). 0 means no signup grant.
|
|
327
487
|
*/
|
|
328
|
-
defaultFreeCredits: z.number().
|
|
488
|
+
defaultFreeCredits: z.number().min(0).default(25),
|
|
329
489
|
/**
|
|
330
490
|
* Grace period in days after subscription expires before downgrade
|
|
331
491
|
*/
|
|
@@ -755,11 +915,36 @@ function toDate(value) {
|
|
|
755
915
|
return /* @__PURE__ */ new Date();
|
|
756
916
|
}
|
|
757
917
|
function getNextMonthlyReset(from = /* @__PURE__ */ new Date()) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
next
|
|
762
|
-
|
|
918
|
+
return new Date(from.getFullYear(), from.getMonth() + 1, 1);
|
|
919
|
+
}
|
|
920
|
+
function validateBalanceUpdate(current, updates) {
|
|
921
|
+
const next = (base, absolute, increment) => sumAmounts(absolute ?? base, increment ?? 0);
|
|
922
|
+
const balance = next(current.balance, updates.balance, updates.balanceIncrement);
|
|
923
|
+
const bonusCredits = next(
|
|
924
|
+
current.bonusCredits ?? 0,
|
|
925
|
+
updates.bonusCredits,
|
|
926
|
+
updates.bonusCreditsIncrement
|
|
927
|
+
);
|
|
928
|
+
const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
|
|
929
|
+
const errors = [];
|
|
930
|
+
if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
|
|
931
|
+
const marked = updates.balance === void 0 ? heldExpiredBacking(current.balance, current.expiredHoldBacking) : 0;
|
|
932
|
+
if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
|
|
933
|
+
errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
|
|
934
|
+
}
|
|
935
|
+
if (bonusCredits < 0) errors.push(`BonusCredits cannot be negative: ${bonusCredits}`);
|
|
936
|
+
if (reserved < 0) errors.push(`Reserved cannot be negative: ${reserved}`);
|
|
937
|
+
const covering = sumAmounts(balance, bonusCredits);
|
|
938
|
+
if (reserved > covering) {
|
|
939
|
+
errors.push(`Reserved (${reserved}) exceeds balance + bonusCredits (${covering})`);
|
|
940
|
+
}
|
|
941
|
+
return { valid: errors.length === 0, errors };
|
|
942
|
+
}
|
|
943
|
+
function assertValidBalanceUpdate(current, updates) {
|
|
944
|
+
const result = validateBalanceUpdate(current, updates);
|
|
945
|
+
if (!result.valid) {
|
|
946
|
+
throw new Error(`Invalid balance update: ${result.errors.join("; ")}`);
|
|
947
|
+
}
|
|
763
948
|
}
|
|
764
949
|
|
|
765
950
|
// src/repository/memory/snapshot.ts
|
|
@@ -996,6 +1181,7 @@ function commitTransitionJournal(store, input, existingId) {
|
|
|
996
1181
|
userId: input.userId,
|
|
997
1182
|
entryType: input.entryType,
|
|
998
1183
|
amount: input.amount,
|
|
1184
|
+
balanceBefore: input.balanceBefore,
|
|
999
1185
|
balanceAfter: input.balanceAfter,
|
|
1000
1186
|
source: input.source,
|
|
1001
1187
|
referenceId: input.reservationId,
|
|
@@ -1040,6 +1226,34 @@ function assertTrustworthy(reservation, transition) {
|
|
|
1040
1226
|
assertLockedAmount(reservation, transition);
|
|
1041
1227
|
assertHoldPlaced(reservation, transition);
|
|
1042
1228
|
}
|
|
1229
|
+
function openHolds(store, userId) {
|
|
1230
|
+
return [...store.reservations.get(userId)?.values() ?? []].filter(
|
|
1231
|
+
(reservation) => reservation.status === "reserved"
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
function expiredBackingInForce(store, credits) {
|
|
1235
|
+
const total = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
1236
|
+
if (total > 0 && (!credits.expiredBackingFrontier || credits.expiredHoldBacking !== total)) {
|
|
1237
|
+
return allocateExpiredBacking(openHolds(store, credits.userId), total);
|
|
1238
|
+
}
|
|
1239
|
+
return { total, frontier: credits.expiredBackingFrontier ?? null };
|
|
1240
|
+
}
|
|
1241
|
+
function writeRecordedShares(store, userId, allocation) {
|
|
1242
|
+
for (const { id, expiredBacking } of allocation.rewrites ?? []) {
|
|
1243
|
+
const hold = getReservation(store, userId, id);
|
|
1244
|
+
if (hold) hold.expiredBacking = expiredBacking;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function writeExpiredHoldBacking(store, credits, next) {
|
|
1248
|
+
if (next <= 0) {
|
|
1249
|
+
Object.assign(credits, expiredBackingPatch({ total: 0 }, credits));
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (next === credits.expiredHoldBacking) return;
|
|
1253
|
+
const allocation = allocateExpiredBacking(openHolds(store, credits.userId), next);
|
|
1254
|
+
writeRecordedShares(store, credits.userId, allocation);
|
|
1255
|
+
Object.assign(credits, expiredBackingPatch(allocation, credits));
|
|
1256
|
+
}
|
|
1043
1257
|
function getReservation(store, userId, reservationId) {
|
|
1044
1258
|
return store.reservations.get(userId)?.get(reservationId);
|
|
1045
1259
|
}
|
|
@@ -1087,6 +1301,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1087
1301
|
};
|
|
1088
1302
|
}
|
|
1089
1303
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1304
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1090
1305
|
const reservation = {
|
|
1091
1306
|
id: generateId(),
|
|
1092
1307
|
userId: input.userId,
|
|
@@ -1099,7 +1314,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1099
1314
|
// The hold-origin fact. Written here and nowhere else, in the same
|
|
1100
1315
|
// critical section that raises `reserved` below, so the row and the hold
|
|
1101
1316
|
// become visible together. `createReservation` cannot set it.
|
|
1102
|
-
holdPlacedAt: now
|
|
1317
|
+
holdPlacedAt: holdPlacedAtAfter(now, backing.total > 0 ? backing.frontier : null)
|
|
1103
1318
|
};
|
|
1104
1319
|
if (!store.reservations.has(input.userId)) {
|
|
1105
1320
|
store.reservations.set(input.userId, /* @__PURE__ */ new Map());
|
|
@@ -1117,6 +1332,8 @@ async function reserveCreditsV2(store, input) {
|
|
|
1117
1332
|
);
|
|
1118
1333
|
}
|
|
1119
1334
|
credits.reserved = nextReserved;
|
|
1335
|
+
writeRecordedShares(store, input.userId, backing);
|
|
1336
|
+
Object.assign(credits, expiredBackingPatch(backing, credits));
|
|
1120
1337
|
credits.updatedAt = now;
|
|
1121
1338
|
return { outcome: "created", reservation: copyRecord(reservation) };
|
|
1122
1339
|
});
|
|
@@ -1137,22 +1354,30 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1137
1354
|
const amount = reservation.amount;
|
|
1138
1355
|
await yieldPoint(store);
|
|
1139
1356
|
assertReservedCoversHold(credits, userId, reservationId, amount);
|
|
1140
|
-
|
|
1357
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1358
|
+
const paid = planHoldCommit(
|
|
1359
|
+
credits.balance,
|
|
1360
|
+
credits.bonusCredits,
|
|
1361
|
+
backing.total,
|
|
1362
|
+
holdExpiredBacking(reservation, backing.frontier, backing.rewrites),
|
|
1363
|
+
amount
|
|
1364
|
+
);
|
|
1365
|
+
if (!paid.covered) {
|
|
1141
1366
|
throw new CreditError(
|
|
1142
1367
|
`Insufficient credits to commit reservation ${reservationId}`,
|
|
1143
1368
|
CreditErrorCode.INSUFFICIENT_CREDITS,
|
|
1144
1369
|
{ userId, reservationId, required: amount }
|
|
1145
1370
|
);
|
|
1146
1371
|
}
|
|
1147
|
-
const
|
|
1148
|
-
const
|
|
1149
|
-
const nextBonusCredits = sumAmounts(credits.bonusCredits, -sumAmounts(amount, -fromBalance));
|
|
1372
|
+
const nextBalance = paid.balance;
|
|
1373
|
+
const nextBonusCredits = paid.bonusCredits;
|
|
1150
1374
|
const balanceAfter = sumAmounts(nextBalance, nextBonusCredits);
|
|
1151
1375
|
const journal = {
|
|
1152
1376
|
userId,
|
|
1153
1377
|
operation: "commitReservation",
|
|
1154
1378
|
entryType: "debit",
|
|
1155
1379
|
amount,
|
|
1380
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1156
1381
|
balanceAfter,
|
|
1157
1382
|
source: "operation_commit",
|
|
1158
1383
|
reservationId,
|
|
@@ -1180,6 +1405,11 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1180
1405
|
credits.bonusCredits = nextBonusCredits;
|
|
1181
1406
|
credits.reserved = nextReserved;
|
|
1182
1407
|
credits.monthlyUsed = nextMonthlyUsed;
|
|
1408
|
+
writeRecordedShares(store, userId, backing);
|
|
1409
|
+
Object.assign(
|
|
1410
|
+
credits,
|
|
1411
|
+
expiredBackingPatch({ total: paid.expiredHoldBacking, frontier: backing.frontier }, credits)
|
|
1412
|
+
);
|
|
1183
1413
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1184
1414
|
const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
|
|
1185
1415
|
return {
|
|
@@ -1259,19 +1489,28 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1259
1489
|
reservation.id,
|
|
1260
1490
|
reservation.amount
|
|
1261
1491
|
);
|
|
1492
|
+
const inForce = expiredBackingInForce(store, credits);
|
|
1493
|
+
const backing = settleExpiredHoldBacking(
|
|
1494
|
+
credits.balance,
|
|
1495
|
+
inForce.total,
|
|
1496
|
+
holdExpiredBacking(reservation, inForce.frontier, inForce.rewrites)
|
|
1497
|
+
);
|
|
1498
|
+
const nextBalance = sumAmounts(credits.balance, -backing.used);
|
|
1262
1499
|
const journal = {
|
|
1263
1500
|
userId: reservation.userId,
|
|
1264
1501
|
operation: `${transition.transition}Reservation`,
|
|
1265
1502
|
entryType: "credit",
|
|
1266
1503
|
amount: 0,
|
|
1267
|
-
|
|
1504
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1505
|
+
balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
|
|
1268
1506
|
source: transition.source,
|
|
1269
1507
|
reservationId: reservation.id,
|
|
1270
1508
|
description: transition.options?.description ?? `${transition.verb} ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
|
|
1271
1509
|
metadata: {
|
|
1272
1510
|
...transition.options?.metadata,
|
|
1273
1511
|
operationType: reservation.operationType,
|
|
1274
|
-
amount: reservation.amount
|
|
1512
|
+
amount: reservation.amount,
|
|
1513
|
+
...backing.used > 0 ? { expiredCreditsRemoved: backing.used } : {}
|
|
1275
1514
|
},
|
|
1276
1515
|
idempotencyKey: reservationJournalKey(reservation.id, transition.transition)
|
|
1277
1516
|
};
|
|
@@ -1284,6 +1523,12 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1284
1523
|
reservation.status = transition.status;
|
|
1285
1524
|
reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1286
1525
|
credits.reserved = nextReserved;
|
|
1526
|
+
credits.balance = nextBalance;
|
|
1527
|
+
writeRecordedShares(store, reservation.userId, inForce);
|
|
1528
|
+
Object.assign(
|
|
1529
|
+
credits,
|
|
1530
|
+
expiredBackingPatch({ total: backing.remaining, frontier: inForce.frontier }, credits)
|
|
1531
|
+
);
|
|
1287
1532
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1288
1533
|
return commitTransitionJournal(store, journal, existingEntryId);
|
|
1289
1534
|
}
|
|
@@ -1294,6 +1539,10 @@ function isoOf(value) {
|
|
|
1294
1539
|
}
|
|
1295
1540
|
var InMemoryCreditRepository = class {
|
|
1296
1541
|
store = new MemoryStore();
|
|
1542
|
+
options;
|
|
1543
|
+
constructor(options = {}) {
|
|
1544
|
+
this.options = options;
|
|
1545
|
+
}
|
|
1297
1546
|
// ==================== User Credits ====================
|
|
1298
1547
|
/**
|
|
1299
1548
|
* A copy, never the live record.
|
|
@@ -1310,7 +1559,8 @@ var InMemoryCreditRepository = class {
|
|
|
1310
1559
|
return copyRecord(credits) ?? null;
|
|
1311
1560
|
}
|
|
1312
1561
|
async initializeUserCredits(userId, tier, initialBalance) {
|
|
1313
|
-
|
|
1562
|
+
const existing = this.store.users.get(userId);
|
|
1563
|
+
return copyRecord(existing ?? this.seedUser(userId, tier, initialBalance));
|
|
1314
1564
|
}
|
|
1315
1565
|
/**
|
|
1316
1566
|
* The synchronous body of `initializeUserCredits`.
|
|
@@ -1321,7 +1571,7 @@ var InMemoryCreditRepository = class {
|
|
|
1321
1571
|
* simultaneous first deliveries of the same reference would then both credit.
|
|
1322
1572
|
*/
|
|
1323
1573
|
seedUser(userId, tier, initialBalance) {
|
|
1324
|
-
|
|
1574
|
+
assertValidBalanceTarget(initialBalance, "initialBalance", { userId });
|
|
1325
1575
|
const configured = getConfigMonthlyLimit(tier);
|
|
1326
1576
|
assertRepresentableTierAmount(configured, "monthlyLimit", { userId, tier });
|
|
1327
1577
|
const monthlyLimit = storedMonthlyLimit(configured);
|
|
@@ -1361,8 +1611,11 @@ var InMemoryCreditRepository = class {
|
|
|
1361
1611
|
if (!credits) {
|
|
1362
1612
|
throw new Error(`User ${userId} not found`);
|
|
1363
1613
|
}
|
|
1614
|
+
assertValidBalanceUpdate(credits, updates);
|
|
1364
1615
|
const next = { ...credits };
|
|
1365
|
-
if (updates.balance !== void 0)
|
|
1616
|
+
if (updates.balance !== void 0) {
|
|
1617
|
+
next.balance = updates.balance;
|
|
1618
|
+
}
|
|
1366
1619
|
if (updates.bonusCredits !== void 0) next.bonusCredits = updates.bonusCredits;
|
|
1367
1620
|
if (updates.reserved !== void 0) next.reserved = updates.reserved;
|
|
1368
1621
|
if (updates.tier !== void 0) next.tier = updates.tier;
|
|
@@ -1399,6 +1652,11 @@ var InMemoryCreditRepository = class {
|
|
|
1399
1652
|
userId,
|
|
1400
1653
|
operation: "updateUserCredits"
|
|
1401
1654
|
});
|
|
1655
|
+
writeExpiredHoldBacking(
|
|
1656
|
+
this.store,
|
|
1657
|
+
next,
|
|
1658
|
+
updates.balance !== void 0 ? 0 : Math.min(heldExpiredBacking(credits.balance, credits.expiredHoldBacking), next.balance)
|
|
1659
|
+
);
|
|
1402
1660
|
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1403
1661
|
this.store.users.set(userId, next);
|
|
1404
1662
|
}
|
|
@@ -1407,18 +1665,34 @@ var InMemoryCreditRepository = class {
|
|
|
1407
1665
|
{ monthlyLimit: input.monthlyLimit, balance: input.balance, monthlyUsed: input.monthlyUsed },
|
|
1408
1666
|
{ userId, operation: "updateUserTier" }
|
|
1409
1667
|
);
|
|
1668
|
+
assertValidTierBalanceInput(input, { userId, operation: "updateUserTier" });
|
|
1410
1669
|
const credits = this.store.users.get(userId);
|
|
1411
1670
|
if (!credits) {
|
|
1412
1671
|
throw new Error(`User ${userId} not found`);
|
|
1413
1672
|
}
|
|
1673
|
+
if (input.balanceCap !== void 0 && !isFreeTier(credits.tier)) {
|
|
1674
|
+
const capped = planBalanceCap(
|
|
1675
|
+
credits.balance,
|
|
1676
|
+
input.balanceCap,
|
|
1677
|
+
credits.reserved,
|
|
1678
|
+
credits.bonusCredits,
|
|
1679
|
+
credits.expiredHoldBacking
|
|
1680
|
+
);
|
|
1681
|
+
credits.balance = capped.balance;
|
|
1682
|
+
writeExpiredHoldBacking(this.store, credits, capped.expiredHoldBacking);
|
|
1683
|
+
}
|
|
1414
1684
|
credits.tier = input.tier;
|
|
1415
1685
|
credits.monthlyLimit = input.monthlyLimit;
|
|
1416
1686
|
if (input.balance !== void 0) {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1687
|
+
const planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);
|
|
1688
|
+
credits.balance = planned.balance;
|
|
1689
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
1690
|
+
}
|
|
1691
|
+
writeExpiredHoldBacking(
|
|
1692
|
+
this.store,
|
|
1693
|
+
credits,
|
|
1694
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
1695
|
+
);
|
|
1422
1696
|
if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
|
|
1423
1697
|
if (input.subscriptionExpiresAt !== void 0) {
|
|
1424
1698
|
if (input.subscriptionExpiresAt === null) {
|
|
@@ -1580,6 +1854,11 @@ var InMemoryCreditRepository = class {
|
|
|
1580
1854
|
{ userId, operation: "addCredits" }
|
|
1581
1855
|
);
|
|
1582
1856
|
credits.bonusCredits = nextBonusCredits;
|
|
1857
|
+
writeExpiredHoldBacking(
|
|
1858
|
+
this.store,
|
|
1859
|
+
credits,
|
|
1860
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
1861
|
+
);
|
|
1583
1862
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1584
1863
|
this.store.users.set(userId, credits);
|
|
1585
1864
|
const transaction = this.recordTransaction({
|
|
@@ -1599,6 +1878,7 @@ var InMemoryCreditRepository = class {
|
|
|
1599
1878
|
userId,
|
|
1600
1879
|
entryType: "credit",
|
|
1601
1880
|
amount,
|
|
1881
|
+
balanceBefore: previousTotal,
|
|
1602
1882
|
balanceAfter: newTotal,
|
|
1603
1883
|
source: payload.source,
|
|
1604
1884
|
referenceId: transaction.id ?? paymentRef ?? "unknown",
|
|
@@ -1632,19 +1912,23 @@ var InMemoryCreditRepository = class {
|
|
|
1632
1912
|
});
|
|
1633
1913
|
}
|
|
1634
1914
|
}
|
|
1635
|
-
async deductCreditsAtomic(userId, amount) {
|
|
1915
|
+
async deductCreditsAtomic(userId, amount, journal) {
|
|
1636
1916
|
assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
|
|
1637
1917
|
const credits = this.store.users.get(userId);
|
|
1638
1918
|
if (!credits) {
|
|
1639
1919
|
throw new Error(`User credits not found for userId: ${userId}`);
|
|
1640
1920
|
}
|
|
1641
|
-
const
|
|
1921
|
+
const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
|
|
1922
|
+
const available = Math.min(
|
|
1923
|
+
sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved),
|
|
1924
|
+
sumAmounts(spendable, credits.bonusCredits)
|
|
1925
|
+
);
|
|
1642
1926
|
if (available < amount) {
|
|
1643
1927
|
throw new Error(
|
|
1644
1928
|
`Insufficient credits. Available: ${available}, requested: ${amount}`
|
|
1645
1929
|
);
|
|
1646
1930
|
}
|
|
1647
|
-
const balanceDeduction = Math.min(
|
|
1931
|
+
const balanceDeduction = Math.min(spendable, amount);
|
|
1648
1932
|
const bonusDeduction = sumAmounts(amount, -balanceDeduction);
|
|
1649
1933
|
const previousBalance = sumAmounts(credits.balance, credits.bonusCredits);
|
|
1650
1934
|
const nextBalance = sumAmounts(credits.balance, -balanceDeduction);
|
|
@@ -1654,11 +1938,26 @@ var InMemoryCreditRepository = class {
|
|
|
1654
1938
|
{ previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },
|
|
1655
1939
|
{ userId, operation: "deductCredits" }
|
|
1656
1940
|
);
|
|
1941
|
+
const held = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
1657
1942
|
credits.balance = nextBalance;
|
|
1658
1943
|
credits.bonusCredits = nextBonusCredits;
|
|
1944
|
+
writeExpiredHoldBacking(this.store, credits, held);
|
|
1659
1945
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1660
1946
|
this.store.users.set(userId, credits);
|
|
1661
|
-
return { previousBalance, newBalance };
|
|
1947
|
+
if (!journal) return { previousBalance, newBalance };
|
|
1948
|
+
const entry = this.recordJournalEntry({
|
|
1949
|
+
userId,
|
|
1950
|
+
entryType: "debit",
|
|
1951
|
+
amount,
|
|
1952
|
+
balanceBefore: previousBalance,
|
|
1953
|
+
balanceAfter: newBalance,
|
|
1954
|
+
source: journal.source ?? "operation_commit",
|
|
1955
|
+
referenceId: journal.referenceId,
|
|
1956
|
+
referenceType: journal.referenceType ?? "adjustment",
|
|
1957
|
+
description: journal.description,
|
|
1958
|
+
metadata: journal.metadata
|
|
1959
|
+
});
|
|
1960
|
+
return { previousBalance, newBalance, journalEntryId: entry.id };
|
|
1662
1961
|
}
|
|
1663
1962
|
// ==================== Transactions ====================
|
|
1664
1963
|
async createTransaction(input) {
|
|
@@ -1772,6 +2071,11 @@ var InMemoryCreditRepository = class {
|
|
|
1772
2071
|
assertRepresentableAmount(input.balanceAfter, "journal balanceAfter", {
|
|
1773
2072
|
userId: input.userId
|
|
1774
2073
|
});
|
|
2074
|
+
if (input.balanceBefore !== void 0) {
|
|
2075
|
+
assertRepresentableAmount(input.balanceBefore, "journal balanceBefore", {
|
|
2076
|
+
userId: input.userId
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
1775
2079
|
assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });
|
|
1776
2080
|
assertPublicJournalKey(input.idempotencyKey, input.userId);
|
|
1777
2081
|
const key = input.idempotencyKey !== void 0 ? scopedKey(input.userId, input.idempotencyKey) : void 0;
|
|
@@ -1787,6 +2091,7 @@ var InMemoryCreditRepository = class {
|
|
|
1787
2091
|
userId: input.userId,
|
|
1788
2092
|
entryType: input.entryType,
|
|
1789
2093
|
amount: input.amount,
|
|
2094
|
+
...input.balanceBefore !== void 0 ? { balanceBefore: input.balanceBefore } : {},
|
|
1790
2095
|
balanceAfter: input.balanceAfter,
|
|
1791
2096
|
source: input.source,
|
|
1792
2097
|
referenceId: input.referenceId,
|
|
@@ -1881,7 +2186,11 @@ var InMemoryCreditRepository = class {
|
|
|
1881
2186
|
return { expiredCount, creditsReleased, errors };
|
|
1882
2187
|
}
|
|
1883
2188
|
// ==================== Atomic Monthly Reset ====================
|
|
1884
|
-
|
|
2189
|
+
/**
|
|
2190
|
+
* @param _tier - Not trusted. The caller read it earlier and the account may
|
|
2191
|
+
* have changed tier since, so the reset uses the tier stored on the account.
|
|
2192
|
+
*/
|
|
2193
|
+
async atomicMonthlyReset(userId, _tier, expectedResetAt) {
|
|
1885
2194
|
const credits = this.store.users.get(userId);
|
|
1886
2195
|
if (!credits) {
|
|
1887
2196
|
throw new Error(`User ${userId} not found`);
|
|
@@ -1891,17 +2200,41 @@ var InMemoryCreditRepository = class {
|
|
|
1891
2200
|
if (currentResetAt !== expected) {
|
|
1892
2201
|
return { wasReset: false, credits: { ...credits } };
|
|
1893
2202
|
}
|
|
1894
|
-
|
|
2203
|
+
const tier = credits.tier;
|
|
2204
|
+
const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
|
|
2205
|
+
const decision = this.options.resolveMonthlyReset?.({ tier, balance: spendable });
|
|
2206
|
+
if (decision === null) {
|
|
2207
|
+
return { wasReset: false, credits: { ...credits } };
|
|
2208
|
+
}
|
|
2209
|
+
if (decision) {
|
|
2210
|
+
assertValidBalanceTarget(decision.balance, "resolveMonthlyReset balance", { userId, tier });
|
|
2211
|
+
if (decision.monthlyLimit !== void 0) {
|
|
2212
|
+
assertValidBalanceTarget(decision.monthlyLimit, "resolveMonthlyReset monthlyLimit", {
|
|
2213
|
+
userId,
|
|
2214
|
+
tier
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
} else {
|
|
2218
|
+
assertRepresentableTierAmount(getConfigMonthlyLimit(tier), "monthlyLimit", { userId, tier });
|
|
2219
|
+
}
|
|
1895
2220
|
const nextReset = getNextMonthlyReset();
|
|
1896
|
-
const target = monthlyResetBalance(tier);
|
|
1897
|
-
const floor = backedBalanceFloor(credits.reserved, credits.bonusCredits);
|
|
1898
2221
|
const previousBalance = credits.balance;
|
|
1899
|
-
|
|
2222
|
+
let target;
|
|
2223
|
+
if (decision) {
|
|
2224
|
+
target = decision.balance;
|
|
2225
|
+
} else {
|
|
2226
|
+
const configured = monthlyResetBalance(tier);
|
|
2227
|
+
target = configured.kind === "atLeast" ? Math.max(spendable, configured.value) : configured.value;
|
|
2228
|
+
}
|
|
2229
|
+
const planned = planBalanceTarget(target, credits.reserved, credits.bonusCredits);
|
|
2230
|
+
const newBalance = planned.balance;
|
|
1900
2231
|
const change = sumAmounts(newBalance, -previousBalance);
|
|
1901
2232
|
const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
|
|
1902
2233
|
assertRepresentableAmount(Math.abs(change), "journal amount", { userId });
|
|
1903
2234
|
assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
|
|
1904
2235
|
credits.balance = newBalance;
|
|
2236
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
2237
|
+
if (decision?.monthlyLimit !== void 0) credits.monthlyLimit = decision.monthlyLimit;
|
|
1905
2238
|
credits.monthlyUsed = 0;
|
|
1906
2239
|
credits.monthlyResetAt = nextReset.toISOString();
|
|
1907
2240
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1911,6 +2244,7 @@ var InMemoryCreditRepository = class {
|
|
|
1911
2244
|
userId,
|
|
1912
2245
|
entryType: change > 0 ? "credit" : "debit",
|
|
1913
2246
|
amount: Math.abs(change),
|
|
2247
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
1914
2248
|
balanceAfter,
|
|
1915
2249
|
source: "monthly_reset",
|
|
1916
2250
|
referenceId: `reset-${Date.now()}`,
|
|
@@ -1955,23 +2289,34 @@ var InMemoryCreditRepository = class {
|
|
|
1955
2289
|
};
|
|
1956
2290
|
}
|
|
1957
2291
|
const defaultTier = getDefaultTier();
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
tier: defaultTier
|
|
1962
|
-
}
|
|
1963
|
-
|
|
2292
|
+
let limit;
|
|
2293
|
+
if (this.options.getDowngradeBalanceCap) {
|
|
2294
|
+
limit = this.options.getDowngradeBalanceCap();
|
|
2295
|
+
assertValidBalanceTarget(limit, "getDowngradeBalanceCap", { userId, tier: defaultTier });
|
|
2296
|
+
} else {
|
|
2297
|
+
const configured = getConfigMonthlyLimit(defaultTier);
|
|
2298
|
+
assertRepresentableTierAmount(configured, "monthlyLimit", {
|
|
2299
|
+
userId,
|
|
2300
|
+
tier: defaultTier
|
|
2301
|
+
});
|
|
2302
|
+
limit = storedMonthlyLimit(configured);
|
|
2303
|
+
}
|
|
1964
2304
|
const previousTier = credits.tier;
|
|
1965
2305
|
const previousBalance = credits.balance;
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
2306
|
+
const planned = planBalanceCap(
|
|
2307
|
+
credits.balance,
|
|
2308
|
+
limit,
|
|
2309
|
+
credits.reserved,
|
|
2310
|
+
credits.bonusCredits,
|
|
2311
|
+
credits.expiredHoldBacking
|
|
1969
2312
|
);
|
|
2313
|
+
const newBalance = planned.balance;
|
|
1970
2314
|
const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
|
|
1971
2315
|
assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
|
|
1972
2316
|
credits.tier = defaultTier;
|
|
1973
2317
|
credits.monthlyLimit = limit;
|
|
1974
2318
|
credits.balance = newBalance;
|
|
2319
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
1975
2320
|
credits.subscriptionExpiresAt = null;
|
|
1976
2321
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1977
2322
|
this.store.users.set(userId, credits);
|
|
@@ -1980,6 +2325,7 @@ var InMemoryCreditRepository = class {
|
|
|
1980
2325
|
entryType: "debit",
|
|
1981
2326
|
amount: 0,
|
|
1982
2327
|
// No credits deducted, just tier change
|
|
2328
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
1983
2329
|
balanceAfter,
|
|
1984
2330
|
source: "subscription_downgrade",
|
|
1985
2331
|
referenceId: `downgrade-${Date.now()}`,
|
|
@@ -2029,12 +2375,13 @@ var InMemoryCreditRepository = class {
|
|
|
2029
2375
|
return copyRecords(Array.from(userReservations.values()));
|
|
2030
2376
|
}
|
|
2031
2377
|
};
|
|
2032
|
-
function createInMemoryCreditRepository() {
|
|
2033
|
-
return new InMemoryCreditRepository();
|
|
2378
|
+
function createInMemoryCreditRepository(options = {}) {
|
|
2379
|
+
return new InMemoryCreditRepository(options);
|
|
2034
2380
|
}
|
|
2035
2381
|
export {
|
|
2036
2382
|
InMemoryCreditRepository,
|
|
2037
2383
|
addCreditsThroughRepository,
|
|
2384
|
+
assertValidBalanceUpdate,
|
|
2038
2385
|
commitThroughRepository,
|
|
2039
2386
|
createInMemoryCreditRepository,
|
|
2040
2387
|
generateId,
|
|
@@ -2045,6 +2392,7 @@ export {
|
|
|
2045
2392
|
supportsCreditsV2,
|
|
2046
2393
|
supportsIdempotentCredit,
|
|
2047
2394
|
toClientUserCredits,
|
|
2048
|
-
toDate
|
|
2395
|
+
toDate,
|
|
2396
|
+
validateBalanceUpdate
|
|
2049
2397
|
};
|
|
2050
2398
|
//# sourceMappingURL=index.js.map
|