CluPilotCloud/app/Models
nexxo 35b312d5ef Invoice every renewal, and tell Stripe when the package changes
Two things Stripe was doing on its own, without us.

**A renewal produced no invoice and no mail.** InvoiceMail went out from
exactly one place — the first purchase — so a customer paying every month
for a year received one invoice, for month one. Stripe already says when
the money lands: invoice.paid with billing_reason subscription_cycle. What
was missing was the document.

A renewal has no Order behind it, and one was not invented for the
occasion: an Order is something a customer bought, and writing a purchase
nobody made would corrupt the record of what they have actually ordered.
IssueInvoice::forBilledPeriod() issues from the CONTRACT instead, at its
frozen net price, for the term Stripe billed. Idempotent against the Stripe
invoice id, which is unique in the database rather than checked in PHP —
and because the number is drawn inside the same transaction as the row, a
redelivery takes its number back with it and the series keeps its sequence.
Nothing in the paperwork can fail the webhook: issuing and mailing are both
caught and logged, exactly as confirmByMail() does.

Only a cycle renewal gets a document. The checkout's own invoice already
has one; an upgrade's proration was worked out against Stripe's boundaries
and would not match a document written from our snapshot; a charge raised
by hand is for something this catalogue cannot describe. All three stay in
the register, and are logged once as money no document of ours covers.

**After a plan change, Stripe went on billing the old price.** There was no
way to move a subscription onto another price at all, so a customer who
upgraded paid for the smaller package every month afterwards. The client
can now swap one, the item id it needs is learnt from Stripe's own events
(and asked for once, for contracts older than the column), and the
behaviour is chosen per direction: an upgrade settles immediately, so the
cycle invoice stays exactly the contract price the renewal document states;
a downgrade lands at the period boundary and settles nothing.

A granted contract has no Stripe subscription and is left untouched. A
change that reaches the machine and not Stripe is never rolled back — it is
parked on the contract with the error and the behaviour it needed, logged
as an error, and retried hourly by clupilot:sync-stripe-subscriptions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:35:15 +02:00
..
Builders feat(billing): a proof register, and modules frozen at their booked price 2026-07-26 13:07:34 +02:00
Concerns feat(engine): core provisioning data model + hosts 2026-07-25 09:49:43 +02:00
Backup.php feat(engine-b): customer domain models + migrations 2026-07-25 11:38:01 +02:00
Customer.php Close the domain page to packages that may not have one 2026-07-29 16:20:21 +02:00
Datacenter.php Let a datacenter code be corrected while nothing depends on it, and say which building 2026-07-28 22:25:21 +02:00
DnsRecord.php feat(engine-b): customer domain models + migrations 2026-07-25 11:38:01 +02:00
ExportTarget.php Let a destination say how it is laid out and how long anything stays 2026-07-29 03:08:27 +02:00
Host.php Take the order, park it, and say when it will be delivered 2026-07-29 18:50:46 +02:00
Incident.php Rebuild the status page as a status page 2026-07-29 12:45:18 +02:00
IncidentUpdate.php Rebuild the status page as a status page 2026-07-29 12:45:18 +02:00
Instance.php Deliver the storage a customer actually buys 2026-07-29 19:13:10 +02:00
InstanceMetric.php Measure availability, and let a customer move down again 2026-07-27 16:41:15 +02:00
InstanceTraffic.php feat(traffic): meter the monthly allowance, show it, throttle instead of blocking 2026-07-25 23:33:47 +02:00
Invoice.php Invoice every renewal, and tell Stripe when the package changes 2026-07-29 19:35:15 +02:00
InvoiceExport.php Let the export have as many destinations as somebody wants 2026-07-29 02:43:15 +02:00
InvoiceSeries.php Lay the foundation for self-issued invoices: series, numbers, frozen documents 2026-07-29 00:57:06 +02:00
LoginSession.php Recognise the devices an account signs in from, and warn about a new one 2026-07-28 23:28:34 +02:00
Mailbox.php Clear every mailbox's verification when the shared server config changes 2026-07-28 06:36:06 +02:00
MaintenanceNotification.php fix(admin): in-flight claim (claimed_at) for exactly-once maintenance send; scope permission rollback 2026-07-25 16:38:44 +02:00
MaintenanceWindow.php fix(portal): scope the per-instance maintenance badge to that instance's host 2026-07-25 19:12:33 +02:00
MonitoringTarget.php Move the console off /admin, give the status page its own address, and measure monitoring 2026-07-27 06:05:40 +02:00
OnboardingTask.php feat(engine-b): customer domain models + migrations 2026-07-25 11:38:01 +02:00
Operator.php Give the people who run CluPilot a table of their own 2026-07-28 09:20:54 +02:00
Order.php Apply a bought plan change instead of only pricing it 2026-07-29 17:18:55 +02:00
PlanFamily.php Let the owner mark one plan as recommended, from the console 2026-07-29 14:22:22 +02:00
PlanPrice.php feat(billing): the plan catalogue becomes three tables, and config stops selling 2026-07-26 12:05:56 +02:00
PlanVersion.php feat(billing): the plan catalogue becomes three tables, and config stops selling 2026-07-26 12:05:56 +02:00
ProvisioningRun.php feat(engine): core provisioning data model + hosts 2026-07-25 09:49:43 +02:00
ProvisioningStepEvent.php feat(engine): core provisioning data model + hosts 2026-07-25 09:49:43 +02:00
RunResource.php feat(engine): core provisioning data model + hosts 2026-07-25 09:49:43 +02:00
Seat.php feat(portal): settings page — company profile, branding (logo+colors), cancel package, close account 2026-07-25 14:36:52 +02:00
StatusDay.php Rebuild the status page as a status page 2026-07-29 12:45:18 +02:00
StripePendingEvent.php feat(billing): Stripe owns the billing cycle, we own capability 2026-07-26 13:36:28 +02:00
Subscription.php Invoice every renewal, and tell Stripe when the package changes 2026-07-29 19:35:15 +02:00
SubscriptionAddon.php Let a subscription or add-on be opened for free, with who and why on the row 2026-07-29 12:41:06 +02:00
SubscriptionRecord.php feat(billing): Stripe owns the billing cycle, we own capability 2026-07-26 13:36:28 +02:00
SupportRequest.php Editing in modals, an update button that is not gated on a stale reading, and a support page that is real 2026-07-27 17:55:49 +02:00
User.php Give people a way back in, and put the URL in English 2026-07-29 16:50:29 +02:00
UserDevice.php Repair two comment blocks the admin-hosts edit ran together 2026-07-28 23:20:43 +02:00
VpnPeer.php Move the console's identity out of the customer table 2026-07-28 10:31:43 +02:00