Carbon::setTestNow()); function overdueAbo(string $nextDate, int $status = 2, bool $active = true): UserAbo { return UserAbo::create([ 'user_id' => null, 'member_id' => 1, 'shopping_user_id' => 0, 'is_for' => 'ot', 'email' => 'abo-'.uniqid('', true).'@example.com', 'payone_userid' => 1, 'clearingtype' => 'cc', 'active' => $active, 'status' => $status, 'abo_interval' => (int) Carbon::parse($nextDate)->day, 'start_date' => '2026-01-01', 'next_date' => $nextDate, ]); } it('meldet laufende Abos mit verstrichenem Termin und bucht nichts nach', function () { $stuck = overdueAbo('2026-03-25'); overdueAbo('2026-09-23'); // heute faellig: laeuft regulaer overdueAbo('2026-10-05'); // Zukunft overdueAbo('2026-08-10', status: 3); // Zahlung fehlgeschlagen: eigener Zustand overdueAbo('2026-08-10', active: false); // inaktiv $this->artisan('abo:check-overdue') ->expectsOutputToContain('Überfällige Abos: 1') ->assertSuccessful(); Mail::assertSent(AboOverdueAlertMail::class, function (AboOverdueAlertMail $mail) use ($stuck) { $html = $mail->render(); return $mail->hasTo(config('app.default_mail')) && count($mail->overdueAbos) === 1 && str_contains($html, '#'.$stuck->id) && str_contains($html, '25.03.2026') && str_contains($html, '/admin/abos/detail/'.$stuck->id); }); expect(Carbon::parse($stuck->fresh()->getRawOriginal('next_date'))->toDateString())->toBe('2026-03-25') ->and($stuck->user_abo_orders()->count())->toBe(0); }); it('schickt ohne ueberfaellige Abos keine Mail', function () { overdueAbo('2026-10-05'); $this->artisan('abo:check-overdue')->expectsOutputToContain('Keine überfälligen Abos.')->assertSuccessful(); Mail::assertNothingSent(); }); it('schickt mit --no-mail nur die Ausgabe', function () { overdueAbo('2026-09-01'); $this->artisan('abo:check-overdue', ['--no-mail' => true])->assertSuccessful(); Mail::assertNothingSent(); });