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, '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); });