Commit Graph

2 Commits (main)

Author SHA1 Message Date
nexxo 32a8bd9a88 Refuse the sale instead of reaching Stripe without a key
Task 5: Billing::purchase() now checks SecretVault for a stripe.secret before
touching anything else — no customer, no plan lookup, no Order row — and the
customer reads a plain sentence instead of a 500 page. HttpStripeClient's own
secret() throws the same StripeNotConfigured exception rather than sending a
request with an empty bearer token and letting Stripe's 401 stand in for the
real cause.

isConfigured() was fixed alongside it: it used to call secret() too, which
would have turned every "is Stripe set up?" check (the checkout controller,
the catalogue sync commands) into an uncaught exception the moment a key went
missing — the opposite of what this task is for. It now reads the vault
directly.

The new guard is unconditional, including for the cart-only purchase types
(storage, addon, plan changes) that never call Stripe themselves — a cart
order nobody can ever pay for is pointless to create. That broke every
existing Billing/Cart/Downgrade test that calls purchase() without a stored
key; each affected fixture now deposits one via the new withStripeSecret()
Pest helper. A few of those tests (CustomDomainAccessTest, DowngradeTest) were
passing already but for the wrong reason — the new guard, not the check they
were named for — since both only assert an order was NOT created; they get
the same fixture fix so they still prove what they claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:58:01 +02:00
nexxo 8ca0d4d257 Say the same thing to the customer, the register and the bank
tests / pest (push) Failing after 8m5s Details
tests / assets (push) Successful in 25s Details
tests / release (push) Has been skipped Details
Five places where the product told somebody a figure that nothing behind
it produced.

**The invoice page showed five invoices nobody was ever issued.** A mock
that outlived its purpose: CP-2026-0003 to CP-2026-0007, a prefix
belonging to no configured series, all "paid", a spend curve from 179 to
198 € and a next charge of 198 € on 01.08.2026 — behind
['auth','verified','customer.active'], so every signed-in customer saw
it. Their real invoices existed in `invoices`, were rendered as PDFs and
mailed to them, and appeared nowhere in the portal at all. The page now
lists their own documents, newest first, with the numbers, dates and
totals off the frozen snapshot, a Storno marked as one, and a download.
The PDF route resolves the document against the signed-in customer in
the query — a URL anybody can type is not made private by not printing
it. No next charge and no chart: nothing here computes what Stripe will
take next cycle, and a figure nobody has worked out is worse than a
blank space.

**The setup fee was quoted everywhere and charged nowhere.** On the
price sheet, on the booking page, set by the operator in the console —
and CompanyProfile::setupFeeCents() had no reader outside the two
sentences that quoted it. It is charged now, as a second non-recurring
line on the same checkout session, which Stripe puts on the initial
invoice only. `orders.setup_fee_cents` records how much of the charge it
was, so the whole total still goes back on a withdrawal while the
register holds the PACKAGE's charge against the contract price and the
invoice prints the fee as a line of its own. The booking page was also
quoting the fee NET under the same sentence the website quotes GROSS —
99,00 against 118,80 — and now quotes what will be taken.

**An upgrade click cancelled a booked downgrade and delivered nothing.**
Nothing in this application marks a cart order paid, so the customer
traded a downgrade they had genuinely booked for a cart line that would
never be fulfilled. The booking now stands until the upgrade actually
lands on the contract, where ApplyPlanChange clears it. The fulfilment
seam is reachable for every type that can be paid — a module and a
storage pack book through BookAddon, which was the missing path from a
paid order to a SubscriptionAddon and left AddonPrices,
SyncStripeAddonItems and clupilot:end-cancelled-addons with no caller
behind a payment. A paid traffic pack logs an error rather than pretending:
the metered allowance is a standing count of packs while a top-up is sold
for one month, and which of the two is meant is not a guess to make here.
That no path to `paid` exists yet is now stated in both places rather
than in one.

**The proof register called correct charges wrong.** expected_gross_cents
used the CUSTOMER's rate, which is zero under reverse charge, against
the domestic gross Stripe actually took — so matches_catalogue was false
for every EU business sale charged exactly the advertised amount, and
for every renewal on a contract with a module, which was held against
`price_cents` alone. It compares against TaxTreatment::chargedCents()
and against the whole term now. Where no money moved, charged and the
flag are null instead of the expected figure agreeing with itself, and a
plan change records the terms with the pro-rata figures in the snapshot
rather than money that arrives again on Stripe's proration invoice.

**The seller's own VAT id was on the invoice, unlabelled**, between the
register number and the court. Labelled — and the footer's composition
moved out of the TCPDF method, because inside one the only way to read
what it says is to render a PDF and un-subset its fonts.

One test enshrined a figure the code does not produce: a reverse-charge
purchase recorded with no charged amount, asserting 17900/0/17900, where
OpenSubscription always passes the charged figure and a real one records
21480/0/21480. Another asserted that an upgrade click cancels a booked
downgrade.

Full suite: 1676 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 01:30:24 +02:00