Stripe: Kundendaten, Rechnungsnummer, manueller Zahlungseingang

Rueckmeldung der Buchhaltung und des Stripe-Calls (dev/2026-08-26-stripe/):
Stripe kannte bisher nur die E-Mail des Kunden, Zahlungen waren im Dashboard
weder ueber die Bestell- noch ueber die Rechnungsnummer auffindbar, und ein
Zahlungseingang mit falschem Verwendungszweck zwang die Buchhaltung dazu, die
Rechnung von Hand zu schreiben.

- SE1: Der Checkout haengt einen Stripe-Customer mit Name, E-Mail und
  Rechnungsanschrift an die Session (customer statt customer_email); die ID
  liegt an shopping_users und wird wiederverwendet. Namensformat "Vorname
  Nachname (Firma)", reine Firmenkunden unter der Firma. Faellt die
  Customer-Anlage aus, laeuft der Checkout auf dem bisherigen Weg weiter.
  DSGVO-Loeschungen entfernen jetzt auch das Customer-Objekt; die Charges
  bleiben als Buchungsbeleg bestehen.
- SE2: StripeMetadataBuilder als einzige Quelle fuer Metadaten und die
  description (Bestellnummer, Referenz, Kundenname) - damit greift die
  Dashboard-Suche, die bisher nichts Lesbares zu fassen bekam.
- SE3: Die Rechnungsnummer entsteht erst nach der Zahlung und wird daher per
  Job am PaymentIntent nachgetragen (Vorab-Reservierung wuerde Luecken im
  Nummernkreis erzeugen). stripe:backfill-invoice-metadata traegt sie
  rueckwirkend ab dem Go-Live 2026-07-10 nach.
- SE4: Neuer Admin-Button "Zahlungseingang erfassen" an jeder unbezahlten
  Bestellung - bisher gab es ihn nur bei Rechnungskauf, und er setzte nur
  paid, ohne Rechnung, Punkte, Abo-Aktivierung oder Mail. Der neue Weg laeuft
  durch paymentStatusPaidAction, verlangt den Verwendungszweck als Beleg und
  laesst einen abweichenden Betrag nur mit ausdruecklicher Bestaetigung zu.
- S6.4 (aus dem Juli-Plan vorgezogen): stripe:sync-payment-status gleicht
  offene Zahlungen aktiv gegen die API ab. Aufgefallen beim Testlauf - ein nie
  zugestellter Webhook war bisher nicht reparierbar, das Geld lag bei Stripe
  waehrend die Bestellung auf offen stand. Laeuft stuendlich im Scheduler.
- 110 Tests (Pest) fuer alle Pfade, Stripe-Client durchgehend gemockt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-08-28 09:04:28 +00:00
parent c3dc7eb35e
commit 2e71173c66
31 changed files with 3612 additions and 65 deletions

View file

@ -2,6 +2,8 @@
namespace App\Http\Controllers;
use App\Exceptions\ManualPaymentException;
use App\Http\Requests\Payment\RecordManualPaymentRequest;
use App\Models\PaymentTransaction;
use App\Models\ShoppingOrder;
use App\Models\ShoppingPayment;
@ -11,7 +13,10 @@ use App\Repositories\InvoiceRepository;
use App\Services\AboExternalCompletionService;
use App\Services\BusinessPlan\SalesPointsVolume;
use App\Services\CustomerPriority;
use App\Services\ManualPaymentService;
use App\Services\Payment;
use App\Services\Payment\Stripe\StripeInvoiceReferenceService;
use Illuminate\Http\RedirectResponse;
use Request;
class SalesController extends Controller
@ -400,6 +405,43 @@ class SalesController extends Controller
}
}
/**
* Zahlungseingang manuell erfassen (Plan dev/2026-08-26-stripe/, SE4).
*
* Für Zahlungen, die kein Anbieter melden kann — vor allem Vorkasse mit
* falschem Verwendungszweck. Löst denselben Paid-Pfad aus wie ein
* Anbieter-Callback: Rechnung, Umsatz/Punkte, Abo-Aktivierung und Mail.
*/
public function recordManualPayment(
RecordManualPaymentRequest $request,
ManualPaymentService $manualPayments,
): RedirectResponse {
$shoppingOrder = ShoppingOrder::findOrFail($request->integer('id'));
$shoppingPayment = ShoppingPayment::findOrFail($request->integer('payment_id'));
$back = $request->input('back') ?: route('admin_sales_users_detail', [$shoppingOrder->id]);
try {
$manualPayments->recordIncomingPayment(
$shoppingOrder,
$shoppingPayment,
$request->toServiceData(),
\Auth::user(),
);
\Session()->flash('alert-success', __('payment.manual_success'));
} catch (ManualPaymentException $e) {
\Session()->flash('alert-error', $e->getMessage());
} catch (\Throwable $e) {
\Log::channel('payment')->error('Manueller Zahlungseingang fehlgeschlagen: '.$e->getMessage(), [
'shopping_order_id' => $shoppingOrder->id,
'shopping_payment_id' => $shoppingPayment->id,
]);
\Session()->flash('alert-error', __('payment.manual_error_general'));
}
return redirect($back);
}
/*
Manuelle Rechnung erstellen.*/
public function invoice()
@ -419,6 +461,10 @@ class SalesController extends Controller
$invoice_repo->createAndSalesVolume($data);
}
// Rechnungsnummer an eine Stripe-Zahlung nachtragen (SE3)
$shopping_order->refresh();
StripeInvoiceReferenceService::dispatchForOrder($shopping_order);
if (isset($data['view']) && $data['view'] === 'sales_customer') {
return redirect(route('admin_sales_customers_detail', [$shopping_order->id]));
}