243 lines
12 KiB
PHP
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);
|