177 lines
4.9 KiB
JavaScript
177 lines
4.9 KiB
JavaScript
|
|
import express from 'express';
|
|||
|
|
import { stripe, webhookSecret } from '../config/stripe.js';
|
|||
|
|
import { Order } from '../models/Order.js';
|
|||
|
|
import { createWrenOffsetOrder } from '../utils/wrenClient.js';
|
|||
|
|
|
|||
|
|
const router = express.Router();
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* POST /api/webhooks/stripe
|
|||
|
|
* Handle Stripe webhook events
|
|||
|
|
*
|
|||
|
|
* IMPORTANT: This endpoint requires raw body, not JSON parsed
|
|||
|
|
*/
|
|||
|
|
router.post('/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
|
|||
|
|
const sig = req.headers['stripe-signature'];
|
|||
|
|
|
|||
|
|
let event;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Verify webhook signature
|
|||
|
|
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('❌ Webhook signature verification failed:', err.message);
|
|||
|
|
return res.status(400).send(`Webhook Error: ${err.message}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`📬 Received webhook: ${event.type}`);
|
|||
|
|
|
|||
|
|
// Handle different event types
|
|||
|
|
switch (event.type) {
|
|||
|
|
case 'checkout.session.completed':
|
|||
|
|
await handleCheckoutSessionCompleted(event.data.object);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'checkout.session.async_payment_succeeded':
|
|||
|
|
await handleAsyncPaymentSucceeded(event.data.object);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case 'checkout.session.async_payment_failed':
|
|||
|
|
await handleAsyncPaymentFailed(event.data.object);
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
console.log(`ℹ️ Unhandled event type: ${event.type}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Return 200 to acknowledge receipt
|
|||
|
|
res.json({ received: true });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Handle checkout.session.completed event
|
|||
|
|
* This fires when payment succeeds (or session completes for delayed payment methods)
|
|||
|
|
*/
|
|||
|
|
async function handleCheckoutSessionCompleted(session) {
|
|||
|
|
console.log(`✅ Checkout session completed: ${session.id}`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Find order in database
|
|||
|
|
const order = Order.findBySessionId(session.id);
|
|||
|
|
|
|||
|
|
if (!order) {
|
|||
|
|
console.error(`❌ Order not found for session: ${session.id}`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update order with payment intent ID
|
|||
|
|
if (session.payment_intent) {
|
|||
|
|
Order.updatePaymentIntent(order.id, session.payment_intent);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update status to paid
|
|||
|
|
Order.updateStatus(order.id, 'paid');
|
|||
|
|
|
|||
|
|
console.log(`💳 Payment confirmed for order: ${order.id}`);
|
|||
|
|
console.log(` Customer: ${session.customer_details?.email}`);
|
|||
|
|
console.log(` Amount: $${(order.total_amount / 100).toFixed(2)}`);
|
|||
|
|
|
|||
|
|
// Fulfill order via Wren API
|
|||
|
|
await fulfillOrder(order, session);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ Error handling checkout session completed:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Handle async payment succeeded (e.g., ACH, bank transfer)
|
|||
|
|
*/
|
|||
|
|
async function handleAsyncPaymentSucceeded(session) {
|
|||
|
|
console.log(`✅ Async payment succeeded: ${session.id}`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const order = Order.findBySessionId(session.id);
|
|||
|
|
|
|||
|
|
if (!order) {
|
|||
|
|
console.error(`❌ Order not found for session: ${session.id}`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update status to paid
|
|||
|
|
Order.updateStatus(order.id, 'paid');
|
|||
|
|
|
|||
|
|
// Fulfill order
|
|||
|
|
await fulfillOrder(order, session);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ Error handling async payment succeeded:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Handle async payment failed
|
|||
|
|
*/
|
|||
|
|
async function handleAsyncPaymentFailed(session) {
|
|||
|
|
console.log(`❌ Async payment failed: ${session.id}`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const order = Order.findBySessionId(session.id);
|
|||
|
|
|
|||
|
|
if (!order) {
|
|||
|
|
console.error(`❌ Order not found for session: ${session.id}`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update status to failed
|
|||
|
|
Order.updateStatus(order.id, 'failed');
|
|||
|
|
|
|||
|
|
console.log(`💔 Order ${order.id} marked as failed`);
|
|||
|
|
|
|||
|
|
// TODO: Send notification to customer about failed payment
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ Error handling async payment failed:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Fulfill order by creating carbon offset via Wren API
|
|||
|
|
* @param {Object} order - Database order object
|
|||
|
|
* @param {Object} session - Stripe session object
|
|||
|
|
*/
|
|||
|
|
async function fulfillOrder(order, session) {
|
|||
|
|
try {
|
|||
|
|
console.log(`🌱 Fulfilling order ${order.id} via Wren API...`);
|
|||
|
|
|
|||
|
|
// Create Wren offset order
|
|||
|
|
const wrenOrder = await createWrenOffsetOrder({
|
|||
|
|
tons: order.tons,
|
|||
|
|
portfolioId: order.portfolio_id,
|
|||
|
|
customerEmail: session.customer_details?.email || order.customer_email,
|
|||
|
|
currency: order.currency,
|
|||
|
|
amountCents: order.total_amount,
|
|||
|
|
dryRun: false, // Set to true for testing without creating real offsets
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Update order with Wren order ID
|
|||
|
|
Order.updateWrenOrder(order.id, wrenOrder.id, 'fulfilled');
|
|||
|
|
|
|||
|
|
console.log(`✅ Order ${order.id} fulfilled successfully`);
|
|||
|
|
console.log(` Wren Order ID: ${wrenOrder.id}`);
|
|||
|
|
console.log(` Tons offset: ${order.tons}`);
|
|||
|
|
|
|||
|
|
// TODO: Send confirmation email to customer
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`❌ Order fulfillment failed for order ${order.id}:`, error);
|
|||
|
|
|
|||
|
|
// Mark order as paid but unfulfilled (manual intervention needed)
|
|||
|
|
Order.updateStatus(order.id, 'paid');
|
|||
|
|
|
|||
|
|
// TODO: Send alert to admin about failed fulfillment
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default router;
|