Skip to content

feat: subscription billing correctness (#1684) - #2

Merged
github-actions[bot] merged 1 commit into
feat/1683-crypto-payment-resiliencefrom
feat/1684-subscription-billing-correctness
Oct 22, 2026
Merged

github-actions[bot] merged 1 commit into
feat/1683-crypto-payment-resiliencefrom
feat/1684-subscription-billing-correctness

Conversation

@akansnoro

Copy link
Copy Markdown
Owner

Context

This is the second of four pull requests that re-land the contents of the squash-merged PR Smartdevs17#1703 one issue at a time.

Smartdevs17#1703 was merged as a single commit (5afe754) covering four unrelated issues. The companion pull request (the revert) undoes that squash so each issue can be merged on its own. This pull request restores only the part of Smartdevs17#1703 that belongs to:

  • #1684 — subscription billing correctness

What this changes

One place that computes invoice totals (src/utils/invoiceTotals.ts)

createInvoice and updateInvoice each rolled their own arithmetic and the two did not agree — createInvoice subtracted the discount, updateInvoice did not. Editing one line on a discounted invoice therefore silently increased the amount payable, and because the discount was never persisted on the record there was nothing left to subtract on the second pass.

calculateInvoiceTotals() is now the single source of truth:

  • every component is rounded to minor units (half-up, no binary float drift);
  • discount line items (kind: 'discount') are kept out of the subtotal and summed into discount instead, so an itemised discount is subtracted exactly once;
  • the discount is clamped to the subtotal. Without the clamp a discount larger than the invoice produced a negative tax base and a negative total, which downstream reconciliation reads as a credit owed rather than an over-discounted invoice.

Invoice service (src/services/invoiceService.ts)

Both the create and the update path go through the shared totals function, and the discount is persisted on the record so a second pass subtracts the same amount.

Billing cycle maths (src/utils/billingCycleMath.ts, new)

monthlyEquivalent, yearlyEquivalent, isAnnualCycle and normalizeBillingCycle replace the ad-hoc cycle conversions that were scattered across the store and the analytics service, which disagreed about what a weekly or yearly cycle was worth.

Revenue recognition (backend/billing/jobs/monthlyRevenueRecognitionJob.ts, backend/billing/jobs/index.ts)

The job now tracks cumulative per-entry recognition progress (recordEntryProgress, recognisedToDate). Previously each run re-recognised the whole elapsed portion of an entry, so recognised revenue was inflated on every run and the deferred balance grew without bound.

Metering and group billing (backend/services/billing/meteringService.ts, groupBilling.ts, interfaces.ts)

  • Metered billing aggregates over the requested window instead of the last N days from "now", so a period that ended long ago no longer picks up today's usage.
  • aggregateCharges(group, from, to) takes an explicit window.
  • Amounts are rounded to minor units on the way out.

Analytics (src/services/analyticsService.ts, src/store/slices/analyticsSlice.ts, src/store/subscriptionStore.ts, backend/services/analytics/subscriptionAnalyticsService.ts)

Monthly and yearly equivalents now come from the shared billing-cycle helpers, so the dashboard, the store and the backend report the same numbers.

Batch charges (backend/services/batchChargeService.ts)

Uses the shared rounding helper.

Test (src/services/__tests__/groupBilling.test.ts)

Updated for the explicit-window aggregateCharges signature.

Merge order

This branch is stacked on the #1683 pull request and must be merged after it. The remaining pull requests re-land #1686 and #1685 in that order.

The issues referenced above (#1683–#1686) have been deleted from the tracker, so they are referenced as plain text rather than with closing keywords.

Re-lands the part of the reverted Smartdevs17#1703 squash that belongs to issue
1684 only. The remainder of the squash is re-landed by the other
branches of the split, so this diff contains this issue's work exclusively.
@github-actions
github-actions Bot merged commit ba70476 into feat/1683-crypto-payment-resilience Oct 22, 2026
3 checks passed
@akansnoro

Copy link
Copy Markdown
Owner Author

Superseded: this pull request is being opened directly against \Smartdevs17/SubTrackr\ instead, one issue at a time, so that the whole split lives in the upstream tracker. The branch and its diff stay as they are; the upstream pull request carries the same diff.

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.

1 participant