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

@ -7,9 +7,11 @@ use App\Models\UserAboOneTimeItem;
use App\Services\AboHelper;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(Tests\TestCase::class, RefreshDatabase::class);
uses(TestCase::class, RefreshDatabase::class);
afterEach(function () {
Carbon::setTestNow();
@ -102,19 +104,34 @@ describe('isOneTimeFeatureVisible', function () {
expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeTrue();
});
it('ist nicht sichtbar bei geschlossenem Fenster', function () {
it('ist für normale User nicht sichtbar bei geschlossenem Fenster', function () {
$user = new User;
$user->admin = 0;
$this->actingAs($user, 'user');
$abo = new UserAbo;
$abo->next_date = now()->addDays(30); // außerhalb des Fensters
expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeFalse();
});
it('ist für VIP (Admin) dauerhaft sichtbar, auch bei geschlossenem Fenster', function () {
$user = new User;
$user->admin = 1;
$this->actingAs($user, 'user');
$abo = new UserAbo;
$abo->next_date = now()->addDays(30); // außerhalb des Fensters
expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeTrue();
});
});
describe('UserAboOneTimeItem Model', function () {
it('definiert die Beziehung one_time_items auf UserAbo', function () {
$relation = (new UserAbo)->one_time_items();
expect($relation)->toBeInstanceOf(Illuminate\Database\Eloquent\Relations\HasMany::class)
expect($relation)->toBeInstanceOf(HasMany::class)
->and($relation->getRelated())->toBeInstanceOf(UserAboOneTimeItem::class);
});