CluPilotCloud/tests/Feature/Provisioning
nexxo 6387c747d0 feat(billing): the plan catalogue becomes three tables, and config stops selling
Plans lived in config/provisioning.php, where the owner cannot reach them and
where a plan has no history. They now live in plan_families / plan_versions /
plan_prices — a name that never moves, what the plan WAS at a point in time,
and a price per version and term with its own Stripe Price id.

- Availability is computed on every read (available_from <= now < until,
  half-open, UTC) plus a per-family sales kill switch. Nothing schedules a plan
  into or out of sale: a job that fails to run is a plan that silently
  misbehaves.
- Overlaps crash rather than resolve. currentVersion() uses sole(), and
  scheduling takes a lock on the family and rejects an overlapping window —
  two versions on sale at once would decide a customer's price by row order.
- A version is frozen from publication, not from first sale, and so is its
  price: a checkout is not instant, and an amount edited between the session
  opening and the webhook landing would contract someone at a price they were
  never quoted. Repricing publishes a new version, as Stripe requires anyway.
- Neither a published version, its price, nor a family with customers can be
  deleted, and a family key cannot be renamed. All of those would null the
  provenance off existing contracts.
- Subscriptions and orders record plan_version_id. A historical reference
  resolved by plan NAME hands back today's terms, which is the split-brain one
  level up. A checkout that carried its version is honoured even after the
  window closes — they paid for what they were shown.

Switched atomically and failing closed: config('provisioning.plans') and
plan_features are gone, and nothing falls back to them. A fallback would
resurrect a plan the owner had just switched off. The seed lives in the
migration, and PlanCatalogueTest pins what the config catalogue sold as the
shadow comparison. `php artisan plans:check` reports overlaps, gaps and
missing prices before a customer finds them.

Contract-backed displays, which were reading the live catalogue:
seat limits, the cloud card, the plan card, and admin MRR — the last three now
divide a yearly contract down via Subscription::monthlyPriceCents(), since all
three label the figure per month.

Two recovery gaps closed along the way: the order now commits before the
contract is opened (so a contract failure can never erase the record of a
payment), a Stripe retry repairs a missing contract and restarts a run stranded
with no_subscription, and TickProvisioning sweeps runs left pending by a crash
rather than only running/waiting ones.

402 tests green. Codex review clean after thirteen rounds. Verified in the
browser: portal, billing and console render unchanged off the new catalogue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:05:56 +02:00
..
CustomerDataModelTest.php fix(engine-b): default-deny firewall, disk-based capacity, synchronous credential mail 2026-07-25 12:39:15 +02:00
CustomerProvisioningEndToEndTest.php fix(billing): a paid order opens a contract, and provisioning obeys it 2026-07-26 11:10:00 +02:00
CustomerStepBaseTest.php feat(billing): the plan catalogue becomes three tables, and config stops selling 2026-07-26 12:05:56 +02:00
CustomerStepsTest.php fix(billing): a paid order opens a contract, and provisioning obeys it 2026-07-26 11:10:00 +02:00
DataModelTest.php feat(engine): core provisioning data model + hosts 2026-07-25 09:49:43 +02:00
HostOnboardingEndToEndTest.php fix(engine): honour backoff timing + don't double-count Proxmox storage 2026-07-25 10:32:14 +02:00
HostStepsTest.php fix(hosts): re-read the host before deciding there is no DNS record 2026-07-26 00:02:18 +02:00
LiveProgressTest.php feat(engine-b): live provisioning progress (admin console + customer dashboard) 2026-07-25 11:57:35 +02:00
PipelineRegistryTest.php feat(engine): orchestrator core (state machine + tick + lock) 2026-07-25 09:54:52 +02:00
RunRunnerTest.php fix(engine): don't reset started_at on poll so step deadlines accumulate 2026-07-25 12:15:26 +02:00
ServicesTest.php fix(dns): deleting a record that is already gone is success 2026-07-25 23:54:45 +02:00
StripeWebhookTest.php fix(engine-b): async stripe payments, capacity honesty, durable credential mail 2026-07-25 12:29:47 +02:00
SubscriptionSnapshotTest.php feat(billing): the plan catalogue becomes three tables, and config stops selling 2026-07-26 12:05:56 +02:00
TickProvisioningTest.php feat(billing): the plan catalogue becomes three tables, and config stops selling 2026-07-26 12:05:56 +02:00