mivita/tests/Feature/AboPauseCancelSectionTest.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

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