Skip to content

feat(billing): proration, dunning, promo codes and metered pricing (#812 #813 #814 #815) - #992

Merged
github-actions[bot] merged 1 commit into
Smartdevs17:mainfrom
olathedev:feat/billing-proration-dunning-promos-metering
Sep 28, 2026
Merged

github-actions[bot] merged 1 commit into
Smartdevs17:mainfrom
olathedev:feat/billing-proration-dunning-promos-metering

Conversation

@olathedev

Copy link
Copy Markdown
Contributor

Summary

Closes #812
Closes #813
Closes #814
Closes #815

Builds on the subscription billing service from #914 and adds four billing capabilities. The pricing logic for each lives in a small pure module under backend/src/services/billing/. SubscriptionBillingService connects them to plans, subscriptions and invoices.

#815 — Usage-based billing with metered pricing

  • Plans can define meters, one metered price per metric.
  • Aggregation per meter: sum, max or last.
  • Pricing models: per_unit, package, graduated and volume. Tiers can have a flatFee, and the last tier must be open-ended.
  • Each meter has its own includedUnits.
  • Invoices get one line per meter, and GET /subscriptions/:id/usage returns a per-meter breakdown.
  • Usage for a metric the plan does not meter is rejected.

#812 — Proration for mid-cycle plan changes

  • POST /subscriptions/:id/change-plan and GET /subscriptions/:id/change-plan/preview.
  • Invoices are raised in arrears, so a change splits the period into plan segments and each plan is billed for its share.
  • The preview returns unusedCredit, remainingCharge and net.
  • Three behaviours:
    • create_prorations (default): the closing invoice carries prorated base lines for each plan.
    • always_invoice: the old plan's elapsed time and its usage are invoiced immediately.
    • none: the whole period is billed on the new plan.
  • A billing interval change settles the current period and starts a new one. none is rejected for interval changes.
  • Trials switch plan without proration. Currency changes and changes on cancelled subscriptions are rejected.

#814 — Promotional codes and discounts

  • CRUD, validate and deactivate endpoints under /promo-codes.
  • Codes can be redeemed at subscribe time (promoCode) or later via POST/DELETE /subscriptions/:id/discount.
  • Discount types: percent or fixed (currency must match the plan).
  • Durations: once, repeating for N periods, or forever.
  • Restrictions: merchant, plan list, validity window, total redemptions, per-customer limit and minimum plan price.
  • The discount appears as a negative invoice line and is capped at the subtotal. Invoices now expose subtotal, discountAmount and discountCode.

#813 — Dunning management with configurable retry schedules

  • GET/PUT /dunning/config/:merchantId. The default schedule is retries at days [1, 3, 5, 7]; it is validated to be ascending, at most 10 retries and within 60 days.
  • POST /invoices/:id/payment-attempts records an attempt. The first failure starts dunning and moves the subscription to past_due.
  • POST /dunning/process retries due invoices through a pluggable payment attempter (setPaymentAttempter). An attempter that throws counts as a failed attempt.
  • When dunning starts, the schedule is copied onto the invoice, so later config edits don't affect it. Every attempt is recorded in dunning.history.
  • Outcomes:
    • Recovery (a successful retry or a manual payment) sets dunning to recovered.
    • Voiding the invoice sets dunning to stopped.
    • If every retry fails, the invoice becomes uncollectible and the final action runs: cancel_subscription (default) or mark_uncollectible.
    • Once no invoice for the subscription is in dunning, the subscription returns to active.

Behaviour changes to existing code

  • past_due subscriptions can still record usage, since service continues while dunning runs. Cancelled subscriptions are still rejected.
  • InvoiceStatus gains uncollectible. Such an invoice can still be paid.
  • Invoices gain kind, subtotal, discountAmount, discountCode and dunning. Existing fields and line items are unchanged for plans without these features.

Tests

  • backend/src/services/billing/__tests__/{metered-pricing,proration,discounts,dunning}.test.ts: unit tests for the pricing logic, covering success and failure cases.
  • backend/src/services/__tests__/subscription-billing-lifecycle.test.ts: tests of the full flows through the service for all four features, covering success and failure cases.
  • The existing subscription-billing.test.ts is unchanged.
cd backend && npx vitest run src/services/__tests__/subscription-billing src/services/billing

Docs

docs/SUBSCRIPTION_USAGE_BILLING.md now documents all new endpoints and behaviour.

🤖 Generated with Claude Code

- Prorate mid-cycle plan changes with create_prorations, always_invoice
  and none behaviours, plus a change preview (Smartdevs17#812)
- Dunning with merchant-configurable retry schedules, payment attempts,
  automated retry processing and final actions (Smartdevs17#813)
- Promo codes with percent/fixed discounts, once/repeating/forever
  durations and redemption limits (Smartdevs17#814)
- Per-metric metered pricing with sum/max/last aggregation and per-unit,
  package, graduated and volume models (Smartdevs17#815)

Closes Smartdevs17#812, closes Smartdevs17#813, closes Smartdevs17#814, closes Smartdevs17#815
@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

@olathedev is attempting to deploy a commit to the smartdevs17's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Sep 28, 2026

Copy link
Copy Markdown

@olathedev Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@github-actions
github-actions Bot merged commit f369a10 into Smartdevs17:main Sep 28, 2026
14 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant