All checks were successful
Build and Push Docker Image / docker (push) Successful in 42s
## Stripe Payment Integration - Add Express.js backend server with Stripe Checkout Sessions - Create SQLite database for order tracking - Implement Stripe webhook handlers for payment events - Integrate with Wren Climate API for carbon offset fulfillment - Add CheckoutSuccess and CheckoutCancel pages - Create checkout API client for frontend - Update OffsetOrder component to redirect to Stripe Checkout - Add processing fee calculation (3% of base amount) - Implement order status tracking (pending → paid → fulfilled) Backend (server/): - Express server with CORS and middleware - SQLite database with Order schema - Stripe configuration and client - Order CRUD operations model - Checkout session creation endpoint - Webhook handler for payment confirmation - Wren API client for offset fulfillment Frontend: - CheckoutSuccess page with order details display - CheckoutCancel page with retry encouragement - Updated OffsetOrder to use Stripe checkout flow - Added checkout routes to App.tsx - TypeScript interfaces for checkout flow ## Visual & UX Enhancements - Add CertificationBadge component for project verification status - Create PortfolioDonutChart for visual portfolio allocation - Implement RadialProgress for percentage displays - Add reusable form components (FormInput, FormTextarea, FormSelect, FormFieldWrapper) - Refactor OffsetOrder with improved layout and animations - Add offset percentage slider with visual feedback - Enhance MobileOffsetOrder with better responsive design - Improve TripCalculator with cleaner UI structure - Update CurrencySelect with better styling - Add portfolio distribution visualization - Enhance project cards with hover effects and animations - Improve color palette and gradient usage throughout ## Configuration - Add VITE_API_BASE_URL environment variable - Create backend .env.example template - Update frontend .env.example with API URL - Add Stripe documentation references 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
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;
|