$returnUrls successurl/errorurl/backurl for the 3DS redirect. */ public function updateCreditCard(UserAbo $abo, CreditCardCheckData $card, array $returnUrls, ?string $source = null): CardUpdateResult { if (app(PaymentProviderManager::class)->aboPaymentsFrozen()) { return CardUpdateResult::error(__('payment.abo_payments_frozen')); } if (! $abo->canUpdatePaymentMethod()) { return CardUpdateResult::error(__('abo.card_update_not_allowed')); } $shoppingUser = $abo->shopping_user; if (! $shoppingUser) { return CardUpdateResult::error(__('abo.card_update_failed')); } $mode = $this->resolveMode($abo); // Konsistenz-Spur: vor externem PAYONE-Call + DB-Write setzen, damit ein Crash // zwischen erfolgreichem PAYONE-Call und DB-Write auffindbar bleibt. $abo->forceFill(['card_update_pending_at' => now()])->save(); $result = $this->payone->refreshAboCardToken($shoppingUser, $card, $returnUrls, $mode); if ($result->isApproved()) { $this->applyNewCard($abo, $result->carddata, $result->userid, $source); // 1-Cent-Reservierung freigeben; Fehler hier ist unkritisch (verfällt sonst). $this->safelyReleasePreauth($result->txid, $mode); return $result; } if ($result->isRedirect()) { // Pending-Spur bleibt bestehen; Finalisierung nach 3DS-Rückkehr (Controller). return $result; } // Fehler: Spur zurücknehmen, kein DB-Update. $abo->forceFill(['card_update_pending_at' => null])->save(); return $result; } /** * Writes the new card token atomically and idempotently and clears the pending trace. * Also used by the 3-D-Secure return path after successful authentication. * * @param array $carddata */ public function applyNewCard(UserAbo $abo, array $carddata, int $userid, ?string $source = null): void { try { DB::transaction(function () use ($abo, $carddata, $userid, $source) { $abo->forceFill([ 'carddata' => $carddata, 'payone_userid' => $userid, 'card_update_pending_at' => null, ])->save(); $abo->card_changes()->create([ 'masked_card' => UserAbo::formatMaskedCard($carddata), 'source' => $source, ]); }); } catch (Throwable $e) { // PAYONE hat die neue Karte, der lokale DB-Write schlug fehl → Inkonsistenz: // card_update_pending_at bleibt gesetzt (auffindbare Spur), CRITICAL-Alert. MyLog::writeLog( 'payment', 'critical', 'Kreditkarten-Update: DB-Write nach erfolgreichem PAYONE-Call fehlgeschlagen (Abo '.$abo->id.')', ['user_abo_id' => $abo->id, 'payone_userid' => $userid, 'exception' => $e->getMessage()], ); throw $e; } } private function safelyReleasePreauth(string $txid, string $mode): void { if ($txid === '') { return; } try { $this->payone->releasePreauth($txid, $mode); } catch (Throwable $e) { MyLog::writeLog( 'payone', 'warning', 'Preauth-Freigabe (capture amount=0) fehlgeschlagen, Reservierung verfällt', ['txid' => $txid, 'error' => $e->getMessage()], false, ); } } private function resolveMode(UserAbo $abo): string { return $abo->initialShoppingPayment()?->mode ?: config('payone.defaults.mode'); } }