Stripe-Konfigurationsfehler in abo-upgrade.php sauber abfangen
billing_stripe_price_id() und der komplette abo-upgrade.php-Ablauf warfen bisher eine ungefangene RuntimeException (Fatal Error), sobald STRIPE_SECRET_KEY nicht gesetzt ist (z. B. lokale Dev-Umgebung ohne echte Stripe-Zugangsdaten) - bestand bereits im alten Code, ist durch die neue Tarif-Vergleichstabelle mit mehr Buchen-Buttons aber leichter auslösbar geworden. Jetzt: sauberer 502 'Bezahldienst nicht verfuegbar' statt Fatal-Error-Seite. Erstmals lokal per HTTP end-to-end gegen echten Dev-Server + MariaDB getestet (PHP 8.3 + MariaDB 11.4 als portable Binaries installiert, siehe docs/dev-mysql.md): eigener Test-Mandant mit 12 aktiven Teilnehmern, Tarif-Tabelle korrekt gerendert (5 Tarife, Pflicht-Upgrade-Hinweis, Buchen- Buttons, Enterprise-Anfrage-Link), Buchung von 'free' bei Ueberschreitung korrekt mit 400 abgelehnt, Buchung eines bezahlten Tarifs ohne Stripe-Key jetzt sauber mit 502 statt Fatal-Error-Crash. Alle bestehenden Regressionstests weiterhin gruen: Golden Master (104), Billing-Capacity (10), M8-Isolation (10), M8-Rollenmatrix (55).
This commit is contained in:
+88
-78
@@ -37,95 +37,105 @@ if (!billing_plan_selectable_for($pdo, $tenantId, $planCode)) {
|
||||
$billing = billing_fetch_or_init($pdo, $tenantId);
|
||||
$baseUrl = saas_app_url('mandant-einstellungen.php');
|
||||
|
||||
// Fall 1: Wechsel auf "free" (kein Stripe-Preis) mit bestehender bezahlter
|
||||
// Subscription -> echtes Kuendigen der Subscription, kein Checkout noetig.
|
||||
if ($plans[$planCode]['stripe_lookup_key'] === null) {
|
||||
if ($planCode !== 'free') {
|
||||
// enterprise hat ebenfalls keinen Stripe-Preis, ist aber kein
|
||||
// Selbstbedienungs-Downgrade-Ziel - manuelle Absprache erforderlich.
|
||||
http_response_code(400);
|
||||
exit('Für diesen Tarif meldet euch bitte per E-Mail bei uns.');
|
||||
}
|
||||
|
||||
if ($billing['stripe_subscription_id'] !== null) {
|
||||
$cancelResult = stripe_cancel_subscription((string)$billing['stripe_subscription_id']);
|
||||
if (!$cancelResult['ok']) {
|
||||
app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.downgrade_failed', 'tenant', $tenantId, ['plan_code' => $planCode, 'error' => $cancelResult['error']]);
|
||||
http_response_code(502);
|
||||
exit('Das Abo konnte nicht gekündigt werden: ' . saas_html((string)$cancelResult['error']));
|
||||
try {
|
||||
// Fall 1: Wechsel auf "free" (kein Stripe-Preis) mit bestehender bezahlter
|
||||
// Subscription -> echtes Kuendigen der Subscription, kein Checkout noetig.
|
||||
if ($plans[$planCode]['stripe_lookup_key'] === null) {
|
||||
if ($planCode !== 'free') {
|
||||
// enterprise hat ebenfalls keinen Stripe-Preis, ist aber kein
|
||||
// Selbstbedienungs-Downgrade-Ziel - manuelle Absprache erforderlich.
|
||||
http_response_code(400);
|
||||
exit('Für diesen Tarif meldet euch bitte per E-Mail bei uns.');
|
||||
}
|
||||
}
|
||||
|
||||
billing_update($pdo, $tenantId, [
|
||||
'plan_code' => 'free',
|
||||
'subscription_status' => 'canceled',
|
||||
]);
|
||||
app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.downgraded_to_free', 'tenant', $tenantId);
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
// 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
|
||||
// Differenz automatisch anteilig ab (Proration).
|
||||
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']));
|
||||
if ($billing['stripe_subscription_id'] !== null) {
|
||||
$cancelResult = stripe_cancel_subscription((string)$billing['stripe_subscription_id']);
|
||||
if (!$cancelResult['ok']) {
|
||||
app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.downgrade_failed', 'tenant', $tenantId, ['plan_code' => $planCode, 'error' => $cancelResult['error']]);
|
||||
http_response_code(502);
|
||||
exit('Das Abo konnte nicht gekündigt werden: ' . saas_html((string)$cancelResult['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',
|
||||
'plan_code' => 'free',
|
||||
'subscription_status' => 'canceled',
|
||||
]);
|
||||
app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.plan_switched', 'tenant', $tenantId, ['plan_code' => $planCode]);
|
||||
app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.downgraded_to_free', 'tenant', $tenantId);
|
||||
|
||||
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]
|
||||
);
|
||||
$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 (!$result['ok']) {
|
||||
app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.checkout_failed', 'tenant', $tenantId, ['plan_code' => $planCode, 'error' => $result['error']]);
|
||||
// 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
|
||||
// Differenz automatisch anteilig ab (Proration).
|
||||
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]);
|
||||
|
||||
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 Bezahlvorgang konnte nicht gestartet werden: ' . saas_html((string)$result['error']));
|
||||
exit('Der Bezahldienst ist aktuell nicht verfügbar. Bitte später erneut versuchen oder uns kontaktieren.');
|
||||
}
|
||||
|
||||
app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.checkout_started', 'tenant', $tenantId, ['plan_code' => $planCode]);
|
||||
|
||||
header('Location: ' . $result['url']);
|
||||
exit;
|
||||
|
||||
Reference in New Issue
Block a user