*/ public static function methods(): array { return [ self::METHOD_BANK_TRANSFER => __('payment.manual_method_bank_transfer'), self::METHOD_CASH => __('payment.manual_method_cash'), self::METHOD_OTHER => __('payment.manual_method_other'), ]; } /** * Verbucht den Zahlungseingang und löst alle Folgeschritte aus: * Rechnung, Umsatz/Punkte, Abo-Aktivierung und Bestätigungsmail. * * @param array{amount_cents: int, paid_at: string, method: string, bank_reference: string, note?: string|null, send_mail?: bool} $data * * @throws ManualPaymentException */ public function recordIncomingPayment( ShoppingOrder $shoppingOrder, ShoppingPayment $shoppingPayment, array $data, ?User $admin = null, ): PaymentTransaction { if ((int) $shoppingPayment->shopping_order_id !== (int) $shoppingOrder->id) { throw new ManualPaymentException(__('payment.manual_error_payment_mismatch')); } if ($shoppingOrder->paid) { throw new ManualPaymentException(__('payment.manual_error_already_paid')); } if (in_array($shoppingOrder->txaction, ['cancelled'], true)) { throw new ManualPaymentException(__('payment.manual_error_cancelled')); } $sendLink = false; $transaction = DB::transaction(function () use ($shoppingOrder, $shoppingPayment, $data, $admin, &$sendLink) { // Gegen Doppelklick und parallelen Anbieter-Callback absichern — // gleiches Muster wie im Stripe-/PAYONE-Webhook. $lockedOrder = ShoppingOrder::where('id', $shoppingOrder->id)->lockForUpdate()->first(); if ($lockedOrder->paid) { throw new ManualPaymentException(__('payment.manual_error_already_paid')); } $transaction = PaymentTransaction::create([ 'shopping_payment_id' => $shoppingPayment->id, 'provider' => 'manual', 'request' => 'manual_payment', 'txid' => 0, 'userid' => 0, 'status' => 'MANUAL', 'txaction' => 'paid', 'transmitted_data' => $this->auditPayload($shoppingPayment, $data, $admin), 'mode' => $shoppingPayment->mode, ]); $shoppingPayment->txaction = 'paid'; $shoppingPayment->save(); $lockedOrder->txaction = 'paid'; $lockedOrder->save(); $sendLink = Payment::paymentStatusPaidAction($lockedOrder, true, $shoppingPayment); return $transaction; }); if (! empty($data['send_mail'])) { Payment::paymentStatusSendMail($shoppingOrder->fresh(), $shoppingPayment, [ 'txaction' => 'paid', 'mode' => $shoppingPayment->mode, 'send_link' => $sendLink, ]); } $this->logAction($shoppingOrder, $shoppingPayment, $data, $admin); return $transaction; } /** * Revisionsspur: Was wurde erfasst, von wem, und weicht der Betrag ab. * * @param array $data * @return array */ private function auditPayload(ShoppingPayment $shoppingPayment, array $data, ?User $admin): array { $amountCents = (int) $data['amount_cents']; return [ 'source' => 'admin_manual_payment', 'amount_cents' => $amountCents, 'expected_cents' => (int) $shoppingPayment->amount, 'amount_matches' => $amountCents === (int) $shoppingPayment->amount, 'paid_at' => $data['paid_at'], 'method' => $data['method'], 'bank_reference' => $data['bank_reference'], 'note' => $data['note'] ?? null, 'recorded_by_user_id' => $admin?->id, 'recorded_by_email' => $admin?->email, 'recorded_at' => now()->toDateTimeString(), ]; } /** * @param array $data */ private function logAction(ShoppingOrder $shoppingOrder, ShoppingPayment $shoppingPayment, array $data, ?User $admin): void { $message = sprintf( 'Zahlungseingang manuell erfasst: %s (Beleg: %s, Zahlungsweg: %s, Zahlung #%d)', Util::formatNumber(((int) $data['amount_cents']) / 100).' '.$shoppingPayment->currency, $data['bank_reference'], $data['method'], $shoppingPayment->id, ); try { SysLog::action('manual-payment', 'payment', 3) ->setModel($shoppingOrder->id, ShoppingOrder::class) ->setUserId($admin?->id) ->setMessage($message) ->save(); } catch (\Throwable $e) { // Das Protokoll darf die Buchung nicht kippen — sie ist bereits committed. Log::channel('payment')->error('SysLog für manuellen Zahlungseingang fehlgeschlagen: '.$e->getMessage(), [ 'shopping_order_id' => $shoppingOrder->id, ]); } Log::channel('payment')->info($message, [ 'shopping_order_id' => $shoppingOrder->id, 'shopping_payment_id' => $shoppingPayment->id, 'recorded_by_user_id' => $admin?->id, ]); } }