@nehorai/credits 2.0.0 → 2.2.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 -21
- package/README.md +384 -377
- package/dist/adapters/index.cjs +12 -8
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +3 -2
- package/dist/adapters/index.d.ts +3 -2
- package/dist/adapters/index.js +12 -8
- package/dist/adapters/index.js.map +1 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.cjs +106 -4
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +92 -5
- package/dist/core/index.d.ts +92 -5
- package/dist/core/index.js +96 -4
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +282 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.js +270 -45
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +251 -41
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +34 -6
- package/dist/repository/index.d.ts +34 -6
- package/dist/repository/index.js +248 -40
- package/dist/repository/index.js.map +1 -1
- 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.map +1 -1
- package/dist/service/index.cjs +12 -6
- 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 +12 -6
- package/dist/service/index.js.map +1 -1
- package/dist/{types-BmG5Omzj.d.ts → types-3c-IAwZI.d.ts} +38 -3
- package/dist/{types-DJ8yqaVr.d.cts → types-MaViK5tD.d.cts} +23 -1
- package/dist/{types-DJ8yqaVr.d.ts → types-MaViK5tD.d.ts} +23 -1
- package/dist/{types-C8hZMe8a.d.cts → types-XSjNXO5l.d.cts} +38 -3
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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 CreditRepositoryFactory, E as ExpireOutcome,
|
|
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, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, backedBalanceFloor, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.cjs';
|
|
1
|
+
import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-MaViK5tD.cjs';
|
|
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-MaViK5tD.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, 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-XSjNXO5l.cjs';
|
|
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, attributeExpiredBacking, backedBalanceFloor, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredHoldBackingPatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, 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
|
-
export { InMemoryCreditRepository, addCreditsThroughRepository, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository } from './repository/index.cjs';
|
|
7
|
+
export { BalanceValidationResult, InMemoryCreditRepository, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, validateBalanceUpdate } from './repository/index.cjs';
|
|
8
8
|
export { C as CreditsAuthProviderFactory, a as CreditsUser, I as ICreditsAuthProvider } from './types-OZU3I5OM.cjs';
|
|
9
9
|
export { ApiKeyCreditsProvider, createApiKeyProvider, verifyAdminApiKey, verifyBearerToken } from './auth/index.cjs';
|
|
10
10
|
export { CreditsService, DeductCreditsOptions, LowBalanceNotificationCallback, ReserveCreditsOptions, SubscriptionExpiredNotificationCallback, createCreditsService } from './service/index.cjs';
|
|
@@ -18,7 +18,9 @@ declare const creditSystemConfigSchema: z.ZodObject<{
|
|
|
18
18
|
/**
|
|
19
19
|
* Operation costs in credits
|
|
20
20
|
* Keys are operation type names (any non-empty string)
|
|
21
|
-
* Values are
|
|
21
|
+
* Values are non-negative numbers representing credit cost. 0 marks a free
|
|
22
|
+
* operation: `withCredits` runs it without touching the ledger. Amounts that
|
|
23
|
+
* move credits stay strictly positive, so `reserveCredits` refuses a 0.
|
|
22
24
|
*/
|
|
23
25
|
operationCosts: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
24
26
|
/**
|
|
@@ -59,7 +61,7 @@ declare const creditSystemConfigSchema: z.ZodObject<{
|
|
|
59
61
|
*/
|
|
60
62
|
reservationExpiryMs: z.ZodDefault<z.ZodNumber>;
|
|
61
63
|
/**
|
|
62
|
-
* Default credits for new users (free tier)
|
|
64
|
+
* Default credits for new users (free tier). 0 means no signup grant.
|
|
63
65
|
*/
|
|
64
66
|
defaultFreeCredits: z.ZodDefault<z.ZodNumber>;
|
|
65
67
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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 CreditRepositoryFactory, E as ExpireOutcome,
|
|
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, assertValidCreditAmount, assertValidIdempotencyKey, assertValidStoredAmount, assertValidStoredAmountRaw, backedBalanceFloor, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, storedMonthlyLimit, sumAmounts, terminalStatusOf, toCents } from './core/index.js';
|
|
1
|
+
import { S as SubscriptionTier, T as TierConfig, a as PortableReservation } from './types-MaViK5tD.js';
|
|
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-MaViK5tD.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, 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-3c-IAwZI.js';
|
|
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, attributeExpiredBacking, backedBalanceFloor, cappedBalance, classifyDatabaseError, commitReservationWithJournal, createIdempotencyConflictError, createInsufficientCreditsError, createInvalidAmountError, createInvalidOperationTypeError, createPaymentRefConflictError, createReservationAlreadyProcessedError, createReservationExpiredError, createReservationNotFoundError, createTransientError, createUnsupportedOperationError, createUserNotFoundError, describePaymentMismatch, expiredHoldBackingPatch, getSqlState, hasPlacedHold, isCreditError, isIdempotencyConflictError, isInsufficientCreditsError, isRepresentableAmount, isReservationStatus, isReservedJournalKey, isTerminalReservationStatus, isTransientDatabaseError, isTransientError, isUniqueViolation, isValidCreditAmount, isValidIdempotencyKey, normalizePaymentRef, numericToCents, planBalanceCap, planBalanceTarget, planHoldCommit, releaseReservationWithJournal, reserveCreditsForOperation, sameAmount, settleExpiredHoldBacking, spendableBalance, 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
|
-
export { InMemoryCreditRepository, addCreditsThroughRepository, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository } from './repository/index.js';
|
|
7
|
+
export { BalanceValidationResult, InMemoryCreditRepository, addCreditsThroughRepository, assertValidBalanceUpdate, commitThroughRepository, createInMemoryCreditRepository, generateId, getNextMonthlyReset, releaseThroughRepository, reserveThroughRepository, validateBalanceUpdate } from './repository/index.js';
|
|
8
8
|
export { C as CreditsAuthProviderFactory, a as CreditsUser, I as ICreditsAuthProvider } from './types-OZU3I5OM.js';
|
|
9
9
|
export { ApiKeyCreditsProvider, createApiKeyProvider, verifyAdminApiKey, verifyBearerToken } from './auth/index.js';
|
|
10
10
|
export { CreditsService, DeductCreditsOptions, LowBalanceNotificationCallback, ReserveCreditsOptions, SubscriptionExpiredNotificationCallback, createCreditsService } from './service/index.js';
|
|
@@ -18,7 +18,9 @@ declare const creditSystemConfigSchema: z.ZodObject<{
|
|
|
18
18
|
/**
|
|
19
19
|
* Operation costs in credits
|
|
20
20
|
* Keys are operation type names (any non-empty string)
|
|
21
|
-
* Values are
|
|
21
|
+
* Values are non-negative numbers representing credit cost. 0 marks a free
|
|
22
|
+
* operation: `withCredits` runs it without touching the ledger. Amounts that
|
|
23
|
+
* move credits stay strictly positive, so `reserveCredits` refuses a 0.
|
|
22
24
|
*/
|
|
23
25
|
operationCosts: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
24
26
|
/**
|
|
@@ -59,7 +61,7 @@ declare const creditSystemConfigSchema: z.ZodObject<{
|
|
|
59
61
|
*/
|
|
60
62
|
reservationExpiryMs: z.ZodDefault<z.ZodNumber>;
|
|
61
63
|
/**
|
|
62
|
-
* Default credits for new users (free tier)
|
|
64
|
+
* Default credits for new users (free tier). 0 means no signup grant.
|
|
63
65
|
*/
|
|
64
66
|
defaultFreeCredits: z.ZodDefault<z.ZodNumber>;
|
|
65
67
|
/**
|
package/dist/index.js
CHANGED
|
@@ -219,6 +219,86 @@ function backedBalanceFloor(reserved, bonusCredits) {
|
|
|
219
219
|
assertRepresentableAmount(floor, "backedBalanceFloor", { reserved, bonusCredits });
|
|
220
220
|
return floor;
|
|
221
221
|
}
|
|
222
|
+
function cappedBalance(balance, cap, reserved, bonusCredits) {
|
|
223
|
+
return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
|
|
224
|
+
}
|
|
225
|
+
function spendableBalance(balance, expiredHoldBacking = 0) {
|
|
226
|
+
return sumAmounts(balance, -Math.min(expiredHoldBacking, balance));
|
|
227
|
+
}
|
|
228
|
+
function planBalanceTarget(target, reserved, bonusCredits) {
|
|
229
|
+
const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
|
|
230
|
+
return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
|
|
231
|
+
}
|
|
232
|
+
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking = 0) {
|
|
233
|
+
const spendable = spendableBalance(balance, expiredHoldBacking);
|
|
234
|
+
const target = Math.min(spendable, cap);
|
|
235
|
+
const kept = Math.max(
|
|
236
|
+
sumAmounts(balance, -spendable),
|
|
237
|
+
sumAmounts(backedBalanceFloor(reserved, bonusCredits), -target)
|
|
238
|
+
);
|
|
239
|
+
const next = Math.min(balance, sumAmounts(target, kept));
|
|
240
|
+
return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
|
|
241
|
+
}
|
|
242
|
+
function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
|
|
243
|
+
const held = Math.min(expiredHoldBacking, balance);
|
|
244
|
+
const used = Math.min(held, holdBacking);
|
|
245
|
+
return { used, remaining: sumAmounts(held, -used) };
|
|
246
|
+
}
|
|
247
|
+
function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
|
|
248
|
+
const backing = settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking);
|
|
249
|
+
const rest = sumAmounts(amount, -backing.used);
|
|
250
|
+
const fromSpendable = Math.min(spendableBalance(balance, expiredHoldBacking), rest);
|
|
251
|
+
const fromBonus = sumAmounts(rest, -fromSpendable);
|
|
252
|
+
return {
|
|
253
|
+
covered: fromBonus <= bonusCredits,
|
|
254
|
+
balance: sumAmounts(balance, -backing.used, -fromSpendable),
|
|
255
|
+
bonusCredits: sumAmounts(bonusCredits, -fromBonus),
|
|
256
|
+
expiredHoldBacking: backing.remaining
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function attributeExpiredBacking(holds, total) {
|
|
260
|
+
const placed = holds.filter((hold) => hold.holdPlacedAt).sort(
|
|
261
|
+
(a, b) => a.holdPlacedAt === b.holdPlacedAt ? a.id < b.id ? -1 : 1 : a.holdPlacedAt < b.holdPlacedAt ? -1 : 1
|
|
262
|
+
);
|
|
263
|
+
const shares = /* @__PURE__ */ new Map();
|
|
264
|
+
let left = total;
|
|
265
|
+
for (const hold of placed) {
|
|
266
|
+
const kept = Math.min(hold.expiredBacking ?? 0, hold.amount, left);
|
|
267
|
+
shares.set(hold.id, kept);
|
|
268
|
+
left = sumAmounts(left, -kept);
|
|
269
|
+
}
|
|
270
|
+
for (const hold of placed) {
|
|
271
|
+
const share = shares.get(hold.id);
|
|
272
|
+
const added = Math.min(sumAmounts(hold.amount, -share), left);
|
|
273
|
+
shares.set(hold.id, sumAmounts(share, added));
|
|
274
|
+
left = sumAmounts(left, -added);
|
|
275
|
+
}
|
|
276
|
+
return shares;
|
|
277
|
+
}
|
|
278
|
+
function expiredHoldBackingPatch(next, stored = 0) {
|
|
279
|
+
return next > 0 || stored > 0 ? { expiredHoldBacking: next } : {};
|
|
280
|
+
}
|
|
281
|
+
function assertValidBalanceTarget(value, field, context) {
|
|
282
|
+
assertRepresentableAmount(value, field, context);
|
|
283
|
+
if (value < 0) {
|
|
284
|
+
throw new CreditError(
|
|
285
|
+
`${field} cannot be negative (got ${value})`,
|
|
286
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
287
|
+
{ field, value, ...context }
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function assertValidTierBalanceInput(input, context) {
|
|
292
|
+
if (input.balanceCap === void 0) return;
|
|
293
|
+
if (input.balance !== void 0) {
|
|
294
|
+
throw new CreditError(
|
|
295
|
+
"A tier update cannot set both balance and balanceCap",
|
|
296
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
297
|
+
{ balance: input.balance, balanceCap: input.balanceCap, ...context }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
assertValidBalanceTarget(input.balanceCap, "balanceCap", context);
|
|
301
|
+
}
|
|
222
302
|
function assertRepresentableTierAmount(value, field, context) {
|
|
223
303
|
if (value === Number.POSITIVE_INFINITY) return;
|
|
224
304
|
assertRepresentableAmount(value, field, context);
|
|
@@ -599,11 +679,13 @@ var creditSystemConfigSchema = z.object({
|
|
|
599
679
|
/**
|
|
600
680
|
* Operation costs in credits
|
|
601
681
|
* Keys are operation type names (any non-empty string)
|
|
602
|
-
* Values are
|
|
682
|
+
* Values are non-negative numbers representing credit cost. 0 marks a free
|
|
683
|
+
* operation: `withCredits` runs it without touching the ledger. Amounts that
|
|
684
|
+
* move credits stay strictly positive, so `reserveCredits` refuses a 0.
|
|
603
685
|
*/
|
|
604
686
|
operationCosts: z.record(
|
|
605
687
|
z.string().min(1),
|
|
606
|
-
z.number().
|
|
688
|
+
z.number().nonnegative()
|
|
607
689
|
),
|
|
608
690
|
/**
|
|
609
691
|
* Display labels for operation types (English canonical labels).
|
|
@@ -625,9 +707,9 @@ var creditSystemConfigSchema = z.object({
|
|
|
625
707
|
*/
|
|
626
708
|
reservationExpiryMs: z.number().positive().default(5 * 60 * 1e3),
|
|
627
709
|
/**
|
|
628
|
-
* Default credits for new users (free tier)
|
|
710
|
+
* Default credits for new users (free tier). 0 means no signup grant.
|
|
629
711
|
*/
|
|
630
|
-
defaultFreeCredits: z.number().
|
|
712
|
+
defaultFreeCredits: z.number().min(0).default(25),
|
|
631
713
|
/**
|
|
632
714
|
* Grace period in days after subscription expires before downgrade
|
|
633
715
|
*/
|
|
@@ -927,6 +1009,8 @@ function toClientUserCredits(credits) {
|
|
|
927
1009
|
monthlyUsed: credits.monthlyUsed,
|
|
928
1010
|
monthlyResetAt: toISOString(credits.monthlyResetAt),
|
|
929
1011
|
subscriptionExpiresAt: credits.subscriptionExpiresAt ? toISOString(credits.subscriptionExpiresAt) : null,
|
|
1012
|
+
...credits.expiredHoldBacking !== void 0 ? { expiredHoldBacking: credits.expiredHoldBacking } : {},
|
|
1013
|
+
...credits.signupGrant !== void 0 ? { signupGrant: credits.signupGrant } : {},
|
|
930
1014
|
createdAt: toISOString(credits.createdAt),
|
|
931
1015
|
updatedAt: toISOString(credits.updatedAt)
|
|
932
1016
|
};
|
|
@@ -1158,11 +1242,36 @@ function toDate2(value) {
|
|
|
1158
1242
|
return /* @__PURE__ */ new Date();
|
|
1159
1243
|
}
|
|
1160
1244
|
function getNextMonthlyReset(from = /* @__PURE__ */ new Date()) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
next
|
|
1165
|
-
|
|
1245
|
+
return new Date(from.getFullYear(), from.getMonth() + 1, 1);
|
|
1246
|
+
}
|
|
1247
|
+
function validateBalanceUpdate(current, updates) {
|
|
1248
|
+
const next = (base, absolute, increment) => sumAmounts(absolute ?? base, increment ?? 0);
|
|
1249
|
+
const balance = next(current.balance, updates.balance, updates.balanceIncrement);
|
|
1250
|
+
const bonusCredits = next(
|
|
1251
|
+
current.bonusCredits ?? 0,
|
|
1252
|
+
updates.bonusCredits,
|
|
1253
|
+
updates.bonusCreditsIncrement
|
|
1254
|
+
);
|
|
1255
|
+
const reserved = next(current.reserved ?? 0, updates.reserved, updates.reservedIncrement);
|
|
1256
|
+
const errors = [];
|
|
1257
|
+
if (balance < 0) errors.push(`Balance cannot be negative: ${balance}`);
|
|
1258
|
+
const marked = updates.balance === void 0 ? current.expiredHoldBacking ?? 0 : 0;
|
|
1259
|
+
if ((updates.balanceIncrement ?? 0) < 0 && balance >= 0 && balance < marked) {
|
|
1260
|
+
errors.push(`Balance (${balance}) cannot go below expired hold backing (${marked})`);
|
|
1261
|
+
}
|
|
1262
|
+
if (bonusCredits < 0) errors.push(`BonusCredits cannot be negative: ${bonusCredits}`);
|
|
1263
|
+
if (reserved < 0) errors.push(`Reserved cannot be negative: ${reserved}`);
|
|
1264
|
+
const covering = sumAmounts(balance, bonusCredits);
|
|
1265
|
+
if (reserved > covering) {
|
|
1266
|
+
errors.push(`Reserved (${reserved}) exceeds balance + bonusCredits (${covering})`);
|
|
1267
|
+
}
|
|
1268
|
+
return { valid: errors.length === 0, errors };
|
|
1269
|
+
}
|
|
1270
|
+
function assertValidBalanceUpdate(current, updates) {
|
|
1271
|
+
const result = validateBalanceUpdate(current, updates);
|
|
1272
|
+
if (!result.valid) {
|
|
1273
|
+
throw new Error(`Invalid balance update: ${result.errors.join("; ")}`);
|
|
1274
|
+
}
|
|
1166
1275
|
}
|
|
1167
1276
|
|
|
1168
1277
|
// src/repository/memory/snapshot.ts
|
|
@@ -1342,6 +1451,22 @@ function assertTrustworthy(reservation, transition) {
|
|
|
1342
1451
|
assertLockedAmount(reservation, transition);
|
|
1343
1452
|
assertHoldPlaced(reservation, transition);
|
|
1344
1453
|
}
|
|
1454
|
+
function openHolds(store, userId) {
|
|
1455
|
+
return [...store.reservations.get(userId)?.values() ?? []].filter(
|
|
1456
|
+
(reservation) => reservation.status === "reserved"
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
function writeExpiredHoldBacking(store, credits, next) {
|
|
1460
|
+
const stored = credits.expiredHoldBacking;
|
|
1461
|
+
if (next <= 0 && !stored) return;
|
|
1462
|
+
const holds = openHolds(store, credits.userId);
|
|
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;
|
|
1467
|
+
}
|
|
1468
|
+
Object.assign(credits, expiredHoldBackingPatch(sumAmounts(...shares.values()), stored));
|
|
1469
|
+
}
|
|
1345
1470
|
function getReservation(store, userId, reservationId) {
|
|
1346
1471
|
return store.reservations.get(userId)?.get(reservationId);
|
|
1347
1472
|
}
|
|
@@ -1439,16 +1564,22 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1439
1564
|
const amount = reservation.amount;
|
|
1440
1565
|
await yieldPoint(store);
|
|
1441
1566
|
assertReservedCoversHold(credits, userId, reservationId, amount);
|
|
1442
|
-
|
|
1567
|
+
const paid = planHoldCommit(
|
|
1568
|
+
credits.balance,
|
|
1569
|
+
credits.bonusCredits,
|
|
1570
|
+
credits.expiredHoldBacking ?? 0,
|
|
1571
|
+
reservation.expiredBacking ?? 0,
|
|
1572
|
+
amount
|
|
1573
|
+
);
|
|
1574
|
+
if (!paid.covered) {
|
|
1443
1575
|
throw new CreditError(
|
|
1444
1576
|
`Insufficient credits to commit reservation ${reservationId}`,
|
|
1445
1577
|
CreditErrorCode.INSUFFICIENT_CREDITS,
|
|
1446
1578
|
{ userId, reservationId, required: amount }
|
|
1447
1579
|
);
|
|
1448
1580
|
}
|
|
1449
|
-
const
|
|
1450
|
-
const
|
|
1451
|
-
const nextBonusCredits = sumAmounts(credits.bonusCredits, -sumAmounts(amount, -fromBalance));
|
|
1581
|
+
const nextBalance = paid.balance;
|
|
1582
|
+
const nextBonusCredits = paid.bonusCredits;
|
|
1452
1583
|
const balanceAfter = sumAmounts(nextBalance, nextBonusCredits);
|
|
1453
1584
|
const journal = {
|
|
1454
1585
|
userId,
|
|
@@ -1482,6 +1613,10 @@ async function commitReservationV2(store, userId, reservationId, options) {
|
|
|
1482
1613
|
credits.bonusCredits = nextBonusCredits;
|
|
1483
1614
|
credits.reserved = nextReserved;
|
|
1484
1615
|
credits.monthlyUsed = nextMonthlyUsed;
|
|
1616
|
+
Object.assign(
|
|
1617
|
+
credits,
|
|
1618
|
+
expiredHoldBackingPatch(paid.expiredHoldBacking, credits.expiredHoldBacking)
|
|
1619
|
+
);
|
|
1485
1620
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1486
1621
|
const journalEntryId = commitTransitionJournal(store, journal, existingEntryId);
|
|
1487
1622
|
return {
|
|
@@ -1561,19 +1696,26 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1561
1696
|
reservation.id,
|
|
1562
1697
|
reservation.amount
|
|
1563
1698
|
);
|
|
1699
|
+
const backing = settleExpiredHoldBacking(
|
|
1700
|
+
credits.balance,
|
|
1701
|
+
credits.expiredHoldBacking ?? 0,
|
|
1702
|
+
reservation.expiredBacking ?? 0
|
|
1703
|
+
);
|
|
1704
|
+
const nextBalance = sumAmounts(credits.balance, -backing.used);
|
|
1564
1705
|
const journal = {
|
|
1565
1706
|
userId: reservation.userId,
|
|
1566
1707
|
operation: `${transition.transition}Reservation`,
|
|
1567
1708
|
entryType: "credit",
|
|
1568
1709
|
amount: 0,
|
|
1569
|
-
balanceAfter: sumAmounts(
|
|
1710
|
+
balanceAfter: sumAmounts(nextBalance, credits.bonusCredits),
|
|
1570
1711
|
source: transition.source,
|
|
1571
1712
|
reservationId: reservation.id,
|
|
1572
1713
|
description: transition.options?.description ?? `${transition.verb} ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
|
|
1573
1714
|
metadata: {
|
|
1574
1715
|
...transition.options?.metadata,
|
|
1575
1716
|
operationType: reservation.operationType,
|
|
1576
|
-
amount: reservation.amount
|
|
1717
|
+
amount: reservation.amount,
|
|
1718
|
+
...backing.used > 0 ? { expiredCreditsRemoved: backing.used } : {}
|
|
1577
1719
|
},
|
|
1578
1720
|
idempotencyKey: reservationJournalKey(reservation.id, transition.transition)
|
|
1579
1721
|
};
|
|
@@ -1586,6 +1728,8 @@ function finishUnspent(store, reservation, transition) {
|
|
|
1586
1728
|
reservation.status = transition.status;
|
|
1587
1729
|
reservation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1588
1730
|
credits.reserved = nextReserved;
|
|
1731
|
+
credits.balance = nextBalance;
|
|
1732
|
+
Object.assign(credits, expiredHoldBackingPatch(backing.remaining, credits.expiredHoldBacking));
|
|
1589
1733
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1590
1734
|
return commitTransitionJournal(store, journal, existingEntryId);
|
|
1591
1735
|
}
|
|
@@ -1596,6 +1740,10 @@ function isoOf(value) {
|
|
|
1596
1740
|
}
|
|
1597
1741
|
var InMemoryCreditRepository = class {
|
|
1598
1742
|
store = new MemoryStore();
|
|
1743
|
+
options;
|
|
1744
|
+
constructor(options = {}) {
|
|
1745
|
+
this.options = options;
|
|
1746
|
+
}
|
|
1599
1747
|
// ==================== User Credits ====================
|
|
1600
1748
|
/**
|
|
1601
1749
|
* A copy, never the live record.
|
|
@@ -1612,7 +1760,8 @@ var InMemoryCreditRepository = class {
|
|
|
1612
1760
|
return copyRecord(credits) ?? null;
|
|
1613
1761
|
}
|
|
1614
1762
|
async initializeUserCredits(userId, tier, initialBalance) {
|
|
1615
|
-
|
|
1763
|
+
const existing = this.store.users.get(userId);
|
|
1764
|
+
return copyRecord(existing ?? this.seedUser(userId, tier, initialBalance));
|
|
1616
1765
|
}
|
|
1617
1766
|
/**
|
|
1618
1767
|
* The synchronous body of `initializeUserCredits`.
|
|
@@ -1663,8 +1812,11 @@ var InMemoryCreditRepository = class {
|
|
|
1663
1812
|
if (!credits) {
|
|
1664
1813
|
throw new Error(`User ${userId} not found`);
|
|
1665
1814
|
}
|
|
1815
|
+
assertValidBalanceUpdate(credits, updates);
|
|
1666
1816
|
const next = { ...credits };
|
|
1667
|
-
if (updates.balance !== void 0)
|
|
1817
|
+
if (updates.balance !== void 0) {
|
|
1818
|
+
next.balance = updates.balance;
|
|
1819
|
+
}
|
|
1668
1820
|
if (updates.bonusCredits !== void 0) next.bonusCredits = updates.bonusCredits;
|
|
1669
1821
|
if (updates.reserved !== void 0) next.reserved = updates.reserved;
|
|
1670
1822
|
if (updates.tier !== void 0) next.tier = updates.tier;
|
|
@@ -1701,6 +1853,9 @@ var InMemoryCreditRepository = class {
|
|
|
1701
1853
|
userId,
|
|
1702
1854
|
operation: "updateUserCredits"
|
|
1703
1855
|
});
|
|
1856
|
+
if ((credits.expiredHoldBacking ?? 0) > 0 && updates.balance !== void 0) {
|
|
1857
|
+
writeExpiredHoldBacking(this.store, next, 0);
|
|
1858
|
+
}
|
|
1704
1859
|
next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1705
1860
|
this.store.users.set(userId, next);
|
|
1706
1861
|
}
|
|
@@ -1709,17 +1864,28 @@ var InMemoryCreditRepository = class {
|
|
|
1709
1864
|
{ monthlyLimit: input.monthlyLimit, balance: input.balance, monthlyUsed: input.monthlyUsed },
|
|
1710
1865
|
{ userId, operation: "updateUserTier" }
|
|
1711
1866
|
);
|
|
1867
|
+
assertValidTierBalanceInput(input, { userId, operation: "updateUserTier" });
|
|
1712
1868
|
const credits = this.store.users.get(userId);
|
|
1713
1869
|
if (!credits) {
|
|
1714
1870
|
throw new Error(`User ${userId} not found`);
|
|
1715
1871
|
}
|
|
1872
|
+
if (input.balanceCap !== void 0 && !isFreeTier(credits.tier)) {
|
|
1873
|
+
const capped = planBalanceCap(
|
|
1874
|
+
credits.balance,
|
|
1875
|
+
input.balanceCap,
|
|
1876
|
+
credits.reserved,
|
|
1877
|
+
credits.bonusCredits,
|
|
1878
|
+
credits.expiredHoldBacking
|
|
1879
|
+
);
|
|
1880
|
+
credits.balance = capped.balance;
|
|
1881
|
+
writeExpiredHoldBacking(this.store, credits, capped.expiredHoldBacking);
|
|
1882
|
+
}
|
|
1716
1883
|
credits.tier = input.tier;
|
|
1717
1884
|
credits.monthlyLimit = input.monthlyLimit;
|
|
1718
1885
|
if (input.balance !== void 0) {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
);
|
|
1886
|
+
const planned = planBalanceTarget(input.balance, credits.reserved, credits.bonusCredits);
|
|
1887
|
+
credits.balance = planned.balance;
|
|
1888
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
1723
1889
|
}
|
|
1724
1890
|
if (input.monthlyUsed !== void 0) credits.monthlyUsed = input.monthlyUsed;
|
|
1725
1891
|
if (input.subscriptionExpiresAt !== void 0) {
|
|
@@ -1940,13 +2106,17 @@ var InMemoryCreditRepository = class {
|
|
|
1940
2106
|
if (!credits) {
|
|
1941
2107
|
throw new Error(`User credits not found for userId: ${userId}`);
|
|
1942
2108
|
}
|
|
1943
|
-
const
|
|
2109
|
+
const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
|
|
2110
|
+
const available = Math.min(
|
|
2111
|
+
sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved),
|
|
2112
|
+
sumAmounts(spendable, credits.bonusCredits)
|
|
2113
|
+
);
|
|
1944
2114
|
if (available < amount) {
|
|
1945
2115
|
throw new Error(
|
|
1946
2116
|
`Insufficient credits. Available: ${available}, requested: ${amount}`
|
|
1947
2117
|
);
|
|
1948
2118
|
}
|
|
1949
|
-
const balanceDeduction = Math.min(
|
|
2119
|
+
const balanceDeduction = Math.min(spendable, amount);
|
|
1950
2120
|
const bonusDeduction = sumAmounts(amount, -balanceDeduction);
|
|
1951
2121
|
const previousBalance = sumAmounts(credits.balance, credits.bonusCredits);
|
|
1952
2122
|
const nextBalance = sumAmounts(credits.balance, -balanceDeduction);
|
|
@@ -2183,7 +2353,11 @@ var InMemoryCreditRepository = class {
|
|
|
2183
2353
|
return { expiredCount, creditsReleased, errors };
|
|
2184
2354
|
}
|
|
2185
2355
|
// ==================== Atomic Monthly Reset ====================
|
|
2186
|
-
|
|
2356
|
+
/**
|
|
2357
|
+
* @param _tier - Not trusted. The caller read it earlier and the account may
|
|
2358
|
+
* have changed tier since, so the reset uses the tier stored on the account.
|
|
2359
|
+
*/
|
|
2360
|
+
async atomicMonthlyReset(userId, _tier, expectedResetAt) {
|
|
2187
2361
|
const credits = this.store.users.get(userId);
|
|
2188
2362
|
if (!credits) {
|
|
2189
2363
|
throw new Error(`User ${userId} not found`);
|
|
@@ -2193,17 +2367,41 @@ var InMemoryCreditRepository = class {
|
|
|
2193
2367
|
if (currentResetAt !== expected) {
|
|
2194
2368
|
return { wasReset: false, credits: { ...credits } };
|
|
2195
2369
|
}
|
|
2196
|
-
|
|
2370
|
+
const tier = credits.tier;
|
|
2371
|
+
const spendable = spendableBalance(credits.balance, credits.expiredHoldBacking);
|
|
2372
|
+
const decision = this.options.resolveMonthlyReset?.({ tier, balance: spendable });
|
|
2373
|
+
if (decision === null) {
|
|
2374
|
+
return { wasReset: false, credits: { ...credits } };
|
|
2375
|
+
}
|
|
2376
|
+
if (decision) {
|
|
2377
|
+
assertValidBalanceTarget(decision.balance, "resolveMonthlyReset balance", { userId, tier });
|
|
2378
|
+
if (decision.monthlyLimit !== void 0) {
|
|
2379
|
+
assertValidBalanceTarget(decision.monthlyLimit, "resolveMonthlyReset monthlyLimit", {
|
|
2380
|
+
userId,
|
|
2381
|
+
tier
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
} else {
|
|
2385
|
+
assertRepresentableTierAmount(getConfigMonthlyLimit(tier), "monthlyLimit", { userId, tier });
|
|
2386
|
+
}
|
|
2197
2387
|
const nextReset = getNextMonthlyReset();
|
|
2198
|
-
const target = monthlyResetBalance(tier);
|
|
2199
|
-
const floor = backedBalanceFloor(credits.reserved, credits.bonusCredits);
|
|
2200
2388
|
const previousBalance = credits.balance;
|
|
2201
|
-
|
|
2389
|
+
let target;
|
|
2390
|
+
if (decision) {
|
|
2391
|
+
target = decision.balance;
|
|
2392
|
+
} else {
|
|
2393
|
+
const configured = monthlyResetBalance(tier);
|
|
2394
|
+
target = configured.kind === "atLeast" ? Math.max(spendable, configured.value) : configured.value;
|
|
2395
|
+
}
|
|
2396
|
+
const planned = planBalanceTarget(target, credits.reserved, credits.bonusCredits);
|
|
2397
|
+
const newBalance = planned.balance;
|
|
2202
2398
|
const change = sumAmounts(newBalance, -previousBalance);
|
|
2203
2399
|
const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
|
|
2204
2400
|
assertRepresentableAmount(Math.abs(change), "journal amount", { userId });
|
|
2205
2401
|
assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
|
|
2206
2402
|
credits.balance = newBalance;
|
|
2403
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
2404
|
+
if (decision?.monthlyLimit !== void 0) credits.monthlyLimit = decision.monthlyLimit;
|
|
2207
2405
|
credits.monthlyUsed = 0;
|
|
2208
2406
|
credits.monthlyResetAt = nextReset.toISOString();
|
|
2209
2407
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2257,23 +2455,34 @@ var InMemoryCreditRepository = class {
|
|
|
2257
2455
|
};
|
|
2258
2456
|
}
|
|
2259
2457
|
const defaultTier = getDefaultTier();
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
tier: defaultTier
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2458
|
+
let limit;
|
|
2459
|
+
if (this.options.getDowngradeBalanceCap) {
|
|
2460
|
+
limit = this.options.getDowngradeBalanceCap();
|
|
2461
|
+
assertValidBalanceTarget(limit, "getDowngradeBalanceCap", { userId, tier: defaultTier });
|
|
2462
|
+
} else {
|
|
2463
|
+
const configured = getConfigMonthlyLimit(defaultTier);
|
|
2464
|
+
assertRepresentableTierAmount(configured, "monthlyLimit", {
|
|
2465
|
+
userId,
|
|
2466
|
+
tier: defaultTier
|
|
2467
|
+
});
|
|
2468
|
+
limit = storedMonthlyLimit(configured);
|
|
2469
|
+
}
|
|
2266
2470
|
const previousTier = credits.tier;
|
|
2267
2471
|
const previousBalance = credits.balance;
|
|
2268
|
-
const
|
|
2269
|
-
|
|
2270
|
-
|
|
2472
|
+
const planned = planBalanceCap(
|
|
2473
|
+
credits.balance,
|
|
2474
|
+
limit,
|
|
2475
|
+
credits.reserved,
|
|
2476
|
+
credits.bonusCredits,
|
|
2477
|
+
credits.expiredHoldBacking
|
|
2271
2478
|
);
|
|
2479
|
+
const newBalance = planned.balance;
|
|
2272
2480
|
const balanceAfter = sumAmounts(newBalance, credits.bonusCredits);
|
|
2273
2481
|
assertRepresentableAmount(balanceAfter, "journal balanceAfter", { userId });
|
|
2274
2482
|
credits.tier = defaultTier;
|
|
2275
2483
|
credits.monthlyLimit = limit;
|
|
2276
2484
|
credits.balance = newBalance;
|
|
2485
|
+
writeExpiredHoldBacking(this.store, credits, planned.expiredHoldBacking);
|
|
2277
2486
|
credits.subscriptionExpiresAt = null;
|
|
2278
2487
|
credits.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2279
2488
|
this.store.users.set(userId, credits);
|
|
@@ -2331,8 +2540,8 @@ var InMemoryCreditRepository = class {
|
|
|
2331
2540
|
return copyRecords(Array.from(userReservations.values()));
|
|
2332
2541
|
}
|
|
2333
2542
|
};
|
|
2334
|
-
function createInMemoryCreditRepository() {
|
|
2335
|
-
return new InMemoryCreditRepository();
|
|
2543
|
+
function createInMemoryCreditRepository(options = {}) {
|
|
2544
|
+
return new InMemoryCreditRepository(options);
|
|
2336
2545
|
}
|
|
2337
2546
|
|
|
2338
2547
|
// src/auth/api-key.ts
|
|
@@ -2495,9 +2704,11 @@ var CreditsService = class {
|
|
|
2495
2704
|
source: "monthly_reset",
|
|
2496
2705
|
referenceId: `reset-${Date.now()}`,
|
|
2497
2706
|
referenceType: "reset",
|
|
2498
|
-
|
|
2707
|
+
// The tier the reset ran on, which a concurrent downgrade may have
|
|
2708
|
+
// changed since `data` was read.
|
|
2709
|
+
description: `Monthly credit reset for ${resetResult.credits.tier} tier.`,
|
|
2499
2710
|
metadata: {
|
|
2500
|
-
tier:
|
|
2711
|
+
tier: resetResult.credits.tier,
|
|
2501
2712
|
previousBalance: data.balance,
|
|
2502
2713
|
newBalance: resetResult.credits.balance
|
|
2503
2714
|
}
|
|
@@ -2936,9 +3147,11 @@ function createGenericAdapter(config) {
|
|
|
2936
3147
|
return handler(user, data, dummyReservation);
|
|
2937
3148
|
}
|
|
2938
3149
|
let reservation;
|
|
3150
|
+
let isFree = false;
|
|
2939
3151
|
try {
|
|
2940
3152
|
const cost = options.customCost ?? getOperationCost2(options.operationType);
|
|
2941
|
-
|
|
3153
|
+
isFree = options.customCost === void 0 && cost === 0;
|
|
3154
|
+
reservation = isFree ? { ...createDummyReservation(user.id, options.operationType), id: "free-operation" } : await reserveCreditsForOperation(
|
|
2942
3155
|
repository,
|
|
2943
3156
|
user.id,
|
|
2944
3157
|
cost,
|
|
@@ -2961,7 +3174,7 @@ function createGenericAdapter(config) {
|
|
|
2961
3174
|
try {
|
|
2962
3175
|
const result = await handler(user, data, reservation);
|
|
2963
3176
|
if (result.success) {
|
|
2964
|
-
await commitReservationWithJournal(repository, user.id, reservation.id);
|
|
3177
|
+
if (!isFree) await commitReservationWithJournal(repository, user.id, reservation.id);
|
|
2965
3178
|
logUsage({
|
|
2966
3179
|
userId: user.id,
|
|
2967
3180
|
operationType: options.operationType,
|
|
@@ -2972,7 +3185,7 @@ function createGenericAdapter(config) {
|
|
|
2972
3185
|
requestId
|
|
2973
3186
|
});
|
|
2974
3187
|
} else {
|
|
2975
|
-
await releaseReservationWithJournal(repository, user.id, reservation.id);
|
|
3188
|
+
if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);
|
|
2976
3189
|
logUsage({
|
|
2977
3190
|
userId: user.id,
|
|
2978
3191
|
operationType: options.operationType,
|
|
@@ -2987,7 +3200,7 @@ function createGenericAdapter(config) {
|
|
|
2987
3200
|
return result;
|
|
2988
3201
|
} catch (error) {
|
|
2989
3202
|
const message = error instanceof Error ? error.message : "An unexpected error occurred";
|
|
2990
|
-
await releaseReservationWithJournal(repository, user.id, reservation.id);
|
|
3203
|
+
if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);
|
|
2991
3204
|
logUsage({
|
|
2992
3205
|
userId: user.id,
|
|
2993
3206
|
operationType: options.operationType,
|
|
@@ -3781,12 +3994,17 @@ export {
|
|
|
3781
3994
|
assertRepresentableTierAmount,
|
|
3782
3995
|
assertUnkeyedDirectReservation,
|
|
3783
3996
|
assertUnreferencedDirectTransaction,
|
|
3997
|
+
assertValidBalanceTarget,
|
|
3998
|
+
assertValidBalanceUpdate,
|
|
3784
3999
|
assertValidCreditAmount,
|
|
3785
4000
|
assertValidIdempotencyKey,
|
|
3786
4001
|
assertValidStoredAmount,
|
|
3787
4002
|
assertValidStoredAmountRaw,
|
|
4003
|
+
assertValidTierBalanceInput,
|
|
4004
|
+
attributeExpiredBacking,
|
|
3788
4005
|
backedBalanceFloor,
|
|
3789
4006
|
calculateAvailableCredits,
|
|
4007
|
+
cappedBalance,
|
|
3790
4008
|
checkAndNotifyLowBalance,
|
|
3791
4009
|
classifyDatabaseError,
|
|
3792
4010
|
clearConfigProvider,
|
|
@@ -3812,6 +4030,7 @@ export {
|
|
|
3812
4030
|
createUnsupportedOperationError,
|
|
3813
4031
|
createUserNotFoundError,
|
|
3814
4032
|
describePaymentMismatch,
|
|
4033
|
+
expiredHoldBackingPatch,
|
|
3815
4034
|
generateId,
|
|
3816
4035
|
generateRequestId2 as generateRequestId,
|
|
3817
4036
|
genericDeferred,
|
|
@@ -3870,6 +4089,9 @@ export {
|
|
|
3870
4089
|
parseApiError,
|
|
3871
4090
|
parseCreditError,
|
|
3872
4091
|
parseTier,
|
|
4092
|
+
planBalanceCap,
|
|
4093
|
+
planBalanceTarget,
|
|
4094
|
+
planHoldCommit,
|
|
3873
4095
|
registerConfigProvider,
|
|
3874
4096
|
registerNotificationHandler,
|
|
3875
4097
|
releaseReservationWithJournal,
|
|
@@ -3879,6 +4101,8 @@ export {
|
|
|
3879
4101
|
reserveThroughRepository,
|
|
3880
4102
|
resetConfig,
|
|
3881
4103
|
sameAmount,
|
|
4104
|
+
settleExpiredHoldBacking,
|
|
4105
|
+
spendableBalance,
|
|
3882
4106
|
storedMonthlyLimit,
|
|
3883
4107
|
sumAmounts,
|
|
3884
4108
|
supportsCreditsV2,
|
|
@@ -3890,6 +4114,7 @@ export {
|
|
|
3890
4114
|
toClientUserCredits,
|
|
3891
4115
|
toDate,
|
|
3892
4116
|
toPortableTimestamp,
|
|
4117
|
+
validateBalanceUpdate,
|
|
3893
4118
|
verifyAdminApiKey,
|
|
3894
4119
|
verifyBearerToken
|
|
3895
4120
|
};
|