create([ 'user_id' => $user->id, 'status' => UserPaymentOptionStatus::Grandfathered->value, 'stripe_subscription_id' => null, 'cancelled_at' => null, 'legacy_conditions' => [ 'legacy_portal' => $portal, 'net_cents' => $netCents, 'name' => 'Legacy '.$portal, 'interval' => 'yearly', ], ]); } // ===== M1: portalübergreifend ===== test('grandfathered bestandsschutz counts as booking regardless of active portal', function () { /** @var TestCase $this */ config()->set('billing.enforce_booking', true); $user = User::factory()->create(); grandfatheredPost($user, Portal::Presseecho->value); // Aktiver Portal-Kontext businessportal24 darf den presseecho-Posten nicht ausschließen. CurrentPortalContext::set(Portal::Businessportal24); expect($user->hasActiveBooking())->toBeTrue(); CurrentPortalContext::clear(); expect($user->fresh()->hasActiveBooking())->toBeTrue(); }); // ===== M2: gekoppelte Kündigung ===== test('cancelling a bundle cancels all grandfathered posts of the customer together', function () { /** @var TestCase $this */ $user = User::factory()->create(); $a = grandfatheredPost($user, Portal::Presseecho->value); $b = grandfatheredPost($user, Portal::Businessportal24->value); $count = app(LegacySubscriptionService::class)->cancelBundleFor($user); expect($count)->toBe(2); expect($a->fresh()->status)->toBe(UserPaymentOptionStatus::Cancelled); expect($b->fresh()->status)->toBe(UserPaymentOptionStatus::Cancelled); expect($a->fresh()->cancelled_at)->not->toBeNull(); }); test('cancelling a bundle leaves other customers untouched', function () { /** @var TestCase $this */ $user = User::factory()->create(); $other = User::factory()->create(); grandfatheredPost($user, Portal::Presseecho->value); $otherPost = grandfatheredPost($other, Portal::Presseecho->value); app(LegacySubscriptionService::class)->cancelBundleFor($user); expect($otherPost->fresh()->status)->toBe(UserPaymentOptionStatus::Grandfathered); }); test('a cancelled bundle no longer counts as an active booking', function () { /** @var TestCase $this */ config()->set('billing.enforce_booking', true); $user = User::factory()->create(); grandfatheredPost($user, Portal::Presseecho->value); app(LegacySubscriptionService::class)->cancelBundleFor($user); expect($user->fresh()->hasActiveBooking())->toBeFalse(); }); // ===== M3: Admin-Sichtbarkeit + Bündel-Kündigung über die UI ===== test('the payments page lists bestandsschutz posts and cancels a whole bundle', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $customer = User::factory()->create(['name' => 'Bestandskunde Test', 'is_active' => true]); grandfatheredPost($customer, Portal::Presseecho->value); grandfatheredPost($customer, Portal::Businessportal24->value); Volt::actingAs($admin) ->test('admin.payments.index') ->assertOk() ->assertSee('Bestandskunde Test') ->assertSee('Bestandsschutz') ->call('cancelBundle', $customer->id) ->assertHasNoErrors(); $stillOpen = UserPaymentOption::query() ->where('user_id', $customer->id) ->where('status', UserPaymentOptionStatus::Grandfathered->value) ->count(); expect($stillOpen)->toBe(0); });