mivita/tests/Feature/AboUserRetryTest.php
Kevin 0660f4e0a0 Abo-Recovery: fehlgeschlagene Folgezahlung im Checkout nachholen
- Pay-now-Flow: bei fehlgeschlagenem Abo (Status 3) begleicht der Inhaber die
  offene Rechnung im Standard-Checkout mit frei waehlbarer, abo-faehiger Zahlart
  (nur eigenes Abo: Berater me-View + Kundenportal), payNow-Controller + Routen
- AboRecoveryCheckoutService baut die ShoppingInstance aus den Abo-Artikeln;
  neue Spalte shopping_orders.recovery_user_abo_id verknuepft Bestellung<->Abo
- Bei Erfolg: AboHelper::advanceAboAfterRecoveryPayment schreibt das Abo fort
  (Status->2, next_date) und uebernimmt die gewaehlte Zahlart als neues Token
  fuer kuenftige Abbuchungen
- Neue Pay-now-Fehlermail (AboPaymentFailedMail) ersetzt die generische
  Fehlermail im Abo-Cron; Link fuehrt auf die Abo-Detailseite (CRM bzw. Portal)
- Retry-/"extern erledigt"-Buttons nur noch fuer Admins; tote user/portal-
  Retry-Routen entfernt
- Checkout: Recovery-Hinweis statt "Abschluss des Abonnements", Lieferadresse
  vorbelegt, korrekter Zurueck-Link (Abo statt Shop)
- UserAbo::selfServiceDetailUrl zentralisiert die Abo-URL (Mail, Back-Links)
- Test-Command abo:test-recovery; Tests fuer payNow, Advance-Logik, Fehlermail

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:38:05 +00:00

90 lines
3 KiB
PHP

<?php
use App\Http\Controllers\User\AboController;
use App\Models\UserAbo;
use App\Repositories\AboRepository;
use App\Services\AboRecoveryCheckoutService;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Route;
use Symfony\Component\HttpKernel\Exception\HttpException;
use function Pest\Laravel\mock;
uses(Tests\TestCase::class, RefreshDatabase::class);
function makeRetryOwnerAbo(int $status = 3, bool $active = true): array
{
$owner = User::forceCreate([
'email' => 'owner-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$abo = UserAbo::create([
'user_id' => $owner->id,
'member_id' => $owner->id,
'shopping_user_id' => 1,
'is_for' => 'me',
'email' => $owner->email,
'payone_userid' => 900300,
'clearingtype' => 'cc',
'active' => $active,
'status' => $status,
'abo_interval' => 5,
'next_date' => now()->toDateString(),
]);
return ['owner' => $owner, 'abo' => $abo];
}
it('startet den Recovery-Checkout für das eigene fehlgeschlagene Abo und leitet dorthin weiter', function () {
$fixture = makeRetryOwnerAbo();
$this->actingAs($fixture['owner']);
mock(AboRecoveryCheckoutService::class)
->shouldReceive('buildCheckoutUrl')
->once()
->andReturn('https://checkout.test/checkout/card/abc123');
$controller = new AboController(app(AboRepository::class));
$response = $controller->payNow('me', $fixture['abo']->id, app(AboRecoveryCheckoutService::class));
expect($response->getTargetUrl())->toBe('https://checkout.test/checkout/card/abc123');
});
it('verhindert die nachträgliche Zahlung für ein fremdes Abo (403)', function () {
$fixture = makeRetryOwnerAbo();
$stranger = User::forceCreate([
'email' => 'stranger-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'lang' => 'de',
]);
$this->actingAs($stranger);
$service = mock(AboRecoveryCheckoutService::class);
$service->shouldNotReceive('buildCheckoutUrl');
$controller = new AboController(app(AboRepository::class));
expect(fn () => $controller->payNow('me', $fixture['abo']->id, $service))
->toThrow(HttpException::class);
});
it('lehnt die nachträgliche Zahlung ab, wenn das Abo nicht fehlgeschlagen ist', function () {
Route::post('/user/abos/detail/{view}/{id}', fn () => '')->name('user_abos_detail');
app('router')->getRoutes()->refreshNameLookups();
$fixture = makeRetryOwnerAbo(status: 2);
$this->actingAs($fixture['owner']);
$service = mock(AboRecoveryCheckoutService::class);
$service->shouldNotReceive('buildCheckoutUrl');
$controller = new AboController(app(AboRepository::class));
$response = $controller->payNow('me', $fixture['abo']->id, $service);
expect($response->getTargetUrl())->toContain('/user/abos/detail/me/'.$fixture['abo']->id);
expect(session('alert-error'))->toBe(__('abo.pay_now_not_allowed'));
});