whereNotNull('pending_plan') ->whereNotNull('pending_effective_at') ->where('pending_effective_at', '<=', now()) ->where('status', 'active') ->with('customer') ->orderBy('id') ->cursor(); foreach ($contracts as $subscription) { $due++; $plan = (string) $subscription->pending_plan; try { $apply($subscription, $plan, $this->orderFor($subscription, $plan)); // Read back rather than assumed: a package the catalogue has // since withdrawn cannot be moved onto, and ApplyPlanChange // says so by logging and leaving the contract alone. Clearing // the stamp there would drop the customer's request silently, // so it stays booked and this says so on every tick. $subscription->refresh(); if ((string) $subscription->plan !== $plan) { $this->warn("Contract {$subscription->uuid}: still on {$subscription->plan}, {$plan} was not applied."); continue; } $subscription->clearPendingPlanChange(); $applied++; } catch (Throwable $e) { // One customer's withdrawn package must not stop everybody // else's downgrade from landing. $this->warn("Contract {$subscription->uuid}: {$e->getMessage()}"); } } $this->info("{$due} downgrade(s) due, {$applied} applied."); return self::SUCCESS; } /** * The cart entry this booking came from, so it is consumed with the change * and the register row files under it. * * Null is a perfectly ordinary answer — an operator can book a change * without a purchase, and the contract is the authority on what was agreed * either way. */ private function orderFor(Subscription $subscription, string $plan): ?Order { return Order::query() ->where('customer_id', $subscription->customer_id) ->where('type', 'downgrade') ->where('status', 'pending') ->where('plan', $plan) ->orderBy('id') ->first(); } }