getMessage()); http_response_code(400); exit(); } catch(\Stripe\Exception\SignatureVerificationException $e) { // Invalid signature log_webhook_event("Error: Invalid signature. " . $e->getMessage()); http_response_code(400); exit(); } log_webhook_event("Received event: " . $event->type); log_webhook_event(json_encode($event->data->object)); // Handle the event switch ($event->type) { case 'checkout.session.completed': $session = $event->data->object; $metadata = $session->metadata; // --- Idempotency Check --- // Check if we have already processed this session for a package or gift $stmt_check = db()->prepare("SELECT id FROM client_packages WHERE stripe_session_id = ? UNION SELECT id FROM gift_codes WHERE stripe_session_id = ?"); $stmt_check->execute([$session->id, $session->id]); if ($stmt_check->fetch()) { log_webhook_event("Event {$session->id} already processed. Exiting."); http_response_code(200); exit(); } // --- Validate Metadata --- $client_id = $metadata->client_id ?? null; $package_id = $metadata->package_id ?? null; $payment_option = $metadata->payment_option ?? 'full_price'; // e.g., 'full_price', 'deposit' $is_gift = $metadata->is_gift === 'true'; if (!$client_id || !$package_id) { log_webhook_event("Webhook Error: Missing client_id or package_id in session metadata for session {$session->id}."); http_response_code(400); exit(); } // --- Get Package and Client Details --- $stmt_pkg = db()->prepare("SELECT * FROM service_packages WHERE id = ?"); $stmt_pkg->execute([$package_id]); $package = $stmt_pkg->fetch(PDO::FETCH_ASSOC); $stmt_client = db()->prepare("SELECT * FROM clients WHERE id = ?"); $stmt_client->execute([$client_id]); $client = $stmt_client->fetch(PDO::FETCH_ASSOC); if (!$package || !$client) { log_webhook_event("Webhook Error: Package or Client not found for session {$session->id}."); http_response_code(404); exit(); } // --- Update Client with Stripe Customer ID --- $stripe_customer_id = $session->customer ?? $client['stripe_customer_id']; if ($stripe_customer_id && empty($client['stripe_customer_id'])) { $update_stmt = db()->prepare("UPDATE clients SET stripe_customer_id = ? WHERE id = ?"); $update_stmt->execute([$stripe_customer_id, $client_id]); log_webhook_event("Updated stripe_customer_id for client {$client_id}."); } // --- Handle Purchase based on Type --- if ($is_gift) { // --- Handle Gift Purchase --- $gift_code = 'GIFT' . strtoupper(bin2hex(random_bytes(8))); $stmt_gift = db()->prepare("INSERT INTO gift_codes (code, package_id, purchaser_client_id, stripe_session_id, is_redeemed, created_at) VALUES (?, ?, ?, ?, 0, NOW())"); $stmt_gift->execute([$gift_code, $package_id, $client_id, $session->id]); log_webhook_event("Created gift code {$gift_code} for package {$package_id} purchased by client {$client_id}."); } else { // --- Handle Regular Purchase (Package for Self) --- // 1. Create the client package record $sessions_to_add = $package['sessions_per_package'] ?? 0; if ($package['payment_type'] === 'subscription') { $sessions_to_add = 0; // Sessions are added on invoice payment for subscriptions } $stmt_cp = db()->prepare("INSERT INTO client_packages (client_id, package_id, stripe_session_id, purchase_date, sessions_remaining) VALUES (?, ?, ?, NOW(), ?)"); $stmt_cp->execute([$client_id, $package_id, $session->id, $sessions_to_add]); log_webhook_event("Created client_packages record for client {$client_id}, package {$package_id}."); // 2. Handle Subscription Creation if ($session->mode === 'subscription') { // This was a recurring package from the start $stmt_sub = db()->prepare("INSERT INTO client_subscriptions (client_id, package_id, stripe_subscription_id, status, start_date) VALUES (?, ?, ?, 'active', NOW())"); $stmt_sub->execute([$client_id, $package_id, $session->subscription]); log_webhook_event("Created client_subscriptions record for Stripe subscription {$session->subscription}."); } else if ($payment_option === 'deposit' && $package['payment_type'] === 'payment_plan' && $package['installments'] > 1) { // This was a deposit, now create the subscription for the rest $remaining_amount = $package['price'] - $package['deposit_amount']; $installments_count = $package['installments'] - 1; // Already paid one installment (the deposit) if ($installments_count > 0 && $remaining_amount > 0) { $installment_amount = round(($remaining_amount / $installments_count) * 100); try { // Create a new Price for the installments $stripe_price = \Stripe\Price::create([ 'product_data' => ['name' => $package['name'] . ' - Installment Plan'], 'unit_amount' => $installment_amount, 'currency' => 'usd', 'recurring' => ['interval' => $package['installment_interval']], ]); // Create the subscription $subscription = \Stripe\Subscription::create([ 'customer' => $stripe_customer_id, 'items' => [['price' => $stripe_price->id]], 'metadata' => [ 'client_id' => $client_id, 'package_id' => $package_id, 'is_installment_plan' => 'true' ], 'payment_behavior' => 'default_incomplete', 'payment_settings' => ['save_default_payment_method' => 'on_subscription'], 'proration_behavior' => 'none', 'trial_end' => strtotime('+1 ' . $package['installment_interval']), // Start billing after one interval ]); // Save subscription to our DB $stmt_db_sub = db()->prepare("INSERT INTO client_subscriptions (client_id, package_id, stripe_subscription_id, status, start_date) VALUES (?, ?, ?, 'active', NOW())"); $stmt_db_sub->execute([$client_id, $package_id, $subscription->id]); log_webhook_event("Created Stripe subscription {$subscription->id} for client {$client_id} after deposit."); } catch(\Exception $e) { log_webhook_event("Webhook Error: Failed to create subscription for client {$client_id} after deposit. Error: " . $e->getMessage()); } } } } // --- Update Coupon Usage --- if (isset($metadata->coupon_code)) { $stmt_coupon = db()->prepare('UPDATE discounts SET times_used = times_used + 1 WHERE code = ?'); $stmt_coupon->execute([$metadata->coupon_code]); log_webhook_event("Incremented usage count for coupon {$metadata->coupon_code}."); } break; case 'invoice.payment_succeeded': $invoice = $event->data->object; $subscription_id = $invoice->subscription; if (!$subscription_id) break; // Retrieve our subscription record $stmt = db()->prepare('SELECT * FROM client_subscriptions WHERE stripe_subscription_id = ?'); $stmt->execute([$subscription_id]); $subscription_record = $stmt->fetch(PDO::FETCH_ASSOC); if ($subscription_record) { $client_id = $subscription_record['client_id']; $package_id = $subscription_record['package_id']; // Get the package details to find out how many sessions to add $stmt_pkg = db()->prepare("SELECT sessions_per_package FROM service_packages WHERE id = ?"); $stmt_pkg->execute([$package_id]); $package = $stmt_pkg->fetch(PDO::FETCH_ASSOC); $sessions_to_add = $package['sessions_per_package'] ?? 0; if ($sessions_to_add > 0) { // Find the corresponding client_package to update sessions $update_stmt = db()->prepare('UPDATE client_packages SET sessions_remaining = sessions_remaining + ? WHERE client_id = ? AND package_id = ?'); $update_stmt->execute([$sessions_to_add, $client_id, $package_id]); log_webhook_event("Added {$sessions_to_add} sessions to client {$client_id} for package {$package_id} after invoice payment."); } // Ensure subscription status is active $update_sub_stmt = db()->prepare('UPDATE client_subscriptions SET status = ? WHERE id = ?'); $update_sub_stmt->execute(['active', $subscription_record['id']]); } else { log_webhook_event("Webhook Warning: Received invoice.payment_succeeded for an unknown subscription: {$subscription_id}"); } break; case 'invoice.payment_failed': $invoice = $event->data->object; $customer_id = $invoice->customer; $subscription_id = $invoice->subscription; if (!$subscription_id) break; // Update subscription status in our DB $stmt = db()->prepare("UPDATE client_subscriptions SET status = 'past_due' WHERE stripe_subscription_id = ?"); $stmt->execute([$subscription_id]); log_webhook_event("Set subscription status to 'past_due' for {$subscription_id}."); // Find client to send email $stmt_client = db()->prepare("SELECT * FROM clients WHERE stripe_customer_id = ?"); $stmt_client->execute([$customer_id]); $client = $stmt_client->fetch(PDO::FETCH_ASSOC); if ($client) { $to = $client['email']; $subject = 'Your Subscription Payment Failed'; $message_html = '

Hi ' . htmlspecialchars($client['name']) . ',

' . '

We were unable to process your recent subscription payment. Please update your payment method to keep your subscription active.

' . '

You can manage your subscription here: Manage Subscription

'; MailService::sendMail($to, $subject, $message_html, strip_tags($message_html)); log_webhook_event("Sent payment failed email to {$to}."); } break; case 'customer.subscription.deleted': $subscription = $event->data->object; $stmt = db()->prepare("UPDATE client_subscriptions SET status = 'canceled' WHERE stripe_subscription_id = ?"); $stmt->execute([$subscription->id]); log_webhook_event("Set subscription status to 'canceled' for {$subscription->id}."); break; default: log_webhook_event("Received unhandled event type: " . $event->type); } http_response_code(200);