You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
What — Users on a plan without a weekly quota (a one-shot credit grant) get an email when their credits run low and when they run out. Why — Today those users get no warning at all: their runs simply stop when the balance hits zero.
Scope
Root cause: modules/billing/services/billing.usage.service.js:236 wraps the whole 80 %/100 % alert block in if (effectiveQuota > 0). A plan with meterQuota: 0 sends every unit to extras (168-169), debited from BillingExtraBalance.cachedBalance; the real limit is that balance, and no alert ever fires. The reference config ships this exact shape (billing.development.config.js:86: meterQuota: 0, signupGrant: 500, oneShot: true).
Fix: in incrementMeter, inside the existing extrasConsumed > 0 branch that already reads debitResult.doc.cachedBalance (195-221), stateless crossing detection: pre = cachedBalance + extrasConsumed, post = cachedBalance; for each threshold in config.billing.alerts.thresholdPercents (default [80,100]), fire when remaining credits cross (1 - T/100) * plan.signupGrant between pre and post. Atomic debits make exactly one caller cross each threshold.
Do NOT reuse alertedAt80/alertedAt100 (weekly usage doc, would re-fire every week against a lifetime balance), and do NOT reuse billing-quota-*.html (they say "weekly quota"). Add a new event (e.g. billing.extras.balance_threshold_crossed) and new templates (e.g. billing-credit-warning.html, billing-credit-exhausted.html), wired in billing.email.js.
Limitation — using signupGrant as the denominator only fits the one-shot grant; once a pack purchase tops up the same balance, "% of grant" is ambiguous. Scope to the signup-grant case and document it.
Tests — billing.usage.service.unit.tests.js (crossing on a meterQuota: 0 plan), billing.extra.service.unit.tests.js, new listener case in billing.init.email-alerts.unit.tests.js.
Explicit guard: run the check only when effectiveQuota === 0 && Number.isFinite(plan.signupGrant) && plan.signupGrant > 0 (the extrasConsumed > 0 branch also runs for quota plans that overflow).
Crossing rule: level L = (1 - T/100) * signupGrant, fire when pre > L && post <= L. Thresholds from billing.alerts.thresholdPercents, filtered to 80/100 (as billing.email.js:65 does).
One debit crossing both levels → emit only the deepest (one email).
Emit inside its own try/catch, like the runaway block (208-219).
Copy speaks in absolute credits left ("N credits left" / "you are out of credits") + link to plans/packs — never "% of grant" (wrong once packs or referral grants top up the balance). No org name.
A credit (pack, referral) that pushes the balance back above a level means the next crossing alerts again — intended, test it.
Accepted limitation (document in README): balance drops from pack expiry or refunds don't alert.
Tests: drop billing.extra.service.unit.tests.js (service unchanged). Add: boundary post === L; two adjacent concurrent debits → exactly one crosser; double crossing → one event; re-cross after credit; quota>0 overflow → no event; quota-0 plan without grant → no event.
What — Users on a plan without a weekly quota (a one-shot credit grant) get an email when their credits run low and when they run out.
Why — Today those users get no warning at all: their runs simply stop when the balance hits zero.
Scope
modules/billing/services/billing.usage.service.js:236wraps the whole 80 %/100 % alert block inif (effectiveQuota > 0). A plan withmeterQuota: 0sends every unit to extras (168-169), debited fromBillingExtraBalance.cachedBalance; the real limit is that balance, and no alert ever fires. The reference config ships this exact shape (billing.development.config.js:86:meterQuota: 0, signupGrant: 500, oneShot: true).incrementMeter, inside the existingextrasConsumed > 0branch that already readsdebitResult.doc.cachedBalance(195-221), stateless crossing detection:pre = cachedBalance + extrasConsumed,post = cachedBalance; for each threshold inconfig.billing.alerts.thresholdPercents(default[80,100]), fire when remaining credits cross(1 - T/100) * plan.signupGrantbetween pre and post. Atomic debits make exactly one caller cross each threshold.alertedAt80/alertedAt100(weekly usage doc, would re-fire every week against a lifetime balance), and do NOT reusebilling-quota-*.html(they say "weekly quota"). Add a new event (e.g.billing.extras.balance_threshold_crossed) and new templates (e.g.billing-credit-warning.html,billing-credit-exhausted.html), wired inbilling.email.js.Limitation — using
signupGrantas the denominator only fits the one-shot grant; once a pack purchase tops up the same balance, "% of grant" is ambiguous. Scope to the signup-grant case and document it.Tests —
billing.usage.service.unit.tests.js(crossing on ameterQuota: 0plan),billing.extra.service.unit.tests.js, new listener case inbilling.init.email-alerts.unit.tests.js.Refs: #3536 (meter/extras/threshold design)
Update (scope re-validated)
effectiveQuota === 0 && Number.isFinite(plan.signupGrant) && plan.signupGrant > 0(theextrasConsumed > 0branch also runs for quota plans that overflow).L = (1 - T/100) * signupGrant, fire whenpre > L && post <= L. Thresholds frombilling.alerts.thresholdPercents, filtered to 80/100 (asbilling.email.js:65does).{ organizationId, threshold, remaining: max(0, post), planId }.billing.extra.service.unit.tests.js(service unchanged). Add: boundarypost === L; two adjacent concurrent debits → exactly one crosser; double crossing → one event; re-cross after credit; quota>0 overflow → no event; quota-0 plan without grant → no event.Scope: validated 2026-09-25
Created via /dev:issue