mivita/tests/Feature/AboDeliveryDayTest.php
Kevin cf0311afc1 Abo (A1+A2): Liefertag frei waehlbar 1-31, Aenderung nur nach vorn, Sperre erst am Ausfuehrungstag
A1: Jeder Monatstag ist als Liefertag erlaubt (UserAbo::allowedDeliveryDays),
Tage 29-31 liefern in kuerzeren Monaten am letzten Tag. Eine Stelle
AboHelper::dateForDayInMonth klammert; setNextDate, Erstausfuehrung,
Admin-Termin und Pause rechnen darueber, der Folgemonat immer aus dem
Liefertag (31.01 -> 28.02 -> 31.03). Liefertag-Aenderung gilt fuer den
laufenden Zyklus und nur nach vorn: ist der Tag im Referenzmonat vorbei,
wird abgelehnt, der Monat verschiebt sich nie (Rueckfrage R1).
Admin-Filter "naechste Ausfuehrung" als Datumsbereich. Prognose-Kacheln
bekommen die Kachel Monatsende, Team-Abos rechnen bis Monatsende.

A2: Sperrfristen Default 0, Setting 0 ist ausdruecklich 0 (fiel bisher auf
den Default zurueck). Bedingung Kalendertage <= Sperre, zentral in
AboHelper::isWithinLock: am Ausfuehrungstag ab 0:00 Uhr fixiert, Vortag bis
23:59 Uhr frei, fuer Liefertag, Produkte, Pause und Kuendigung (R2). Texte
mit _zero-Varianten ueber AboHelper::lockMessage.

Sprachdateien de/en/es/fr, neuer AboDeliveryDayTest, bestehende Tests
angepasst, Plan-Status mit Deploy-Schritt (Settings auf 0).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 11:28:55 +00:00

204 lines
9 KiB
PHP

<?php
use App\Models\Setting;
use App\Models\UserAbo;
use App\Repositories\AboRepository;
use App\Services\AboHelper;
use App\Services\HTMLHelper;
use App\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Tickets A1/A2 (Entwicklungsplan September 2026): jeder Monatstag 1-31 als
* Liefertag, Klammerung auf das Monatsende, Aenderung nur "nach vorn" im
* laufenden Zyklus (Rueckfrage R1), Sperre erst am Ausfuehrungstag ab 0:00 Uhr.
*/
uses(TestCase::class, RefreshDatabase::class);
afterEach(fn () => Carbon::setTestNow());
function addUser(): User
{
return User::forceCreate([
'email' => 'add-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
'm_level' => 1,
'payment_account' => now()->addYear(),
]);
}
function addAbo(User $user, int $day, string $nextDate): UserAbo
{
return UserAbo::create([
'user_id' => $user->id,
'member_id' => $user->id,
'shopping_user_id' => 0,
'email' => 'add-abo-'.uniqid('', true).'@example.com',
'is_for' => 'me',
'payone_userid' => 0,
'clearingtype' => 'cc',
'amount' => 5000,
'active' => true,
'status' => 2,
'abo_interval' => $day,
'start_date' => '2026-01-05',
'last_date' => null,
'next_date' => $nextDate,
]);
}
function addChange(UserAbo $abo, int $day, string $view = 'me'): bool
{
$repository = new AboRepository;
$repository->setModel($abo);
return $repository->update([
'action' => 'abo_update_settings',
'id' => $abo->id,
'view' => $view,
'abo_interval' => $day,
'abo_is_active' => 1,
]);
}
it('erlaubt jeden Monatstag 1-31 und lehnt alles andere ab', function () {
expect(UserAbo::allowedDeliveryDays())->toBe(range(1, 31))
->and(UserAbo::isAllowedDeliveryDay(1))->toBeTrue()
->and(UserAbo::isAllowedDeliveryDay('31'))->toBeTrue()
->and(UserAbo::isAllowedDeliveryDay(0))->toBeFalse()
->and(UserAbo::isAllowedDeliveryDay(32))->toBeFalse()
->and(UserAbo::isAllowedDeliveryDay('5.5'))->toBeFalse()
->and(UserAbo::isAllowedDeliveryDay(null))->toBeFalse();
});
it('bietet 31 Liefertage an und kennzeichnet 29-31 als letzter Tag in kuerzeren Monaten', function () {
Carbon::setTestNow('2026-09-16');
$options = HTMLHelper::getAboDeliveryOptions(31);
expect(substr_count($options, '<option'))->toBe(31)
->and($options)->toContain('value="1"')
->and($options)->toContain('value="31" data-days="45" data-date="31.10.2026" selected="selected"')
->and(HTMLHelper::getAboStrLang(31))->toBe('31. '.__('abo.of_month').' '.__('abo.of_month_short_hint'))
->and(HTMLHelper::getAboStrLang(28))->toBe('28. '.__('abo.of_month'));
});
it('klammert Tag 29-31 auf das Monatsende und rechnet den Folgemonat wieder aus dem Liefertag', function () {
expect(AboHelper::dateForDayInMonth('2026-02-01', 31)->format('Y-m-d'))->toBe('2026-02-28')
->and(AboHelper::dateForDayInMonth('2028-02-01', 30)->format('Y-m-d'))->toBe('2028-02-29')
->and(AboHelper::dateForDayInMonth('2026-04-15', 31)->format('Y-m-d'))->toBe('2026-04-30')
->and(AboHelper::dateForDayInMonth('2026-06-01', 31)->format('Y-m-d'))->toBe('2026-06-30')
->and(AboHelper::dateForDayInMonth('2026-09-01', 31)->format('Y-m-d'))->toBe('2026-09-30')
->and(AboHelper::dateForDayInMonth('2026-11-01', 30)->format('Y-m-d'))->toBe('2026-11-30')
->and(AboHelper::dateForDayInMonth('2026-03-01', 31)->format('Y-m-d'))->toBe('2026-03-31');
// Ein 31er-Abo: Januar 31 -> Februar 28 -> Maerz 31 -> April 30 (kein Monat doppelt, keiner uebersprungen).
$next = AboHelper::setNextDate(Carbon::parse('2026-01-31 04:00:00'), 31);
expect($next->format('Y-m-d'))->toBe('2026-02-28');
$next = AboHelper::setNextDate($next->copy()->setTime(4, 0), 31);
expect($next->format('Y-m-d'))->toBe('2026-03-31');
$next = AboHelper::setNextDate($next->copy()->setTime(4, 0), 31);
expect($next->format('Y-m-d'))->toBe('2026-04-30');
// Tag 1: Ausfuehrung am 1. um 04:00 -> naechster Termin ist der 1. des Folgemonats, nicht uebernaechster.
expect(AboHelper::setNextDate(Carbon::parse('2026-09-01 04:00:00'), 1)->format('Y-m-d'))->toBe('2026-10-01')
->and(AboHelper::setNextDate(Carbon::parse('2026-09-16 10:00:00'), 20)->format('Y-m-d'))->toBe('2026-09-20')
->and(AboHelper::setNextDate(Carbon::parse('2026-09-16 10:00:00'), 10)->format('Y-m-d'))->toBe('2026-10-10');
});
it('setzt die Erstausfuehrung fuer Tag 31 im Folgemonat auf dessen letzten Tag', function () {
Carbon::setTestNow('2026-01-15');
expect(AboHelper::getFirstAboDate(now(), 31)->format('Y-m-d'))->toBe('2026-02-28')
->and(AboHelper::getFirstAboDate(now(), 1)->format('Y-m-d'))->toBe('2026-02-01');
// Mindestabstand 10 Tage: Bestellung am 25.01. fuer Tag 1 -> nicht 01.02. (7 Tage), sondern 01.03.
Carbon::setTestNow('2026-01-25');
expect(AboHelper::getFirstAboDate(now(), 1)->format('Y-m-d'))->toBe('2026-03-01');
});
it('verschiebt den Liefertag im laufenden Zyklus nur nach vorn und nie in einen anderen Monat', function () {
Carbon::setTestNow('2026-09-16 10:00:00');
app('request')->setLaravelSession(app('session.store'));
$user = addUser();
$this->actingAs($user);
// 10. -> 24.: Lieferung dieses Monats ist noch offen (next_date 20.09.), neuer Termin 24.09.
$abo = addAbo($user, 20, '2026-09-20');
expect(addChange($abo, 24))->toBeTrue()
->and($abo->fresh()->getRawOriginal('next_date'))->toBe('2026-09-24')
->and($abo->fresh()->abo_interval)->toBe(24);
// 24. -> 5.: der 5. ist im September vorbei -> Ablehnung, Termin bleibt.
expect(addChange($abo->fresh(), 5))->toBeFalse()
->and(session('alert-error'))->toBe(__('abo.error_abo_interval_passed', ['day' => 5, 'next' => '24.09.2026']))
->and($abo->fresh()->getRawOriginal('next_date'))->toBe('2026-09-24');
// Heute selbst (16.) ist ebenfalls vorbei: Aenderung muss nach heute liegen.
expect(addChange($abo->fresh(), 16))->toBeFalse();
// 17. (morgen) ist erlaubt.
expect(addChange($abo->fresh(), 17))->toBeTrue()
->and($abo->fresh()->getRawOriginal('next_date'))->toBe('2026-09-17');
// Lieferung schon gelaufen (next_date im Oktober): Referenzmonat Oktober, 5. erlaubt.
$delivered = addAbo($user, 10, '2026-10-10');
expect(addChange($delivered, 5))->toBeTrue()
->and($delivered->fresh()->getRawOriginal('next_date'))->toBe('2026-10-05');
// Tag 31 im Referenzmonat November -> 30.11.
$november = addAbo($user, 10, '2026-11-10');
expect(addChange($november, 31))->toBeTrue()
->and($november->fresh()->getRawOriginal('next_date'))->toBe('2026-11-30')
->and($november->fresh()->abo_interval)->toBe(31);
});
it('sperrt die Liefertag-Aenderung erst am Ausfuehrungstag, am Vortag bis 23:59 Uhr ist sie frei (A2)', function () {
app('request')->setLaravelSession(app('session.store'));
$user = addUser();
$this->actingAs($user);
$abo = addAbo($user, 20, '2026-09-20');
Carbon::setTestNow('2026-09-19 23:59:00');
expect(addChange($abo, 25))->toBeTrue()
->and($abo->fresh()->getRawOriginal('next_date'))->toBe('2026-09-25');
Carbon::setTestNow('2026-09-25 00:00:00');
expect(addChange($abo->fresh(), 28))->toBeFalse()
->and(session('alert-error'))->toBe(__('abo.error_change_locked_zero', ['lock' => 0, 'days' => 0, 'date' => '25.09.2026']))
->and($abo->fresh()->getRawOriginal('next_date'))->toBe('2026-09-25');
// Mit gepflegter Sperrfrist 3: am 22. (3 Tage vorher) bereits gesperrt, am 21. frei.
Setting::setContentBySlug('abo-lock-days-change', 3, 'int');
Carbon::setTestNow('2026-09-22 08:00:00');
expect(addChange($abo->fresh(), 28))->toBeFalse()
->and(session('alert-error'))->toContain('mindestens 3 Tage');
Carbon::setTestNow('2026-09-21 08:00:00');
// Neuer Termin 28.09. liegt 7 Tage entfernt (> 3) -> erlaubt.
expect(addChange($abo->fresh(), 28))->toBeTrue();
// Neuer Termin 23.09. laege nur 2 Tage entfernt (<= 3) -> zu frueh.
expect(addChange($abo->fresh(), 23))->toBeFalse()
->and(session('alert-error'))->toBe(__('abo.error_abo_interval_too_soon', ['days' => 2, 'lock' => 3]));
});
it('klammert auch den Admin-Termin und laesst den Admin frei waehlen', function () {
Carbon::setTestNow('2026-09-16 10:00:00');
app('request')->setLaravelSession(app('session.store'));
$admin = addUser();
$this->actingAs($admin);
$abo = addAbo($admin, 5, '2026-10-05');
$repository = new AboRepository;
$repository->setModel($abo);
expect($repository->update([
'action' => 'abo_update_settings', 'id' => $abo->id, 'view' => 'admin',
'abo_interval' => 31, 'abo_next_month' => '2026-11', 'abo_is_active' => 'true',
]))->toBeTrue()
->and($abo->fresh()->getRawOriginal('next_date'))->toBe('2026-11-30')
->and($abo->fresh()->abo_interval)->toBe(31);
});