Repository navigation
billing: add --dry-run mode and always send manual invoices - #3099
Conversation
473d8e8 to
5810d41
Compare
288ed60 to
fed183d
Compare
753d45e to
6a7f4e3
Compare
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.
d65c35a to
7bbfa22
Compare
Strix Security ReviewNo security issues found. Review summaryReviewed the full billing-integrations diff, with focused attention on the new commit delta (failure reporting and re-verification in Updated for Reviewed by Strix |
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.
7bbfa22 to
a954e01
Compare
--dry-runThe trial-run workflow used testmode Stripe, which lacks livemode invoice state, so already-invoiced manual bills showed up as phantom creates.
--dry-runmakes the run read-only so it can be pointed at livemode.Per-invoice work is split into read-only
classifyand write-onlyexecute; a dry run stops afterclassify, 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_invoiceContracts 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 tocharge_automatically.Hardening
--recreate-finalizedvoids open invoices instead of attempting a delete Stripe rejects, and recovers a run that voided but failed to recreate. Replacements are reported as a distinctReplacedresult.AlreadyProcessedinstead of erroring, and are excluded from--recreate-finalized.