'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); });