Skip to content

feat(billing): credit-balance alerts for plans without a weekly quota - #4126

Merged
PierreBrisorgueil merged 6 commits into
masterfrom
feat/4117-credit-balance-alerts
Sep 25, 2026
Merged

PierreBrisorgueil merged 6 commits into
masterfrom
feat/4117-credit-balance-alerts

Conversation

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor

What

Users on a plan with no weekly quota (a one-shot signup credit grant) now get an email when their credits run low and when they run out.

Why

The 80 % / 100 % alerts only ran for plans with a weekly quota. On a meterQuota: 0 plan every unit is debited from the extras balance, and nothing warned the user before their usage stopped at zero.

How

  • In incrementMeter, inside the existing extras-debit branch, stateless crossing detection: post = the debit's own returned cachedBalance (atomic $inc, returnDocument: 'after'), pre = post + extrasConsumed. Level L = signupGrant * (100 - T) / 100 for T in billing.alerts.thresholdPercents filtered to 80/100; fires when pre > L && post <= L. Concurrent debits get disjoint (post, pre] intervals, so exactly one caller crosses each level.
  • Only when effectiveQuota === 0 and signupGrant is a positive finite number. Quota plans that overflow into extras are untouched.
  • One debit crossing both levels → only the deepest is emitted (one email).
  • New event billing.extras.balance_threshold_crossed { organizationId, threshold, remaining, planId }, emitted in its own try/catch. Listener in billing.email.js → new billing-credit-warning.html / billing-credit-exhausted.html. Copy states absolute credits left, never a % of the grant (wrong once packs or other grants top up the balance), and never the org name.
  • A credit that lifts the balance back above a level re-arms it (intended, tested).
  • Limitation (README): balance drops from pack expiry or refunds don't alert.
  • The level formula avoids (1 - T/100) * grant (float drift: 500 * 0.2 = 99.99… misses an exact boundary). Noted in ERRORS.md.

Tests

Unit: boundary post === L, adjacent concurrent debits → one crosser, double crossing → one event, re-cross after a credit, quota>0 overflow → none, quota-0 without grant → none, listener → both templates.
Integration (real DB): incrementMeter on a quota-0 plan crosses the low level once, a further debit above zero emits nothing.

Pre-push gate: kimi OK with nits (neutral copy + integration test applied; a concurrency concern was checked and rejected — each atomic debit returns its own post-state).

Closes #4117

https://claude.ai/code/session_01TTK9g6SFCfjfuWvB3MLFr3

A one-shot signup-grant plan (meterQuota=0) has no weekly usage doc to
alert against — every unit debits BillingExtraBalance.cachedBalance
directly, and that balance IS the real limit, so users ran out silently.

incrementMeter now detects extras-balance threshold crossings (80/100,
stateless — no alertedAtN dedup field, since a lifetime balance would
falsely dedup a weekly-scoped field) against the debit's own pre/post
balance and emits billing.extras.balance_threshold_crossed. billing.email.js
sends a credit-warning or credit-exhausted email off of it, in absolute
credits left, never a percentage.

Scoped to meterQuota=0 plans with a valid signupGrant only — once a pack
tops up the same balance, percent-of-grant is ambiguous (documented in
README.md, along with the pack-expiry/refund alerting gap).

Refs #3536

Claude-Session: https://claude.ai/code/session_01TTK9g6SFCfjfuWvB3MLFr3
README documented the level formula as (1 - threshold/100) * signupGrant —
the exact form the code comment (previous commit) warns against: it hits
float imprecision at common values (500 * (1 - 80/100) = 99.99999999999997,
not 100), silently missing an exact boundary crossing. Correct it to
signupGrant * (100 - threshold) / 100, and add the ERRORS.md entry.

Claude-Session: https://claude.ai/code/session_01TTK9g6SFCfjfuWvB3MLFr3
Rewrite billing-credit-warning/exhausted template copy to stack-neutral
"usage" wording (was "Runs are paused"/"Runs keep working"), matching the
billing-quota-* templates' register.

Add an integration test driving incrementMeter on a meterQuota=0,
signupGrant plan against the real test DB: a debit crossing the 80%
level emits billing.extras.balance_threshold_crossed once with the
expected {threshold, remaining}; a later debit that stays above zero
emits nothing new.

Claude-Session: https://claude.ai/code/session_01TTK9g6SFCfjfuWvB3MLFr3
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 23 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: pierreb-devkit/Node/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: e022b65b-7dc5-4bdb-946a-97023c07ff7d

📥 Commits

Reviewing files that changed from the base of the PR and between cc97e14 and b56cc68.

📒 Files selected for processing (10)
  • ERRORS.md
  • config/templates/billing-credit-exhausted.html
  • config/templates/billing-credit-warning.html
  • modules/billing/README.md
  • modules/billing/billing.email.js
  • modules/billing/lib/events.js
  • modules/billing/services/billing.usage.service.js
  • modules/billing/tests/billing.init.email-alerts.unit.tests.js
  • modules/billing/tests/billing.usage.creditAlert.integration.tests.js
  • modules/billing/tests/billing.usage.service.unit.tests.js
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.44%. Comparing base (cc97e14) to head (b56cc68).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4126      +/-   ##
==========================================
+ Coverage   94.42%   94.44%   +0.02%     
==========================================
  Files         173      173              
  Lines        6042     6072      +30     
  Branches     1945     1961      +16     
==========================================
+ Hits         5705     5735      +30     
  Misses        274      274              
  Partials       63       63              
Flag Coverage Δ
integration 63.83% <46.66%> (-0.07%) ⬇️
unit 79.59% <100.00%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update cc97e14...b56cc68. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Fallback /critical-review (Opus, CodeRabbit rate-limited) findings:
- the "no signupGrant" unit test used numbers that could never cross
  regardless of the guard (pre=0); reseed it so it WOULD cross without
  the guard, and add a signupGrant=0 case — the one that actually
  isolates the `> 0` half of the check (undefined already short-circuits
  via NaN arithmetic on its own). Verified both fail with the guard
  removed, then restored it.
- add a duplicate_step replay case: a replayed debit (applied=false)
  must not re-derive a crossing.
- drop the second, duplicated SUPPORTED_CREDIT_ALERT_THRESHOLDS set;
  filter with the existing thresholdFields map instead (single source).
- document the real accepted limitation on a debit that throws outright:
  the outbox/retry-cron this module's own docs describe was removed
  (see crons/README.md), so an unreconciled failed debit never reaches
  the crossing check either — not "reconciled by a retry cron" as
  originally suggested, since that cron no longer exists.
- drop the redundant "(0 credits left)" from the exhausted template.

Claude-Session: https://claude.ai/code/session_01TTK9g6SFCfjfuWvB3MLFr3
Codecov patch check flagged 2 uncovered added lines: the catch block
around billingEvents.emit('billing.extras.balance_threshold_crossed', ...)
had no test forcing the emit to throw (mirrors the existing coverage
of the sibling runaway_debit emit-failure path).

Claude-Session: https://claude.ai/code/session_01TTK9g6SFCfjfuWvB3MLFr3
The comment on the "without a signupGrant" test claimed the
missing-signupGrant guard alone kept it silent. Verified experimentally
(guard removed, test still passed) that undefined already short-circuits
via Number.isFinite/NaN arithmetic on its own — it's the signupGrant=0
case added alongside it that isolates the guard's `> 0` half. Comment
was misleading; fix it rather than ship a self-contradicting note.

Claude-Session: https://claude.ai/code/session_01TTK9g6SFCfjfuWvB3MLFr3
@PierreBrisorgueil
PierreBrisorgueil merged commit 36703d2 into master Sep 25, 2026
8 checks passed
@PierreBrisorgueil
PierreBrisorgueil deleted the feat/4117-credit-balance-alerts branch September 25, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Credit-balance alerts for plans without a weekly quota

1 participant