Fix: Bestandsschutz ohne explizites Vertragsende zaehlte nie als aktiv

Der grandfatheredNow-Scope verlangte ein gesetztes grandfathered_until —
das ist aber das explizite Legacy-Vertragsende (valid_until_date) und im
Import-Regelfall NULL (offene Dauervereinbarung). Ergebnis: praktisch
kein Legacy-Kunde hatte eine aktive Zahlungsoption; eingeloggte
Bestandskunden sahen "Buchung erforderlich" und konnten nicht einreichen.
Jetzt gilt: grandfathered_until deckelt, wenn gesetzt; sonst zaehlt die
laufende, vom MAN-Rechnungslauf fortgerollte Periode (current_period_end
= next_due_date). Tests decken beide NULL-Faelle und den Deckel ab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-06 11:40:39 +00:00
parent 6ef4a0d061
commit 3364fd13b8
2 changed files with 57 additions and 4 deletions

View file

@ -80,12 +80,26 @@ class UserPaymentOption extends Model
}
/**
* Laufender Bestandsschutz (Grandfathered mit gültigem grandfathered_until).
* Laufender Bestandsschutz. `grandfathered_until` ist das explizite
* Legacy-Vertragsende (valid_until_date) und deckelt, wenn gesetzt.
* Bei offenen Dauervereinbarungen ist es NULL (Regelfall im Import) —
* dann zählt die laufende, vom MAN-Rechnungslauf fortgerollte Periode
* (`current_period_end` = next_due_date). Fix 06.07.2026: vorher galt
* NULL pauschal als abgelaufen, wodurch praktisch kein Legacy-Kunde
* eine aktive Zahlungsoption hatte („Buchung erforderlich").
*/
public function scopeGrandfatheredNow(Builder $query): Builder
{
return $query
->where('status', UserPaymentOptionStatus::Grandfathered->value)
->whereDate('grandfathered_until', '>=', now());
->where(function (Builder $query): void {
$query
->whereDate('grandfathered_until', '>=', now())
->orWhere(function (Builder $rolling): void {
$rolling
->whereNull('grandfathered_until')
->whereDate('current_period_end', '>=', now());
});
});
}
}