Payment Phase R: PAYONE-Integration entflechten (R1–R7 + Review)

Strangler-Refactoring der PAYONE-Zahlungslogik in eine testbare
Service-Schicht, ohne Verhalten in Checkout/Cron/IPN zu ändern.

Neue Services (app/Services/Payment/Payone):
- PayonePaymentService (+ PayonePaymentResult-DTO) für CC-/SEPA-Check
  und Abo-Abbuchung
- PayoneRequestBuilder, PaymentMethodResolver, PayoneMethodConfig
- PayoneTransactionRecorder: zentrale PaymentTransaction-Erstellung
  (API-Response, Invoice, Webhook) inkl. PCI-Redaction (cardpan/cardcvc2)
- PayoneWebhookHandler (+ PayoneWebhookResult): IPN/TSOK-Logik aus
  Api\PayoneController; response('TSOK') statt echo/exit
- PayoneTransportException + Handler-Mapping (403) statt abort() im Service

Hotfixes (R1): var_dump/exit & bare exit() entfernt, Log-Redaction.

Security (R-Review):
- PAYONE_KEY_HASH (MD5-Hash, kein Klartext) mit Boot-Hard-Fail; schließt
  zugleich die ''-Webhook-Bypass-Lücke. Artisan-Command payone:hash-key
  (verdeckte Eingabe, optional --write) zur Hash-Erzeugung.
- Webhook: txaction Pflichtfeld, mode-Fallback auf payment->mode.

Pay\PayoneController ist @deprecated-Fassade (614 → 328 Zeilen),
delegiert an Services via Container-Singleton.

Migration: fehlende Spalte shopping_payments.carddata nachgezogen.

Tests: 47 grün (PayonePaymentService, Recorder, Webhook, Resolver,
TransportException, hash-key). End-to-End im Test-System verifiziert
(siehe dev/2026-06-09-payments).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-06-10 13:42:45 +00:00
parent 1668e5a879
commit ccc6f85809
31 changed files with 3420 additions and 717 deletions

View file

@ -13,6 +13,7 @@ use App\Services\AboHelper;
use App\Services\CheckoutFunnelTracker;
use App\Services\CustomerPriority;
use App\Services\Payment;
use App\Services\Payment\Payone\Exceptions\PayoneTransportException;
use App\Services\Shop;
use App\Services\Util;
use App\User;
@ -258,31 +259,39 @@ class CheckoutController extends Controller
$result = [];
$payment_method = Request::get('payment_method');
// Kreditkarte prüfen
if ($payment_method === 'cc') {
$result = $this->checkCreditCard($data, $shopping_user, $shopping_order);
if (! is_array($result) || ! isset($result['returnstatus']) || $result['returnstatus'] !== 'VALID') {
return $result;
try {
// Kreditkarte prüfen
if ($payment_method === 'cc') {
$result = $this->checkCreditCard($data, $shopping_user, $shopping_order);
if (! is_array($result) || ! isset($result['returnstatus']) || $result['returnstatus'] !== 'VALID') {
return $result;
}
}
}
// SEPA prüfen
if ($payment_method === 'elv') {
$result = $this->checkSepaAccount($data, $shopping_user, $shopping_order);
if (! is_array($result) || ! isset($result['returnstatus']) || $result['returnstatus'] !== 'VALID') {
return $result;
// SEPA prüfen
if ($payment_method === 'elv') {
$result = $this->checkSepaAccount($data, $shopping_user, $shopping_order);
if (! is_array($result) || ! isset($result['returnstatus']) || $result['returnstatus'] !== 'VALID') {
return $result;
}
}
// Zahlung vorbereiten
$pay = new PayoneController;
$pay->init($shopping_user, $shopping_order);
$amount = Yard::instance($this->instance)->totalWithShipping(2, '.', '') * 100;
$reference = $pay->setPrePayment($payment_method, $amount, 'EUR', $result);
$this->checkoutRepo->putSessionPayments('payment_reference', $reference);
$pay->setPersonalData();
return $pay->ResponseData();
} catch (PayoneTransportException $e) {
// Zahlungsanbieter nicht erreichbar: zurück zur Zahlungsseite mit Fehlermeldung
// (statt 403-Fehlerseite wie früher beim abort()).
Session::flash('errormessage', $e->getMessage());
return redirect(route('checkout.checkout_card'))->withInput(Request::all());
}
// Zahlung vorbereiten
$pay = new PayoneController;
$pay->init($shopping_user, $shopping_order);
$amount = Yard::instance($this->instance)->totalWithShipping(2, '.', '') * 100;
$reference = $pay->setPrePayment($payment_method, $amount, 'EUR', $result);
$this->checkoutRepo->putSessionPayments('payment_reference', $reference);
$pay->setPersonalData();
return $pay->ResponseData();
}
/**