$planCode, 'subscription_status' => 'active', 'stripe_customer_id' => (string)$object['customer'], 'stripe_subscription_id' => (string)$object['subscription'], ]); app_audit_log($pdo, $tenantId, null, 'billing.subscription_started', 'tenant', $tenantId, ['plan_code' => $planCode]); } break; case 'customer.subscription.updated': $tenantId = isset($object['metadata']['tenant_id']) ? (int)$object['metadata']['tenant_id'] : null; $tenantId ??= billing_find_tenant_id_by_stripe_subscription($pdo, (string)($object['id'] ?? '')); if ($tenantId !== null) { $status = (string)($object['status'] ?? 'active'); $periodEnd = isset($object['current_period_end']) ? date('Y-m-d H:i:s', (int)$object['current_period_end']) : null; billing_update($pdo, $tenantId, [ 'subscription_status' => $status, 'current_period_end' => $periodEnd, ]); app_audit_log($pdo, $tenantId, null, 'billing.subscription_updated', 'tenant', $tenantId, ['status' => $status]); } break; case 'customer.subscription.deleted': $tenantId = isset($object['metadata']['tenant_id']) ? (int)$object['metadata']['tenant_id'] : null; $tenantId ??= billing_find_tenant_id_by_stripe_subscription($pdo, (string)($object['id'] ?? '')); if ($tenantId !== null) { billing_update($pdo, $tenantId, [ 'plan_code' => 'free', 'subscription_status' => 'canceled', ]); app_audit_log($pdo, $tenantId, null, 'billing.subscription_canceled', 'tenant', $tenantId); } break; case 'invoice.payment_failed': $tenantId = billing_find_tenant_id_by_stripe_customer($pdo, (string)($object['customer'] ?? '')); if ($tenantId !== null) { billing_update($pdo, $tenantId, ['subscription_status' => 'past_due']); app_audit_log($pdo, $tenantId, null, 'billing.payment_failed', 'tenant', $tenantId); } break; case 'invoice.paid': $tenantId = billing_find_tenant_id_by_stripe_customer($pdo, (string)($object['customer'] ?? '')); if ($tenantId !== null) { app_audit_log($pdo, $tenantId, null, 'billing.invoice_paid', 'tenant', $tenantId, [ 'amount_paid_cents' => $object['amount_paid'] ?? null, ]); // Dolibarr-Rechnungssynchronisation folgt in Phase 3. } break; default: // Unbehandelte Ereignistypen werden bewusst ignoriert, aber mit 200 // bestaetigt, damit Stripe sie nicht wiederholt zustellt. break; } http_response_code(200); echo 'ok';