148 lines
5.3 KiB
PHP
148 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Billing;
|
|
|
|
use App\Enums\VatIdCheckStatus;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Prüft USt-IDs in zwei Stufen (User-Panel-Restarbeiten, 12.06.2026):
|
|
*
|
|
* 1. **Formatprüfung** (immer): EU-Formatrahmen über den VatResolver,
|
|
* inkl. Abgleich mit dem Land der Rechnungsadresse.
|
|
* 2. **eVatR-Online-Abfrage** (wenn möglich): REST-API des BZSt
|
|
* (`POST /app/v1/abfrage`, Status `evatr-0000` = gültig). Die Abfrage
|
|
* setzt die eigene deutsche USt-ID des Betreibers voraus
|
|
* (`billing.own_vat_id`) und kann nur ausländische EU-IDs bestätigen —
|
|
* deutsche IDs und Ausfälle der API führen zu `Unverified`, nie zu
|
|
* einem harten Fehler. Stripe validiert die im Checkout erfasste
|
|
* USt-ID zusätzlich selbst; diese Prüfung verbessert die Eingabe-UX
|
|
* und sichert den manuellen MAN-Rechnungskreis ab.
|
|
*/
|
|
class VatIdValidationService
|
|
{
|
|
private const EVATR_ENDPOINT = 'https://api.evatr.vies.bzst.de/app/v1/abfrage';
|
|
|
|
private const STATUS_VALID = 'evatr-0000';
|
|
|
|
public function __construct(private readonly VatResolver $vatResolver) {}
|
|
|
|
/**
|
|
* @return array{status: VatIdCheckStatus, message: string}
|
|
*/
|
|
public function check(string $vatId, ?string $billingCountryCode = null): array
|
|
{
|
|
$vatId = strtoupper((string) preg_replace('/\s+/', '', $vatId));
|
|
|
|
if ($vatId === '') {
|
|
return $this->result(VatIdCheckStatus::Unverified, __('Keine USt-ID angegeben.'));
|
|
}
|
|
|
|
$prefix = substr($vatId, 0, 2);
|
|
$prefixCountry = $prefix === 'EL' ? 'GR' : $prefix;
|
|
|
|
if (! $this->vatResolver->isPlausibleVatId($vatId, $prefixCountry)) {
|
|
return $this->result(
|
|
VatIdCheckStatus::FormatInvalid,
|
|
__('Das Format der USt-ID ist ungültig (erwartet: Ländercode + Kennziffer, z. B. DE123456789).'),
|
|
);
|
|
}
|
|
|
|
$billingCountryCode = strtoupper((string) $billingCountryCode);
|
|
|
|
if ($billingCountryCode !== '' && ! $this->vatResolver->isPlausibleVatId($vatId, $billingCountryCode)) {
|
|
return $this->result(
|
|
VatIdCheckStatus::FormatInvalid,
|
|
__('Die USt-ID passt nicht zum Land der Rechnungsadresse (:country).', ['country' => $billingCountryCode]),
|
|
);
|
|
}
|
|
|
|
if ($prefix === 'DE') {
|
|
return $this->result(
|
|
VatIdCheckStatus::Unverified,
|
|
__('Format plausibel. Deutsche USt-IDs können über eVatR nicht online bestätigt werden.'),
|
|
);
|
|
}
|
|
|
|
if (! in_array($prefixCountry, (array) config('billing.eu_country_codes', []), true)) {
|
|
return $this->result(
|
|
VatIdCheckStatus::Unverified,
|
|
__('Format plausibel. Online-Bestätigung ist nur für EU-USt-IDs möglich.'),
|
|
);
|
|
}
|
|
|
|
$ownVatId = (string) config('billing.own_vat_id');
|
|
|
|
if ($ownVatId === '') {
|
|
return $this->result(
|
|
VatIdCheckStatus::Unverified,
|
|
__('Format plausibel. Online-Prüfung ist nicht konfiguriert (BILLING_OWN_VAT_ID).'),
|
|
);
|
|
}
|
|
|
|
return $this->confirmViaEvatr($vatId, $ownVatId);
|
|
}
|
|
|
|
/**
|
|
* @return array{status: VatIdCheckStatus, message: string}
|
|
*/
|
|
private function confirmViaEvatr(string $vatId, string $ownVatId): array
|
|
{
|
|
$cacheKey = 'evatr-check:'.$vatId;
|
|
|
|
/** @var array{status: VatIdCheckStatus, message: string}|null $cached */
|
|
$cached = Cache::get($cacheKey);
|
|
|
|
if ($cached !== null) {
|
|
return $cached;
|
|
}
|
|
|
|
try {
|
|
$response = Http::timeout(8)
|
|
->acceptJson()
|
|
->post(self::EVATR_ENDPOINT, [
|
|
'anfragendeUstid' => $ownVatId,
|
|
'angefragteUstid' => $vatId,
|
|
]);
|
|
} catch (\Throwable $exception) {
|
|
Log::warning('eVatR-Abfrage fehlgeschlagen.', ['vat_id' => $vatId, 'error' => $exception->getMessage()]);
|
|
|
|
return $this->result(
|
|
VatIdCheckStatus::Unverified,
|
|
__('Format plausibel. Die Online-Prüfung (eVatR) ist derzeit nicht erreichbar.'),
|
|
);
|
|
}
|
|
|
|
if (! $response->successful()) {
|
|
return $this->result(
|
|
VatIdCheckStatus::Unverified,
|
|
__('Format plausibel. Die Online-Prüfung (eVatR) ist derzeit nicht verfügbar.'),
|
|
);
|
|
}
|
|
|
|
$status = (string) $response->json('status');
|
|
|
|
$result = $status === self::STATUS_VALID
|
|
? $this->result(VatIdCheckStatus::Valid, __('USt-ID per eVatR (BZSt) bestätigt — gültig.'))
|
|
: $this->result(
|
|
VatIdCheckStatus::Invalid,
|
|
__('Die USt-ID wurde von eVatR (BZSt) nicht bestätigt (Status :status).', ['status' => $status ?: 'unbekannt']),
|
|
);
|
|
|
|
// Nur definitive Ergebnisse cachen — Ausfälle der API sollen beim
|
|
// nächsten Versuch erneut geprüft werden.
|
|
Cache::put($cacheKey, $result, now()->addHours(6));
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @return array{status: VatIdCheckStatus, message: string}
|
|
*/
|
|
private function result(VatIdCheckStatus $status, string $message): array
|
|
{
|
|
return ['status' => $status, 'message' => $message];
|
|
}
|
|
}
|