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>
232 lines
6.5 KiB
PHP
232 lines
6.5 KiB
PHP
<?php
|
|
|
|
use App\Models\Country;
|
|
use App\Models\Setting;
|
|
use App\Models\ShoppingUser;
|
|
use App\Models\UserAbo;
|
|
use App\Repositories\AboRepository;
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(Tests\TestCase::class, RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->repository = new AboRepository;
|
|
});
|
|
|
|
afterEach(function () {
|
|
Carbon::setTestNow();
|
|
});
|
|
|
|
/**
|
|
* Szenario: Heute = 15. März, next_date = 5. April (März-Ausführung bereits erfolgt).
|
|
* Änderung auf Tag 20 → neues Datum soll der 20. April sein, NICHT der 20. März.
|
|
*/
|
|
it('erlaubt Datumsänderung auf den 20. wenn next_date im April liegt und heute der 15. März ist', function () {
|
|
Carbon::setTestNow('2026-03-15');
|
|
|
|
$abo = new UserAbo;
|
|
$abo->next_date = '2026-04-05';
|
|
|
|
$this->repository->setModel($abo);
|
|
|
|
$newNextDate = invadePrivateMethod($this->repository, 'calculateNewNextDate', [20]);
|
|
|
|
expect($newNextDate->format('Y-m-d'))->toBe('2026-04-20');
|
|
|
|
Carbon::setTestNow();
|
|
});
|
|
|
|
/**
|
|
* Szenario: Heute = 15. März, next_date = 25. März (im selben Monat, noch nicht ausgeführt).
|
|
* Änderung auf Tag 20 → 20. März ist nur 5 Tage entfernt → Referenz bleibt now().
|
|
* setNextDate gibt 20. März zurück (5 Tage → zu nah).
|
|
*/
|
|
it('berechnet neues Datum im selben Monat wenn next_date noch im März liegt', function () {
|
|
Carbon::setTestNow('2026-03-15');
|
|
|
|
$abo = new UserAbo;
|
|
$abo->next_date = '2026-03-25';
|
|
|
|
$this->repository->setModel($abo);
|
|
|
|
$newNextDate = invadePrivateMethod($this->repository, 'calculateNewNextDate', [20]);
|
|
|
|
expect($newNextDate->format('Y-m-d'))->toBe('2026-03-20');
|
|
|
|
Carbon::setTestNow();
|
|
});
|
|
|
|
/**
|
|
* Szenario: Heute = 15. März, next_date = 5. April.
|
|
* Änderung auf Tag 5 → neues Datum soll 5. April sein (identisch mit aktuell).
|
|
*/
|
|
it('setzt Datum auf den 5. April wenn Tag 5 gewählt und next_date im April liegt', function () {
|
|
Carbon::setTestNow('2026-03-15');
|
|
|
|
$abo = new UserAbo;
|
|
$abo->next_date = '2026-04-05';
|
|
|
|
$this->repository->setModel($abo);
|
|
|
|
$newNextDate = invadePrivateMethod($this->repository, 'calculateNewNextDate', [5]);
|
|
|
|
expect($newNextDate->format('Y-m-d'))->toBe('2026-04-05');
|
|
|
|
Carbon::setTestNow();
|
|
});
|
|
|
|
/**
|
|
* Szenario: Heute = 15. März, kein next_date gesetzt → Referenz ist now().
|
|
*/
|
|
it('verwendet heute als Referenz wenn kein next_date gesetzt ist', function () {
|
|
Carbon::setTestNow('2026-03-15');
|
|
|
|
$abo = new UserAbo;
|
|
$abo->next_date = null;
|
|
|
|
$this->repository->setModel($abo);
|
|
|
|
$newNextDate = invadePrivateMethod($this->repository, 'calculateNewNextDate', [20]);
|
|
|
|
expect($newNextDate->format('Y-m-d'))->toBe('2026-03-20');
|
|
|
|
Carbon::setTestNow();
|
|
});
|
|
|
|
it('erlaubt Admins den nächsten Ausführungstermin ohne User-Sperrfrist direkt zu setzen', function () {
|
|
Carbon::setTestNow('2026-03-19 12:00:00');
|
|
|
|
$abo = createRepositoryTestAbo([
|
|
'abo_interval' => 5,
|
|
'next_date' => '2026-03-20',
|
|
]);
|
|
|
|
$this->repository->setModel($abo);
|
|
|
|
$result = $this->repository->update([
|
|
'action' => 'abo_update_settings',
|
|
'id' => $abo->id,
|
|
'view' => 'admin',
|
|
'abo_interval' => 20,
|
|
'abo_next_month' => '2026-03',
|
|
'abo_is_active' => 'true',
|
|
]);
|
|
|
|
expect($result)->toBeTrue();
|
|
expect($abo->refresh()->abo_interval)->toBe(20);
|
|
expect($abo->getRawOriginal('next_date'))->toBe('2026-03-20');
|
|
});
|
|
|
|
it('behält eine gepflegte User-Sperrfrist für normale Abo-Änderungen bei', function () {
|
|
Carbon::setTestNow('2026-03-19 12:00:00');
|
|
Setting::setContentBySlug('abo-lock-days-change', 10, 'int');
|
|
|
|
$abo = createRepositoryTestAbo([
|
|
'abo_interval' => 5,
|
|
'next_date' => '2026-03-20',
|
|
]);
|
|
|
|
$this->actingAs($abo->user);
|
|
$this->repository->setModel($abo);
|
|
|
|
$result = $this->repository->update([
|
|
'action' => 'abo_update_settings',
|
|
'id' => $abo->id,
|
|
'view' => 'me',
|
|
'abo_interval' => 20,
|
|
'abo_is_active' => 'true',
|
|
]);
|
|
|
|
expect($result)->toBeFalse();
|
|
expect($abo->refresh()->abo_interval)->toBe(5);
|
|
expect($abo->getRawOriginal('next_date'))->toBe('2026-03-20');
|
|
});
|
|
|
|
it('erlaubt ohne Vorlaufsperre die Aenderung noch am Vortag der Lieferung (A2)', function () {
|
|
Carbon::setTestNow('2026-03-19 12:00:00');
|
|
|
|
$abo = createRepositoryTestAbo([
|
|
'abo_interval' => 5,
|
|
'next_date' => '2026-03-20',
|
|
]);
|
|
|
|
$this->actingAs($abo->user);
|
|
$this->repository->setModel($abo);
|
|
|
|
$result = $this->repository->update([
|
|
'action' => 'abo_update_settings',
|
|
'id' => $abo->id,
|
|
'view' => 'me',
|
|
'abo_interval' => 20,
|
|
'abo_is_active' => 'true',
|
|
]);
|
|
|
|
expect($result)->toBeTrue();
|
|
expect($abo->refresh()->abo_interval)->toBe(20);
|
|
expect($abo->getRawOriginal('next_date'))->toBe('2026-03-20');
|
|
});
|
|
|
|
/**
|
|
* Hilfsfunktion zum Aufruf privater Methoden.
|
|
*
|
|
* @param array<mixed> $args
|
|
*/
|
|
function invadePrivateMethod(object $object, string $methodName, array $args = []): mixed
|
|
{
|
|
$reflection = new ReflectionMethod($object, $methodName);
|
|
$reflection->setAccessible(true);
|
|
|
|
return $reflection->invoke($object, ...$args);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
*/
|
|
function createRepositoryTestAbo(array $overrides = []): UserAbo
|
|
{
|
|
$country = Country::create([
|
|
'code' => 'DE',
|
|
'phone' => '49',
|
|
'en' => 'Germany',
|
|
'de' => 'Deutschland',
|
|
'es' => 'Alemania',
|
|
'fr' => 'Allemagne',
|
|
'it' => 'Germania',
|
|
'ru' => 'Deutschland',
|
|
]);
|
|
|
|
$user = User::forceCreate([
|
|
'email' => 'abo-repository-'.uniqid('', true).'@example.com',
|
|
'password' => bcrypt('secret'),
|
|
'lang' => 'de',
|
|
'admin' => 0,
|
|
]);
|
|
|
|
$shoppingUser = ShoppingUser::create([
|
|
'auth_user_id' => $user->id,
|
|
'member_id' => $user->id,
|
|
'billing_country_id' => $country->id,
|
|
'shipping_country_id' => $country->id,
|
|
'billing_email' => $user->email,
|
|
'is_for' => 'me',
|
|
'is_from' => 'user_order',
|
|
]);
|
|
|
|
return UserAbo::create(array_merge([
|
|
'user_id' => $user->id,
|
|
'member_id' => $user->id,
|
|
'shopping_user_id' => $shoppingUser->id,
|
|
'is_for' => 'me',
|
|
'email' => $user->email,
|
|
'payone_userid' => random_int(100000, 999999),
|
|
'clearingtype' => 'cc',
|
|
'active' => true,
|
|
'status' => 2,
|
|
'abo_interval' => 5,
|
|
'start_date' => '2026-01-05',
|
|
'last_date' => '2026-02-05',
|
|
'next_date' => '2026-03-05',
|
|
], $overrides));
|
|
}
|