36716-vm/stripe/webhook.php
2025-12-07 05:00:42 +00:00

243 lines
12 KiB
PHP

<?php
require_once '../db/config.php';
require_once 'init.php';
require_once '../mail/MailService.php';
// Function to log messages
function log_webhook_event($message) {
$log_file = __DIR__ . '/webhook_events.log';
file_put_contents($log_file, date('[Y-m-d H:i:s] ') . $message . "\n", FILE_APPEND);
}
$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$event = null;
try {
$event = \Stripe\Webhook::constructEvent(
$payload, $sig_header, STRIPE_WEBHOOK_SECRET
);
} catch(\UnexpectedValueException $e) {
// Invalid payload
log_webhook_event("Error: Invalid payload. " . $e->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 = '<p>Hi ' . htmlspecialchars($client['name']) . ',</p>' .
'<p>We were unable to process your recent subscription payment. Please update your payment method to keep your subscription active.</p>' .
'<p>You can manage your subscription here: <a href="' . getenv('APP_URL') . '/manage-subscription.php">Manage Subscription</a></p>';
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);