normalizeCountryCode($countryCode); if ($countryCode === '' || $countryCode === 'DE') { return VatTreatment::Domestic; } if (! $this->isEuCountry($countryCode)) { return VatTreatment::ThirdCountry; } return $this->isPlausibleVatId($vatId, $countryCode) ? VatTreatment::ReverseCharge : VatTreatment::EuConsumer; } /** * Normalisiert einen Ländercode auf den ISO-3166-1-alpha-2-Großbuchstaben. * Single Source of Truth, damit alle USt-Pfade dieselbe Schreibweise nutzen. */ public function normalizeCountryCode(?string $countryCode): string { return strtoupper((string) $countryCode); } /** * Ist der (bereits normalisierte) Ländercode ein EU-Mitgliedstaat? DE zählt * hier bewusst NICHT mit (Inland, siehe config `billing.eu_country_codes`). */ public function isEuCountry(string $countryCode): bool { return in_array($countryCode, (array) config('billing.eu_country_codes', []), true); } public function rateFor(VatTreatment $treatment): float { return $treatment->isTaxExempt() ? 0.0 : (float) config('billing.vat_rate', 0.19); } public function taxCentsFor(int $netCents, VatTreatment $treatment): int { return (int) round($netCents * $this->rateFor($treatment)); } /** * Formale Plausibilität: beginnt mit dem Ländercode der Adresse und * trägt danach 2–13 alphanumerische Zeichen (EU-Formatrahmen). * Public, damit der VatIdValidationService dieselbe Definition nutzt. */ public function isPlausibleVatId(?string $vatId, string $countryCode): bool { $vatId = strtoupper(preg_replace('/\s+/', '', (string) $vatId) ?? ''); if ($vatId === '') { return false; } // Griechenland nutzt das Präfix EL statt GR. $expectedPrefix = $countryCode === 'GR' ? 'EL' : $countryCode; return (bool) preg_match('/^'.preg_quote($expectedPrefix, '/').'[A-Z0-9]{2,13}$/', $vatId); } }