/** * Stripe Webhook Handler * Handles payment confirmations, refunds, and disputes * * Webhook events to configure in Stripe: * - payment_intent.succeeded * - payment_intent.payment_failed * - charge.refunded * - charge.dispute.created */ response = { "OK": false }; try { // Get raw request body payload = toString(getHttpRequestData().content); // Get Stripe signature header sigHeader = getHttpRequestData().headers["Stripe-Signature"] ?: ""; // Webhook secret (set in Stripe dashboard) webhookSecret = application.stripeWebhookSecret ?: ""; // For now, skip signature verification in development // In production, uncomment and implement signature verification: /* if (webhookSecret != "" && sigHeader != "") { // Verify webhook signature // This requires computing HMAC-SHA256 and comparing // See: https://stripe.com/docs/webhooks/signatures } */ // Parse event event = deserializeJSON(payload); eventType = event.type ?: ""; eventData = event.data.object ?: {}; // Log the webhook writeLog(file="stripe_webhooks", text="Received: #eventType# - #eventData.id ?: 'unknown'#"); switch (eventType) { case "payment_intent.succeeded": // Payment was successful paymentIntentID = eventData.id; orderID = val(eventData.metadata.order_id ?: 0); if (orderID > 0) { // Update order status to paid/submitted queryExecute(" UPDATE Orders SET OrderPaymentStatus = 'paid', OrderPaymentCompletedOn = NOW(), OrderStatusID = CASE WHEN OrderStatusID = 0 THEN 1 ELSE OrderStatusID END WHERE OrderID = :orderID ", { orderID: orderID }); // Create a task for the new order queryExecute(" INSERT INTO Tasks (TaskBusinessID, TaskCategoryID, TaskTitle, TaskCreatedOn, TaskStatusID, TaskSourceType, TaskSourceID) SELECT o.OrderBusinessID, 1, CONCAT('Order #', o.OrderID), NOW(), 0, 'order', o.OrderID FROM Orders o WHERE o.OrderID = :orderID AND NOT EXISTS (SELECT 1 FROM Tasks t WHERE t.TaskSourceType = 'order' AND t.TaskSourceID = :orderID) ", { orderID: orderID }); writeLog(file="stripe_webhooks", text="Order #orderID# marked as paid"); } break; case "payment_intent.payment_failed": // Payment failed paymentIntentID = eventData.id; orderID = val(eventData.metadata.order_id ?: 0); failureMessage = eventData.last_payment_error.message ?: "Payment failed"; if (orderID > 0) { queryExecute(" UPDATE Orders SET OrderPaymentStatus = 'failed', OrderPaymentError = :failureMessage WHERE OrderID = :orderID ", { orderID: orderID, failureMessage: failureMessage }); writeLog(file="stripe_webhooks", text="Order #orderID# payment failed: #failureMessage#"); } break; case "charge.refunded": // Handle refund chargeID = eventData.id; paymentIntentID = eventData.payment_intent ?: ""; refundAmount = eventData.amount_refunded / 100; if (paymentIntentID != "") { qOrder = queryExecute(" SELECT OrderID FROM Orders WHERE OrderStripePaymentIntentID = :paymentIntentID ", { paymentIntentID: paymentIntentID }); if (qOrder.recordCount > 0) { queryExecute(" UPDATE Orders SET OrderPaymentStatus = 'refunded', OrderRefundAmount = :refundAmount, OrderRefundedOn = NOW() WHERE OrderStripePaymentIntentID = :paymentIntentID ", { paymentIntentID: paymentIntentID, refundAmount: refundAmount }); writeLog(file="stripe_webhooks", text="Order refunded: $#refundAmount#"); } } break; case "charge.dispute.created": // Customer disputed a charge chargeID = eventData.id; paymentIntentID = eventData.payment_intent ?: ""; if (paymentIntentID != "") { qOrder = queryExecute(" SELECT OrderID, OrderBusinessID FROM Orders WHERE OrderStripePaymentIntentID = :paymentIntentID ", { paymentIntentID: paymentIntentID }); if (qOrder.recordCount > 0) { queryExecute(" UPDATE Orders SET OrderPaymentStatus = 'disputed' WHERE OrderStripePaymentIntentID = :paymentIntentID ", { paymentIntentID: paymentIntentID }); // Create a task for the dispute queryExecute(" INSERT INTO Tasks (TaskBusinessID, TaskCategoryID, TaskTitle, TaskDetails, TaskCreatedOn, TaskStatusID, TaskSourceType, TaskSourceID) VALUES (:businessID, 4, 'Payment Dispute', 'Order ###qOrder.OrderID# has been disputed. Review immediately.', NOW(), 0, 'dispute', :orderID) ", { businessID: qOrder.OrderBusinessID, orderID: qOrder.OrderID }); writeLog(file="stripe_webhooks", text="Dispute created for order #qOrder.OrderID#"); } } break; case "account.updated": // Connected account was updated accountID = eventData.id; chargesEnabled = eventData.charges_enabled ?: false; payoutsEnabled = eventData.payouts_enabled ?: false; if (chargesEnabled && payoutsEnabled) { queryExecute(" UPDATE Businesses SET BusinessStripeOnboardingComplete = 1 WHERE BusinessStripeAccountID = :accountID ", { accountID: accountID }); writeLog(file="stripe_webhooks", text="Account #accountID# is now fully active"); } break; default: writeLog(file="stripe_webhooks", text="Unhandled event type: #eventType#"); } response["OK"] = true; response["RECEIVED"] = true; } catch (any e) { writeLog(file="stripe_webhooks", text="Error: #e.message#"); response["ERROR"] = e.message; } writeOutput(serializeJSON(response));