Befund 06.07.: Der MAN-Rechnungslauf ueberspringt Vereinbarungen ohne Rechnungsadresse — der Bestandsschutz laeuft dann am Periodenende faktisch aus. Der Grandfather-Import nahm die Adressen bisher nicht mit, weil die Altportale sie nur als Freitext-Blob fuehren. - legacy:import-billing-addresses: uebernimmt Adressen der grandfathered Kunden aus der Quell-Rechnung im Archiv (pdf_payload.billing_address), nie ueberschreibend, mit Dry-Run und Report der manuell nachzupflegenden Faelle (leere/unparsebare Blobs, unbekannte Laender) - LegacyAddressParser: Best-Effort-Strukturierung der Blobs (mehrzeilig, kommasepariert, CH-PLZ-Praefix, Firmen-/Zusatzzeilen); raet nie - Laenderaufloesung ueber die country-Tabellen der Legacy-Verbindungen - config/countries.php: CY ergaenzt (reale Bestandskunden aus Zypern) - WS-7- und QA-Checkliste: Import-Schritt nach dem Grandfathering Lokal: 11 von 21 Bestandskunden importiert, 9 im Report (7 leere Blobs); MAN-Lauf rechnet zuvor blockierte Vereinbarungen jetzt ab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
129 lines
4.1 KiB
PHP
129 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Billing;
|
|
|
|
/**
|
|
* Zerlegt die unstrukturierten Adress-Blobs der Altportale (Freitextfeld
|
|
* `invoice_billing_address.address`, z. B. "Einbock GmbH\r\nPrinzenstraße 1\r\n
|
|
* 30419 Hannover") in die strukturierten Felder der neuen `billing_addresses`.
|
|
*
|
|
* Best-Effort: Kommt keine PLZ+Ort-Zeile vor (leerer Blob, "N/A", nur eine
|
|
* Straße), gibt `parse()` null zurück — solche Fälle landen im Report des
|
|
* Imports und müssen manuell nachgepflegt werden. Es wird nie geraten.
|
|
*/
|
|
class LegacyAddressParser
|
|
{
|
|
/**
|
|
* Firmenkennungen, an denen eine Zeile als Firmenname erkannt wird.
|
|
*/
|
|
private const COMPANY_PATTERN = '/\b(GmbH|mbH|AG|Ltd\.?|UG|KG|OHG|GbR|e\.\s?K\.|e\.?V\.?|Inc\.?|LLC|S\.?A\.?|Corp\.?)(\b|$)/iu';
|
|
|
|
/**
|
|
* PLZ+Ort-Zeile, optional mit Länderpräfix ("CH-8048 Zürich",
|
|
* "30419 Hannover", "1070 Nicosia").
|
|
*/
|
|
private const POSTAL_CITY_PATTERN = '/^(?:[A-Z]{1,3}[- ])?(\d{4,5})\s+(\S.*)$/u';
|
|
|
|
/**
|
|
* @return array{company: ?string, address1: string, address2: ?string, postal_code: string, city: string}|null
|
|
*/
|
|
public function parse(?string $blob, ?string $countryName = null): ?array
|
|
{
|
|
$lines = $this->splitIntoLines((string) $blob, $countryName);
|
|
|
|
if ($lines === []) {
|
|
return null;
|
|
}
|
|
|
|
// PLZ+Ort von hinten suchen — alles danach wurde bereits als
|
|
// Länderzeile aussortiert.
|
|
$postalIndex = null;
|
|
$postalCode = null;
|
|
$city = null;
|
|
|
|
for ($i = count($lines) - 1; $i >= 0; $i--) {
|
|
if (preg_match(self::POSTAL_CITY_PATTERN, $lines[$i], $matches) === 1) {
|
|
$postalIndex = $i;
|
|
$postalCode = $matches[1];
|
|
$city = trim($matches[2]);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($postalIndex === null || $postalIndex === 0) {
|
|
// Keine PLZ+Ort-Zeile oder nichts davor (keine Straße) → manuell.
|
|
return null;
|
|
}
|
|
|
|
$before = array_slice($lines, 0, $postalIndex);
|
|
|
|
$company = null;
|
|
|
|
foreach ($before as $index => $line) {
|
|
if (preg_match(self::COMPANY_PATTERN, $line) === 1) {
|
|
$company = $line;
|
|
unset($before[$index]);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$before = array_values($before);
|
|
|
|
if ($before === []) {
|
|
return null;
|
|
}
|
|
|
|
// Straße = erste Zeile mit Hausnummer (Ziffer); Zusätze wie
|
|
// "5th Floor" oder Ansprechpartner wandern nach address2.
|
|
$address1 = null;
|
|
|
|
foreach ($before as $index => $line) {
|
|
if (preg_match('/\d/', $line) === 1) {
|
|
$address1 = $line;
|
|
unset($before[$index]);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($address1 === null) {
|
|
$address1 = array_shift($before);
|
|
}
|
|
|
|
$address2 = $before === [] ? null : implode(', ', $before);
|
|
|
|
return [
|
|
'company' => $company,
|
|
'address1' => $address1,
|
|
'address2' => $address2,
|
|
'postal_code' => $postalCode,
|
|
'city' => $city,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Blob in bereinigte Zeilen zerlegen: Zeilenumbrüche, bei einzeiligen
|
|
* Blobs Kommata als Trenner; eine reine Länderzeile am Ende (z. B.
|
|
* "Cyprus") wird entfernt.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
private function splitIntoLines(string $blob, ?string $countryName): array
|
|
{
|
|
$lines = preg_split('/\R/u', trim($blob)) ?: [];
|
|
$lines = array_values(array_filter(array_map(trim(...), $lines), fn (string $line): bool => $line !== ''));
|
|
|
|
if (count($lines) === 1 && str_contains($lines[0], ',') && preg_match(self::POSTAL_CITY_PATTERN, $lines[0]) !== 1) {
|
|
$lines = array_values(array_filter(array_map(trim(...), explode(',', $lines[0])), fn (string $line): bool => $line !== ''));
|
|
}
|
|
|
|
if ($countryName !== null && $lines !== []
|
|
&& strcasecmp((string) end($lines), $countryName) === 0) {
|
|
array_pop($lines);
|
|
}
|
|
|
|
return $lines;
|
|
}
|
|
}
|