mivita/tests/Feature/PaymentProviderManagerTest.php
Kevin ce4750fd1a Stripe-Integration (S4, S5, S7.6): Abos, Zahlungsart ändern, Zahlarten-Kommunikation
Phase S4 – Abos über Stripe (E2E komplett im Testmodus):
- Abo-/Recovery-Checkout: customer_creation=always + setup_future_usage=off_session
- StripePaymentService::chargeAbo (off-session PaymentIntent, Idempotency-Key),
  StripeGateway, Provider-Dispatch im Abbuchungs-Cron (PAYONE-Pfad unverändert)
- Webhook: Zahlungsmittel-Capture vor Paid-Pfad, 503 payment_not_yet_visible bei
  Cron-Transaktions-Race; Command stripe:process-pending-events (Cron-Ende + 10-min
  Scheduler) verarbeitet liegengebliebene Events nach
- createNewAbo/advanceAboAfterRecoveryPayment Stripe-aware inkl. Race-Absicherung;
  Recovery-Orders nutzen advanceAboAfterRecoveryPayment (latenter PAYONE-Duplikat-Bug)
- Migrationen: Stripe-Abo-Spalten auf shopping_payments; clearingtype varchar(3)→(20)
  inkl. Reparatur der zu 'str' gekürzten Live-Zeilen

Phase S5 – Zahlungsart ändern (Scope: jede Zahlart, nicht nur Kreditkarte):
- Stripe Checkout mode=setup ohne Abbuchung; PAYONE-Abos ziehen beim Wechsel auf
  Stripe um (Auslaufmodell §7.2)
- AboPaymentMethodService: beginStripeChange/finalize/applyStripePaymentMethod
  (idempotent, detacht alte PM, card_changes-Log); Webhook setup_intent.succeeded
  als Sicherheitsnetz; Einstiege Portal/CRM/Berater-Kunden-Link flow-abhängig
- Views/Mails flow-abhängig („Zahlungsweise" statt „Kreditkarte")

Phase S7.6 – Zahlarten-Kommunikation:
- Einheitliche Zahlarten-Logos (SVG) + wiederverwendbares Partial payment_logos;
  Checkout/Footer zeigen Kern-Zahlarten + „···"-Icon, /zahlungsarten komplett neu
  mit allen aktiven Stripe-Zahlarten (gegen Account-Konfiguration verifiziert)
  und Vorkasse (PAYONE); Sofort/SEPA entfernt

Tests: StripeAboTest (15), StripePaymentMethodChangeTest (10),
PaymentMethodsCommunicationTest (28), PaymentProviderManagerTest angepasst.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:45:17 +00:00

154 lines
5.9 KiB
PHP

<?php
use App\Cron\UserMakeOrder;
use App\Enums\PaymentProvider;
use App\Models\Setting;
use App\Models\ShoppingOrder;
use App\Models\ShoppingPayment;
use App\Models\ShoppingUser;
use App\Models\UserAbo;
use App\Services\Payment\Contracts\PaymentGatewayInterface;
use App\Services\Payment\DTOs\AboChargeResult;
use App\Services\Payment\PaymentProviderManager;
use App\Services\Payment\Payone\DTOs\PayonePaymentResult;
use App\Services\Payment\Payone\PayonePaymentService;
use App\Services\Payment\PayoneGateway;
use App\Services\Payment\StripeGateway;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
// --- Schalter: Anbieter für neue Vorgänge ---
it('defaults to payone when no provider switch is configured', function () {
expect(app(PaymentProviderManager::class)->providerForNewCheckout())
->toBe(PaymentProvider::Payone);
});
it('reads the active provider for new checkouts from the settings switch', function () {
Setting::setContentBySlug(PaymentProviderManager::SETTING_SLUG, 'stripe', 'text');
expect(app(PaymentProviderManager::class)->providerForNewCheckout())
->toBe(PaymentProvider::Stripe);
});
it('falls back to payone for unknown switch values', function () {
Setting::setContentBySlug(PaymentProviderManager::SETTING_SLUG, 'klarna', 'text');
expect(app(PaymentProviderManager::class)->providerForNewCheckout())
->toBe(PaymentProvider::Payone);
});
// --- Bestandsvorgänge: Anbieter aus der Spalte, schalterunabhängig ---
it('backfills existing abos with payone as their provider', function () {
$abo = makeMeAbo();
expect($abo->fresh()->payment_provider)->toBe('payone');
});
it('resolves the provider of an existing abo from its column even when the switch says stripe', function () {
Setting::setContentBySlug(PaymentProviderManager::SETTING_SLUG, 'stripe', 'text');
$abo = makeMeAbo();
expect(app(PaymentProviderManager::class)->providerFor($abo->fresh()))
->toBe(PaymentProvider::Payone);
});
it('resolves a stripe abo and a stripe shopping payment from their columns', function () {
$abo = makeMeAbo();
$abo->update(['payment_provider' => 'stripe']);
$payment = new ShoppingPayment(['payment_provider' => 'stripe']);
$manager = app(PaymentProviderManager::class);
expect($manager->providerFor($abo->fresh()))->toBe(PaymentProvider::Stripe)
->and($manager->providerFor($payment))->toBe(PaymentProvider::Stripe);
});
// --- Gateway-Auflösung ---
it('resolves the payone gateway adapter for the payone provider', function () {
$gateway = app(PaymentProviderManager::class)->gateway(PaymentProvider::Payone);
expect($gateway)->toBeInstanceOf(PayoneGateway::class)
->and($gateway)->toBeInstanceOf(PaymentGatewayInterface::class)
->and($gateway->provider())->toBe(PaymentProvider::Payone);
});
it('resolves the stripe gateway adapter for the stripe provider', function () {
$gateway = app(PaymentProviderManager::class)->gateway(PaymentProvider::Stripe);
expect($gateway)->toBeInstanceOf(StripeGateway::class)
->and($gateway)->toBeInstanceOf(PaymentGatewayInterface::class)
->and($gateway->provider())->toBe(PaymentProvider::Stripe);
});
// --- AboChargeResult-Mapping ---
it('maps an approved payone result to a successful charge result', function () {
$result = AboChargeResult::fromPayoneResult(
PayonePaymentResult::fromResponse(['status' => 'APPROVED', 'txid' => 'tx-123'])
);
expect($result->success)->toBeTrue()
->and($result->provider)->toBe(PaymentProvider::Payone)
->and($result->status)->toBe('APPROVED')
->and($result->transactionId)->toBe('tx-123')
->and($result->errorMessage)->toBeNull();
});
it('maps a declined payone result with its error messages', function () {
$result = AboChargeResult::fromPayoneResult(PayonePaymentResult::fromResponse([
'status' => 'ERROR',
'errormessage' => 'Card declined',
'customermessage' => 'Bitte andere Karte verwenden.',
]));
expect($result->success)->toBeFalse()
->and($result->status)->toBe('ERROR')
->and($result->errorMessage)->toBe('Card declined')
->and($result->customerMessage)->toBe('Bitte andere Karte verwenden.');
});
// --- PayoneGateway delegiert 1:1 an den bestehenden Service ---
it('delegates abo charges to the payone payment service', function () {
$abo = new UserAbo;
$order = new ShoppingOrder;
$shoppingUser = new ShoppingUser;
$service = Mockery::mock(PayonePaymentService::class);
$service->shouldReceive('chargeAbo')
->once()
->with($abo, $order, $shoppingUser, 1999, 'EUR')
->andReturn(PayonePaymentResult::fromResponse(['status' => 'APPROVED', 'txid' => 'tx-9']));
$result = (new PayoneGateway($service))->chargeAbo($abo, $order, $shoppingUser, 1999, 'EUR');
expect($result->success)->toBeTrue()
->and($result->transactionId)->toBe('tx-9');
});
// --- Cron-Dispatch: Stripe-Abos laufen NICHT durch den PAYONE-Pfad (S4.4) ---
it('routes stripe abos in the cron to the stripe gateway instead of payone', function () {
$abo = makeMeAbo();
$abo->update(['payment_provider' => 'stripe']);
$userOrder = new UserMakeOrder($abo->fresh());
$reflection = new ReflectionClass($userOrder);
foreach (['shopping_user' => new ShoppingUser, 'shopping_order' => new ShoppingOrder] as $property => $value) {
$prop = $reflection->getProperty($property);
$prop->setValue($userOrder, $value);
}
// Ohne gespeicherte Stripe-PaymentMethod liefert das Gateway ein
// ERROR-Ergebnis (kein PAYONE-Aufruf, keine Exception).
$response = $userOrder->makePayment();
expect($response['status'])->toBe('ERROR')
->and($response['provider'])->toBe('stripe')
->and($response['customermessage'])->toBe(__('payment.stripe_abo_payment_method_missing'));
});