2025-10-29 21:45:14 +01:00
|
|
|
import express from 'express';
|
|
|
|
|
import { stripe } from '../config/stripe.js';
|
|
|
|
|
import { Order } from '../models/Order.js';
|
|
|
|
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
|
|
|
|
// Portfolio pricing configuration (price per ton in USD)
|
|
|
|
|
const PORTFOLIO_PRICING = {
|
|
|
|
|
1: 15, // Balanced portfolio
|
|
|
|
|
2: 18, // High-impact portfolio
|
|
|
|
|
3: 20 // Premium portfolio
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Processing fee percentage
|
|
|
|
|
const PROCESSING_FEE_PERCENT = 0.03; // 3%
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate pricing with processing fee
|
|
|
|
|
* @param {number} tons - Number of tons
|
2025-10-30 13:00:13 +01:00
|
|
|
* @param {number} pricePerTon - Price per ton from Wren API
|
2025-10-29 21:45:14 +01:00
|
|
|
* @returns {Object} Pricing breakdown
|
|
|
|
|
*/
|
2025-10-30 13:00:13 +01:00
|
|
|
function calculatePricing(tons, pricePerTon) {
|
2025-10-29 21:45:14 +01:00
|
|
|
const baseAmount = Math.round(tons * pricePerTon * 100); // Convert to cents
|
|
|
|
|
const processingFee = Math.round(baseAmount * PROCESSING_FEE_PERCENT);
|
|
|
|
|
const totalAmount = baseAmount + processingFee;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
baseAmount,
|
|
|
|
|
processingFee,
|
|
|
|
|
totalAmount,
|
|
|
|
|
pricePerTon
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* POST /api/checkout/create-session
|
|
|
|
|
* Create a Stripe Checkout Session
|
|
|
|
|
*/
|
|
|
|
|
router.post('/create-session', async (req, res) => {
|
|
|
|
|
try {
|
2025-10-30 13:00:13 +01:00
|
|
|
const { tons, portfolioId, pricePerTon, customerEmail } = req.body;
|
2025-10-29 21:45:14 +01:00
|
|
|
|
|
|
|
|
// Validation
|
|
|
|
|
if (!tons || tons <= 0) {
|
|
|
|
|
return res.status(400).json({ error: 'Invalid tons value' });
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 12:54:45 +01:00
|
|
|
// Accept any valid positive integer portfolio ID (from Wren API)
|
|
|
|
|
if (!portfolioId || !Number.isInteger(portfolioId) || portfolioId <= 0) {
|
|
|
|
|
console.error('❌ Invalid portfolio ID received:', portfolioId);
|
2025-10-29 21:45:14 +01:00
|
|
|
return res.status(400).json({ error: 'Invalid portfolio ID' });
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 13:00:13 +01:00
|
|
|
// Validate price per ton
|
|
|
|
|
if (!pricePerTon || pricePerTon <= 0) {
|
|
|
|
|
console.error('❌ Invalid pricePerTon received:', pricePerTon);
|
|
|
|
|
return res.status(400).json({ error: 'Invalid price per ton' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`📦 Creating checkout for portfolio ID: ${portfolioId}, tons: ${tons}, price: $${pricePerTon}/ton`);
|
2025-10-30 12:54:45 +01:00
|
|
|
|
2025-10-30 13:00:13 +01:00
|
|
|
// Calculate pricing using the price from frontend (Wren API)
|
|
|
|
|
const { baseAmount, processingFee, totalAmount } = calculatePricing(tons, pricePerTon);
|
2025-10-29 21:45:14 +01:00
|
|
|
|
|
|
|
|
// Create line items for Stripe
|
|
|
|
|
const lineItems = [
|
|
|
|
|
{
|
|
|
|
|
price_data: {
|
|
|
|
|
currency: 'usd',
|
|
|
|
|
product_data: {
|
|
|
|
|
name: `Carbon Offset - ${tons} tons`,
|
2025-10-30 13:05:52 +01:00
|
|
|
description: `Puffin Portfolio at $${Math.ceil(pricePerTon)}/ton`,
|
2025-10-30 12:18:57 +01:00
|
|
|
// images: ['https://puffinoffset.com/images/carbon-offset.png'], // Optional: Add your logo when available
|
2025-10-29 21:45:14 +01:00
|
|
|
},
|
|
|
|
|
unit_amount: baseAmount, // Base amount in cents
|
|
|
|
|
},
|
|
|
|
|
quantity: 1,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
price_data: {
|
|
|
|
|
currency: 'usd',
|
|
|
|
|
product_data: {
|
|
|
|
|
name: 'Processing Fee (3%)',
|
|
|
|
|
description: 'Transaction processing fee',
|
|
|
|
|
},
|
|
|
|
|
unit_amount: processingFee, // Processing fee in cents
|
|
|
|
|
},
|
|
|
|
|
quantity: 1,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Create Stripe Checkout Session
|
|
|
|
|
const session = await stripe.checkout.sessions.create({
|
|
|
|
|
payment_method_types: ['card'],
|
|
|
|
|
line_items: lineItems,
|
|
|
|
|
mode: 'payment',
|
|
|
|
|
success_url: `${process.env.FRONTEND_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
|
|
|
cancel_url: `${process.env.FRONTEND_URL}/checkout/cancel`,
|
|
|
|
|
customer_email: customerEmail,
|
|
|
|
|
metadata: {
|
|
|
|
|
tons: tons.toString(),
|
|
|
|
|
portfolioId: portfolioId.toString(),
|
|
|
|
|
baseAmount: baseAmount.toString(),
|
|
|
|
|
processingFee: processingFee.toString(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Store order in database
|
|
|
|
|
const order = Order.create({
|
|
|
|
|
stripeSessionId: session.id,
|
|
|
|
|
customerEmail: customerEmail || null,
|
|
|
|
|
tons,
|
|
|
|
|
portfolioId,
|
|
|
|
|
baseAmount,
|
|
|
|
|
processingFee,
|
|
|
|
|
totalAmount,
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`✅ Created checkout session: ${session.id}`);
|
|
|
|
|
console.log(` Order ID: ${order.id}`);
|
|
|
|
|
console.log(` Amount: $${(totalAmount / 100).toFixed(2)} (${tons} tons @ $${pricePerTon}/ton + 3% fee)`);
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
sessionId: session.id,
|
|
|
|
|
url: session.url,
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('❌ Checkout session creation error:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
error: 'Failed to create checkout session',
|
|
|
|
|
message: error.message,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* GET /api/checkout/session/:sessionId
|
|
|
|
|
* Retrieve checkout session details
|
|
|
|
|
*/
|
|
|
|
|
router.get('/session/:sessionId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { sessionId } = req.params;
|
|
|
|
|
|
|
|
|
|
// Get order from database
|
|
|
|
|
const order = Order.findBySessionId(sessionId);
|
|
|
|
|
|
|
|
|
|
if (!order) {
|
|
|
|
|
return res.status(404).json({ error: 'Order not found' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get Stripe session (optional - for additional details)
|
|
|
|
|
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
order: {
|
|
|
|
|
id: order.id,
|
|
|
|
|
tons: order.tons,
|
|
|
|
|
portfolioId: order.portfolio_id,
|
|
|
|
|
baseAmount: order.base_amount,
|
|
|
|
|
processingFee: order.processing_fee,
|
|
|
|
|
totalAmount: order.total_amount,
|
|
|
|
|
currency: order.currency,
|
|
|
|
|
status: order.status,
|
|
|
|
|
wrenOrderId: order.wren_order_id,
|
|
|
|
|
createdAt: order.created_at,
|
|
|
|
|
},
|
|
|
|
|
session: {
|
|
|
|
|
paymentStatus: session.payment_status,
|
|
|
|
|
customerEmail: session.customer_details?.email,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('❌ Session retrieval error:', error);
|
|
|
|
|
res.status(500).json({
|
|
|
|
|
error: 'Failed to retrieve session',
|
|
|
|
|
message: error.message,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default router;
|