$planCode, 'error' => $cancelResult['error']]); http_response_code(502); exit('Das Abo konnte nicht gekündigt werden: ' . saas_html((string)$cancelResult['error'])); } } if ($billing['stripe_subscription_id'] === null) { billing_update($pdo, $tenantId, [ 'plan_code' => 'free', 'subscription_status' => 'active', ]); } else { billing_update($pdo, $tenantId, [ 'subscription_status' => 'canceling', 'current_period_end' => $cancelResult['current_period_end'], ]); } app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.cancellation_scheduled', 'tenant', $tenantId, [ 'current_period_end' => $cancelResult['current_period_end'] ?? null, ]); header('Location: ' . $baseUrl . '?upgrade=success'); exit; } $priceId = billing_stripe_price_id($planCode); if ($priceId === null) { http_response_code(502); exit('Der Tarif ist bei Stripe aktuell nicht verfügbar. Bitte später erneut versuchen.'); } if (empty($_POST['accept_terms']) || empty($_POST['acknowledge_withdrawal']) || empty($_POST['request_early_performance'])) { http_response_code(400); exit('Bitte bestätige AGB, Widerrufsbelehrung und den gewünschten sofortigen Leistungsbeginn auf der Bestellseite.'); } $orderMetadata = [ 'plan_code' => $planCode, 'monthly_price_cents' => (int)$plans[$planCode]['price_cents'], 'customer_type' => (string)($user['customer_type'] ?? 'business'), ]; app_record_legal_acceptance($pdo, $tenantId, (int)$user['user_id'], 'terms', 'paid_order', $orderMetadata); app_record_legal_acceptance($pdo, $tenantId, (int)$user['user_id'], 'withdrawal_information', 'paid_order', $orderMetadata); app_record_legal_acceptance($pdo, $tenantId, (int)$user['user_id'], 'early_performance', 'paid_order', $orderMetadata); // Fall 2: Es gibt bereits eine aktive/bezahlte Subscription bei Stripe -> // Preis in-place wechseln (Up- oder Downgrade zwischen bezahlten Stufen), // statt eine weitere Checkout-Session zu erzeugen. Stripe rechnet die // neue Preis gilt ohne Zwischenbelastung ab der nächsten Verlängerung. if ($billing['stripe_subscription_id'] !== null) { $subscription = stripe_get_subscription((string)$billing['stripe_subscription_id']); if ($subscription['ok'] && in_array($subscription['status'], ['active', 'trialing', 'past_due'], true) && $subscription['item_id'] !== null) { $switchResult = stripe_update_subscription_price( (string)$billing['stripe_subscription_id'], (string)$subscription['item_id'], $priceId ); if (!$switchResult['ok']) { app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.plan_switch_failed', 'tenant', $tenantId, ['plan_code' => $planCode, 'error' => $switchResult['error']]); http_response_code(502); exit('Der Tarifwechsel konnte nicht durchgeführt werden: ' . saas_html((string)$switchResult['error'])); } // Lokal sofort spiegeln statt auf den Webhook zu warten, damit die // UI direkt den neuen Tarif zeigt; der Webhook (subscription.updated) // bestaetigt denselben Stand nochmal redundant. billing_update($pdo, $tenantId, [ 'plan_code' => $planCode, 'subscription_status' => 'active', ]); app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.plan_switched', 'tenant', $tenantId, ['plan_code' => $planCode]); $confirmation = saas_send_paid_contract_confirmation($pdo, $tenantId, $planCode); if (empty($confirmation['ok'])) { app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.contract_confirmation_failed', 'tenant', $tenantId, [ 'error' => $confirmation['error'] ?? 'unknown', ]); header('Location: ' . $baseUrl . '?upgrade=success&contract_mail_failed=1'); exit; } header('Location: ' . $baseUrl . '?upgrade=success'); exit; } // Falls die bestehende Subscription nicht mehr aktiv abrufbar ist // (z. B. bereits gekuendigt), faellt der Ablauf unten auf eine neue // Checkout-Session zurueck. } // Fall 3: Kein aktives Abo bisher (Neu-Abschluss) -> Stripe Checkout Session. $result = stripe_create_checkout_session( $priceId, (string)$user['email'], $billing['stripe_customer_id'], $baseUrl . '?upgrade=success', $baseUrl . '?upgrade=cancelled', ['tenant_id' => $tenantId, 'plan_code' => $planCode] ); if (!$result['ok']) { app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.checkout_failed', 'tenant', $tenantId, ['plan_code' => $planCode, 'error' => $result['error']]); http_response_code(502); exit('Der Bezahlvorgang konnte nicht gestartet werden: ' . saas_html((string)$result['error'])); } app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.checkout_started', 'tenant', $tenantId, ['plan_code' => $planCode]); header('Location: ' . $result['url']); exit; } catch (RuntimeException $e) { // Stripe ist in dieser Umgebung nicht konfiguriert (fehlender // STRIPE_SECRET_KEY, z. B. lokale Dev-Umgebung ohne echte Zugangsdaten) // oder eine andere Konfigurationsvoraussetzung fehlt - sauber als // Dienst-nicht-verfuegbar melden statt eine Fatal-Error-Seite zu zeigen. app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.stripe_unavailable', 'tenant', $tenantId, ['plan_code' => $planCode, 'error' => $e->getMessage()]); http_response_code(502); exit('Der Bezahldienst ist aktuell nicht verfügbar. Bitte später erneut versuchen oder uns kontaktieren.'); }