'presseecho', 'legacy_id' => ++$legacyId, 'user_id' => $user->id, 'legacy_user_id' => 90000 + $legacyId, 'number' => 'PE-'.$legacyId, 'amount_cents' => 4900, 'tax_cents' => 0, 'total_cents' => 4900, 'status' => 'paid', 'invoice_date' => '2025-08-01', 'raw_snapshot' => [ 'is_netto' => false, 'service_period_begin_date' => '2025-08-01', 'service_period_end_date' => '2026-07-31', ], 'pdf_payload' => [ 'user_payment_option' => [ 'id' => 42, 'status' => 'active', 'next_due_date' => '2026-08-01', 'valid_until_date' => null, 'payment_option_id' => 1, ], 'payment_option' => [ 'id' => 1, 'type' => 'recurring', 'article_number' => 'PK-01', ], 'payment_option_translation' => ['name' => 'Pressemappe klein'], ], 'imported_at' => now(), ]; return LegacyInvoice::query()->create(array_replace_recursive($defaults, $overrides)); } beforeEach(function (): void { Carbon::setTestNow('2026-06-12 09:00:00'); }); afterEach(function (): void { Carbon::setTestNow(); }); test('an active recurring legacy agreement is migrated as grandfathered with the legacy rhythm', function () { $user = User::factory()->create(); legacyArchiveInvoice($user); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); $agreement = UserPaymentOption::sole(); expect($agreement->user_id)->toBe($user->id); expect($agreement->status)->toBe(UserPaymentOptionStatus::Grandfathered); expect($agreement->stripe_subscription_id)->toBeNull(); expect($agreement->current_period_start->toDateString())->toBe('2025-08-01'); expect($agreement->current_period_end->toDateString())->toBe('2026-08-01'); expect($agreement->legacy_conditions['interval'])->toBe('yearly'); // Legacy fakturierte brutto: 49,00 € inkl. USt → Netto-Basis 41,18 €. expect($agreement->legacy_conditions['net_cents'])->toBe(4118); expect($agreement->legacy_conditions['last_total_cents'])->toBe(4900); expect($agreement->legacy_conditions['legacy_user_payment_option_id'])->toBe(42); $catalog = PaymentOption::query()->where('article_number', 'LEGACY-PE-PK-01')->sole(); expect($catalog->is_hidden)->toBeTrue(); }); test('single-type and inactive legacy agreements stay in the archive', function () { $user = User::factory()->create(); legacyArchiveInvoice($user, [ 'pdf_payload' => ['payment_option' => ['type' => 'single']], ]); legacyArchiveInvoice($user, [ 'pdf_payload' => ['user_payment_option' => ['id' => 43, 'status' => 'canceled']], ]); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); expect(UserPaymentOption::count())->toBe(0); }); test('the latest invoice per agreement wins', function () { $user = User::factory()->create(); legacyArchiveInvoice($user, [ 'invoice_date' => '2024-08-01', 'total_cents' => 3900, 'amount_cents' => 3900, 'raw_snapshot' => ['service_period_begin_date' => '2024-08-01', 'service_period_end_date' => '2025-07-31'], 'pdf_payload' => ['user_payment_option' => ['next_due_date' => '2025-08-01']], ]); legacyArchiveInvoice($user); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); $agreement = UserPaymentOption::sole(); expect($agreement->current_period_end->toDateString())->toBe('2026-08-01'); expect($agreement->legacy_conditions['last_total_cents'])->toBe(4900); }); test('a legacy net invoice keeps its amount as the net base', function () { $user = User::factory()->create(); legacyArchiveInvoice($user, [ 'amount_cents' => 16723, 'total_cents' => 16723, 'raw_snapshot' => ['is_netto' => true], ]); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); expect(UserPaymentOption::sole()->legacy_conditions['net_cents'])->toBe(16723); }); test('re-running updates the agreement instead of duplicating it (pre-relaunch replay)', function () { $user = User::factory()->create(); legacyArchiveInvoice($user); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); // Kurz vor dem Relaunch kommt eine neuere Rechnung in den Snapshot. legacyArchiveInvoice($user, [ 'invoice_date' => '2026-06-10', 'raw_snapshot' => ['service_period_begin_date' => '2026-06-10', 'service_period_end_date' => '2027-06-09'], 'pdf_payload' => ['user_payment_option' => ['next_due_date' => '2027-06-10']], ]); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); $agreement = UserPaymentOption::sole(); expect($agreement->current_period_end->toDateString())->toBe('2027-06-10'); }); test('agreements overdue beyond the grace window are skipped as stale', function () { $user = User::factory()->create(); legacyArchiveInvoice($user, [ 'invoice_date' => '2023-08-01', 'raw_snapshot' => ['service_period_begin_date' => '2023-08-01', 'service_period_end_date' => '2024-07-31'], 'pdf_payload' => ['user_payment_option' => ['next_due_date' => '2024-08-01']], ]); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); expect(UserPaymentOption::count())->toBe(0); }); test('dry-run writes nothing', function () { legacyArchiveInvoice(User::factory()->create()); $this->artisan(GrandfatherLegacySubscriptions::class, ['--dry-run' => true, '--no-report' => true]) ->assertSuccessful(); expect(UserPaymentOption::count())->toBe(0); expect(PaymentOption::query()->where('article_number', 'like', 'LEGACY-%')->count())->toBe(0); }); test('after migration the MAN circle invoices a due legacy agreement with proper VAT', function () { $user = User::factory()->create(); BillingAddress::factory()->create(['user_id' => $user->id, 'country_code' => 'DE']); // Fällig: next_due_date liegt vor dem Stichtag (überfälliger Jahreskunde, // Legacy-Brutto 199,00 €). legacyArchiveInvoice($user, [ 'invoice_date' => '2025-05-14', 'amount_cents' => 19900, 'tax_cents' => 0, 'total_cents' => 19900, 'raw_snapshot' => ['service_period_begin_date' => '2025-05-14', 'service_period_end_date' => '2026-05-13'], 'pdf_payload' => ['user_payment_option' => ['next_due_date' => '2026-05-14']], ]); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); $service = app(ManualInvoiceService::class); $due = $service->duePaymentOptions(); expect($due)->toHaveCount(1); $invoice = $service->invoiceFor($due->first()); // DE-Kunde: Netto 167,23 € + 19 % USt = 199,00 € — Brutto bleibt wie im // Legacy, die Steuer wird jetzt aber sauber ausgewiesen. expect($invoice->number)->toBe('MAN-00001'); expect($invoice->amount_cents)->toBe(16723); expect($invoice->tax_cents)->toBe(3177); expect($invoice->total_cents)->toBe(19900); expect($invoice->is_netto)->toBeFalse(); // Periode jährlich weitergeschaltet. expect($due->first()->fresh()->current_period_end->toDateString())->toBe('2027-05-14'); expect(Invoice::count())->toBe(1); }); test('a never-paid (hold) latest invoice yields no grandfathering', function () { $user = User::factory()->create(); legacyArchiveInvoice($user, ['status' => 'hold']); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true]) ->expectsOutputToContain('Übersprungen (hold, nie bezahlt)') ->assertSuccessful(); expect(UserPaymentOption::count())->toBe(0); }); test('an open (reminder) latest invoice is grandfathered with the unpaid period due and the MAN circle re-issues it', function () { $user = User::factory()->create(); BillingAddress::factory()->create(['user_id' => $user->id, 'country_code' => 'DE']); // Legacy stellte am 30.06.2026 die Periode 14.06.2026–13.06.2027 in // Rechnung (199,00 € brutto), im Altsystem bereits gemahnt, nie bezahlt. $legacy = legacyArchiveInvoice($user, [ 'number' => 'BP-42489-10', 'legacy_portal' => 'businessportal24', 'status' => 'reminder', 'invoice_date' => '2026-06-30', 'total_cents' => 19900, 'raw_snapshot' => ['service_period_begin_date' => '2026-06-14', 'service_period_end_date' => '2027-06-13'], 'pdf_payload' => ['user_payment_option' => ['next_due_date' => '2027-06-14']], ]); Carbon::setTestNow('2026-09-16 09:00:00'); $this->artisan(GrandfatherLegacySubscriptions::class, ['--no-report' => true])->assertSuccessful(); $agreement = UserPaymentOption::sole(); expect($agreement->status)->toBe(UserPaymentOptionStatus::Grandfathered); // Fällig ist der Leistungsbeginn der unbezahlten Rechnung, nicht 2027. expect($agreement->current_period_end->toDateString())->toBe('2026-06-14'); expect($agreement->current_period_start->toDateString())->toBe('2025-06-14'); expect($agreement->legacy_conditions['open_legacy_invoice']['number'])->toBe('BP-42489-10'); $service = app(ManualInvoiceService::class); $invoice = $service->invoiceFor($agreement); expect($invoice)->not->toBeNull(); expect($invoice->replaces_legacy_invoice_number)->toBe('BP-42489-10'); expect($invoice->user_payment_option_id)->toBe($agreement->id); expect($invoice->total_cents)->toBe(19900); expect($agreement->fresh()->current_period_end->toDateString())->toBe('2027-06-14'); expect($agreement->fresh()->legacy_conditions)->not->toHaveKey('open_legacy_invoice'); expect($legacy->fresh()->status)->toBe(LegacyInvoice::STATUS_SUPERSEDED); expect($legacy->fresh()->raw_snapshot['superseded_by_invoice_number'])->toBe($invoice->number); });