4 Commits (f961205512e1374dd5cac8fa6c8ce1fbe8bf9749)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
f961205512 |
Die neue Leiter: Start 39, Team 79, Business 139
Zugeschnitten auf die Maschine, die dasteht. 388 GB vergebbar geteilt durch 40 GB Platte sind neun Startkunden statt drei — Speicher und RAM gehen damit gleichzeitig aus, wo vorher sechs RAM-Plaetze verfielen. 351 Euro Umsatz je Server statt 147. Umgeschaltet wird durch Beenden und Nachfolgen, nicht durch Aendern: eine veroeffentlichte Version ist unveraenderlich, und laufende Vertraege tragen ihren eigenen Schnappschuss. Als Migration und nicht als Befehl, weil der Katalog von einer Migration gesetzt wird — sonst blieben Neuinstallation und Testsuite auf einer Leiter, die wir nicht mehr verkaufen. Enterprise verlaesst den Verkauf (2000 GB finden auf 388 GB keinen Host), das Testpaket heisst Intern und wird intern — es steht ausserdem zum ersten Mal im Katalog einer frischen Installation, statt von Hand angelegt werden zu muessen. Dreissig Testdateien nennen die neuen Zahlen. Drei Faelle waren keine Zahlen: Vertraege auf Enterprise entstehen nur noch als Bestandsvertraege (Helfer asGrandfathered() in tests/Pest.php), die Preisblatt-Stufe "Premium" haengt am Paket, das den Laden verlassen hat, und ein Katalogleser, der die einzige Preiszeile einer Familie suchte, findet seit der Handreichung zwei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
afa6183903 |
Zusatzspeicher: 20 GB fuer 15 Euro, hoechstens drei, nicht bei Enterprise
100 GB fuer 10 Euro waren 0,10 Euro/GB — unter Einstandspreis und billiger als jeder Aufstieg. Wer stapelt, belegt dann den knappsten Rohstoff zum niedrigsten Preis. 0,75 Euro/GB liegen ueber beiden Aufstiegen (0,73 und 0,67), und der Deckel bei drei liegt dort, wo Aufsteigen billiger UND besser wird. BookAddon haelt den Ausschluss durch AddonCatalogue::availabilityRefusal() (gleiches Muster wie CustomDomainAccess), damit greift er auch beim Verschenken durch den Betreiber. Billing::purchase() und storageLimitNote fragen dieselbe Regel VOR der Zahlung, sonst haette ein Enterprise-Kunde einen zahlbaren Auftrag anlegen koennen, den BookAddon erst danach abgelehnt haette. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
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> |
|
|
|
b3651a14be |
Decide who may have their own domain, and decide it in one place
tests / pest (push) Failing after 9m46s
Details
tests / assets (push) Successful in 21s
Details
tests / release (push) Has been skipped
Details
The domain page shipped with no access control at all: the `custom_domain`
plan feature was checked nowhere in the codebase, and every customer on
every package could point a domain at their instance. This is the owner's
rule, built as one authority that everything else asks.
Start impossible — not bookable, not upgradable, not offered
Team optional, via the `custom_domain` module
Business included, because the plan version carries the feature
Enterprise included, likewise
App\Services\Billing\CustomDomainAccess answers all of it: may this
contract use one, may it book the module, why not in words a customer can
be shown, what a downgrade would do to a domain they have, and — after a
change lands — making the state match. Read against the FROZEN plan
version, never today's catalogue, so a feature added to a package later
cannot reach into a contract signed before it.
Which packages cannot have one at all is an explicit list in
config/provisioning.php, beside the module's price. The catalogue cannot
answer it: plan versions model what a package HAS, and nothing models what
a package may BUY. Both available proxies are dishonest — "the lowest tier
on sale" hands the right to a grandfathered Start customer the day the
owner stops selling Start or adds something cheaper below it, and reading
it off `branding` would sell a domain to anyone allowed to upload a logo.
The config comment says so.
Enforced where it can be enforced today:
- BookAddon refuses the module, with the sentence the customer is shown.
GrantAddon now writes its synthetic order inside the transaction, so a
refusal cannot leave a paid order for a module nobody got.
- The portal drops the card where it cannot be booked and shows it as
included where the package carries it — no price, no button.
- Billing::purchase() re-checks, because a hidden button is markup.
- DowngradeCheck reports the consequence beside the blockers, so the
customer reads it above the button rather than after it: losing the
domain (Team -> Start), or the choice (Business -> Team, where keeping
it means booking the module).
- PlanChange::settleCustomDomain() applies it once a change lands: the
module stops being charged for and the instance goes back to its
platform address. It clears the whole domain, not only the verified
flag — clupilot:verify-domains re-reads every row that still has a
domain and a token, and would switch it back on the next night.
- The price sheet's own-domain row now reads dash / optional + price /
included, from the same authority; the three cell states already
existed and the view is unchanged.
STILL TO APPLY, ONCE THIS AND THE CUSTOM-DOMAIN BRANCH MEET — one line at
the top of App\Livewire\CustomDomain::mount(), which belongs to the other
session and is deliberately untouched here:
abort_unless(app(\App\Services\Billing\CustomDomainAccess::class)->allowsCustomer($this->customer()), 403);
routes/web.php is left alone for the same reason. The mount() guard covers
the route and the component both; a middleware would only repeat it.
Follow-ups, named rather than built:
- Nothing applies a plan change anywhere yet, so settleCustomDomain()
has no caller. Whoever builds that must call it after the new contract
opens, and must decide whether a booked module is carried onto the new
contract — enforce() cancels the booking on the contract it is given.
- Deactivation sets the state; it does not re-run provisioning. Traefik
and Nextcloud's trusted_domains still name the old address until
ConfigureDnsAndTls / ConfigureNextcloud run again.
- App\Support\Navigation still shows the Domain tab to every customer;
it can ask allowsCustomer() the same way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|