*/ public array $blockers, /** @var array}> */ public array $consequences = [], /** * The numbers behind a storage blocker, or null when storage is not what * is in the way. Real figures, never a shrug: what is stored, what the * target package plus the packs already booked would allow, how much has * to go, and how many further packs would cover it instead. * * @var array{used: string, limit: string, over: string, packs: int, pack_gb: int}|null */ public ?array $storageRemedy = null, ) {} /** * @param array $target the catalogue entry being moved to * @param ?string $targetPlan its plan key — null when a caller is checking raw * limits rather than a package, and then the * consequences that depend on which package it is * cannot be computed and are not guessed at */ public static function for(Customer $customer, ?Instance $instance, array $target, ?string $targetPlan = null): self { $blockers = []; $seats = Seat::query() ->where('customer_id', $customer->id) ->whereIn('status', ['active', 'invited']) ->count(); $seatLimit = (int) ($target['seats'] ?? 0); if ($seatLimit > 0 && $seats > $seatLimit) { $blockers[] = [ 'key' => 'seats', 'current' => (string) $seats, 'limit' => (string) $seatLimit, ]; } // Asked, never re-derived: which packages may carry an own domain is // CustomDomainAccess's question, and this class only reports what it // answers about the move being considered. $access = app(CustomDomainAccess::class); $contract = $access->contractOf($customer); // Storage is checked against the last real reading. Falling back to the // contractual allowance would refuse a downgrade to anyone who has ever // bought a large plan, however empty their instance is. // // What it is checked AGAINST is the target package plus the storage packs // the customer has already booked, because those move with them: a // customer on 200 GB with three packs may hold 500 GB, and refusing them // the smaller package would be refusing them storage they are paying for. $planGb = (int) ($target['quota_gb'] ?? 0); $allowance = StorageAllowance::forPlan($planGb, $contract); $metric = $instance !== null ? InstanceMetric::latestDisk($instance) : null; $storageRemedy = null; if ($planGb > 0 && $metric !== null && $metric->disk_used_bytes > $allowance->totalGb() * 1024 ** 3) { $blockers[] = [ 'key' => 'storage', 'current' => Bytes::human($metric->disk_used_bytes), 'limit' => $allowance->totalGb().' GB', ]; $storageRemedy = [ 'used' => Bytes::human($metric->disk_used_bytes), 'limit' => $allowance->totalGb().' GB', 'over' => Bytes::human($metric->disk_used_bytes - $allowance->totalGb() * 1024 ** 3), 'packs' => $allowance->packsToCover((int) $metric->disk_used_bytes), 'pack_gb' => $allowance->packSizeGb, ]; } $consequences = $targetPlan === null ? [] : $access->consequencesOfMovingTo( current: $contract, instance: $instance, targetPlan: $targetPlan, targetFeatures: array_values((array) ($target['features'] ?? [])), ); return new self($blockers === [], $blockers, $consequences, $storageRemedy); } }