fix: double counting purchased credit usage (#3359)
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled

This commit is contained in:
Daniel Salazar
2026-07-07 21:43:09 -07:00
committed by GitHub
parent 722a6cf50f
commit 97ec5d115c
2 changed files with 51 additions and 4 deletions
@@ -787,6 +787,45 @@ describe('MeteringService', () => {
expect(await target.hasAnyUsage(actor)).toBe(false);
});
it('does not double-charge same-month overage against remaining (usage total + consumed credits)', async () => {
const sub = await target.getActorSubscription(actor);
await target.updateAddonCredit(actor.user.uuid, 5_000_000);
// Exhaust the allowance, then overspend by 1_000_000 — the overage
// is consumed from purchased credits.
await target.incrementUsage(
actor,
'kv:read',
1,
sub.monthUsageAllowance,
);
await target.incrementUsage(actor, 'kv:read', 1, 1_000_000);
await waitFor(async () => {
const addons = await target.getActorAddons(actor);
expect(addons.consumedPurchaseCredits).toBe(1_000_000);
});
// The overage already lives in both this month's usage total and
// consumedPurchaseCredits; remaining must only be reduced once.
const allowed = await target.getAllowedUsage(actor);
expect(allowed.remaining).toBe(4_000_000);
});
it('counts consumed credits from prior months against the credit pool only', async () => {
// Simulate a prior-month overage: consumed credits exist but the
// current month has no usage (monthly usage keys roll over).
await target.updateAddonCredit(actor.user.uuid, 5_000_000);
await server.stores.kv.incr({
key: `${POLICY_PREFIX}:actor:${actor.user.uuid}:addons`,
pathAndAmountMap: { consumedPurchaseCredits: 2_000_000 },
});
const allowed = await target.getAllowedUsage(actor);
expect(allowed.remaining).toBe(
allowed.monthUsageAllowance + 3_000_000,
);
});
it('hasEnoughCredits compares remaining against the requested amount', async () => {
await target.updateAddonCredit(actor.user.uuid, 1_000);
expect(await target.hasEnoughCredits(actor, 100)).toBe(true);
@@ -627,13 +627,21 @@ export class MeteringService extends PuterService {
],
);
const remaining = Math.max(
// Overage past the allowance is already charged to purchased credits
// via consumedPurchaseCredits, so the allowance and the credit pool
// must be netted separately — subtracting month usage AND consumed
// credits from one combined pool would charge the overage twice.
const remainingAllowance = Math.max(
0,
(userSubscription.monthUsageAllowance || 0) +
(addons?.purchasedCredits || 0) -
(currentMonthUsage.usage.total || 0) -
(userSubscription.monthUsageAllowance || 0) -
(currentMonthUsage.usage.total || 0),
);
const remainingPurchasedCredits = Math.max(
0,
(addons?.purchasedCredits || 0) -
(addons?.consumedPurchaseCredits || 0),
);
const remaining = remainingAllowance + remainingPurchasedCredits;
return {
remaining,