presseportale/tests/Feature/Billing/ExpireGrandfatheringTest.php
Kevin Adametz 375f4dffce billing/dev: Vertragsende in der MAN-Abrechnung, Rechnungslauf ohne Tageskappe, Qualitätsgates (Audit Block 5+6, Deploy E)
Block 5 — MAN-Abrechnung

F06 (E6): Ein explizites Legacy-Vertragsende (grandfathered_until) deckelt
jetzt Auswahl und Sperr-Nachprüfung: Beginnt die nächste Periode nach dem
Vertragsende, gibt es keine Rechnung mehr. Neuer täglicher Befehl
billing:expire-grandfathering (04:15, vor dem Rechnungslauf) setzt
abgelaufene Posten auf gekündigt und schickt je Kunde eine Mail
(GrandfatheringExpired). Auf der Dev-Kopie trägt kein Posten ein
Vertragsende; der Fehler war latent.

F16: Nicht abrechenbare Vereinbarungen bekommen einen Sperrvermerk
(billing_hold_reason/-at), sichtbar auf der Admin-Zahlungsseite, und
stehen im Rechnungslauf hinten an. Der Lauf arbeitet den fälligen Bestand
in Seiten ab; --limit deckelt die erstellten Rechnungen (500), nicht die
betrachteten Fälle. Vorher blockierten 50 unvollständige Fälle die 51.
dauerhaft, und der Durchsatz war auf 50 am Tag begrenzt.

Block 6 — Entwicklung

F24: Lint-Workflow prüft mit pint --test statt zu formatieren; keine
Schreibrechte mehr.
F27: composer run dev startet beide Vite-Server (npm run dev:all).
F28: docs/README.md als historischer Stand markiert, toter Link auf das
Hub-Flux-Archiv korrigiert, aktueller Einstieg dev/README.md.
F26: Login-Modal übernimmt den Fokus, hält Tab im Dialog und gibt den
Fokus beim Schließen an den Auslöser zurück – ohne neue Abhängigkeit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:41:04 +00:00

61 lines
2.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Console\Commands\ExpireGrandfathering;
use App\Enums\UserPaymentOptionStatus;
use App\Mail\GrandfatheringExpired;
use App\Models\BillingAddress;
use App\Models\PaymentOption;
use App\Models\User;
use App\Models\UserPaymentOption;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
/**
* Explizites Legacy-Vertragsende (Audit F06, Plan 5.2): Bestandsschutz mit
* überschrittenem `grandfathered_until` wird beendet und der Kunde
* informiert – bisher passierte das nur manuell über die Mahnung.
*/
function grandfatheredAgreement(?string $until = null, array $overrides = []): UserPaymentOption
{
$user = User::factory()->create();
BillingAddress::factory()->create(['user_id' => $user->id]);
return UserPaymentOption::factory()->create([
'user_id' => $user->id,
'payment_option_id' => PaymentOption::factory()->create()->id,
'status' => UserPaymentOptionStatus::Grandfathered->value,
'stripe_subscription_id' => null,
'grandfathered_until' => $until,
'cancelled_at' => null,
...$overrides,
]);
}
test('grandfathering past its contract end is ended and the customer is told once', function () {
/** @var TestCase $this */
Mail::fake();
Carbon::setTestNow('2026-09-18 04:15:00');
$expired = grandfatheredAgreement('2026-09-17');
$endsToday = grandfatheredAgreement('2026-09-18');
$open = grandfatheredAgreement(null);
$alreadyCancelled = grandfatheredAgreement('2026-09-01', ['status' => UserPaymentOptionStatus::Cancelled->value, 'cancelled_at' => now()->subDay()]);
$this->artisan(ExpireGrandfathering::class)->assertSuccessful();
expect($expired->fresh()->status)->toBe(UserPaymentOptionStatus::Cancelled)
->and($expired->fresh()->cancelled_at)->not->toBeNull()
->and($endsToday->fresh()->status)->toBe(UserPaymentOptionStatus::Grandfathered)
->and($open->fresh()->status)->toBe(UserPaymentOptionStatus::Grandfathered);
Mail::assertQueued(GrandfatheringExpired::class, fn (GrandfatheringExpired $mail) => $mail->user->is($expired->user));
Mail::assertQueuedCount(1);
// Wiederholung ist wirkungslos.
$this->artisan(ExpireGrandfathering::class)->assertSuccessful();
Mail::assertQueuedCount(1);
expect($alreadyCancelled->fresh()->cancelled_at->toDateString())->toBe('2026-09-17');
Carbon::setTestNow();
});