courseworx/backend/plugins/financial-plugin/routes/coupons.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

380 lines
12 KiB
JavaScript

/**
* Coupon Management Routes for Financial Plugin
*
* This file handles coupon creation, validation, and management
* for the shopping cart and checkout system.
*/
const express = require('express');
const { body, validationResult } = require('express-validator');
const { auth, requireSuperAdmin } = require('../../../middleware/auth');
const { Coupon } = require('../models');
const router = express.Router();
// @route POST /api/financial/coupons
// @desc Create new coupon
// @access Private (Super Admin)
router.post('/', [
auth,
requireSuperAdmin,
body('name').isLength({ min: 3, max: 100 }).withMessage('Name must be between 3 and 100 characters'),
body('code').optional().isLength({ min: 3, max: 50 }).withMessage('Code must be between 3 and 50 characters'),
body('type').isIn(['percentage', 'fixed', 'free_shipping']).withMessage('Type must be percentage, fixed, or free_shipping'),
body('value').isFloat({ min: 0 }).withMessage('Value must be a positive number'),
body('maxUses').optional().isInt({ min: 1 }).withMessage('Max uses must be a positive integer'),
body('validFrom').optional().isISO8601().withMessage('Valid from must be a valid date'),
body('validTo').optional().isISO8601().withMessage('Valid to must be a valid date'),
body('minOrderAmount').optional().isFloat({ min: 0 }).withMessage('Minimum order amount must be positive'),
body('applicableCourses').optional().isArray().withMessage('Applicable courses must be an array')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
name,
code,
type,
value,
maxUses,
validFrom,
validTo,
minOrderAmount,
applicableCourses,
description
} = req.body;
// Generate code if not provided
let couponCode = code;
if (!couponCode) {
couponCode = await Coupon.createUniqueCode();
} else {
couponCode = couponCode.toUpperCase();
}
// Check if code already exists
const existingCoupon = await Coupon.findByCode(couponCode);
if (existingCoupon) {
return res.status(400).json({
error: 'Coupon code already exists'
});
}
// Validate date range
if (validFrom && validTo && new Date(validFrom) >= new Date(validTo)) {
return res.status(400).json({
error: 'Valid from date must be before valid to date'
});
}
// Create coupon
const coupon = await Coupon.create({
name,
code: couponCode,
description,
type,
value,
maxUses,
validFrom: validFrom ? new Date(validFrom) : new Date(),
validTo: validTo ? new Date(validTo) : null,
minOrderAmount,
applicableCourses,
createdBy: req.user.id
});
res.status(201).json({
message: 'Coupon created successfully',
coupon: {
id: coupon.id,
name: coupon.name,
code: coupon.code,
description: coupon.description,
type: coupon.type,
value: parseFloat(coupon.value),
maxUses: coupon.maxUses,
usedCount: coupon.usedCount,
validFrom: coupon.validFrom,
validTo: coupon.validTo,
minOrderAmount: parseFloat(coupon.minOrderAmount || 0),
applicableCourses: coupon.applicableCourses,
isActive: coupon.isActive,
createdAt: coupon.createdAt
}
});
} catch (error) {
console.error('Create coupon error:', error);
res.status(500).json({ error: 'Failed to create coupon' });
}
});
// @route GET /api/financial/coupons
// @desc Get all coupons
// @access Private (Super Admin)
router.get('/', auth, requireSuperAdmin, async (req, res) => {
try {
const { page = 1, limit = 20, isActive, type } = req.query;
const offset = (page - 1) * limit;
const whereClause = {};
if (isActive !== undefined) {
whereClause.isActive = isActive === 'true';
}
if (type) {
whereClause.type = type;
}
const { count, rows: coupons } = await Coupon.findAndCountAll({
where: whereClause,
order: [['createdAt', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
coupons: coupons.map(coupon => ({
id: coupon.id,
name: coupon.name,
code: coupon.code,
description: coupon.description,
type: coupon.type,
value: parseFloat(coupon.value),
maxUses: coupon.maxUses,
usedCount: coupon.usedCount,
validFrom: coupon.validFrom,
validTo: coupon.validTo,
minOrderAmount: parseFloat(coupon.minOrderAmount || 0),
applicableCourses: coupon.applicableCourses,
isActive: coupon.isActive,
createdAt: coupon.createdAt
})),
pagination: {
currentPage: parseInt(page),
totalPages: Math.ceil(count / limit),
totalItems: count,
itemsPerPage: parseInt(limit)
}
});
} catch (error) {
console.error('Get coupons error:', error);
res.status(500).json({ error: 'Failed to get coupons' });
}
});
// @route GET /api/financial/coupons/:id
// @desc Get coupon details
// @access Private (Super Admin)
router.get('/:id', auth, requireSuperAdmin, async (req, res) => {
try {
const { id } = req.params;
const coupon = await Coupon.findByPk(id);
if (!coupon) {
return res.status(404).json({
error: 'Coupon not found'
});
}
res.json({
coupon: {
id: coupon.id,
name: coupon.name,
code: coupon.code,
description: coupon.description,
type: coupon.type,
value: parseFloat(coupon.value),
maxUses: coupon.maxUses,
usedCount: coupon.usedCount,
validFrom: coupon.validFrom,
validTo: coupon.validTo,
minOrderAmount: parseFloat(coupon.minOrderAmount || 0),
applicableCourses: coupon.applicableCourses,
isActive: coupon.isActive,
createdAt: coupon.createdAt
}
});
} catch (error) {
console.error('Get coupon details error:', error);
res.status(500).json({ error: 'Failed to get coupon details' });
}
});
// @route PUT /api/financial/coupons/:id
// @desc Update coupon
// @access Private (Super Admin)
router.put('/:id', [
auth,
requireSuperAdmin,
body('name').optional().isLength({ min: 3, max: 100 }).withMessage('Name must be between 3 and 100 characters'),
body('type').optional().isIn(['percentage', 'fixed', 'free_shipping']).withMessage('Type must be percentage, fixed, or free_shipping'),
body('value').optional().isFloat({ min: 0 }).withMessage('Value must be a positive number'),
body('maxUses').optional().isInt({ min: 1 }).withMessage('Max uses must be a positive integer'),
body('validFrom').optional().isISO8601().withMessage('Valid from must be a valid date'),
body('validTo').optional().isISO8601().withMessage('Valid to must be a valid date'),
body('minOrderAmount').optional().isFloat({ min: 0 }).withMessage('Minimum order amount must be positive'),
body('applicableCourses').optional().isArray().withMessage('Applicable courses must be an array'),
body('isActive').optional().isBoolean().withMessage('Is active must be a boolean')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const updateData = req.body;
const coupon = await Coupon.findByPk(id);
if (!coupon) {
return res.status(404).json({
error: 'Coupon not found'
});
}
// Validate date range if both dates are provided
if (updateData.validFrom && updateData.validTo) {
if (new Date(updateData.validFrom) >= new Date(updateData.validTo)) {
return res.status(400).json({
error: 'Valid from date must be before valid to date'
});
}
}
// Update coupon
await coupon.update(updateData);
res.json({
message: 'Coupon updated successfully',
coupon: {
id: coupon.id,
name: coupon.name,
code: coupon.code,
description: coupon.description,
type: coupon.type,
value: parseFloat(coupon.value),
maxUses: coupon.maxUses,
usedCount: coupon.usedCount,
validFrom: coupon.validFrom,
validTo: coupon.validTo,
minOrderAmount: parseFloat(coupon.minOrderAmount || 0),
applicableCourses: coupon.applicableCourses,
isActive: coupon.isActive,
updatedAt: coupon.updatedAt
}
});
} catch (error) {
console.error('Update coupon error:', error);
res.status(500).json({ error: 'Failed to update coupon' });
}
});
// @route DELETE /api/financial/coupons/:id
// @desc Delete coupon
// @access Private (Super Admin)
router.delete('/:id', auth, requireSuperAdmin, async (req, res) => {
try {
const { id } = req.params;
const coupon = await Coupon.findByPk(id);
if (!coupon) {
return res.status(404).json({
error: 'Coupon not found'
});
}
// Check if coupon has been used
if (coupon.usedCount > 0) {
return res.status(400).json({
error: 'Cannot delete coupon that has been used'
});
}
await coupon.destroy();
res.json({
message: 'Coupon deleted successfully'
});
} catch (error) {
console.error('Delete coupon error:', error);
res.status(500).json({ error: 'Failed to delete coupon' });
}
});
// @route POST /api/financial/coupons/validate
// @desc Validate coupon code
// @access Private
router.post('/validate', [
auth,
body('code').isLength({ min: 3, max: 50 }).withMessage('Valid coupon code is required'),
body('orderAmount').isFloat({ min: 0 }).withMessage('Order amount must be positive'),
body('courseId').optional().isUUID().withMessage('Valid course ID is required')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { code, orderAmount, courseId } = req.body;
const coupon = await Coupon.findByCode(code);
if (!coupon) {
return res.status(400).json({
error: 'Invalid coupon code'
});
}
// Validate coupon
const validation = coupon.isValid();
if (!validation.valid) {
return res.status(400).json({
error: validation.reason
});
}
// Check minimum order amount
if (!coupon.canApplyToOrder(orderAmount)) {
return res.status(400).json({
error: `Minimum order amount of $${coupon.minOrderAmount} required`
});
}
// Check course applicability
if (courseId && !coupon.canApplyToCourse(courseId)) {
return res.status(400).json({
error: 'Coupon not applicable to this course'
});
}
// Calculate discount
const discountResult = coupon.calculateDiscount(orderAmount, courseId);
res.json({
valid: true,
coupon: {
id: coupon.id,
name: coupon.name,
code: coupon.code,
type: coupon.type,
value: parseFloat(coupon.value)
},
discount: {
amount: discountResult.discount,
reason: discountResult.reason
}
});
} catch (error) {
console.error('Validate coupon error:', error);
res.status(500).json({ error: 'Failed to validate coupon' });
}
});
module.exports = router;