Rechnungsadressen der Bestandskunden aus dem Legacy-Archiv uebernehmen
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>
This commit is contained in:
parent
387d41a782
commit
3320aaaa85
7 changed files with 538 additions and 0 deletions
78
app/Console/Commands/ImportLegacyBillingAddresses.php
Normal file
78
app/Console/Commands/ImportLegacyBillingAddresses.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Billing\LegacyBillingAddressImporter;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Übernimmt die Rechnungsadressen der Bestandskunden aus dem Rechnungsarchiv
|
||||
* (Befund 06.07.2026: ohne Adresse überspringt der MAN-Rechnungslauf die
|
||||
* Vereinbarung und der Bestandsschutz läuft am Periodenende aus). Läuft
|
||||
* NACH legacy:grandfather-subscriptions; die Legacy-Verbindungen werden nur
|
||||
* für die Länderauflösung (country_id → Name) gebraucht. Wiederholbar:
|
||||
* bestehende Adressen werden nie überschrieben.
|
||||
*
|
||||
* Verwendung:
|
||||
* php artisan legacy:import-billing-addresses --dry-run
|
||||
* php artisan legacy:import-billing-addresses
|
||||
*/
|
||||
class ImportLegacyBillingAddresses extends Command
|
||||
{
|
||||
protected $signature = 'legacy:import-billing-addresses
|
||||
{--dry-run : Nur anzeigen, nichts schreiben}';
|
||||
|
||||
protected $description = 'Übernimmt Rechnungsadressen der grandfathered Bestandskunden aus dem Legacy-Rechnungsarchiv.';
|
||||
|
||||
private const CONNECTIONS = [
|
||||
'presseecho' => 'mysql_presseecho',
|
||||
'businessportal24' => 'mysql_businessportal',
|
||||
];
|
||||
|
||||
public function handle(LegacyBillingAddressImporter $importer): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('[DRY-RUN] Kein tatsächlicher Schreibvorgang.');
|
||||
}
|
||||
|
||||
$countryNamesByPortal = [];
|
||||
|
||||
foreach (self::CONNECTIONS as $portal => $connection) {
|
||||
$countryNamesByPortal[$portal] = DB::connection($connection)
|
||||
->table('country')
|
||||
->pluck('name', 'id')
|
||||
->map(fn ($name): string => (string) $name)
|
||||
->all();
|
||||
}
|
||||
|
||||
$report = $importer->import($countryNamesByPortal, $dryRun);
|
||||
|
||||
$this->table(
|
||||
['Importiert', 'Adresse vorhanden', 'Nicht parsebar', 'Land unbekannt', 'Quell-Rechnung fehlt'],
|
||||
[[
|
||||
$report['imported'],
|
||||
$report['skipped_existing'],
|
||||
count($report['unparseable']),
|
||||
count($report['unknown_country']),
|
||||
count($report['missing_invoice']),
|
||||
]],
|
||||
);
|
||||
|
||||
foreach ($report['unparseable'] as $line) {
|
||||
$this->warn("Manuell nachpflegen (Adresse nicht parsebar): {$line}");
|
||||
}
|
||||
|
||||
foreach ($report['unknown_country'] as $line) {
|
||||
$this->warn("Manuell nachpflegen (Land nicht zuordenbar): {$line}");
|
||||
}
|
||||
|
||||
foreach ($report['missing_invoice'] as $email) {
|
||||
$this->warn("Manuell prüfen (Quell-Rechnung nicht im Archiv): {$email}");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
129
app/Services/Billing/LegacyAddressParser.php
Normal file
129
app/Services/Billing/LegacyAddressParser.php
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
161
app/Services/Billing/LegacyBillingAddressImporter.php
Normal file
161
app/Services/Billing/LegacyBillingAddressImporter.php
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Billing;
|
||||
|
||||
use App\Enums\UserPaymentOptionStatus;
|
||||
use App\Models\BillingAddress;
|
||||
use App\Models\LegacyInvoice;
|
||||
use App\Models\UserPaymentOption;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Übernimmt die Rechnungsadressen der Bestandskunden (grandfathered) aus dem
|
||||
* Rechnungsarchiv nach `billing_addresses` — ohne Adresse überspringt der
|
||||
* MAN-Rechnungslauf die Vereinbarung und der Bestandsschutz läuft am
|
||||
* Periodenende faktisch aus (Befund 06.07.2026).
|
||||
*
|
||||
* Quelle ist die Quell-Rechnung der Grandfather-Migration
|
||||
* (`legacy_conditions.source_invoice_number` → `legacy_invoices.pdf_payload.
|
||||
* billing_address`). Die Alt-Adresse ist ein Freitext-Blob und wird per
|
||||
* LegacyAddressParser strukturiert; Unparsebares landet im Report zur
|
||||
* manuellen Nachpflege. Bestehende Adressen werden nie überschrieben.
|
||||
*/
|
||||
class LegacyBillingAddressImporter
|
||||
{
|
||||
/**
|
||||
* Legacy-`country.name` (englisch) → ISO-3166-1-alpha-2. Nicht gelistete
|
||||
* Länder landen im Report (`unknown_country`).
|
||||
*/
|
||||
private const COUNTRY_NAME_TO_ISO = [
|
||||
'Germany' => 'DE',
|
||||
'Austria' => 'AT',
|
||||
'Switzerland' => 'CH',
|
||||
'Liechtenstein' => 'LI',
|
||||
'Luxembourg' => 'LU',
|
||||
'Belgium' => 'BE',
|
||||
'Netherlands' => 'NL',
|
||||
'France' => 'FR',
|
||||
'Italy' => 'IT',
|
||||
'Spain' => 'ES',
|
||||
'Portugal' => 'PT',
|
||||
'Poland' => 'PL',
|
||||
'Czech Republic' => 'CZ',
|
||||
'Denmark' => 'DK',
|
||||
'Sweden' => 'SE',
|
||||
'Norway' => 'NO',
|
||||
'Finland' => 'FI',
|
||||
'Ireland' => 'IE',
|
||||
'Greece' => 'GR',
|
||||
'Hungary' => 'HU',
|
||||
'Cyprus' => 'CY',
|
||||
'United Kingdom' => 'GB',
|
||||
'United States' => 'US',
|
||||
'India' => 'IN',
|
||||
];
|
||||
|
||||
public function __construct(private readonly LegacyAddressParser $parser) {}
|
||||
|
||||
/**
|
||||
* @param array<string, array<int, string>> $countryNamesByPortal Portal → [legacy country_id → englischer Name]
|
||||
* @return array{imported: int, skipped_existing: int, unparseable: list<string>, unknown_country: list<string>, missing_invoice: list<string>}
|
||||
*/
|
||||
public function import(array $countryNamesByPortal, bool $dryRun = false): array
|
||||
{
|
||||
$report = [
|
||||
'imported' => 0,
|
||||
'skipped_existing' => 0,
|
||||
'unparseable' => [],
|
||||
'unknown_country' => [],
|
||||
'missing_invoice' => [],
|
||||
];
|
||||
|
||||
$optionsByUser = UserPaymentOption::query()
|
||||
->where('status', UserPaymentOptionStatus::Grandfathered->value)
|
||||
->with('user.billingAddress')
|
||||
->get()
|
||||
->filter(fn (UserPaymentOption $option): bool => $option->user !== null)
|
||||
->groupBy('user_id');
|
||||
|
||||
foreach ($optionsByUser as $options) {
|
||||
$user = $options->first()->user;
|
||||
|
||||
if ($user->billingAddress) {
|
||||
$report['skipped_existing']++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$invoice = $this->latestSourceInvoice($options);
|
||||
|
||||
if (! $invoice) {
|
||||
$report['missing_invoice'][] = $user->email;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$legacyAddress = $invoice->pdf_payload['billing_address'] ?? [];
|
||||
$portal = $invoice->legacy_portal->value;
|
||||
$countryName = $countryNamesByPortal[$portal][(int) ($legacyAddress['country_id'] ?? 0)] ?? null;
|
||||
$countryCode = self::COUNTRY_NAME_TO_ISO[$countryName] ?? null;
|
||||
|
||||
if ($countryCode === null) {
|
||||
$report['unknown_country'][] = sprintf('%s (country_id %s)', $user->email, $legacyAddress['country_id'] ?? '—');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsed = $this->parser->parse($legacyAddress['address'] ?? null, $countryName);
|
||||
|
||||
if ($parsed === null) {
|
||||
$report['unparseable'][] = sprintf('%s ("%s")', $user->email, str_replace(["\r\n", "\n"], ' / ', trim((string) ($legacyAddress['address'] ?? ''))));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $dryRun) {
|
||||
BillingAddress::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'name' => (string) ($legacyAddress['name'] ?? $user->name),
|
||||
'company' => $parsed['company'],
|
||||
'address1' => $parsed['address1'],
|
||||
'address2' => $parsed['address2'],
|
||||
'postal_code' => $parsed['postal_code'],
|
||||
'city' => $parsed['city'],
|
||||
'country_code' => $countryCode,
|
||||
]);
|
||||
}
|
||||
|
||||
$report['imported']++;
|
||||
}
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jüngste Quell-Rechnung über alle Vereinbarungen des Users — hat er
|
||||
* mehrere Pakete (z. B. je Spoke eines), zählt der neueste Adress-Stand.
|
||||
*
|
||||
* @param Collection<int, UserPaymentOption> $options
|
||||
*/
|
||||
private function latestSourceInvoice(Collection $options): ?LegacyInvoice
|
||||
{
|
||||
return $options
|
||||
->map(function (UserPaymentOption $option): ?LegacyInvoice {
|
||||
$conditions = $option->legacy_conditions ?? [];
|
||||
$number = $conditions['source_invoice_number'] ?? null;
|
||||
$portal = $conditions['legacy_portal'] ?? null;
|
||||
|
||||
if (! $number || ! $portal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return LegacyInvoice::query()
|
||||
->where('legacy_portal', $portal)
|
||||
->where('number', $number)
|
||||
->first();
|
||||
})
|
||||
->filter()
|
||||
->sortByDesc(fn (LegacyInvoice $invoice) => [$invoice->invoice_date, $invoice->id])
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,5 +14,7 @@ return [
|
|||
'FR' => 'Frankreich',
|
||||
'GB' => 'Vereinigtes Koenigreich',
|
||||
'US' => 'Vereinigte Staaten',
|
||||
// Zypern: reale Bestandskunden aus den Altportalen (Adress-Import 06.07.2026).
|
||||
'CY' => 'Zypern',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ Staging-Rehearsal (R6.3) mit echtem Datenbestand wiederholen.
|
|||
- [ ] **Stripe-Checkout End-to-End inkl. Webhook** — Automatisiert: `Billing/CheckoutFlowTest`, `Billing/StripeWebhookProcessingTest`, `CreditTopupTest` (Gutschrift genau einmal). Manuell auf Staging: echter Testmode-Checkout (Karte 4242…), Webhook via `stripe listen`, Buchung erscheint im Panel.
|
||||
- [ ] **Rechnungen**: STR-Kreis (Stripe/Billing-Portal) + MAN-Kreis (`billing:generate-manual-invoices`) — Automatisiert: `Billing/InvoiceNumberGeneratorTest`, `Billing/ManualInvoiceGenerationTest`. Manuell: Rechnung im Stripe-Billing-Portal abrufbar.
|
||||
- [ ] **Grandfathering**: Alt-Abo wird korrekt angezeigt/weiterberechnet, Bestandskunde sieht keinen Buchungs-Hinweis — Automatisiert: `Billing/GrandfatherLegacySubscriptionsTest`, `Billing/HasActiveBookingTest`, `Billing/LegacyBundleTest`. Manuell: mit echtem Grandfather-User nach dem Staging-Import (R6.3).
|
||||
- [ ] **Rechnungsadressen der Bestandskunden** (Befund 06.07.: ohne Adresse überspringt der MAN-Lauf die Vereinbarung → Bestandsschutz läuft am Periodenende aus) — Automatisiert: `Billing/LegacyBillingAddressImportTest`. Manuell nach Staging-Import: `legacy:import-billing-addresses --dry-run` prüfen, echter Lauf, dann die im Report gelisteten Kunden (leere/unparsebare Alt-Adressen) manuell nachpflegen oder anschreiben.
|
||||
- [ ] **Legacy-Rechnungen im Archiv** (Admin + Customer, PDF) — Automatisiert: `Admin/AdminLegacyInvoiceArchiveTest`, `LegacyInvoiceArchiveCommandTest`. Manuell: PDF-Download eines Altbelegs.
|
||||
- [ ] **Depublikations-Checkout** (35 € netto) — Automatisiert: `Billing/DepublicationTest`. Manuell auf Staging: einmal komplett bis Tombstone.
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,13 @@ php artisan billing:sync-stripe-plans
|
|||
php artisan legacy:grandfather-subscriptions --dry-run
|
||||
php artisan legacy:grandfather-subscriptions
|
||||
|
||||
# Rechnungsadressen der Bestandskunden aus dem Archiv übernehmen (Befund
|
||||
# 06.07.: ohne Adresse überspringt der MAN-Rechnungslauf die Vereinbarung und
|
||||
# der Bestandsschutz läuft am Periodenende aus). Läuft NACH dem Grandfathering.
|
||||
# Der Report listet manuell nachzupflegende Kunden (leere/unparsebare Alt-Adressen).
|
||||
php artisan legacy:import-billing-addresses --dry-run
|
||||
php artisan legacy:import-billing-addresses
|
||||
|
||||
# Favicons für alle Domains generieren
|
||||
php artisan domains:generate-favicons
|
||||
|
||||
|
|
|
|||
160
tests/Feature/Billing/LegacyBillingAddressImportTest.php
Normal file
160
tests/Feature/Billing/LegacyBillingAddressImportTest.php
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\UserPaymentOptionStatus;
|
||||
use App\Models\BillingAddress;
|
||||
use App\Models\LegacyInvoice;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPaymentOption;
|
||||
use App\Services\Billing\LegacyAddressParser;
|
||||
use App\Services\Billing\LegacyBillingAddressImporter;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
const COUNTRY_NAMES = [
|
||||
'presseecho' => [108 => 'Cyprus', 177 => 'Germany', 196 => 'Switzerland'],
|
||||
'businessportal24' => [108 => 'Cyprus', 177 => 'Germany', 196 => 'Switzerland'],
|
||||
];
|
||||
|
||||
function createGrandfatheredUserWithArchiveAddress(array $billingAddressPayload, string $portal = 'presseecho'): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
LegacyInvoice::query()->create([
|
||||
'legacy_portal' => $portal,
|
||||
'legacy_id' => fake()->unique()->numberBetween(1, 100000),
|
||||
'user_id' => $user->id,
|
||||
'legacy_user_id' => $user->id,
|
||||
'number' => 'LEG-'.fake()->unique()->numberBetween(1, 100000),
|
||||
'total_cents' => 119000,
|
||||
'status' => 'paid',
|
||||
'invoice_date' => '2025-06-15',
|
||||
'paid_at' => now()->subYear(),
|
||||
'imported_at' => now(),
|
||||
'pdf_payload' => ['billing_address' => $billingAddressPayload],
|
||||
]);
|
||||
|
||||
$invoiceNumber = LegacyInvoice::query()->where('user_id', $user->id)->value('number');
|
||||
|
||||
UserPaymentOption::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'status' => UserPaymentOptionStatus::Grandfathered->value,
|
||||
'grandfathered_until' => null,
|
||||
'stripe_subscription_id' => null,
|
||||
'legacy_conditions' => [
|
||||
'legacy_portal' => $portal,
|
||||
'source_invoice_number' => $invoiceNumber,
|
||||
'interval' => 'yearly',
|
||||
'net_cents' => 100000,
|
||||
],
|
||||
]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
test('importer creates a structured billing address from the legacy blob', function () {
|
||||
$user = createGrandfatheredUserWithArchiveAddress([
|
||||
'name' => 'Sebastian Einbock',
|
||||
'address' => "Einbock GmbH\r\nPrinzenstraße 1\r\n30419 Hannover",
|
||||
'country_id' => 177,
|
||||
]);
|
||||
|
||||
$report = app(LegacyBillingAddressImporter::class)->import(COUNTRY_NAMES);
|
||||
|
||||
expect($report['imported'])->toBe(1);
|
||||
|
||||
$address = BillingAddress::query()->where('user_id', $user->id)->first();
|
||||
|
||||
expect($address)->not->toBeNull()
|
||||
->and($address->name)->toBe('Sebastian Einbock')
|
||||
->and($address->company)->toBe('Einbock GmbH')
|
||||
->and($address->address1)->toBe('Prinzenstraße 1')
|
||||
->and($address->postal_code)->toBe('30419')
|
||||
->and($address->city)->toBe('Hannover')
|
||||
->and($address->country_code)->toBe('DE');
|
||||
});
|
||||
|
||||
test('importer never overwrites an existing billing address', function () {
|
||||
$user = createGrandfatheredUserWithArchiveAddress([
|
||||
'name' => 'Alt Name',
|
||||
'address' => "Altstraße 1\r\n11111 Altstadt",
|
||||
'country_id' => 177,
|
||||
]);
|
||||
|
||||
BillingAddress::factory()->create(['user_id' => $user->id, 'address1' => 'Vom Kunden gepflegt 5']);
|
||||
|
||||
$report = app(LegacyBillingAddressImporter::class)->import(COUNTRY_NAMES);
|
||||
|
||||
expect($report['imported'])->toBe(0)
|
||||
->and($report['skipped_existing'])->toBe(1)
|
||||
->and($user->billingAddress()->first()->address1)->toBe('Vom Kunden gepflegt 5');
|
||||
});
|
||||
|
||||
test('unparseable blobs and unknown countries are reported instead of guessed', function () {
|
||||
$empty = createGrandfatheredUserWithArchiveAddress([
|
||||
'name' => 'Leerer Blob', 'address' => '', 'country_id' => 177,
|
||||
]);
|
||||
$unknownCountry = createGrandfatheredUserWithArchiveAddress([
|
||||
'name' => 'Exot', 'address' => "Somewhere 1\r\n12345 Faraway", 'country_id' => 999,
|
||||
]);
|
||||
|
||||
$report = app(LegacyBillingAddressImporter::class)->import(COUNTRY_NAMES);
|
||||
|
||||
expect($report['imported'])->toBe(0)
|
||||
->and($report['unparseable'])->toHaveCount(1)
|
||||
->and($report['unparseable'][0])->toContain($empty->email)
|
||||
->and($report['unknown_country'])->toHaveCount(1)
|
||||
->and($report['unknown_country'][0])->toContain($unknownCountry->email)
|
||||
->and(BillingAddress::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('dry run reports without writing', function () {
|
||||
createGrandfatheredUserWithArchiveAddress([
|
||||
'name' => 'Trocken Lauf',
|
||||
'address' => "Teststraße 2\r\n22222 Testhausen",
|
||||
'country_id' => 177,
|
||||
]);
|
||||
|
||||
$report = app(LegacyBillingAddressImporter::class)->import(COUNTRY_NAMES, dryRun: true);
|
||||
|
||||
expect($report['imported'])->toBe(1)
|
||||
->and(BillingAddress::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('parser handles the address shapes found in the legacy archive', function (string $blob, ?string $countryName, ?array $expected) {
|
||||
$parsed = app(LegacyAddressParser::class)->parse($blob, $countryName);
|
||||
|
||||
if ($expected === null) {
|
||||
expect($parsed)->toBeNull();
|
||||
} else {
|
||||
expect($parsed)->toMatchArray($expected);
|
||||
}
|
||||
})->with([
|
||||
'mehrzeilig mit Firma' => [
|
||||
"Einbock GmbH\r\nPrinzenstraße 1\r\n30419 Hannover", 'Germany',
|
||||
['company' => 'Einbock GmbH', 'address1' => 'Prinzenstraße 1', 'postal_code' => '30419', 'city' => 'Hannover'],
|
||||
],
|
||||
'Adresszusatz vor PLZ-Zeile' => [
|
||||
"73, Makarios Avenue\r\n5th Floor\r\n1070 Nicosia", 'Cyprus',
|
||||
['company' => null, 'address1' => '73, Makarios Avenue', 'address2' => '5th Floor', 'postal_code' => '1070', 'city' => 'Nicosia'],
|
||||
],
|
||||
'Ansprechpartner statt Firma' => [
|
||||
"Waldemar Eider\r\nHauptstr.29\r\n67806 Bisterschied", 'Germany',
|
||||
['company' => null, 'address1' => 'Hauptstr.29', 'address2' => 'Waldemar Eider', 'postal_code' => '67806', 'city' => 'Bisterschied'],
|
||||
],
|
||||
'einzeilig mit Kommata' => [
|
||||
'Wotanstr. 16, 10365 Berlin', 'Germany',
|
||||
['company' => null, 'address1' => 'Wotanstr. 16', 'postal_code' => '10365', 'city' => 'Berlin'],
|
||||
],
|
||||
'einzeilig mit Landesname am Ende' => [
|
||||
'73 Makarios Avenue, 5th floor, 1070 Nicosia, Cyprus', 'Cyprus',
|
||||
['company' => null, 'address1' => '73 Makarios Avenue', 'address2' => '5th floor', 'postal_code' => '1070', 'city' => 'Nicosia'],
|
||||
],
|
||||
'Schweizer PLZ-Präfix' => [
|
||||
"Max-Högger-Strasse 6\r\nCH-8048 Zürich", 'Switzerland',
|
||||
['company' => null, 'address1' => 'Max-Högger-Strasse 6', 'postal_code' => '8048', 'city' => 'Zürich'],
|
||||
],
|
||||
'nur Straße ohne PLZ/Ort' => ['Christophstraße 15-17', 'Germany', null],
|
||||
'Platzhalter N/A' => ['N/A', 'Germany', null],
|
||||
'leer' => ['', 'Germany', null],
|
||||
]);
|
||||
Loading…
Add table
Add a link
Reference in a new issue