Skip to content

billing: add --dry-run mode and always send manual invoices - #3099

Merged
jshearer merged 6 commits into
masterfrom
jshearer/billing_dry_run
Oct 5, 2026
Merged

jshearer merged 6 commits into
masterfrom
jshearer/billing_dry_run

Conversation

@jshearer

@jshearer jshearer commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

--dry-run

The trial-run workflow used testmode Stripe, which lacks livemode invoice state, so already-invoiced manual bills showed up as phantom creates. --dry-run makes the run read-only so it can be pointed at livemode.

Per-invoice work is split into read-only classify and write-only execute; a dry run stops after classify, so both modes share one decision path. The preview also mirrors the billing-email resolution of a real run and reports tenants that would fail. It cannot validate the final invoice total, which requires creating the invoice.

Manual invoices always use send_invoice

Contracts and one-off charges are sent for the customer to pay rather than auto-charged, regardless of --charge-type: at creation, when refreshing an existing draft, and in the send phase. Nothing ever switches a draft back to charge_automatically.

Hardening

  • --recreate-finalized voids open invoices instead of attempting a delete Stripe rejects, and recovers a run that voided but failed to recreate. Replacements are reported as a distinct Replaced result.
  • Manual bills that already have an open, paid, void, or uncollectible invoice are skipped as AlreadyProcessed instead of erroring, and are excluded from --recreate-finalized.
  • Payment-method state is read from Stripe directly

@jshearer
jshearer force-pushed the jshearer/billing_dry_run branch from 473d8e8 to 5810d41 Compare July 2, 2026 03:18
@jshearer jshearer self-assigned this Jul 2, 2026
@jshearer
jshearer force-pushed the jshearer/billing_dry_run branch 4 times, most recently from 288ed60 to fed183d Compare July 30, 2026 17:12
@jshearer
jshearer marked this pull request as ready for review July 30, 2026 17:41
@jshearer
jshearer requested a review from a team July 30, 2026 17:41
@jshearer
jshearer force-pushed the jshearer/billing_dry_run branch from 753d45e to 6a7f4e3 Compare August 3, 2026 20:02
@GregorShear
GregorShear self-requested a review September 14, 2026 20:10
Comment thread crates/billing-integrations/src/publish.rs Outdated
Comment thread crates/billing-integrations/src/publish.rs Outdated
Comment thread crates/billing-integrations/src/send.rs Outdated
@jshearer
jshearer requested a review from GregorShear October 5, 2026 19:21
The invoice generator's trial-run workflow ran against a sandbox Stripe account, but produced inaccurate results because the sandbox lacked livemode invoice state (manual bills with existing open/paid invoices appeared as phantom creates). `--dry-run` runs against livemode Stripe in read-only mode, showing what would happen without creating invoices, customers, or modifying anything.

Structural changes:

* Split `upsert_invoice` into `classify` (read-only: validation, Stripe searches) and `execute` (writes: customer/invoice creation, line items, verification). `--dry-run` stops after classify.
* Decompose `get_or_create_customer_for_tenant` into `find_customer` (read-only search) and `ensure_customer_for_invoicing` (find-or-create + email backfill).
* Reorder classify checks so cheap local validations (FreeTier, FutureTrialStart, LessThanMinimum) run before any Stripe API calls.
* Multi-month manual bills that already have an `open`, `paid`, `void`, or `uncollectible` invoice in Stripe are now classified as `AlreadyProcessed` instead of erroring. These are expected when date-range-overlapping manual bills were invoiced in a previous billing run.
* Per-tenant summary output annotates manual bills with their date range (`[manual: 2026-01-01 - 2026-06-30]`).
* Dry-run with `--clean-up` previews which stale draft invoices would be deleted. `--recreate-finalized` logs which invoices would be deleted and recreated.
Customers' stored payment methods are for monthly usage overages. Manual bills (contracts, one-off charges, etc.) should be sent as invoices so the customer can decide how to pay, rather than being automatically charged to their payment method.

* Override `charge_type` to `SendInvoice` for manual invoices during creation in `publish`
* Switch manual invoices from `charge_automatically` to `send_invoice` during the send phase, even if the customer has a payment method on file
Preserve cleanup behavior and dry-run fidelity across the classification and execution split, including billing-email validation, fail-fast propagation, customer reuse, and consistent reporting. Replacements are reported as a distinct `Replaced` result, so run summaries show how many invoices were (or would be) voided or deleted and reissued.

Reconcile collection methods toward `send_invoice` only, so refreshing a draft never reverts the send workflow's correction for tenants without a payment method, and re-check invoice state before update or replacement. Void open invoices during replacement and recover cleanly from interrupted runs.

Determine payment-method state directly from Stripe rather than the DB's `stripe.customers` capture, which has been unreliable.
@jshearer
jshearer force-pushed the jshearer/billing_dry_run branch from d65c35a to 7bbfa22 Compare October 5, 2026 19:28
@strix-security

strix-security Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Review summary

Reviewed the full billing-integrations diff, with focused attention on the new commit delta (failure reporting and re-verification in crates/billing-integrations/src/send.rs) plus the --dry-run classify/execute split and manual-invoice send_invoice enforcement in publish.rs. All new and touched SQL remains fully parameterized via sqlx bind parameters ($1/$2), with the only change being removal of the now-unreliable has_payment_method lookup and schema-qualifying internal.invoices_ext. The new Stripe interactions (customer search, invoice retrieval/void/delete, collection-method reconciliation, finalization re-checks) operate on operator-supplied CLI arguments and control-plane database values, not attacker-controlled input, and add read-only and re-verification hardening rather than new exposure. No injection, authentication/authorization, secret-handling, SSRF, path traversal, or data-exposure issues were identified in the changed code.

Updated for a954e01.


Reviewed by Strix
Re-run review 路 Configure security review settings

GregorShear
GregorShear previously approved these changes Oct 5, 2026
Continue processing valid invoices, but return an error when preparation, finalization, or auto-advance updates fail. Include tenant and invoice context and distinguish draft republishing from open-invoice correction.
@jshearer
jshearer merged commit 7a64608 into master Oct 5, 2026
9 of 12 checks passed
@github-actions github-actions Bot added pending:agent Merged, in the control-plane-agent image, and not yet rolled to flow-agent pending:agent-api Merged, ships via Deploy agent-api, and not yet deployed pending:flowctl Merged, changes the flowctl binary, and not in a published release and removed pending:agent-api Merged, ships via Deploy agent-api, and not yet deployed labels Oct 5, 2026
@github-actions github-actions Bot removed the pending:agent Merged, in the control-plane-agent image, and not yet rolled to flow-agent label Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending:flowctl Merged, changes the flowctl binary, and not in a published release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants