Rechtstexte und B2C-Vertragsabläufe absichern

This commit is contained in:
2026-08-22 14:31:56 +02:00
parent d320a4fd7a
commit a31a235422
58 changed files with 2502 additions and 316 deletions
+51 -8
View File
@@ -4,6 +4,7 @@ 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();
@@ -34,12 +35,22 @@ if (!billing_plan_selectable_for($pdo, $tenantId, $planCode)) {
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 {
// Fall 1: Wechsel auf "free" (kein Stripe-Preis) mit bestehender bezahlter
// Subscription -> echtes Kuendigen der Subscription, kein Checkout noetig.
// 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
@@ -49,7 +60,7 @@ try {
}
if ($billing['stripe_subscription_id'] !== null) {
$cancelResult = stripe_cancel_subscription((string)$billing['stripe_subscription_id']);
$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);
@@ -57,11 +68,20 @@ try {
}
}
billing_update($pdo, $tenantId, [
'plan_code' => 'free',
'subscription_status' => 'canceled',
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,
]);
app_audit_log($pdo, $tenantId, (int)$user['user_id'], 'billing.downgraded_to_free', 'tenant', $tenantId);
header('Location: ' . $baseUrl . '?upgrade=success');
exit;
@@ -73,10 +93,24 @@ try {
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
// Differenz automatisch anteilig ab (Proration).
// 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']);
@@ -102,6 +136,15 @@ try {
]);
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;
}