Abo: Kreditkarte aktualisieren (CRM, Portal, Berater-Link) + Admin-Abo-Löschung

- Karten-Update ohne Abbuchung via PAYONE creditcardcheck/pseudocardpan
  (AboPaymentMethodService, CardUpdateResult/CreditCardCheckData-DTOs)
- Eingabe nur durch Abo-Inhaber: CRM- und Portal-Formular + zeitlich
  begrenzter Berater→Kunde-Link (AboCardUpdateLink + Mail)
- Änderungshistorie in user_abo_card_changes, Anzeige im Admin-Abo-Detail
- Admin: Abo löschen (destroy) inkl. Test
- Portal-Fix: myOrderCreate wirft 404 statt TypeError bei ungültiger ID
- VIP-Sichtbarkeitstests für Einmalartikel-Fenster ergänzt
- Enthält bereits die Freeze-Guards für den Stripe-Notbetrieb (Folge-Commit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-07-08 15:56:19 +00:00
parent 63c36555a5
commit 671022a28d
36 changed files with 2366 additions and 26 deletions

View file

@ -0,0 +1,55 @@
<?php
use App\Http\Controllers\User\AboController;
use App\Repositories\AboRepository;
use App\Services\AboPaymentMethodService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function makeCrmController(): AboController
{
return new AboController(app(AboRepository::class));
}
function stubCrmRoutes(): void
{
URL::resolveMissingNamedRoutesUsing(
fn (string $name, array $parameters) => '/'.$name.'/'.implode('/', $parameters)
);
}
it('applies the stashed card on a successful 3-D Secure return for the owning consultant', function () {
stubCrmRoutes();
$abo = makeMeAbo(); // is_for=me, clearingtype=cc, status=2, user_id=consultant
$abo->forceFill(['carddata' => ['pseudocardpan' => 'OLD'], 'card_update_pending_at' => now()])->save();
$this->actingAs($abo->user);
session(['abo_card_update.'.$abo->id => [
'carddata' => ['pseudocardpan' => 'NEW_CRM', 'truncatedcardpan' => '444444XXXXXX1111', 'cardexpiredate' => '2812'],
'userid' => 888,
]]);
makeCrmController()->paymentMethodReturn('me', $abo->id, 'success', app(AboPaymentMethodService::class));
$abo->refresh();
expect($abo->carddata['pseudocardpan'])->toBe('NEW_CRM')
->and($abo->payone_userid)->toEqual(888)
->and($abo->card_update_pending_at)->toBeNull()
->and($abo->card_changes()->count())->toBe(1)
->and($abo->card_changes()->first()->source)->toBe('crm')
->and($abo->card_changes()->first()->masked_card)->toBe('444444XXXXXX1111 (12/28)');
});
it('forbids card entry for a customer abo (ot) in the CRM', function () {
stubCrmRoutes();
$abo = makeMeAbo();
$this->actingAs($abo->user);
expect(fn () => makeCrmController()->paymentMethodReturn('ot', $abo->id, 'success', app(AboPaymentMethodService::class)))
->toThrow(HttpException::class);
});