mivita/routes/domains/portal.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

80 lines
5.9 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------
| Main Website Routes (mivita.care)
|--------------------------------------------------------------------------
|
*/
use App\Http\Controllers\Portal\AboController;
use App\Http\Controllers\Portal\Auth\LoginController;
use App\Http\Controllers\Portal\CustomerController;
use App\Http\Controllers\Portal\InController;
use App\Http\Controllers\Portal\OrderController;
use App\Http\Controllers\Web\AboCardUpdateController;
/* ROUTING FOR CRM my.mivita / CMS */
Route::domain(config('app.pre_url_portal').config('app.domain').config('app.tld_care'))->group(function () {
Route::get('/', [InController::class, 'index'])->name('/portal.home');
Route::post('/loading/modal', [InController::class, 'loadingModal'])->name('portal.loading_modal');
Route::get('portal/change-login', [InController::class, 'changeLogin'])->name('portal.change_login');
// Standard login route für Laravel Auth-Kompatibilität
Route::get('login', function () {
return redirect()->route('portal.login.form');
})->name('login');
Route::get('portal/login', [LoginController::class, 'showLoginForm'])->name('portal.login.form');
Route::post('portal/login/send-otp', [LoginController::class, 'sendOtp'])->name('portal.login.send-otp');
Route::get('portal/login/verify/{email?}/{otp?}', [LoginController::class, 'showOtpForm'])->name('portal.login.otp.form'); // Zeigt OTP-Eingabeformular
Route::post('portal/login/verify', [LoginController::class, 'verifyOtpAndLogin'])->name('portal.login.verify-otp');
Route::get('portal/logout', [LoginController::class, 'logout'])->name('portal.logout');
Route::post('portal/logout-change', [LoginController::class, 'logoutChange'])->name('portal.logout_change');
// Login-freier Kreditkarten-Update-Link (Berater → Kunde); Kunde gibt selbst ein. Bewusst
// ausserhalb von auth:customers, da der Kunde nicht eingeloggt ist (Token = Berechtigung).
Route::get('abo/karte/{token}', [AboCardUpdateController::class, 'show'])->middleware('throttle:10,1')->name('abo_card_update_show');
Route::post('abo/karte/{token}', [AboCardUpdateController::class, 'store'])->middleware('throttle:10,1')->name('abo_card_update_store');
Route::post('abo/karte/{token}/stripe', [AboCardUpdateController::class, 'startStripe'])->middleware('throttle:10,1')->name('abo_card_update_stripe');
Route::get('abo/karte/{token}/return/{status}', [AboCardUpdateController::class, 'return'])->middleware('throttle:10,1')->name('abo_card_update_return');
// Geschützte Kunden-Routen
Route::middleware('auth:customers')->group(function () {
Route::get('portal/dashboard', [InController::class, 'dashboard'])->name('portal.dashboard');
// Route::get('portal/go-to-shop', [InController::class, 'goToShop'])->name('portal.go_to_shop');
Route::get('portal/my-data/edit', [CustomerController::class, 'myDataEdit'])->name('portal.my_data.edit');
Route::post('portal/my-data/store', [CustomerController::class, 'myDataStore'])->name('portal.my_data.store');
Route::get('portal/my-orders', [OrderController::class, 'myOrders'])->name('portal.my_orders');
Route::get('portal/my-orders/datatable', [OrderController::class, 'ordersDatatable'])->name('portal.my_orders.datatable');
Route::get('portal/my-orders/show/{id}', [OrderController::class, 'myOrderShow'])->name('portal.my_orders.show');
Route::get('portal/my-orders/{id}/payments/datatable', [OrderController::class, 'paymentsDatatable'])->name('portal.my_orders.payments_datatable');
Route::get('portal/my-orders/create/{id}', [OrderController::class, 'myOrderCreate'])->name('portal.my_orders.create');
Route::get('portal/my-subscriptions', [AboController::class, 'myAbo'])->name('portal.my_subscriptions');
Route::match(['get', 'post'], 'portal/my-subscriptions/create/{step}', [AboController::class, 'myAboCreate'])->name('portal.my_subscriptions.create');
Route::post('portal/my-subscriptions/update/{view}/{id}', [AboController::class, 'update'])->name('user_abos_update');
Route::post('portal/my-subscriptions/onetime/{view}/{id}', [AboController::class, 'oneTime'])->name('user_abos_onetime');
Route::post('portal/my-subscriptions/pay-now/{id}', [AboController::class, 'payNow'])->name('portal_abos_pay_now');
Route::get('portal/my-subscriptions/payment-method/{id}', [AboController::class, 'editPaymentMethod'])->name('portal_abos_payment_method_edit');
Route::post('portal/my-subscriptions/payment-method/{id}', [AboController::class, 'updatePaymentMethod'])->name('portal_abos_payment_method_update');
Route::post('portal/my-subscriptions/payment-method/{id}/stripe', [AboController::class, 'startStripePaymentMethodChange'])->name('portal_abos_payment_method_stripe');
Route::get('portal/my-subscriptions/payment-method/{id}/return/{status}', [AboController::class, 'paymentMethodReturn'])->name('portal_abos_payment_method_return');
Route::get('portal/my-subscriptions/datatable/{id}', [AboController::class, 'datatable'])->name('user_abo_datatable');
Route::get('portal/my-subscriptions/onetime-datatable/{id}', [AboController::class, 'oneTimeDatatable'])->name('user_abo_onetime_datatable');
Route::post('portal/modal/load', [AboController::class, 'modalLoad'])->name('modal_load');
// Routen-Aliase für shared Views (admin.abo._detail, admin.abo._executions)
Route::get('portal/my-data/edit/{id?}', function () {
return redirect()->route('portal.my_data.edit');
})->name('user_customer_edit');
Route::get('portal/my-orders/detail/{id}', [OrderController::class, 'myOrderShow'])->name('user_shop_order_detail');
Route::get('portal/settings', [InController::class, 'settings'])->name('portal.settings');
// Weitere Kunden-spezifische Routen
});
// Logout für Kunden
// Logout für Kunden
});