Files

185 lines
8.3 KiB
PHP

<?php
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/app/billing.php';
require_once __DIR__ . '/app/audit.php';
require_once __DIR__ . '/app/saas-mail.php';
require_once __DIR__ . '/app/legal.php';
$pdo = app_db_pdo();
$user = saas_require_login();
if (!saas_can_manage_tenant_settings($user)) {
http_response_code(403);
exit('Kein Zugriff.');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: mandant-einstellungen.php');
exit;
}
app_require_csrf();
$tenantId = (int)$user['tenant_id'];
$planCode = (string)($_POST['plan_code'] ?? '');
$plans = billing_plans();
if (!isset($plans[$planCode])) {
http_response_code(400);
exit('Ungültiger Tarif.');
}
if (!billing_plan_selectable_for($pdo, $tenantId, $planCode)) {
http_response_code(400);
exit('Dieser Tarif deckt eure aktuelle Teilnehmerzahl nicht ab. Bitte einen passenden Tarif wählen.');
}
if ($planCode !== 'free' && !in_array((string)($user['customer_type'] ?? ''), ['business', 'consumer'], true)) {
http_response_code(400);
exit('Bitte wähle zuerst in den Mandant-Einstellungen, ob der Vertrag geschäftlich oder als Verbraucher geführt wird.');
}
$billing = billing_fetch_or_init($pdo, $tenantId);
$baseUrl = saas_app_url('mandant-einstellungen.php');
if ($planCode !== 'free' && !empty($user['contract_ends_at'])) {
http_response_code(409);
exit('Für diesen Vertrag liegt bereits eine Kündigung vor. Bitte nutze das Ticketsystem, wenn du sie zurücknehmen möchtest.');
}
try {
// Wechsel auf "free": die bezahlte Subscription wird zum Ende der bereits
// bezahlten Periode gekündigt. Bis dahin bleiben Tarif und Funktionen aktiv.
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_schedule_subscription_cancellation((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']));
}
}
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.');
}