courseworx/backend/plugins/financial-plugin/routes/checkout.js
mmabdalla 5477297914 v2.0.2 - Complete Plugin Architecture System and Multi-Currency Implementation
Major Features Added:
- Complete Plugin Architecture System with financial plugin
- Multi-currency support with exchange rates
- Course type system (online, classroom, hybrid)
- Attendance tracking and QR code scanning
- Classroom sessions management
- Course sections and content management
- Professional video player with authentication
- Secure media serving system
- Shopping cart and checkout system
- Financial dashboard and earnings tracking
- Trainee progress tracking
- User notes and assignments system

Backend Infrastructure:
- Plugin loader and registry system
- Multi-currency database models
- Secure media middleware
- Course access middleware
- Financial plugin with payment processing
- Database migrations for new features
- API endpoints for all new functionality

Frontend Components:
- Course management interface
- Content creation and editing
- Section management with drag-and-drop
- Professional video player
- QR scanner for attendance
- Shopping cart and checkout flow
- Financial dashboard
- Plugin management interface
- Trainee details and progress views

This represents a major evolution of CourseWorx from a basic LMS to a comprehensive educational platform with plugin architecture.
2025-09-14 04:20:37 +03:00

376 lines
10 KiB
JavaScript

/**
* Checkout Routes for Financial Plugin
*
* This file handles the checkout process including payment intent creation,
* payment confirmation, and order processing.
*/
const express = require('express');
const { body, validationResult } = require('express-validator');
const { auth } = require('../../../middleware/auth');
const { Cart, Order, OrderItem, Coupon } = require('../models');
// Initialize Stripe only if API key is available
let stripe = null;
try {
if (process.env.STRIPE_SECRET_KEY) {
stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
}
} catch (error) {
console.warn('Stripe not configured, payment processing will be simulated');
}
const { v4: uuidv4 } = require('uuid');
const router = express.Router();
// Middleware to get cart
const getCart = async (req, res, next) => {
try {
const { userId } = req.user || {};
const sessionId = req.headers['x-session-id'] || req.sessionID;
const cart = await Cart.findByUserOrSession(userId, sessionId);
if (!cart || cart.items.length === 0) {
return res.status(400).json({
error: 'Cart is empty'
});
}
if (cart.isExpired()) {
await cart.destroy();
return res.status(400).json({
error: 'Cart has expired'
});
}
req.cart = cart;
next();
} catch (error) {
console.error('Cart middleware error:', error);
res.status(500).json({ error: 'Failed to get cart' });
}
};
// @route POST /api/financial/checkout/create-intent
// @desc Create payment intent for checkout
// @access Private
router.post('/create-intent', [
auth,
getCart,
body('paymentMethodId').optional().isString().withMessage('Valid payment method ID is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { paymentMethodId } = req.body;
const cart = req.cart;
const { userId } = req.user;
// Create order
const order = await Order.create({
userId,
orderNumber: Order.generateOrderNumber(),
status: 'pending',
totalAmount: cart.totalAmount,
discountAmount: cart.discountAmount,
taxAmount: cart.taxAmount,
finalAmount: cart.finalAmount,
couponId: cart.couponCode ? await Coupon.findByCode(cart.couponCode).then(c => c?.id) : null,
metadata: {
cartId: cart.id,
items: cart.items
}
});
// Create order items
for (const item of cart.items) {
await OrderItem.create({
orderId: order.id,
courseId: item.courseId,
courseType: item.type,
enrollmentType: 'one-time',
originalPrice: item.price,
finalPrice: item.price,
quantity: item.quantity,
metadata: {
addedAt: item.addedAt
}
});
}
// Create payment intent (Stripe or simulated)
let paymentIntent;
if (stripe) {
paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(cart.finalAmount * 100), // Convert to cents
currency: 'usd', // This should come from plugin settings
metadata: {
orderId: order.id,
orderNumber: order.orderNumber,
userId: userId
},
payment_method: paymentMethodId,
confirmation_method: 'manual',
confirm: !!paymentMethodId
});
} else {
// Simulate payment intent when Stripe is not configured
paymentIntent = {
id: `pi_simulated_${Date.now()}`,
client_secret: `pi_simulated_${Date.now()}_secret`,
status: 'requires_confirmation'
};
}
res.json({
order: {
id: order.id,
orderNumber: order.orderNumber,
status: order.status,
totalAmount: parseFloat(order.totalAmount),
finalAmount: parseFloat(order.finalAmount)
},
paymentIntent: {
id: paymentIntent.id,
clientSecret: paymentIntent.client_secret,
status: paymentIntent.status
}
});
} catch (error) {
console.error('Create payment intent error:', error);
res.status(500).json({ error: 'Failed to create payment intent' });
}
});
// @route POST /api/financial/checkout/confirm
// @desc Confirm payment and complete order
// @access Private
router.post('/confirm', [
auth,
body('paymentIntentId').isString().withMessage('Payment intent ID is required'),
body('orderId').isUUID().withMessage('Order ID is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { paymentIntentId, orderId } = req.body;
const { userId } = req.user;
// Get order
const order = await Order.findByPk(orderId, {
where: { userId }
});
if (!order) {
return res.status(404).json({
error: 'Order not found'
});
}
if (order.status !== 'pending') {
return res.status(400).json({
error: 'Order is not pending'
});
}
// Confirm payment intent (Stripe or simulated)
let paymentIntent;
if (stripe) {
paymentIntent = await stripe.paymentIntents.confirm(paymentIntentId);
} else {
// Simulate payment confirmation when Stripe is not configured
paymentIntent = {
id: paymentIntentId,
status: 'succeeded'
};
}
if (paymentIntent.status === 'succeeded') {
// Mark order as paid
await order.markAsPaid(paymentIntent.id, 'card');
// Get order items
const orderItems = await OrderItem.findByOrder(orderId);
// Mark items as enrolled (this will trigger course enrollment)
for (const item of orderItems) {
await item.markAsEnrolled();
}
// Clear cart
const cart = await Cart.findByUserOrSession(userId, req.headers['x-session-id']);
if (cart) {
await cart.destroy();
}
res.json({
success: true,
message: 'Payment successful',
order: {
id: order.id,
orderNumber: order.orderNumber,
status: order.status,
totalAmount: parseFloat(order.totalAmount),
finalAmount: parseFloat(order.finalAmount),
paidAt: order.paidAt
}
});
} else {
// Payment failed
await order.markAsFailed();
res.status(400).json({
success: false,
error: 'Payment failed',
order: {
id: order.id,
orderNumber: order.orderNumber,
status: order.status
}
});
}
} catch (error) {
console.error('Confirm payment error:', error);
res.status(500).json({ error: 'Failed to confirm payment' });
}
});
// @route POST /api/financial/checkout/webhook
// @desc Handle Stripe webhooks
// @access Public (but secured with webhook signature)
router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
if (stripe) {
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
} else {
// Simulate webhook event when Stripe is not configured
event = {
type: 'payment_intent.succeeded',
data: {
object: {
id: 'pi_simulated_webhook',
status: 'succeeded'
}
}
};
}
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
try {
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSucceeded(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailed(event.data.object);
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
res.json({ received: true });
} catch (error) {
console.error('Webhook handler error:', error);
res.status(500).json({ error: 'Webhook handler failed' });
}
});
// Webhook handlers
async function handlePaymentSucceeded(paymentIntent) {
try {
const orderId = paymentIntent.metadata.orderId;
const order = await Order.findByPk(orderId);
if (order && order.status === 'pending') {
await order.markAsPaid(paymentIntent.id, 'card');
// Get order items and mark as enrolled
const orderItems = await OrderItem.findByOrder(orderId);
for (const item of orderItems) {
await item.markAsEnrolled();
}
console.log(`Order ${order.orderNumber} payment confirmed via webhook`);
}
} catch (error) {
console.error('Handle payment succeeded error:', error);
}
}
async function handlePaymentFailed(paymentIntent) {
try {
const orderId = paymentIntent.metadata.orderId;
const order = await Order.findByPk(orderId);
if (order && order.status === 'pending') {
await order.markAsFailed();
console.log(`Order ${order.orderNumber} payment failed via webhook`);
}
} catch (error) {
console.error('Handle payment failed error:', error);
}
}
// @route GET /api/financial/checkout/session/:orderId
// @desc Get checkout session details
// @access Private
router.get('/session/:orderId', [
auth
], async (req, res) => {
try {
const { orderId } = req.params;
const { userId } = req.user;
const order = await Order.findByPk(orderId, {
where: { userId },
include: [
{
model: OrderItem,
as: 'items'
}
]
});
if (!order) {
return res.status(404).json({
error: 'Order not found'
});
}
res.json({
order: {
id: order.id,
orderNumber: order.orderNumber,
status: order.status,
totalAmount: parseFloat(order.totalAmount),
discountAmount: parseFloat(order.discountAmount),
taxAmount: parseFloat(order.taxAmount),
finalAmount: parseFloat(order.finalAmount),
paymentMethod: order.paymentMethod,
createdAt: order.createdAt,
paidAt: order.paidAt,
items: order.items
}
});
} catch (error) {
console.error('Get checkout session error:', error);
res.status(500).json({ error: 'Failed to get checkout session' });
}
});
module.exports = router;