$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) { $amountPaidCents = (int)($object['amount_paid'] ?? 0); app_audit_log($pdo, $tenantId, null, 'billing.invoice_paid', 'tenant', $tenantId, [ 'amount_paid_cents' => $amountPaidCents, ]); if ($amountPaidCents > 0) { $billing = billing_fetch_or_init($pdo, $tenantId); try { $sync = dolibarr_sync_invoice_paid($pdo, $tenantId, (string)$billing['plan_code'], $amountPaidCents); } catch (Throwable $e) { $sync = ['ok' => false, 'invoice_id' => null, 'ref' => null, 'error' => $e->getMessage()]; } if ($sync['ok']) { app_audit_log($pdo, $tenantId, null, 'billing.dolibarr_invoice_created', 'tenant', $tenantId, [ 'dolibarr_invoice_id' => $sync['invoice_id'], 'dolibarr_ref' => $sync['ref'], ]); } else { // Wird bewusst nicht als Fehler an Stripe zurueckgegeben (kein Retry // ausgeloest) - Stripe hat das Geld bereits erfolgreich eingezogen, // das ist unabhaengig vom Dolibarr-Spiegel. Fehler landet im Audit- // Log fuer manuelle Nachbearbeitung. app_audit_log($pdo, $tenantId, null, 'billing.dolibarr_invoice_failed', 'tenant', $tenantId, [ 'error' => $sync['error'], ]); } } } break; default: // Unbehandelte Ereignistypen werden bewusst ignoriert, aber mit 200 // bestaetigt, damit Stripe sie nicht wiederholt zustellt. break; } http_response_code(200); echo 'ok';