*/ public array $blockers, /** @var array}> */ public array $consequences = [], ) {} /** * @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, ]; } // 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. $quotaGb = (int) ($target['quota_gb'] ?? 0); $metric = $instance !== null ? InstanceMetric::latestDisk($instance) : null; if ($quotaGb > 0 && $metric !== null && $metric->disk_used_bytes > $quotaGb * 1024 ** 3) { $blockers[] = [ 'key' => 'storage', 'current' => Bytes::human($metric->disk_used_bytes), 'limit' => $quotaGb.' GB', ]; } // 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); $consequences = $targetPlan === null ? [] : $access->consequencesOfMovingTo( current: $access->contractOf($customer), instance: $instance, targetPlan: $targetPlan, targetFeatures: array_values((array) ($target['features'] ?? [])), ); return new self($blockers === [], $blockers, $consequences); } }