Abo (A4): Abschnitt "Abo pausieren oder kuendigen" mit Kuendigungs-Hinweis
Eigener Abschnitt auf der Abo-Seite (CRM me/ot und Kundenportal) mit Fortschritt zur Mindestlaufzeit, Pause und Kuendigung. Die Kuendigung laeuft ueber ein Modal, das die Folgen nennt (Abo endet, Konditionen entfallen, ein neues Abo hat wieder 6 Pflichtlieferungen), die Pause als Alternative anbietet und erst nach Bestaetigungs-Checkbox freigibt. Innerhalb der Sperrfrist vor der Ausfuehrung erscheint statt der Buttons ein Hinweis (AboHelper::isPauseCancelLocked). Das Einstellungs-Modal verweist nur noch auf den Abschnitt. Die Kuendigung im Repository ist jetzt wie die Pause eine eigenstaendige Aktion: Erfolgsmeldung statt Liefertermin-Hinweis, next_date bleibt unangetastet. Sprachdateien de/en/es/fr, Tests in AboPauseCancelSectionTest, Plan-Status. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
74aefa9640
commit
e8057f9f7b
12 changed files with 526 additions and 4 deletions
230
tests/Feature/AboPauseCancelSectionTest.php
Normal file
230
tests/Feature/AboPauseCancelSectionTest.php
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
<?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
|
||||
$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' => AboRepository::getLockDaysPauseCancel()]))
|
||||
->not->toContain('id="modal-abo-cancel"')
|
||||
->not->toContain('name="abo_pause"');
|
||||
});
|
||||
|
||||
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"');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue