wasChanged('status') || $order->status !== 'paid') { return; } try { match ($order->type) { 'upgrade' => app(ApplyPlanChange::class)->forOrder($order), // A module and a storage pack are the same purchase to the // contract: one booking, at the price the customer was shown, // charged onward as an item on the Stripe subscription. Storage // is sold in packs and the shop writes one order per pack, so // each order books exactly one — which is also what the unique // index on (order_id, addon_key) enforces. 'addon' => $this->bookAddon($order, (string) $order->addon_key), 'storage' => $this->bookAddon($order, AddonCatalogue::STORAGE), // A traffic pack is the one thing sold here that nothing can // deliver yet, and it is left loud rather than quietly dropped. // The allowance TrafficMeter meters against is a standing count // of packs on the instance (`traffic_addons`), while a top-up is // sold as a one-off for the month that is running out — so // incrementing it would hand the customer the extra thousand // gigabytes every month for ever off one payment, and doing // nothing leaves them paying for nothing. Which of the two the // owner means is a commercial decision, not a guess to be made // here, and until it is made a paid traffic order must be // impossible to miss. 'traffic' => Log::error('A paid traffic pack has no fulfilment path: nothing raises the metered allowance.', [ 'order_id' => $order->id, 'customer_id' => $order->customer_id, ]), // The first purchase of a package ('new') is delivered by the // webhook that announced its payment, not from here; a downgrade // is never paid at all. Named rather than left to the default so // that a type nobody has thought about still falls through it. 'new', 'downgrade' => null, default => null, }; } catch (Throwable $e) { // Never allowed to fail the write that marked the order paid. That // write is the record that money changed hands, and undoing it over a // delivery would erase the payment and leave nothing to retry from. A // delivery that did not land is loud in its own right: the order stays // unconsumed and the contract still says what it said, both of which // somebody can see. Log::error('Failed to deliver what a paid order bought.', [ 'order_id' => $order->id, 'type' => $order->type, 'plan' => $order->plan, 'addon' => $order->addon_key, 'error' => $e->getMessage(), ]); } } /** * Book the module this order paid for onto the customer's contract. * * The contract is resolved through CustomDomainAccess::contractOf(), which is * already the one place that answers "which contract is this customer on" — * the same resolution ApplyPlanChange::forOrder() makes, and for the same * reason: a second copy of that query is the one that would disagree the day * the rule changes. * * BookAddon is idempotent per order, refuses a module the package may not * have, freezes the price and adds the Stripe item; none of those rules are * repeated here, because a rule enforced in two places is a rule that will * disagree with itself. */ private function bookAddon(Order $order, string $addonKey): void { if ($addonKey === '') { Log::error('A paid module order names no module.', ['order_id' => $order->id]); return; } $subscription = app(CustomDomainAccess::class)->contractOf($order->customer); if (! $subscription instanceof Subscription) { Log::warning('No contract to book a paid module onto.', [ 'order_id' => $order->id, 'addon' => $addonKey, ]); return; } app(BookAddon::class)($subscription, $addonKey, 1, $order); } }