|null $payload */ public function __construct( public readonly int $httpStatus, public readonly ?array $payload, string $message, ) { parent::__construct($message); } public static function fromResponse(int $httpStatus, string $body): self { $payload = json_decode($body, true); $decoded = is_array($payload) ? $payload : null; $apiMessage = ''; if (is_array($decoded)) { $apiMessage = (string) ($decoded['message'] ?? $decoded['error'] ?? $decoded['statusCode']['message'] ?? ''); } $message = $apiMessage !== '' ? $apiMessage : "Contabo API error (HTTP {$httpStatus})"; return new self($httpStatus, $decoded, $message); } public function isPaymentRequired(): bool { if ($this->httpStatus === 402) { return true; } return self::messageIndicatesPaymentRequired($this->getMessage()) || self::messageIndicatesPaymentRequired(json_encode($this->payload) ?: ''); } public static function messageIndicatesPaymentRequired(string $text): bool { if ($text === '') { return false; } return (bool) preg_match( '/\b402\b|payment\s+required|insufficient\s+(?:account\s+)?balance|not\s+enough\s+(?:credit|funds)|account\s+balance|additional\s+pay(?:ed|ment)\s+service|out\s+of\s+funds/i', $text ); } }