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.
490 lines
13 KiB
JavaScript
490 lines
13 KiB
JavaScript
/**
|
|
* Migration: Create Financial Plugin Tables
|
|
*
|
|
* This migration creates all the necessary tables for the financial plugin
|
|
* including cart, orders, coupons, and related tables.
|
|
*/
|
|
|
|
module.exports = {
|
|
up: async (queryInterface, Sequelize) => {
|
|
// Create financial_carts table
|
|
await queryInterface.createTable('financial_carts', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
userId: {
|
|
type: Sequelize.UUID,
|
|
allowNull: true,
|
|
references: {
|
|
model: 'users',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'SET NULL'
|
|
},
|
|
sessionId: {
|
|
type: Sequelize.STRING(255),
|
|
allowNull: true,
|
|
unique: true
|
|
},
|
|
items: {
|
|
type: Sequelize.JSONB,
|
|
allowNull: false,
|
|
defaultValue: []
|
|
},
|
|
totalAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
discountAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
taxAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
finalAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
couponCode: {
|
|
type: Sequelize.STRING(50),
|
|
allowNull: true
|
|
},
|
|
expiresAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP + INTERVAL \'24 hours\'')
|
|
},
|
|
createdAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
updatedAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
}
|
|
});
|
|
|
|
// Create financial_orders table
|
|
await queryInterface.createTable('financial_orders', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
userId: {
|
|
type: Sequelize.UUID,
|
|
allowNull: false,
|
|
references: {
|
|
model: 'users',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'CASCADE'
|
|
},
|
|
orderNumber: {
|
|
type: Sequelize.STRING(50),
|
|
allowNull: false,
|
|
unique: true
|
|
},
|
|
status: {
|
|
type: Sequelize.ENUM('pending', 'paid', 'failed', 'refunded', 'cancelled'),
|
|
allowNull: false,
|
|
defaultValue: 'pending'
|
|
},
|
|
totalAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false
|
|
},
|
|
discountAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
taxAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
finalAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false
|
|
},
|
|
paymentMethod: {
|
|
type: Sequelize.STRING(50),
|
|
allowNull: true
|
|
},
|
|
gatewayTransactionId: {
|
|
type: Sequelize.STRING(255),
|
|
allowNull: true
|
|
},
|
|
couponId: {
|
|
type: Sequelize.UUID,
|
|
allowNull: true
|
|
},
|
|
metadata: {
|
|
type: Sequelize.JSONB,
|
|
allowNull: true,
|
|
defaultValue: {}
|
|
},
|
|
paidAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
refundedAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
refundAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: true,
|
|
defaultValue: 0.00
|
|
},
|
|
createdAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
updatedAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
}
|
|
});
|
|
|
|
// Create financial_order_items table
|
|
await queryInterface.createTable('financial_order_items', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
orderId: {
|
|
type: Sequelize.UUID,
|
|
allowNull: false,
|
|
references: {
|
|
model: 'financial_orders',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'CASCADE'
|
|
},
|
|
courseId: {
|
|
type: Sequelize.UUID,
|
|
allowNull: false,
|
|
references: {
|
|
model: 'courses',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'CASCADE'
|
|
},
|
|
courseType: {
|
|
type: Sequelize.ENUM('online', 'classroom', 'hybrid'),
|
|
allowNull: false
|
|
},
|
|
enrollmentType: {
|
|
type: Sequelize.ENUM('one-time', 'subscription', 'installment'),
|
|
allowNull: false,
|
|
defaultValue: 'one-time'
|
|
},
|
|
originalPrice: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false
|
|
},
|
|
finalPrice: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false
|
|
},
|
|
quantity: {
|
|
type: Sequelize.INTEGER,
|
|
allowNull: false,
|
|
defaultValue: 1
|
|
},
|
|
discountAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
metadata: {
|
|
type: Sequelize.JSONB,
|
|
allowNull: true,
|
|
defaultValue: {}
|
|
},
|
|
enrolledAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
enrollmentStatus: {
|
|
type: Sequelize.ENUM('pending', 'enrolled', 'failed'),
|
|
allowNull: false,
|
|
defaultValue: 'pending'
|
|
},
|
|
createdAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
updatedAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
}
|
|
});
|
|
|
|
// Create financial_coupons table
|
|
await queryInterface.createTable('financial_coupons', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
code: {
|
|
type: Sequelize.STRING(50),
|
|
allowNull: false,
|
|
unique: true
|
|
},
|
|
name: {
|
|
type: Sequelize.STRING(100),
|
|
allowNull: false
|
|
},
|
|
description: {
|
|
type: Sequelize.TEXT,
|
|
allowNull: true
|
|
},
|
|
type: {
|
|
type: Sequelize.ENUM('percentage', 'fixed', 'free_shipping'),
|
|
allowNull: false
|
|
},
|
|
value: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false
|
|
},
|
|
maxUses: {
|
|
type: Sequelize.INTEGER,
|
|
allowNull: true
|
|
},
|
|
usedCount: {
|
|
type: Sequelize.INTEGER,
|
|
allowNull: false,
|
|
defaultValue: 0
|
|
},
|
|
validFrom: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
validTo: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
applicableCourses: {
|
|
type: Sequelize.JSONB,
|
|
allowNull: true,
|
|
defaultValue: null
|
|
},
|
|
minOrderAmount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: true
|
|
},
|
|
isActive: {
|
|
type: Sequelize.BOOLEAN,
|
|
allowNull: false,
|
|
defaultValue: true
|
|
},
|
|
createdBy: {
|
|
type: Sequelize.UUID,
|
|
allowNull: true,
|
|
references: {
|
|
model: 'users',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'SET NULL'
|
|
},
|
|
createdAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
},
|
|
updatedAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
}
|
|
});
|
|
|
|
// Create financial_transactions table
|
|
await queryInterface.createTable('financial_transactions', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
orderId: {
|
|
type: Sequelize.UUID,
|
|
allowNull: false,
|
|
references: {
|
|
model: 'financial_orders',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'CASCADE'
|
|
},
|
|
amount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false
|
|
},
|
|
gatewayFee: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
platformFee: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
status: {
|
|
type: Sequelize.ENUM('pending', 'completed', 'failed', 'refunded'),
|
|
allowNull: false,
|
|
defaultValue: 'pending'
|
|
},
|
|
gatewayResponse: {
|
|
type: Sequelize.JSONB,
|
|
allowNull: true
|
|
},
|
|
webhookData: {
|
|
type: Sequelize.JSONB,
|
|
allowNull: true
|
|
},
|
|
processedAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
createdAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
}
|
|
});
|
|
|
|
// Create financial_payouts table
|
|
await queryInterface.createTable('financial_payouts', {
|
|
id: {
|
|
type: Sequelize.UUID,
|
|
defaultValue: Sequelize.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
trainerId: {
|
|
type: Sequelize.UUID,
|
|
allowNull: false,
|
|
references: {
|
|
model: 'users',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'CASCADE'
|
|
},
|
|
orderId: {
|
|
type: Sequelize.UUID,
|
|
allowNull: false,
|
|
references: {
|
|
model: 'financial_orders',
|
|
key: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'CASCADE'
|
|
},
|
|
amount: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false
|
|
},
|
|
platformFee: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false,
|
|
defaultValue: 0.00
|
|
},
|
|
trainerShare: {
|
|
type: Sequelize.DECIMAL(10, 2),
|
|
allowNull: false
|
|
},
|
|
status: {
|
|
type: Sequelize.ENUM('pending', 'processing', 'completed', 'failed'),
|
|
allowNull: false,
|
|
defaultValue: 'pending'
|
|
},
|
|
stripeTransferId: {
|
|
type: Sequelize.STRING(255),
|
|
allowNull: true
|
|
},
|
|
processedAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: true
|
|
},
|
|
createdAt: {
|
|
type: Sequelize.DATE,
|
|
allowNull: false,
|
|
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
|
}
|
|
});
|
|
|
|
// Add foreign key constraint for couponId in financial_orders
|
|
await queryInterface.addConstraint('financial_orders', {
|
|
fields: ['couponId'],
|
|
type: 'foreign key',
|
|
name: 'fk_financial_orders_coupon_id',
|
|
references: {
|
|
table: 'financial_coupons',
|
|
field: 'id'
|
|
},
|
|
onUpdate: 'CASCADE',
|
|
onDelete: 'SET NULL'
|
|
});
|
|
|
|
// Create indexes
|
|
await queryInterface.addIndex('financial_carts', ['userId']);
|
|
await queryInterface.addIndex('financial_carts', ['sessionId']);
|
|
await queryInterface.addIndex('financial_carts', ['expiresAt']);
|
|
|
|
await queryInterface.addIndex('financial_orders', ['userId']);
|
|
await queryInterface.addIndex('financial_orders', ['orderNumber']);
|
|
await queryInterface.addIndex('financial_orders', ['status']);
|
|
await queryInterface.addIndex('financial_orders', ['gatewayTransactionId']);
|
|
|
|
await queryInterface.addIndex('financial_order_items', ['orderId']);
|
|
await queryInterface.addIndex('financial_order_items', ['courseId']);
|
|
await queryInterface.addIndex('financial_order_items', ['enrollmentStatus']);
|
|
|
|
await queryInterface.addIndex('financial_coupons', ['code']);
|
|
await queryInterface.addIndex('financial_coupons', ['isActive']);
|
|
await queryInterface.addIndex('financial_coupons', ['validFrom', 'validTo']);
|
|
|
|
await queryInterface.addIndex('financial_transactions', ['orderId']);
|
|
await queryInterface.addIndex('financial_transactions', ['status']);
|
|
|
|
await queryInterface.addIndex('financial_payouts', ['trainerId']);
|
|
await queryInterface.addIndex('financial_payouts', ['orderId']);
|
|
await queryInterface.addIndex('financial_payouts', ['status']);
|
|
},
|
|
|
|
down: async (queryInterface, Sequelize) => {
|
|
// Drop tables in reverse order
|
|
await queryInterface.dropTable('financial_payouts');
|
|
await queryInterface.dropTable('financial_transactions');
|
|
await queryInterface.dropTable('financial_coupons');
|
|
await queryInterface.dropTable('financial_order_items');
|
|
await queryInterface.dropTable('financial_orders');
|
|
await queryInterface.dropTable('financial_carts');
|
|
}
|
|
};
|