presseportale/app/Services/Billing/VatResolver.php
Kevin Adametz 894a9436b0 USt-Behandlung: Netto-Preise, VatResolver und Steuer-Ausweis im MAN-Kreis
Einwand/Entscheidung 12.06.2026: Legacy fakturierte brutto (Steuer
inkludiert, z. B. 199 Euro; steuerbefreite Kunden mit Netto-Ausweis
167,23). Alle neuen Preise sind netto; die Steuer wird zur
Rechnungsstellung sauber validiert und ausgewiesen.

- VatResolver + VatTreatment: DE grundsaetzlich immer mit Steuer, EU nur
  mit (formal plausibler) USt-ID befreit (Reverse Charge inkl.
  Pflichthinweis), Drittlaender grundsaetzlich befreit;
  EU-Laenderliste + vat_rate in config/billing.php
- Schema: billing_addresses.vat_id + invoice_billing_addresses.vat_id
  (Snapshot pro Rechnung), invoices.tax_note; Profil-Formular schreibt
  die vorhandene USt-ID jetzt auch an die Rechnungsadresse
- ManualInvoiceService: rechnet auf Netto-Vertragsbasis
  (legacy_conditions.net_cents bzw. Netto-Katalogpreis) und bestimmt
  Steuer/is_netto/tax_note pro Rechnung ueber den VatResolver
- legacy:grandfather-subscriptions: leitet net_cents aus der letzten
  Legacy-Rechnung ab (brutto / 1,19 bzw. is_netto-Betrag direkt);
  fuer DE-Bestandskunden bleibt der Bruttobetrag unveraendert
  (199 brutto -> 167,23 netto + 31,77 USt = 199,00)
- Doku: Decision-Update 2.1 (Netto-Klarstellung), Phase-9-Plan,
  Checkliste, 05-DATABASE-MERGE 5.6; offen: VIES-Validierung der USt-ID

Tests: VatResolverTest (Datasets fuer alle Faelle), Reverse-Charge/
EU-/Drittland-Rechnungen, Netto-Ableitung; Suite 490 passed, 4 skipped.
Pint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:58:43 +00:00

66 lines
2.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\Billing;
use App\Enums\VatTreatment;
/**
* Bestimmt die USt-Behandlung einer Rechnung anhand der Rechnungsadresse
* (Entscheidung 12.06.2026):
*
* - Deutschland → grundsätzlich immer mit Steuer (`billing.vat_rate`)
* - EU-Ausland → nur mit gültiger USt-ID befreit (Reverse Charge),
* sonst mit Steuer
* - Drittland → grundsätzlich steuerbefreit
*
* „Gültig" heißt aktuell: USt-ID vorhanden und formal plausibel
* (Ländercode + Kennziffer). Eine echte VIES-Validierung ist als
* Folgeschritt vorgesehen (siehe Phase-9-Plan).
*/
class VatResolver
{
public function resolve(?string $countryCode, ?string $vatId = null): VatTreatment
{
$countryCode = strtoupper((string) $countryCode);
if ($countryCode === '' || $countryCode === 'DE') {
return VatTreatment::Domestic;
}
if (! in_array($countryCode, (array) config('billing.eu_country_codes', []), true)) {
return VatTreatment::ThirdCountry;
}
return $this->isPlausibleVatId($vatId, $countryCode)
? VatTreatment::ReverseCharge
: VatTreatment::EuConsumer;
}
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 213 alphanumerische Zeichen (EU-Formatrahmen).
*/
private 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);
}
}