[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], ]);