CluPilotCloud/app/Services
nexxo 3cf16ecd63 Name every property that decides what a price charges
The money gate compared amount, currency and interval and left the rest to a
filter that excluded two fields. A recurring, licensed, monthly price with
transform_quantity divide_by 10, at our exact figure, on our product, carrying
our addon key passed all five conditions. Modules bill BY quantity — a pack is
one item at quantity n — so a customer holding three would have been charged
one. A tiered price was excluded only by accident: Stripe reports unit_amount
null for it and we read that as 0, which fails the amount match unless the
caller's own figure is 0, and PlanPrices::ensure() has no zero guard.

activePricesFor() now also skips a non-empty transform_quantity and any
billing_scheme other than per_unit. Absent keys stay Stripe's defaults, which
is the direction that matters: read the other way round the filter would refuse
every legitimate price and turn recognition into a permanent no-op. The
contract docblock names the four properties instead of claiming to cover all of
them, and says the list is not everything Stripe can put on a price — only
what is known to change what one charges.

And a comment that says a gap is covered is worse than the gap. The sync said a
crash between Stripe creating a product and us storing its id gives back the
same product next run: true for twenty-four hours, false afterwards, and the
exact claim this branch was written to refute. There is no recognition step for
products at all, so an interrupted run leaves an orphan and the next run past
the expiry mints a second one — worse than a duplicate price, because
activePricesFor() then looks at the wrong product and recognition goes blind
for the whole family. Written down as a known gap at both createProduct call
sites, with the trade this branch chose: folding name and metadata into the key
means a renamed family now mints a second product where it used to answer 400,
and a blockade reaches a paying customer while a duplicate product does not.

The sweep no longer reports "created" for what it adopted — the count is taken
before ensure() runs, and telling those two apart is the whole point.

The test named after 2026-07-29 could not reproduce it: the fake's key ledger
was empty, so createPrice() could not have thrown and the test could not tell
"adoption prevented the 400" from "no 400 was possible". Seeded now with the
key today's call sends and a fingerprint over the metadata the pre-9da1358 call
sent. Remove the adopt() call from AddonPrices::ensure() and the test dies with
Stripe's own sentence instead of an assertion.

Also: the plan document still instructed migrate:fresh --env=testing. There is
no .env.testing here, so it targets the live development database; struck
through with the reason, not deleted, and a Korrekturen section records the
five defects the plan's own reviews found in it. Four comments corrected —
"below" that meant above, a counter claimed unable to tie that ties with a
planted price, the unique index's second meaning for remember()'s catch, and
that MariaDB DDL is not transactional so the dedupe commits before the index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:50:20 +02:00
..
Billing Name every property that decides what a price charges 2026-07-30 13:50:20 +02:00
Deployment Update on a schedule if the owner wants one, and say where incidents go 2026-07-29 15:01:54 +02:00
Devices Show where an account is signed in, and let it sign the other places out 2026-07-28 23:38:17 +02:00
Dns Fix nine defects in the provisioning pipelines 2026-07-30 01:34:55 +02:00
Domains Prove a custom domain before serving it, and keep proving it 2026-07-29 14:47:51 +02:00
Env Add a raw .env editor to the Integrations page, with a net under it 2026-07-29 01:51:27 +02:00
Mail Stop charging VAT to the customers who owe us none 2026-07-30 02:15:41 +02:00
Maintenance Keep the purpose mailer through the queue instead of losing it to the default 2026-07-28 03:50:02 +02:00
Monitoring Add the SSH identity to the vault, and give deployment config a console page 2026-07-29 00:52:44 +02:00
Provisioning Let the customer buy it themselves, and tab the integrations page 2026-07-29 19:42:28 +02:00
Proxmox Restart a machine, enforce the quota that was sold, end a route that ended 2026-07-29 18:28:28 +02:00
Secrets Read the support mailbox into the console 2026-07-29 21:41:29 +02:00
Ssh Make host onboarding survive the machine it runs on 2026-07-30 00:54:16 +02:00
Status Let an incident be deleted, and start measuring whether the hosts answer 2026-07-29 15:16:48 +02:00
Stripe Name every property that decides what a price charges 2026-07-30 13:50:20 +02:00
Tax Ask the EU register whether a VAT number is real 2026-07-29 22:42:29 +02:00
Traefik Serve the custom domain, not just announce it 2026-07-29 16:44:52 +02:00
Traffic feat(billing): the plan catalogue becomes three tables, and config stops selling 2026-07-26 12:05:56 +02:00
Wireguard Add the SSH identity to the vault, and give deployment config a console page 2026-07-29 00:52:44 +02:00