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.
344 lines
11 KiB
JavaScript
344 lines
11 KiB
JavaScript
/**
|
|
* Multi-Currency System Tests
|
|
*
|
|
* This test suite validates the multi-currency functionality
|
|
* including currency management, exchange rates, and conversions.
|
|
*/
|
|
|
|
const { Currency, ExchangeRate, CourseCurrency, CurrencyService } = require('../models');
|
|
const { sequelize } = require('../../../config/database');
|
|
|
|
describe('Multi-Currency System', () => {
|
|
let testCurrencies = [];
|
|
let testExchangeRates = [];
|
|
|
|
beforeAll(async () => {
|
|
// Setup test database connection
|
|
await sequelize.authenticate();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Create test currencies
|
|
testCurrencies = await Currency.bulkCreate([
|
|
{
|
|
code: 'USD',
|
|
name: 'US Dollar',
|
|
symbol: '$',
|
|
decimalPlaces: 2,
|
|
isActive: true,
|
|
isBaseCurrency: true
|
|
},
|
|
{
|
|
code: 'EUR',
|
|
name: 'Euro',
|
|
symbol: '€',
|
|
decimalPlaces: 2,
|
|
isActive: true,
|
|
isBaseCurrency: false
|
|
},
|
|
{
|
|
code: 'EGP',
|
|
name: 'Egyptian Pound',
|
|
symbol: 'E£',
|
|
decimalPlaces: 2,
|
|
isActive: true,
|
|
isBaseCurrency: false
|
|
}
|
|
]);
|
|
|
|
// Create test exchange rates
|
|
testExchangeRates = await ExchangeRate.bulkCreate([
|
|
{
|
|
fromCurrencyId: testCurrencies[0].id, // USD
|
|
toCurrencyId: testCurrencies[1].id, // EUR
|
|
rate: 0.85,
|
|
effectiveDate: new Date(),
|
|
isActive: true,
|
|
source: 'manual'
|
|
},
|
|
{
|
|
fromCurrencyId: testCurrencies[0].id, // USD
|
|
toCurrencyId: testCurrencies[2].id, // EGP
|
|
rate: 30.50,
|
|
effectiveDate: new Date(),
|
|
isActive: true,
|
|
source: 'manual'
|
|
}
|
|
]);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up test data
|
|
await ExchangeRate.destroy({ where: {} });
|
|
await CourseCurrency.destroy({ where: {} });
|
|
await Currency.destroy({ where: {} });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await sequelize.close();
|
|
});
|
|
|
|
describe('Currency Management', () => {
|
|
test('should create currency with proper validation', async () => {
|
|
const currency = await Currency.create({
|
|
code: 'GBP',
|
|
name: 'British Pound Sterling',
|
|
symbol: '£',
|
|
decimalPlaces: 2,
|
|
isActive: true,
|
|
isBaseCurrency: false
|
|
});
|
|
|
|
expect(currency.code).toBe('GBP');
|
|
expect(currency.name).toBe('British Pound Sterling');
|
|
expect(currency.symbol).toBe('£');
|
|
expect(currency.decimalPlaces).toBe(2);
|
|
expect(currency.isActive).toBe(true);
|
|
expect(currency.isBaseCurrency).toBe(false);
|
|
});
|
|
|
|
test('should enforce unique currency codes', async () => {
|
|
await expect(
|
|
Currency.create({
|
|
code: 'USD', // Duplicate code
|
|
name: 'Duplicate USD',
|
|
symbol: '$',
|
|
decimalPlaces: 2,
|
|
isActive: true
|
|
})
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
test('should get active currencies', async () => {
|
|
const activeCurrencies = await CurrencyService.getActiveCurrencies();
|
|
expect(activeCurrencies.length).toBeGreaterThan(0);
|
|
expect(activeCurrencies.every(c => c.isActive)).toBe(true);
|
|
});
|
|
|
|
test('should get currency by code', async () => {
|
|
const usd = await CurrencyService.getCurrencyByCode('USD');
|
|
expect(usd).toBeTruthy();
|
|
expect(usd.code).toBe('USD');
|
|
});
|
|
|
|
test('should get base currency', async () => {
|
|
const baseCurrency = await CurrencyService.getBaseCurrency();
|
|
expect(baseCurrency).toBeTruthy();
|
|
expect(baseCurrency.isBaseCurrency).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Exchange Rate Management', () => {
|
|
test('should create exchange rate', async () => {
|
|
const exchangeRate = await ExchangeRate.create({
|
|
fromCurrencyId: testCurrencies[1].id, // EUR
|
|
toCurrencyId: testCurrencies[2].id, // EGP
|
|
rate: 35.88,
|
|
effectiveDate: new Date(),
|
|
isActive: true,
|
|
source: 'manual'
|
|
});
|
|
|
|
expect(exchangeRate.rate).toBe('35.88');
|
|
expect(exchangeRate.fromCurrencyId).toBe(testCurrencies[1].id);
|
|
expect(exchangeRate.toCurrencyId).toBe(testCurrencies[2].id);
|
|
});
|
|
|
|
test('should get exchange rate between currencies', async () => {
|
|
const rate = await CurrencyService.getExchangeRate(
|
|
testCurrencies[0].id, // USD
|
|
testCurrencies[1].id // EUR
|
|
);
|
|
|
|
expect(rate).toBeTruthy();
|
|
expect(parseFloat(rate.rate)).toBe(0.85);
|
|
});
|
|
|
|
test('should return null for non-existent exchange rate', async () => {
|
|
const rate = await CurrencyService.getExchangeRate(
|
|
testCurrencies[1].id, // EUR
|
|
testCurrencies[0].id // USD (reverse direction)
|
|
);
|
|
|
|
expect(rate).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Currency Conversion', () => {
|
|
test('should convert amount between currencies', async () => {
|
|
const conversion = await CurrencyService.convertAmount(
|
|
100, // 100 USD
|
|
testCurrencies[0].id, // USD
|
|
testCurrencies[1].id // EUR
|
|
);
|
|
|
|
expect(conversion.originalAmount).toBe(100);
|
|
expect(conversion.convertedAmount).toBe(85); // 100 * 0.85
|
|
expect(conversion.exchangeRate).toBe(0.85);
|
|
});
|
|
|
|
test('should return same amount for same currency', async () => {
|
|
const conversion = await CurrencyService.convertAmount(
|
|
100,
|
|
testCurrencies[0].id, // USD
|
|
testCurrencies[0].id // USD
|
|
);
|
|
|
|
expect(conversion.originalAmount).toBe(100);
|
|
expect(conversion.convertedAmount).toBe(100);
|
|
expect(conversion.exchangeRate).toBe(1);
|
|
});
|
|
|
|
test('should throw error for non-existent exchange rate', async () => {
|
|
await expect(
|
|
CurrencyService.convertAmount(
|
|
100,
|
|
testCurrencies[1].id, // EUR
|
|
testCurrencies[0].id // USD (no rate exists)
|
|
)
|
|
).rejects.toThrow('Exchange rate not found');
|
|
});
|
|
});
|
|
|
|
describe('Course Currency Configuration', () => {
|
|
test('should create course currency configuration', async () => {
|
|
const courseCurrency = await CourseCurrency.create({
|
|
courseId: 'test-course-id',
|
|
baseCurrencyId: testCurrencies[0].id, // USD
|
|
basePrice: 99.99,
|
|
allowedPaymentCurrencies: [testCurrencies[1].id, testCurrencies[2].id], // EUR, EGP
|
|
isActive: true
|
|
});
|
|
|
|
expect(courseCurrency.courseId).toBe('test-course-id');
|
|
expect(courseCurrency.baseCurrencyId).toBe(testCurrencies[0].id);
|
|
expect(courseCurrency.basePrice).toBe('99.99');
|
|
expect(courseCurrency.allowedPaymentCurrencies).toEqual([
|
|
testCurrencies[1].id,
|
|
testCurrencies[2].id
|
|
]);
|
|
});
|
|
|
|
test('should get course currency configuration', async () => {
|
|
const courseCurrency = await CourseCurrency.create({
|
|
courseId: 'test-course-id',
|
|
baseCurrencyId: testCurrencies[0].id,
|
|
basePrice: 99.99,
|
|
allowedPaymentCurrencies: [testCurrencies[1].id],
|
|
isActive: true
|
|
});
|
|
|
|
const config = await CurrencyService.getCourseCurrencyConfig('test-course-id');
|
|
expect(config).toBeTruthy();
|
|
expect(config.courseId).toBe('test-course-id');
|
|
expect(config.baseCurrencyId).toBe(testCurrencies[0].id);
|
|
});
|
|
|
|
test('should return null for non-existent course configuration', async () => {
|
|
const config = await CurrencyService.getCourseCurrencyConfig('non-existent-course');
|
|
expect(config).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Currency Formatting', () => {
|
|
test('should format USD currency', () => {
|
|
const usd = testCurrencies[0];
|
|
const formatted = CurrencyService.formatCurrency(99.99, usd);
|
|
expect(formatted).toBe('$99.99');
|
|
});
|
|
|
|
test('should format EUR currency', () => {
|
|
const eur = testCurrencies[1];
|
|
const formatted = CurrencyService.formatCurrency(85.50, eur);
|
|
expect(formatted).toBe('€85.50');
|
|
});
|
|
|
|
test('should format EGP currency', () => {
|
|
const egp = testCurrencies[2];
|
|
const formatted = CurrencyService.formatCurrency(3050.00, egp);
|
|
expect(formatted).toBe('E£3050.00');
|
|
});
|
|
|
|
test('should format without currency object', () => {
|
|
const formatted = CurrencyService.formatCurrency(99.99);
|
|
expect(formatted).toBe('99.99');
|
|
});
|
|
});
|
|
|
|
describe('Exchange Rate History', () => {
|
|
test('should track exchange rate changes', async () => {
|
|
const { ExchangeRateHistory } = require('../models');
|
|
|
|
// Update an exchange rate
|
|
const updatedRate = await CurrencyService.updateExchangeRate(
|
|
testCurrencies[0].id, // USD
|
|
testCurrencies[1].id, // EUR
|
|
0.90, // New rate
|
|
'test-user-id',
|
|
'Test rate update'
|
|
);
|
|
|
|
// Check history was created
|
|
const history = await ExchangeRateHistory.findOne({
|
|
where: {
|
|
fromCurrencyId: testCurrencies[0].id,
|
|
toCurrencyId: testCurrencies[1].id
|
|
}
|
|
});
|
|
|
|
expect(history).toBeTruthy();
|
|
expect(parseFloat(history.previousRate)).toBe(0.85);
|
|
expect(parseFloat(history.newRate)).toBe(0.90);
|
|
expect(history.changeReason).toBe('manual_update');
|
|
});
|
|
});
|
|
|
|
describe('Validation', () => {
|
|
test('should validate course currency configuration', async () => {
|
|
const validation = await CurrencyService.validateCourseCurrencyConfig(
|
|
'test-course-id',
|
|
testCurrencies[0].id, // Valid base currency
|
|
[testCurrencies[1].id, testCurrencies[2].id] // Valid payment currencies
|
|
);
|
|
|
|
expect(validation.isValid).toBe(true);
|
|
expect(validation.errors).toHaveLength(0);
|
|
});
|
|
|
|
test('should reject invalid base currency', async () => {
|
|
const validation = await CurrencyService.validateCourseCurrencyConfig(
|
|
'test-course-id',
|
|
'invalid-currency-id',
|
|
[testCurrencies[1].id]
|
|
);
|
|
|
|
expect(validation.isValid).toBe(false);
|
|
expect(validation.errors).toContain('Base currency is invalid or inactive');
|
|
});
|
|
|
|
test('should reject invalid payment currencies', async () => {
|
|
const validation = await CurrencyService.validateCourseCurrencyConfig(
|
|
'test-course-id',
|
|
testCurrencies[0].id,
|
|
['invalid-currency-id']
|
|
);
|
|
|
|
expect(validation.isValid).toBe(false);
|
|
expect(validation.errors).toContain('One or more allowed payment currencies are invalid or inactive');
|
|
});
|
|
});
|
|
});
|
|
|
|
// Integration tests
|
|
describe('Multi-Currency Integration', () => {
|
|
test('should handle complete course pricing workflow', async () => {
|
|
// This would test the complete workflow from course creation
|
|
// to payment processing with multiple currencies
|
|
// Implementation would depend on the full system integration
|
|
});
|
|
|
|
test('should handle payment processing with currency conversion', async () => {
|
|
// This would test payment processing with currency conversion
|
|
// Implementation would depend on payment system integration
|
|
});
|
|
});
|