CluPilotCloud/config
nexxo 52b41bb0d5 fix(billing): a paid order opens a contract, and provisioning obeys it
The pipeline re-resolved config('provisioning.plans') by order.plan, so the
subscription snapshot protected a customer's price but not their machine:
shrinking a plan resized an existing customer's VM on its next run. Nothing
created a subscription either, so closing this meant opening the contract at
purchase and pointing provisioning at it.

- OpenSubscription freezes the catalogue onto a subscription when a checkout
  is paid; StartCustomerProvisioning calls it inside the order transaction.
- CustomerStep::plan() reads the frozen snapshot. ValidateOrder and
  ReserveResources fail closed with no_subscription rather than falling back
  to the catalogue, which is the bug itself.
- template_vmid joins the snapshot so a re-clone cannot pick up a blueprint
  published after the sale. Deliberately outside FROZEN: it is how we build
  the machine, not a term the customer is owed, and a dead template must be
  replaceable without cancelling a contract.
- TrafficMeter reads the allowance off the contract too — cutting a plan's
  traffic was otherwise enough to start throttling someone who bought more.
- The migration backfills contracts for orders that already bought something,
  reconstructed from what was actually delivered where an instance exists,
  and adopts an existing order-less contract instead of opening a second.
  Orders paid in a currency the catalogue cannot price get none, matching the
  checkout path.

price_cents stays the catalogue's NET price, which is what PlanChange
prorates against — not Order::amount_cents, which holds Stripe's GROSS total.
Reconciling the two belongs to the proof register and Stripe (phases 4/5).

Also pins STRIPE_WEBHOOK_SECRET blank in phpunit.xml: the operator's real
secret was reaching the suite from .env and rejecting every unsigned test
payload, which is why 7 webhook tests failed before any of this.

Verified in the browser: with team traffic cut from 3000 to 500 GB in the
catalogue, the customer's portal still shows 3 TB.

373 tests green. Codex review clean after three rounds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 11:10:00 +02:00
..
admin_access.php feat(site): switch the public website and portal off from the console 2026-07-25 23:10:27 +02:00
app.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00
auth.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00
broadcasting.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00
cache.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00
countries.php fix(admin): no admin lockout (is_admin fallback); centered modal width; datacenter edit modal + country dropdown 2026-07-25 18:09:17 +02:00
database.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00
filesystems.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00
fortify.php fix(auth): scope registration throttle to its own route (per-IP), not all Fortify endpoints 2026-07-25 18:41:30 +02:00
logging.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00
mail.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00
permission.php feat(admin): staff RBAC (spatie) + admin settings page 2026-07-25 15:47:42 +02:00
provisioning.php fix(billing): a paid order opens a contract, and provisioning obeys it 2026-07-26 11:10:00 +02:00
queue.php fix(engine): durable queueing for long provisioning steps 2026-07-25 10:36:49 +02:00
services.php fix(engine-b): real backup/monitoring registration + OC_PASS scoping 2026-07-25 12:33:45 +02:00
session.php chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack) 2026-07-25 00:21:30 +02:00