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>
243 lines
8.6 KiB
PHP
243 lines
8.6 KiB
PHP
<?php
|
|
|
|
use App\Models\Setting;
|
|
use App\Models\UserAbo;
|
|
use App\Models\UserAboOrder;
|
|
use App\Repositories\AboRepository;
|
|
use App\Services\AboHelper;
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Ticket A4 (Entwicklungsplan September 2026): eigener Abschnitt "Abo pausieren
|
|
* oder kündigen" auf der Abo-Seite (CRM und Portal) mit Fortschritt zur
|
|
* Mindestlaufzeit, Kündigungs-Modal samt Folgen-Hinweis und Pause als
|
|
* Alternative; die Kündigung im Einstellungs-Modal verweist nur noch dorthin.
|
|
*/
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Carbon::setTestNow('2026-09-16 10:00:00');
|
|
app('request')->setLaravelSession(app('session.store'));
|
|
URL::resolveMissingNamedRoutesUsing(fn ($name, $parameters = []) => '/'.$name.'/'.implode('/', (array) $parameters));
|
|
});
|
|
|
|
afterEach(fn () => Carbon::setTestNow());
|
|
|
|
function pcsUser(): User
|
|
{
|
|
return User::forceCreate([
|
|
'email' => 'pcs-'.uniqid('', true).'@example.com',
|
|
'password' => bcrypt('secret'),
|
|
'lang' => 'de',
|
|
'm_level' => 1,
|
|
'payment_account' => now()->addYear(),
|
|
]);
|
|
}
|
|
|
|
function pcsAbo(User $user, int $paidDeliveries = 6, string $nextDate = '2026-10-05', array $attributes = []): UserAbo
|
|
{
|
|
$abo = UserAbo::create(array_merge([
|
|
'user_id' => $user->id,
|
|
'member_id' => $user->id,
|
|
'shopping_user_id' => 0,
|
|
'email' => 'pcs-abo-'.uniqid('', true).'@example.com',
|
|
'is_for' => 'me',
|
|
'payone_userid' => 0,
|
|
'clearingtype' => 'cc',
|
|
'amount' => 5000,
|
|
'active' => true,
|
|
'status' => 2,
|
|
'abo_interval' => 5,
|
|
'start_date' => '2026-01-05',
|
|
'last_date' => '2026-09-05',
|
|
'next_date' => $nextDate,
|
|
], $attributes));
|
|
|
|
for ($i = 0; $i < $paidDeliveries; $i++) {
|
|
UserAboOrder::create([
|
|
'user_abo_id' => $abo->id,
|
|
'shopping_order_id' => 0,
|
|
'status' => 2,
|
|
'paid' => true,
|
|
]);
|
|
}
|
|
|
|
return $abo->fresh();
|
|
}
|
|
|
|
function pcsRender(UserAbo $abo, string $view = 'me'): string
|
|
{
|
|
return view('admin.abo._pause_cancel', ['user_abo' => $abo, 'view' => $view])->render();
|
|
}
|
|
|
|
it('zeigt nach der Mindestlaufzeit Pause und Kuendigung mit Modal, Folgen-Hinweis und Bestaetigungs-Checkbox', function () {
|
|
$abo = pcsAbo(pcsUser());
|
|
|
|
$html = pcsRender($abo);
|
|
|
|
expect($html)
|
|
->toContain('id="abo-pause-cancel"')
|
|
->toContain(__('abo.pause_cancel_title'))
|
|
->toContain(__('abo.pause_cancel_progress', ['count' => 6, 'min' => 6]))
|
|
->toContain('id="abo-pause-form"')
|
|
->toContain('name="abo_pause" value="true"')
|
|
->toContain('id="modal-abo-cancel"')
|
|
->toContain('name="abo_cancel" value="true"')
|
|
->toContain(__('abo.cancel_consequence_restart', ['min' => 6]))
|
|
->toContain(__('abo.cancel_prefer_pause_button'))
|
|
->toContain(__('abo.cancel_anyway_button'))
|
|
->toContain('id="abo-cancel-confirm"')
|
|
->toContain('id="abo-cancel-submit" disabled')
|
|
->toContain('/user_abos_update/me/'.$abo->id)
|
|
->not->toContain(__('abo.info_min_duration_orders_left', ['count' => 0]));
|
|
});
|
|
|
|
it('zeigt vor der Mindestlaufzeit nur den Fortschritt und den Hinweis auf die verbleibenden Lieferungen', function () {
|
|
$abo = pcsAbo(pcsUser(), paidDeliveries: 4);
|
|
|
|
$html = pcsRender($abo, 'portal');
|
|
|
|
expect($html)
|
|
->toContain(__('abo.pause_cancel_progress', ['count' => 4, 'min' => 6]))
|
|
->toContain(__('abo.info_min_duration_orders_left', ['count' => 2]))
|
|
->not->toContain('id="modal-abo-cancel"')
|
|
->not->toContain('name="abo_cancel"')
|
|
->not->toContain('name="abo_pause"');
|
|
});
|
|
|
|
it('zeigt innerhalb der Sperrfrist vor der Ausfuehrung den Hinweis statt der Buttons', function () {
|
|
// Ausfuehrung in 2 Tagen, Sperre pausieren/kuendigen = 3 Tage
|
|
Setting::setContentBySlug('abo-lock-days-pause-cancel', 3, 'int');
|
|
$abo = pcsAbo(pcsUser(), nextDate: '2026-09-18');
|
|
|
|
expect(AboHelper::isPauseCancelLocked($abo, 'me'))->toBeTrue()
|
|
->and(AboHelper::isPauseCancelLocked($abo, 'admin'))->toBeFalse();
|
|
|
|
$html = pcsRender($abo);
|
|
|
|
expect($html)
|
|
->toContain(__('abo.pause_cancel_locked_hint', ['date' => '18.09.2026', 'lock' => 3, 'days' => 2]))
|
|
->not->toContain('id="modal-abo-cancel"')
|
|
->not->toContain('name="abo_pause"');
|
|
});
|
|
|
|
it('zeigt ohne Vorlaufsperre am Ausfuehrungstag den Vortag-Hinweis (A2)', function () {
|
|
$abo = pcsAbo(pcsUser(), nextDate: '2026-09-16');
|
|
|
|
$html = pcsRender($abo);
|
|
|
|
expect($html)
|
|
->toContain(__('abo.pause_cancel_locked_hint_zero', ['date' => '16.09.2026', 'lock' => 0, 'days' => 0]))
|
|
->not->toContain('id="modal-abo-cancel"');
|
|
|
|
expect(pcsRender(pcsAbo(pcsUser(), nextDate: '2026-09-17')))->toContain('id="modal-abo-cancel"');
|
|
});
|
|
|
|
it('bietet im Kuendigungs-Modal keine Pause an, wenn die Pause gerade nicht moeglich ist', function () {
|
|
$abo = pcsAbo(pcsUser(), attributes: ['last_pause_date' => '2026-08-01']);
|
|
|
|
$html = pcsRender($abo);
|
|
|
|
expect($html)
|
|
->toContain('id="modal-abo-cancel"')
|
|
->toContain(__('abo.info_pause_available_from', ['date' => '01.02.2027', 'months' => 6]))
|
|
->not->toContain('id="abo-pause-form"')
|
|
->not->toContain(__('abo.cancel_prefer_pause_button'))
|
|
->not->toContain(__('abo.cancel_alternative_pause'));
|
|
});
|
|
|
|
it('blendet den Abschnitt fuer beendete Abos aus', function () {
|
|
$cancelled = pcsAbo(pcsUser(), attributes: ['status' => 4, 'active' => false]);
|
|
$finished = pcsAbo(pcsUser(), attributes: ['status' => 5, 'active' => false]);
|
|
|
|
expect(pcsRender($cancelled))->not->toContain('abo-pause-cancel')
|
|
->and(pcsRender($finished))->not->toContain('abo-pause-cancel');
|
|
});
|
|
|
|
it('verweist im Einstellungs-Modal nur noch auf den Abschnitt statt direkt zu kuendigen', function () {
|
|
$abo = pcsAbo(pcsUser());
|
|
|
|
$html = view('admin.abo.modal_abo_update', [
|
|
'user_abo' => $abo,
|
|
'data' => ['action' => 'abo_update_settings', 'id' => $abo->id, 'view' => 'portal'],
|
|
'route' => '/portal/abo/update',
|
|
])->render();
|
|
|
|
expect($html)
|
|
->toContain(__('abo.cancel_abo'))
|
|
->toContain(__('abo.cancel_in_section_hint'))
|
|
->toContain('abo-pause-cancel')
|
|
->toContain('name="abo_pause"')
|
|
->not->toContain('name="abo_cancel"');
|
|
});
|
|
|
|
it('kuendigt als eigenstaendige Aktion: Erfolgsmeldung, kein Liefertermin-Hinweis, next_date unveraendert', function () {
|
|
$user = pcsUser();
|
|
$abo = pcsAbo($user);
|
|
$this->actingAs($user);
|
|
|
|
$repository = new AboRepository;
|
|
$repository->setModel($abo);
|
|
|
|
$result = $repository->update([
|
|
'action' => 'abo_update_settings',
|
|
'id' => $abo->id,
|
|
'view' => 'me',
|
|
'abo_interval' => 5,
|
|
'abo_is_active' => 1,
|
|
'abo_cancel' => 'true',
|
|
'abo_cancel_confirm' => '1',
|
|
]);
|
|
|
|
$abo = $abo->fresh();
|
|
|
|
expect($result)->toBeTrue()
|
|
->and((int) $abo->status)->toBe(4)
|
|
->and($abo->active)->toBeFalse()
|
|
->and(Carbon::parse($abo->getRawOriginal('next_date'))->format('Y-m-d'))->toBe('2026-10-05')
|
|
->and(session('alert-success'))->toBe(__('abo.cancel_success'))
|
|
->and(session('alert-warning'))->toBeNull()
|
|
->and(session('alert-error'))->toBeNull();
|
|
});
|
|
|
|
it('lehnt die Kuendigung vor der Mindestlaufzeit ab, ohne den Liefertag anzufassen', function () {
|
|
$user = pcsUser();
|
|
$abo = pcsAbo($user, paidDeliveries: 2);
|
|
$this->actingAs($user);
|
|
|
|
$repository = new AboRepository;
|
|
$repository->setModel($abo);
|
|
|
|
$result = $repository->update([
|
|
'action' => 'abo_update_settings',
|
|
'id' => $abo->id,
|
|
'view' => 'me',
|
|
'abo_interval' => 5,
|
|
'abo_cancel' => 'true',
|
|
]);
|
|
|
|
$abo = $abo->fresh();
|
|
|
|
expect($result)->toBeFalse()
|
|
->and((int) $abo->status)->toBe(2)
|
|
->and($abo->active)->toBeTrue()
|
|
->and(session('alert-error'))->toBe(__('abo.info_min_duration_orders_left', ['count' => 4]))
|
|
->and(session('alert-success'))->toBeNull()
|
|
->and(session('alert-warning'))->toBeNull();
|
|
});
|
|
|
|
it('rendert den Abschnitt mit einer abweichenden Mindestlaufzeit aus dem Setting', function () {
|
|
Setting::setContentBySlug('abo-min-duration', 3, 'int');
|
|
$abo = pcsAbo(pcsUser(), paidDeliveries: 3);
|
|
|
|
$html = pcsRender($abo);
|
|
|
|
expect($html)
|
|
->toContain(__('abo.pause_cancel_progress', ['count' => 3, 'min' => 3]))
|
|
->toContain(__('abo.cancel_consequence_restart', ['min' => 3]))
|
|
->toContain('id="modal-abo-cancel"');
|
|
});
|