@nehorai/credits 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/index.cjs +1 -0
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +139 -23
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +113 -22
- package/dist/core/index.d.ts +113 -22
- package/dist/core/index.js +128 -21
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +246 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +235 -59
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +183 -41
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +6 -5
- package/dist/repository/index.d.ts +6 -5
- package/dist/repository/index.js +183 -41
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.cjs +1 -0
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.d.cts +2 -2
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/service/index.cjs +25 -16
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +2 -2
- package/dist/service/index.d.ts +2 -2
- package/dist/service/index.js +25 -16
- package/dist/service/index.js.map +1 -1
- package/dist/{types-MaViK5tD.d.cts → types-C3sxDvNb.d.cts} +29 -6
- package/dist/{types-MaViK5tD.d.ts → types-C3sxDvNb.d.ts} +29 -6
- package/dist/{types-3c-IAwZI.d.ts → types-CY-oHIMA.d.ts} +19 -3
- package/dist/{types-XSjNXO5l.d.cts → types-DOjguhEX.d.cts} +19 -3
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-
|
|
2
|
-
export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-
|
|
3
|
-
export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-
|
|
4
|
-
export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput,
|
|
1
|
+
import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-C3sxDvNb.cjs';
|
|
2
|
+
export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, E as ExpiredBackingFrontier, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-C3sxDvNb.cjs';
|
|
3
|
+
export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, D as DeductJournalInput, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-DOjguhEX.cjs';
|
|
4
|
+
export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, ExpiredBackingAllocation, ExpiredBackingFields, ExpiredBackingHold, ExpiredBackingRewrite, NO_EXPIRED_BACKING_PREFIX, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, allocateExpiredBacking, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput, backedBalanceFloor, canonicalAmount, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredBackingPatch, getSqlState, hasPlacedHold, heldExpiredBacking, holdExpiredBacking, holdPlacedAtAfter, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, multiplyAmount, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, recordedExpiredBacking, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, storedAmountCeil, storedAmountFloor, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.cjs';
|
|
5
5
|
export { D as DeferredExecutor, c as createDeferredExecutor, g as genericDeferred, n as noopDeferred, s as synchronousDeferred } from './deferred-C7rrRfkm.cjs';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
export { BalanceValidationResult, InMemoryCreditRepository, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, validateBalanceUpdate } from './repository/index.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-
|
|
2
|
-
export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-
|
|
3
|
-
export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-
|
|
4
|
-
export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput,
|
|
1
|
+
import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-C3sxDvNb.js';
|
|
2
|
+
export { A as AIProviderType, B as BuiltinTier, g as CreditCheckResult, C as CreditOperationType, b as CreditSource, D as DeductCreditsResult, E as ExpiredBackingFrontier, J as JournalReferenceType, M as MonthlyResetResult, f as PortableJournalEntry, P as PortableTransaction, d as PortableUsageLog, c as PortableUserCredits, R as ReservationStatus, h as ResourceType, g as SDKCreditCheckResult, e as SubscriptionExpiryResult, i as TransactionType, U as UsageHistoryEntry, j as UsageHistoryResponse, c as UserCredits, W as WithCreditsOptions, k as calculateAvailableCredits, t as toDate, l as toPortableTimestamp } from './types-C3sxDvNb.js';
|
|
3
|
+
export { A as AddCreditsAtomicOptions, a as AddCreditsOutcome, b as AddCreditsV2Input, C as CommitOutcome, c as CreateJournalEntryInput, d as CreateReservationInput, e as CreateTransactionInput, f as CreateUsageLogInput, g as CreditBalanceUpdate, h as CreditPolicyOptions, i as CreditRepositoryFactory, D as DeductJournalInput, E as ExpireOutcome, j as ExpireReservationV2Options, I as ICreditRepository, k as ICreditRepositoryCreditsV2, l as ICreditRepositoryV2, J as JournalEntryQuery, R as ReleaseOutcome, m as ReservationTransitionOptions, n as ReserveCreditsV2Input, o as ReserveOutcome, T as TerminalReservationStatus, p as TierUpdateInput, U as UsageLogQuery, q as isCreditedOutcome, r as isReservedOutcome, s as isWinningOutcome, t as reservationJournalKey, u as supportsCreditsV2, v as supportsIdempotentCredit, w as toClientUserCredits } from './types-CY-oHIMA.js';
|
|
4
|
+
export { CREDIT_AMOUNT_MAX, CREDIT_AMOUNT_SCALE, CreditError, CreditErrorCode, CreditErrorCodeType, ExpiredBackingAllocation, ExpiredBackingFields, ExpiredBackingHold, ExpiredBackingRewrite, NO_EXPIRED_BACKING_PREFIX, PaymentEventPayload, RESERVATION_STATUSES, RESERVED_JOURNAL_KEY_PREFIX, ReservationIntegrityView, StoredPaymentEvent, TERMINAL_RESERVATION_STATUSES, allocateExpiredBacking, assertDirectStatusWriteAllowed, assertHoldPlaced, assertKnownReservationStatus, assertPublicJournalKey, assertRepresentableAmount, assertRepresentableFields, assertRepresentableTierAmount, assertUnkeyedDirectReservation, assertUnreferencedDirectTransaction, assertValidBalanceTarget, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, assertValidTierBalanceInput, backedBalanceFloor, canonicalAmount, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredBackingPatch, getSqlState, hasPlacedHold, heldExpiredBacking, holdExpiredBacking, holdPlacedAtAfter, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, multiplyAmount, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, recordedExpiredBacking, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, storedAmountCeil, storedAmountFloor, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.js';
|
|
5
5
|
export { D as DeferredExecutor, c as createDeferredExecutor, g as genericDeferred, n as noopDeferred, s as synchronousDeferred } from './deferred-C7rrRfkm.js';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
export { BalanceValidationResult, InMemoryCreditRepository, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, validateBalanceUpdate } from './repository/index.js';
|
package/dist/index.js
CHANGED
|
@@ -167,6 +167,40 @@ function floatSum(values) {
|
|
|
167
167
|
for (const value of values) total += value;
|
|
168
168
|
return total;
|
|
169
169
|
}
|
|
170
|
+
function multiplyAmount(unit, quantity) {
|
|
171
|
+
const cents = toCents(unit);
|
|
172
|
+
const product = cents === null ? Number.NaN : cents * quantity;
|
|
173
|
+
if (cents === null || cents < 0 || !Number.isSafeInteger(quantity) || quantity < 0 || !Number.isSafeInteger(product) || product > MAX_CENTS) {
|
|
174
|
+
throw new CreditError(
|
|
175
|
+
`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}`,
|
|
176
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
177
|
+
{ unit, quantity, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return product / 100;
|
|
181
|
+
}
|
|
182
|
+
var FLOAT_NOISE_FLOOR = 1e-6;
|
|
183
|
+
var FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;
|
|
184
|
+
function floatNoise(value) {
|
|
185
|
+
return Math.max(FLOAT_NOISE_FLOOR, Math.abs(value) * FLOAT_NOISE_RELATIVE);
|
|
186
|
+
}
|
|
187
|
+
function canonicalAmount(value) {
|
|
188
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return value;
|
|
189
|
+
const cents = Math.round(value * 100);
|
|
190
|
+
if (!Number.isSafeInteger(cents)) return value;
|
|
191
|
+
const onGrid = cents / 100 + 0;
|
|
192
|
+
return Math.abs(value - onGrid) < floatNoise(value) ? onGrid : value;
|
|
193
|
+
}
|
|
194
|
+
function storedAmountFloor(value) {
|
|
195
|
+
const canonical = canonicalAmount(value);
|
|
196
|
+
if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
|
|
197
|
+
return Math.floor(canonical * 100) / 100;
|
|
198
|
+
}
|
|
199
|
+
function storedAmountCeil(value) {
|
|
200
|
+
const canonical = canonicalAmount(value);
|
|
201
|
+
if (!Number.isFinite(canonical) || toCents(canonical) !== null) return canonical;
|
|
202
|
+
return Math.ceil(canonical * 100) / 100;
|
|
203
|
+
}
|
|
170
204
|
function isValidCreditAmount(value) {
|
|
171
205
|
if (typeof value !== "number") return false;
|
|
172
206
|
const cents = toCents(value);
|
|
@@ -222,14 +256,19 @@ function backedBalanceFloor(reserved, bonusCredits) {
|
|
|
222
256
|
function cappedBalance(balance, cap, reserved, bonusCredits) {
|
|
223
257
|
return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
|
|
224
258
|
}
|
|
225
|
-
function
|
|
226
|
-
|
|
259
|
+
function heldExpiredBacking(balance, stored) {
|
|
260
|
+
if (!(stored !== void 0 && stored > 0)) return 0;
|
|
261
|
+
return Math.max(0, Math.min(stored, balance));
|
|
262
|
+
}
|
|
263
|
+
function spendableBalance(balance, expiredHoldBacking) {
|
|
264
|
+
return sumAmounts(balance, -heldExpiredBacking(balance, expiredHoldBacking));
|
|
227
265
|
}
|
|
228
266
|
function planBalanceTarget(target, reserved, bonusCredits) {
|
|
267
|
+
assertValidBalanceTarget(target, "balance target", { reserved, bonusCredits });
|
|
229
268
|
const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
|
|
230
269
|
return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
|
|
231
270
|
}
|
|
232
|
-
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
|
|
271
|
+
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking) {
|
|
233
272
|
const spendable = spendableBalance(balance, expiredHoldBacking);
|
|
234
273
|
const target = Math.min(spendable, cap);
|
|
235
274
|
const kept = Math.max(
|
|
@@ -240,8 +279,8 @@ function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking
|
|
|
240
279
|
return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
|
|
241
280
|
}
|
|
242
281
|
function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
|
|
243
|
-
const held =
|
|
244
|
-
const used = Math.min(held, holdBacking);
|
|
282
|
+
const held = heldExpiredBacking(balance, expiredHoldBacking);
|
|
283
|
+
const used = Math.max(0, Math.min(held, holdBacking));
|
|
245
284
|
return { used, remaining: sumAmounts(held, -used) };
|
|
246
285
|
}
|
|
247
286
|
function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
|
|
@@ -256,27 +295,83 @@ function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking,
|
|
|
256
295
|
expiredHoldBacking: backing.remaining
|
|
257
296
|
};
|
|
258
297
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
298
|
+
var NO_EXPIRED_BACKING_PREFIX = {
|
|
299
|
+
holdPlacedAt: "",
|
|
300
|
+
id: "",
|
|
301
|
+
share: 0
|
|
302
|
+
};
|
|
303
|
+
function compareHolds(a, b) {
|
|
304
|
+
if (a.holdPlacedAt !== b.holdPlacedAt) return a.holdPlacedAt < b.holdPlacedAt ? -1 : 1;
|
|
305
|
+
if (a.id === b.id) return 0;
|
|
306
|
+
return a.id < b.id ? -1 : 1;
|
|
307
|
+
}
|
|
308
|
+
function recordedExpiredBacking(hold) {
|
|
309
|
+
const stored = hold.expiredBacking;
|
|
310
|
+
if (typeof stored !== "number" || !Number.isFinite(stored) || stored <= 0) return 0;
|
|
311
|
+
if (!isValidCreditAmount(hold.amount)) return 0;
|
|
312
|
+
return Math.min(storedAmountCeil(stored), hold.amount);
|
|
313
|
+
}
|
|
314
|
+
function allocateExpiredBacking(holds, total) {
|
|
315
|
+
const placed = holds.filter((hold) => hold.holdPlacedAt && isValidCreditAmount(hold.amount)).map((hold) => ({ ...hold, holdPlacedAt: hold.holdPlacedAt })).sort(compareHolds);
|
|
316
|
+
const wanted = Math.max(0, total);
|
|
317
|
+
if (wanted === 0) return { total: 0, frontier: null };
|
|
318
|
+
const recorded = placed.filter((hold) => recordedExpiredBacking(hold) > 0);
|
|
319
|
+
const shares = new Map(recorded.map((hold) => [hold.id, recordedExpiredBacking(hold)]));
|
|
320
|
+
let left = sumAmounts(wanted, ...[...shares.values()].map((share) => -share));
|
|
321
|
+
for (const hold of [...recorded].reverse()) {
|
|
322
|
+
if (left >= 0) break;
|
|
323
|
+
const share = shares.get(hold.id);
|
|
324
|
+
const cut = Math.min(share, -left);
|
|
325
|
+
shares.set(hold.id, sumAmounts(share, -cut));
|
|
326
|
+
left = sumAmounts(left, cut);
|
|
269
327
|
}
|
|
328
|
+
let frontier = NO_EXPIRED_BACKING_PREFIX;
|
|
270
329
|
for (const hold of placed) {
|
|
330
|
+
if (left <= 0) break;
|
|
331
|
+
if (shares.has(hold.id)) continue;
|
|
332
|
+
const share = Math.min(hold.amount, left);
|
|
333
|
+
frontier = { holdPlacedAt: hold.holdPlacedAt, id: hold.id, share };
|
|
334
|
+
left = sumAmounts(left, -share);
|
|
335
|
+
}
|
|
336
|
+
for (const hold of recorded) {
|
|
337
|
+
if (left <= 0) break;
|
|
271
338
|
const share = shares.get(hold.id);
|
|
272
339
|
const added = Math.min(sumAmounts(hold.amount, -share), left);
|
|
273
340
|
shares.set(hold.id, sumAmounts(share, added));
|
|
274
341
|
left = sumAmounts(left, -added);
|
|
275
342
|
}
|
|
276
|
-
|
|
343
|
+
const rewrites = recorded.filter((hold) => shares.get(hold.id) !== hold.expiredBacking).map((hold) => ({ id: hold.id, expiredBacking: shares.get(hold.id) }));
|
|
344
|
+
return { total: wanted, frontier, ...rewrites.length > 0 ? { rewrites } : {} };
|
|
277
345
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
346
|
+
function holdExpiredBacking(hold, frontier, rewrites) {
|
|
347
|
+
const rewritten = rewrites?.find((rewrite) => rewrite.id === hold.id);
|
|
348
|
+
const recorded = recordedExpiredBacking(
|
|
349
|
+
rewritten ? { ...hold, expiredBacking: rewritten.expiredBacking } : hold
|
|
350
|
+
);
|
|
351
|
+
if (recorded > 0) return recorded;
|
|
352
|
+
if (!frontier || !hold.holdPlacedAt) return 0;
|
|
353
|
+
const order = compareHolds({ holdPlacedAt: hold.holdPlacedAt, id: hold.id }, frontier);
|
|
354
|
+
if (order > 0) return 0;
|
|
355
|
+
return order < 0 ? hold.amount : Math.min(frontier.share, hold.amount);
|
|
356
|
+
}
|
|
357
|
+
function holdPlacedAtAfter(now, frontier) {
|
|
358
|
+
if (!frontier?.holdPlacedAt) return now;
|
|
359
|
+
const earliest = new Date(Date.parse(frontier.holdPlacedAt) + 1).toISOString();
|
|
360
|
+
return now > earliest ? now : earliest;
|
|
361
|
+
}
|
|
362
|
+
function expiredBackingPatch(next, stored) {
|
|
363
|
+
const total = Math.max(0, next.total);
|
|
364
|
+
const patch = {};
|
|
365
|
+
const storedTotal = stored.expiredHoldBacking;
|
|
366
|
+
if (total > 0 || storedTotal !== void 0 && storedTotal !== 0) {
|
|
367
|
+
patch.expiredHoldBacking = total;
|
|
368
|
+
}
|
|
369
|
+
if (total <= 0) {
|
|
370
|
+
if (stored.expiredBackingFrontier) patch.expiredBackingFrontier = null;
|
|
371
|
+
} else if (next.frontier) {
|
|
372
|
+
patch.expiredBackingFrontier = next.frontier;
|
|
373
|
+
}
|
|
374
|
+
return patch;
|
|
280
375
|
}
|
|
281
376
|
function assertValidBalanceTarget(value, field, context) {
|
|
282
377
|
assertRepresentableAmount(value, field, context);
|
|
@@ -289,7 +384,10 @@ function assertValidBalanceTarget(value, field, context) {
|
|
|
289
384
|
}
|
|
290
385
|
}
|
|
291
386
|
function assertValidTierBalanceInput(input, context) {
|
|
292
|
-
if (input.balanceCap === void 0)
|
|
387
|
+
if (input.balanceCap === void 0) {
|
|
388
|
+
if (input.balance !== void 0) assertValidBalanceTarget(input.balance, "balance", context);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
293
391
|
if (input.balance !== void 0) {
|
|
294
392
|
throw new CreditError(
|
|
295
393
|
"A tier update cannot set both balance and balanceCap",
|
|
@@ -1255,7 +1353,7 @@ function validateBalanceUpdate(current, updates) {
|
|
|
1255
1353
|
const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
|
|
1256
1354
|
const errors = [];
|
|
1257
1355
|
if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
|
|
1258
|
-
const marked = updates.balance === void 0 ? current.expiredHoldBacking
|
|
1356
|
+
const marked = updates.balance === void 0 ? heldExpiredBacking(current.balance, current.expiredHoldBacking) : 0;
|
|
1259
1357
|
if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
|
|
1260
1358
|
errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
|
|
1261
1359
|
}
|
|
@@ -1407,6 +1505,7 @@ function commitTransitionJournal(store, input, existingId) {
|
|
|
1407
1505
|
userId: input.userId,
|
|
1408
1506
|
entryType: input.entryType,
|
|
1409
1507
|
amount: input.amount,
|
|
1508
|
+
balanceBefore: input.balanceBefore,
|
|
1410
1509
|
balanceAfter: input.balanceAfter,
|
|
1411
1510
|
source: input.source,
|
|
1412
1511
|
referenceId: input.reservationId,
|
|
@@ -1456,16 +1555,28 @@ function openHolds(store, userId) {
|
|
|
1456
1555
|
(reservation) => reservation.status === "reserved"
|
|
1457
1556
|
);
|
|
1458
1557
|
}
|
|
1558
|
+
function expiredBackingInForce(store, credits) {
|
|
1559
|
+
const total = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
1560
|
+
if (total > 0 && (!credits.expiredBackingFrontier || credits.expiredHoldBacking !== total)) {
|
|
1561
|
+
return allocateExpiredBacking(openHolds(store, credits.userId), total);
|
|
1562
|
+
}
|
|
1563
|
+
return { total, frontier: credits.expiredBackingFrontier ?? null };
|
|
1564
|
+
}
|
|
1565
|
+
function writeRecordedShares(store, userId, allocation) {
|
|
1566
|
+
for (const { id, expiredBacking } of allocation.rewrites ?? []) {
|
|
1567
|
+
const hold = getReservation(store, userId, id);
|
|
1568
|
+
if (hold) hold.expiredBacking = expiredBacking;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1459
1571
|
function writeExpiredHoldBacking(store, credits, next) {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const shares = attributeExpiredBacking(holds, next);
|
|
1464
|
-
for (const hold of holds) {
|
|
1465
|
-
const share = shares.get(hold.id) ?? 0;
|
|
1466
|
-
if (share !== (hold.expiredBacking ?? 0)) hold.expiredBacking = share;
|
|
1572
|
+
if (next <= 0) {
|
|
1573
|
+
Object.assign(credits, expiredBackingPatch({ total: 0 }, credits));
|
|
1574
|
+
return;
|
|
1467
1575
|
}
|
|
1468
|
-
|
|
1576
|
+
if (next === credits.expiredHoldBacking) return;
|
|
1577
|
+
const allocation = allocateExpiredBacking(openHolds(store, credits.userId), next);
|
|
1578
|
+
writeRecordedShares(store, credits.userId, allocation);
|
|
1579
|
+
Object.assign(credits, expiredBackingPatch(allocation, credits));
|
|
1469
1580
|
}
|
|
1470
1581
|
function getReservation(store, userId, reservationId) {
|
|
1471
1582
|
return store.reservations.get(userId)?.get(reservationId);
|
|
@@ -1514,6 +1625,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1514
1625
|
};
|
|
1515
1626
|
}
|
|
1516
1627
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1628
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1517
1629
|
const reservation = {
|
|
1518
1630
|
id: generateId(),
|
|
1519
1631
|
userId: input.userId,
|
|
@@ -1526,7 +1638,7 @@ async function reserveCreditsV2(store, input) {
|
|
|
1526
1638
|
// The hold-origin fact. Written here and nowhere else, in the same
|
|
1527
1639
|
// critical section that raises `reserved` below, so the row and the hold
|
|
1528
1640
|
// become visible together. `createReservation` cannot set it.
|
|
1529
|
-
holdPlacedAt: now
|
|
1641
|
+
holdPlacedAt: holdPlacedAtAfter(now, backing.total > 0 ? backing.frontier : null)
|
|
1530
1642
|
};
|
|
1531
1643
|
if (!store.reservations.has(input.userId)) {
|
|
1532
1644
|
store.reservations.set(input.userId, /* @__PURE__ */ new Map());
|
|
@@ -1544,6 +1656,8 @@ async function reserveCreditsV2(store, input) {
|
|
|
1544
1656
|
);
|
|
1545
1657
|
}
|
|
1546
1658
|
credits.reserved = nextReserved;
|
|
1659
|
+
writeRecordedShares(store, input.userId, backing);
|
|
1660
|
+
Object.assign(credits, expiredBackingPatch(backing, credits));
|
|
1547
1661
|
credits.updatedAt = now;
|
|
1548
1662
|
return { outcome: "created", reservation: copyRecord(reservation) };
|
|
1549
1663
|
});
|
|
@@ -1564,11 +1678,12 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1564
1678
|
const amount = reservation.amount;
|
|
1565
1679
|
await yieldPoint(store);
|
|
1566
1680
|
assertReservedCoversHold(credits, userId, reservationId, amount);
|
|
1681
|
+
const backing = expiredBackingInForce(store, credits);
|
|
1567
1682
|
const paid = planHoldCommit(
|
|
1568
1683
|
credits.balance,
|
|
1569
1684
|
credits.bonusCredits,
|
|
1570
|
-
|
|
1571
|
-
reservation.
|
|
1685
|
+
backing.total,
|
|
1686
|
+
holdExpiredBacking(reservation, backing.frontier, backing.rewrites),
|
|
1572
1687
|
amount
|
|
1573
1688
|
);
|
|
1574
1689
|
if (!paid.covered) {
|
|
@@ -1586,6 +1701,7 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1586
1701
|
operation: "commitReservation",
|
|
1587
1702
|
entryType: "debit",
|
|
1588
1703
|
amount,
|
|
1704
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1589
1705
|
balanceAfter,
|
|
1590
1706
|
source: "operation_commit",
|
|
1591
1707
|
reservationId,
|
|
@@ -1613,9 +1729,10 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1613
1729
|
credits.bonusCredits = nextBonusCredits;
|
|
1614
1730
|
credits.reserved = nextReserved;
|
|
1615
1731
|
credits.monthlyUsed = nextMonthlyUsed;
|
|
1732
|
+
writeRecordedShares(store, userId, backing);
|
|
1616
1733
|
Object.assign(
|
|
1617
1734
|
credits,
|
|
1618
|
-
|
|
1735
|
+
expiredBackingPatch({ total: paid.expiredHoldBacking, frontier: backing.frontier }, credits)
|
|
1619
1736
|
);
|
|
1620
1737
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1621
1738
|
const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
|
|
@@ -1696,10 +1813,11 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1696
1813
|
reservation.id,
|
|
1697
1814
|
reservation.amount
|
|
1698
1815
|
);
|
|
1816
|
+
const inForce = expiredBackingInForce(store, credits);
|
|
1699
1817
|
const backing = settleExpiredHoldBacking(
|
|
1700
1818
|
credits.balance,
|
|
1701
|
-
|
|
1702
|
-
reservation.
|
|
1819
|
+
inForce.total,
|
|
1820
|
+
holdExpiredBacking(reservation, inForce.frontier, inForce.rewrites)
|
|
1703
1821
|
);
|
|
1704
1822
|
const nextBalance = sumAmounts(credits.balance, -backing.used);
|
|
1705
1823
|
const journal = {
|
|
@@ -1707,6 +1825,7 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1707
1825
|
operation: `${transition.transition}Reservation`,
|
|
1708
1826
|
entryType: "credit",
|
|
1709
1827
|
amount: 0,
|
|
1828
|
+
balanceBefore: sumAmounts(credits.balance, credits.bonusCredits),
|
|
1710
1829
|
balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
|
|
1711
1830
|
source: transition.source,
|
|
1712
1831
|
reservationId: reservation.id,
|
|
@@ -1729,7 +1848,11 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1729
1848
|
reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1730
1849
|
credits.reserved = nextReserved;
|
|
1731
1850
|
credits.balance = nextBalance;
|
|
1732
|
-
|
|
1851
|
+
writeRecordedShares(store, reservation.userId, inForce);
|
|
1852
|
+
Object.assign(
|
|
1853
|
+
credits,
|
|
1854
|
+
expiredBackingPatch({ total: backing.remaining, frontier: inForce.frontier }, credits)
|
|
1855
|
+
);
|
|
1733
1856
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1734
1857
|
return commitTransitionJournal(store, journal, existingEntryId);
|
|
1735
1858
|
}
|
|
@@ -1772,7 +1895,7 @@ var InMemoryCreditRepository = class {
|
|
|
1772
1895
|
* simultaneous first deliveries of the same reference would then both credit.
|
|
1773
1896
|
*/
|
|
1774
1897
|
seedUser(userId, tier, initialBalance) {
|
|
1775
|
-
|
|
1898
|
+
assertValidBalanceTarget(initialBalance, "initialBalance", { userId });
|
|
1776
1899
|
const configured = getConfigMonthlyLimit(tier);
|
|
1777
1900
|
assertRepresentableTierAmount(configured, "monthlyLimit", { userId, tier });
|
|
1778
1901
|
const monthlyLimit = storedMonthlyLimit(configured);
|
|
@@ -1853,9 +1976,11 @@ var InMemoryCreditRepository = class {
|
|
|
1853
1976
|
userId,
|
|
1854
1977
|
operation: "updateUserCredits"
|
|
1855
1978
|
});
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1979
|
+
writeExpiredHoldBacking(
|
|
1980
|
+
this.store,
|
|
1981
|
+
next,
|
|
1982
|
+
updates.balance !== void 0 ? 0 : Math.min(heldExpiredBacking(credits.balance, credits.expiredHoldBacking), next.balance)
|
|
1983
|
+
);
|
|
1859
1984
|
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1860
1985
|
this.store.users.set(userId, next);
|
|
1861
1986
|
}
|
|
@@ -1887,6 +2012,11 @@ var InMemoryCreditRepository = class {
|
|
|
1887
2012
|
credits.balance = planned.balance;
|
|
1888
2013
|
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
1889
2014
|
}
|
|
2015
|
+
writeExpiredHoldBacking(
|
|
2016
|
+
this.store,
|
|
2017
|
+
credits,
|
|
2018
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
2019
|
+
);
|
|
1890
2020
|
if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
|
|
1891
2021
|
if (input.subscriptionExpiresAt !== void 0) {
|
|
1892
2022
|
if (input.subscriptionExpiresAt === null) {
|
|
@@ -2048,6 +2178,11 @@ var InMemoryCreditRepository = class {
|
|
|
2048
2178
|
{ userId, operation: "addCredits" }
|
|
2049
2179
|
);
|
|
2050
2180
|
credits.bonusCredits = nextBonusCredits;
|
|
2181
|
+
writeExpiredHoldBacking(
|
|
2182
|
+
this.store,
|
|
2183
|
+
credits,
|
|
2184
|
+
heldExpiredBacking(credits.balance, credits.expiredHoldBacking)
|
|
2185
|
+
);
|
|
2051
2186
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2052
2187
|
this.store.users.set(userId, credits);
|
|
2053
2188
|
const transaction = this.recordTransaction({
|
|
@@ -2067,6 +2202,7 @@ var InMemoryCreditRepository = class {
|
|
|
2067
2202
|
userId,
|
|
2068
2203
|
entryType: "credit",
|
|
2069
2204
|
amount,
|
|
2205
|
+
balanceBefore: previousTotal,
|
|
2070
2206
|
balanceAfter: newTotal,
|
|
2071
2207
|
source: payload.source,
|
|
2072
2208
|
referenceId: transaction.id ?? paymentRef ?? "unknown",
|
|
@@ -2100,7 +2236,7 @@ var InMemoryCreditRepository = class {
|
|
|
2100
2236
|
});
|
|
2101
2237
|
}
|
|
2102
2238
|
}
|
|
2103
|
-
async deductCreditsAtomic(userId, amount) {
|
|
2239
|
+
async deductCreditsAtomic(userId, amount, journal) {
|
|
2104
2240
|
assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
|
|
2105
2241
|
const credits = this.store.users.get(userId);
|
|
2106
2242
|
if (!credits) {
|
|
@@ -2126,11 +2262,26 @@ var InMemoryCreditRepository = class {
|
|
|
2126
2262
|
{ previousBalance, balance: nextBalance, bonusCredits: nextBonusCredits, newBalance },
|
|
2127
2263
|
{ userId, operation: "deductCredits" }
|
|
2128
2264
|
);
|
|
2265
|
+
const held = heldExpiredBacking(credits.balance, credits.expiredHoldBacking);
|
|
2129
2266
|
credits.balance = nextBalance;
|
|
2130
2267
|
credits.bonusCredits = nextBonusCredits;
|
|
2268
|
+
writeExpiredHoldBacking(this.store, credits, held);
|
|
2131
2269
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2132
2270
|
this.store.users.set(userId, credits);
|
|
2133
|
-
return { previousBalance, newBalance };
|
|
2271
|
+
if (!journal) return { previousBalance, newBalance };
|
|
2272
|
+
const entry = this.recordJournalEntry({
|
|
2273
|
+
userId,
|
|
2274
|
+
entryType: "debit",
|
|
2275
|
+
amount,
|
|
2276
|
+
balanceBefore: previousBalance,
|
|
2277
|
+
balanceAfter: newBalance,
|
|
2278
|
+
source: journal.source ?? "operation_commit",
|
|
2279
|
+
referenceId: journal.referenceId,
|
|
2280
|
+
referenceType: journal.referenceType ?? "adjustment",
|
|
2281
|
+
description: journal.description,
|
|
2282
|
+
metadata: journal.metadata
|
|
2283
|
+
});
|
|
2284
|
+
return { previousBalance, newBalance, journalEntryId: entry.id };
|
|
2134
2285
|
}
|
|
2135
2286
|
// ==================== Transactions ====================
|
|
2136
2287
|
async createTransaction(input) {
|
|
@@ -2244,6 +2395,11 @@ var InMemoryCreditRepository = class {
|
|
|
2244
2395
|
assertRepresentableAmount(input.balanceAfter, "journal balanceAfter", {
|
|
2245
2396
|
userId: input.userId
|
|
2246
2397
|
});
|
|
2398
|
+
if (input.balanceBefore !== void 0) {
|
|
2399
|
+
assertRepresentableAmount(input.balanceBefore, "journal balanceBefore", {
|
|
2400
|
+
userId: input.userId
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2247
2403
|
assertValidIdempotencyKey(input.idempotencyKey, { userId: input.userId });
|
|
2248
2404
|
assertPublicJournalKey(input.idempotencyKey, input.userId);
|
|
2249
2405
|
const key = input.idempotencyKey !== void 0 ? scopedKey(input.userId, input.idempotencyKey) : void 0;
|
|
@@ -2259,6 +2415,7 @@ var InMemoryCreditRepository = class {
|
|
|
2259
2415
|
userId: input.userId,
|
|
2260
2416
|
entryType: input.entryType,
|
|
2261
2417
|
amount: input.amount,
|
|
2418
|
+
...input.balanceBefore !== void 0 ? { balanceBefore: input.balanceBefore } : {},
|
|
2262
2419
|
balanceAfter: input.balanceAfter,
|
|
2263
2420
|
source: input.source,
|
|
2264
2421
|
referenceId: input.referenceId,
|
|
@@ -2411,6 +2568,7 @@ var InMemoryCreditRepository = class {
|
|
|
2411
2568
|
userId,
|
|
2412
2569
|
entryType: change > 0 ? "credit" : "debit",
|
|
2413
2570
|
amount: Math.abs(change),
|
|
2571
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
2414
2572
|
balanceAfter,
|
|
2415
2573
|
source: "monthly_reset",
|
|
2416
2574
|
referenceId: `reset-${Date.now()}`,
|
|
@@ -2491,6 +2649,7 @@ var InMemoryCreditRepository = class {
|
|
|
2491
2649
|
entryType: "debit",
|
|
2492
2650
|
amount: 0,
|
|
2493
2651
|
// No credits deducted, just tier change
|
|
2652
|
+
balanceBefore: sumAmounts(previousBalance, credits.bonusCredits),
|
|
2494
2653
|
balanceAfter,
|
|
2495
2654
|
source: "subscription_downgrade",
|
|
2496
2655
|
referenceId: `downgrade-${Date.now()}`,
|
|
@@ -2654,6 +2813,7 @@ var CreditsService = class {
|
|
|
2654
2813
|
entryType: "debit",
|
|
2655
2814
|
amount: 0,
|
|
2656
2815
|
// No credits deducted, just tier change
|
|
2816
|
+
balanceBefore: sumAmounts(data.balance, data.bonusCredits),
|
|
2657
2817
|
// The ledger balance, matching every other journal writer: the V2
|
|
2658
2818
|
// transitions and add/deduct all record `balance + bonusCredits`, so
|
|
2659
2819
|
// recording `balance` alone made the audit trail disagree with itself
|
|
@@ -2697,6 +2857,7 @@ var CreditsService = class {
|
|
|
2697
2857
|
userId,
|
|
2698
2858
|
entryType: balanceChange > 0 ? "credit" : "debit",
|
|
2699
2859
|
amount: Math.abs(balanceChange),
|
|
2860
|
+
balanceBefore: sumAmounts(data.balance, data.bonusCredits),
|
|
2700
2861
|
balanceAfter: sumAmounts(
|
|
2701
2862
|
resetResult.credits.balance,
|
|
2702
2863
|
resetResult.credits.bonusCredits
|
|
@@ -2931,9 +3092,18 @@ var CreditsService = class {
|
|
|
2931
3092
|
*/
|
|
2932
3093
|
async deductCredits(userId, amount, options) {
|
|
2933
3094
|
assertValidCreditAmount(amount, { userId, operation: "deductCredits" });
|
|
3095
|
+
const operationType = options?.operationType;
|
|
3096
|
+
const journal = {
|
|
3097
|
+
referenceId: options?.requestId ?? `deduct-${Date.now()}`,
|
|
3098
|
+
description: operationType ? `Deducted ${amount} credits for ${getOperationLabel(operationType)}` : `Deducted ${amount} credits`,
|
|
3099
|
+
metadata: {
|
|
3100
|
+
...options?.metadata,
|
|
3101
|
+
...operationType && { operationType }
|
|
3102
|
+
}
|
|
3103
|
+
};
|
|
2934
3104
|
let deduction;
|
|
2935
3105
|
try {
|
|
2936
|
-
deduction = await this.repository.deductCreditsAtomic(userId, amount);
|
|
3106
|
+
deduction = await this.repository.deductCreditsAtomic(userId, amount, journal);
|
|
2937
3107
|
} catch (error) {
|
|
2938
3108
|
if (isCreditError(error) && error.code === CreditErrorCode.INVALID_AMOUNT) throw error;
|
|
2939
3109
|
const credits = await this.repository.getUserCredits(userId);
|
|
@@ -2949,21 +3119,18 @@ var CreditsService = class {
|
|
|
2949
3119
|
shortfall: sumAmounts(amount, -available)
|
|
2950
3120
|
};
|
|
2951
3121
|
}
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
...operationType && { operationType }
|
|
2965
|
-
}
|
|
2966
|
-
});
|
|
3122
|
+
if (deduction.journalEntryId === void 0) {
|
|
3123
|
+
await this.repository.createJournalEntry({
|
|
3124
|
+
userId,
|
|
3125
|
+
entryType: "debit",
|
|
3126
|
+
amount,
|
|
3127
|
+
balanceBefore: deduction.previousBalance,
|
|
3128
|
+
balanceAfter: deduction.newBalance,
|
|
3129
|
+
source: "operation_commit",
|
|
3130
|
+
referenceType: "adjustment",
|
|
3131
|
+
...journal
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
2967
3134
|
if (operationType) {
|
|
2968
3135
|
await this.logUsage({
|
|
2969
3136
|
userId,
|
|
@@ -3973,6 +4140,7 @@ export {
|
|
|
3973
4140
|
CreditsSDKError,
|
|
3974
4141
|
CreditsService,
|
|
3975
4142
|
InMemoryCreditRepository,
|
|
4143
|
+
NO_EXPIRED_BACKING_PREFIX,
|
|
3976
4144
|
NetworkError,
|
|
3977
4145
|
RESERVATION_STATUSES,
|
|
3978
4146
|
RESERVED_JOURNAL_KEY_PREFIX,
|
|
@@ -3985,6 +4153,7 @@ export {
|
|
|
3985
4153
|
UNLIMITED_BALANCE_SENTINEL,
|
|
3986
4154
|
ValidationError,
|
|
3987
4155
|
addCreditsThroughRepository,
|
|
4156
|
+
allocateExpiredBacking,
|
|
3988
4157
|
assertDirectStatusWriteAllowed,
|
|
3989
4158
|
assertHoldPlaced,
|
|
3990
4159
|
assertKnownReservationStatus,
|
|
@@ -4001,9 +4170,9 @@ export {
|
|
|
4001
4170
|
assertValidStoredAmount,
|
|
4002
4171
|
assertValidStoredAmountRaw,
|
|
4003
4172
|
assertValidTierBalanceInput,
|
|
4004
|
-
attributeExpiredBacking,
|
|
4005
4173
|
backedBalanceFloor,
|
|
4006
4174
|
calculateAvailableCredits,
|
|
4175
|
+
canonicalAmount,
|
|
4007
4176
|
cappedBalance,
|
|
4008
4177
|
checkAndNotifyLowBalance,
|
|
4009
4178
|
classifyDatabaseError,
|
|
@@ -4030,7 +4199,7 @@ export {
|
|
|
4030
4199
|
createUnsupportedOperationError,
|
|
4031
4200
|
createUserNotFoundError,
|
|
4032
4201
|
describePaymentMismatch,
|
|
4033
|
-
|
|
4202
|
+
expiredBackingPatch,
|
|
4034
4203
|
generateId,
|
|
4035
4204
|
generateRequestId2 as generateRequestId,
|
|
4036
4205
|
genericDeferred,
|
|
@@ -4056,6 +4225,9 @@ export {
|
|
|
4056
4225
|
getValidOperationTypes,
|
|
4057
4226
|
getValidTiers,
|
|
4058
4227
|
hasPlacedHold,
|
|
4228
|
+
heldExpiredBacking,
|
|
4229
|
+
holdExpiredBacking,
|
|
4230
|
+
holdPlacedAtAfter,
|
|
4059
4231
|
initializeConfig,
|
|
4060
4232
|
isCreditError,
|
|
4061
4233
|
isCreditErrorMessage,
|
|
@@ -4081,6 +4253,7 @@ export {
|
|
|
4081
4253
|
isWinningOutcome,
|
|
4082
4254
|
loadConfigFromEnv,
|
|
4083
4255
|
monthlyResetBalance,
|
|
4256
|
+
multiplyAmount,
|
|
4084
4257
|
noopDeferred,
|
|
4085
4258
|
normalizePaymentRef,
|
|
4086
4259
|
notifySubscriptionExpired,
|
|
@@ -4092,6 +4265,7 @@ export {
|
|
|
4092
4265
|
planBalanceCap,
|
|
4093
4266
|
planBalanceTarget,
|
|
4094
4267
|
planHoldCommit,
|
|
4268
|
+
recordedExpiredBacking,
|
|
4095
4269
|
registerConfigProvider,
|
|
4096
4270
|
registerNotificationHandler,
|
|
4097
4271
|
releaseReservationWithJournal,
|
|
@@ -4103,6 +4277,8 @@ export {
|
|
|
4103
4277
|
sameAmount,
|
|
4104
4278
|
settleExpiredHoldBacking,
|
|
4105
4279
|
spendableBalance,
|
|
4280
|
+
storedAmountCeil,
|
|
4281
|
+
storedAmountFloor,
|
|
4106
4282
|
storedMonthlyLimit,
|
|
4107
4283
|
sumAmounts,
|
|
4108
4284
|
supportsCreditsV2,
|