Skip to content

πŸ”§ Split the subscription-updated webhook handler into sub-handlersΒ #3996

Description

@PierreBrisorgueil

What β€” split the Stripe subscription-updated webhook handler into named sub-handlers.

Why β€” it is 161 lines at cyclomatic complexity 71, the heaviest function in the stack, with six concerns interleaved and coupled through a shared mutable flag. It is on the billing hot path, so it is also the function where a subtle mistake is most expensive.

Scope β€” modules/billing/services/billing.webhook.service.js:419-579 (verified on master by AST parse). Internal decomposition only; the module's exported surface does not change.

This issue deliberately contains no code sketch. Two review rounds found that pre-written sketches for this function dropped a fallback argument and a try/catch. Read the function and derive the split.

Invariants that must survive β€” verify each against the code before you finish:

  1. resolvePlan is called with three arguments: the subscription, the existing plan as a fallback, and a context object. On an unresolvable price it returns the last-known plan, only defaulting to free when there is none, and stamps context into an alert plus a plan-unresolved event. Calling it with the subscription alone downgrades a paying organization to the free plan on any price-map miss β€” a money-visible regression. The organization id must stay hoisted before that call, as it is today.
  2. The meter rotation runs inside the "previous plan differs from new plan" guard. Hoisting it out fires a rotation on every plain renewal.
  3. The rotation is wrapped in a try/catch whose catch body also clears the reset-triggered flag so the period-rollover fallback still runs. Preserve both the catch and the flag reset. An escaping throw here causes a webhook retry storm.
  4. When period and plan change in the same event, the period rollover must still run to archive the previous period.

Definition of done: existing webhook suites pass with unchanged counts, plus tests proving (a) a plain renewal does not trigger a plan-change rotation, and (b) a rotation failure still resolves the webhook and still runs the rollover.

Automerge: no
Scope: validated 2026-07-28
Created via /dev:issue


Baseline: line numbers and counts here were verified against master @ c8049d69 (2026-07-28). Master moves β€” re-verify against current master before starting; the invariants above are what must hold regardless of where the code has drifted to.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions