initial commit

This commit is contained in:
rohit 2025-07-31 08:15:46 +05:30
commit 57d8f3fd5c
74 changed files with 13160 additions and 0 deletions

50
.env.example Normal file
View File

@ -0,0 +1,50 @@
# Server Configuration
NODE_ENV=development
PORT=3000
HOST=localhost
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=cloudtopiaa_reseller_portal
DB_USERNAME=postgres
DB_PASSWORD=your_password
DB_DIALECT=postgres
# JWT Configuration
JWT_SECRET=your_super_secret_jwt_key_here
JWT_REFRESH_SECRET=your_super_secret_refresh_key_here
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password
FROM_EMAIL=noreply@cloudtopiaa.com
FROM_NAME=Cloudtopiaa Reseller Portal
# File Upload Configuration
UPLOAD_PATH=uploads/
MAX_FILE_SIZE=5242880
ALLOWED_FILE_TYPES=pdf,doc,docx,jpg,jpeg,png
# Security Configuration
BCRYPT_ROUNDS=12
OTP_EXPIRES_IN=300000
SESSION_SECRET=your_session_secret_here
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# Frontend URL (for CORS and email links)
FRONTEND_URL=http://localhost:3000

101
.gitignore vendored Normal file
View File

@ -0,0 +1,101 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Uploads directory
uploads/
temp/
# Database
*.sqlite
*.db
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

8
.sequelizerc Normal file
View File

@ -0,0 +1,8 @@
const path = require('path');
module.exports = {
'config': path.resolve('src/config', 'database.js'),
'models-path': path.resolve('src/models'),
'seeders-path': path.resolve('src/seeders'),
'migrations-path': path.resolve('src/migrations')
};

59
package.json Normal file
View File

@ -0,0 +1,59 @@
{
"name": "cloudtopiaa-reseller-portal",
"version": "1.0.0",
"description": "Cloudtopiaa Reseller Portal - A centralized web-based platform for resellers to manage cloud services",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest",
"migrate": "sequelize-cli db:migrate",
"migrate:undo": "sequelize-cli db:migrate:undo",
"seed": "sequelize-cli db:seed:all",
"seed:undo": "sequelize-cli db:seed:undo:all",
"db:create": "sequelize-cli db:create",
"db:drop": "sequelize-cli db:drop"
},
"keywords": [
"reseller",
"portal",
"cloud",
"express",
"nodejs",
"postgresql"
],
"author": "Cloudtopiaa Team",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.15.0",
"helmet": "^6.1.5",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"sequelize": "^6.31.1",
"pg": "^8.11.0",
"pg-hstore": "^2.3.4",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.0",
"redis": "^4.6.7",
"nodemailer": "^6.9.2",
"speakeasy": "^2.0.0",
"qrcode": "^1.5.3",
"multer": "^1.4.5-lts.1",
"uuid": "^9.0.0",
"joi": "^17.9.2",
"morgan": "^1.10.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6"
},
"devDependencies": {
"nodemon": "^2.0.22",
"jest": "^29.5.0",
"supertest": "^6.3.3",
"sequelize-cli": "^6.6.1"
},
"engines": {
"node": ">=16.0.0"
}
}

183
src/app.js Normal file
View File

@ -0,0 +1,183 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const path = require('path');
// Import configurations
require('dotenv').config();
const { sequelize } = require('./models');
const redisClient = require('./config/redis');
// Import routes
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const resellerRoutes = require('./routes/resellers');
const productRoutes = require('./routes/products');
const customerRoutes = require('./routes/customers');
const billingRoutes = require('./routes/billing');
const dashboardRoutes = require('./routes/dashboard');
const reportsRoutes = require('./routes/reports');
const adminRoutes = require('./routes/admin');
const ordersRoutes = require('./routes/orders');
const provisioningRoutes = require('./routes/provisioning');
const legalRoutes = require('./routes/legal');
// Import middleware
const errorHandler = require('./middleware/errorHandler');
const { authenticateToken } = require('./middleware/auth');
const app = express();
// Security middleware
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"]
}
}
}));
// CORS configuration
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
}));
// Rate limiting
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
message: {
error: 'Too many requests from this IP, please try again later.'
},
standardHeaders: true,
legacyHeaders: false
});
app.use(limiter);
// Body parsing middleware
app.use(compression());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());
// Logging
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
} else {
app.use(morgan('combined'));
}
// Static files
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
app.use(express.static(path.join(__dirname, '../public')));
// Health check endpoint
app.get('/health', async (req, res) => {
try {
await sequelize.authenticate();
await redisClient.ping();
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
database: 'Connected',
redis: 'Connected'
});
} catch (error) {
res.status(503).json({
status: 'Service Unavailable',
timestamp: new Date().toISOString(),
error: error.message
});
}
});
// API routes
app.use('/api/auth', authRoutes);
app.use('/api/users', authenticateToken, userRoutes);
app.use('/api/resellers', authenticateToken, resellerRoutes);
app.use('/api/products', authenticateToken, productRoutes);
app.use('/api/customers', authenticateToken, customerRoutes);
app.use('/api/billing', authenticateToken, billingRoutes);
app.use('/api/dashboard', authenticateToken, dashboardRoutes);
app.use('/api/reports', authenticateToken, reportsRoutes);
app.use('/api/admin', authenticateToken, adminRoutes);
app.use('/api/orders', authenticateToken, ordersRoutes);
app.use('/api/provisioning', authenticateToken, provisioningRoutes);
app.use('/api/legal', legalRoutes); // Legal routes have their own auth middleware
// Serve frontend for SPA
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
// Error handling middleware
app.use(errorHandler);
// Database connection and server startup
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || 'localhost';
async function startServer() {
try {
// Test database connection
await sequelize.authenticate();
console.log('✅ Database connection established successfully.');
// Test Redis connection
await redisClient.ping();
console.log('✅ Redis connection established successfully.');
// Sync database (only in development)
if (process.env.NODE_ENV === 'development') {
await sequelize.sync({ alter: true });
console.log('✅ Database synchronized successfully.');
}
// Start server
app.listen(PORT, HOST, () => {
console.log(`🚀 Server running on http://${HOST}:${PORT}`);
console.log(`📊 Environment: ${process.env.NODE_ENV}`);
console.log(`🔗 Health check: http://${HOST}:${PORT}/health`);
});
} catch (error) {
console.error('❌ Unable to start server:', error);
process.exit(1);
}
}
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('🔄 SIGTERM received, shutting down gracefully...');
await sequelize.close();
await redisClient.quit();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('🔄 SIGINT received, shutting down gracefully...');
await sequelize.close();
await redisClient.quit();
process.exit(0);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
console.error('❌ Unhandled Promise Rejection:', err);
process.exit(1);
});
startServer();
module.exports = app;

70
src/config/database.js Normal file
View File

@ -0,0 +1,70 @@
require('dotenv').config();
module.exports = {
development: {
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'cloudtopiaa_reseller_portal_dev',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: console.log,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
define: {
timestamps: true,
underscored: true,
paranoid: true
}
},
test: {
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME + '_test' || 'cloudtopiaa_reseller_portal_test',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
define: {
timestamps: true,
underscored: true,
paranoid: true
}
},
production: {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false,
pool: {
max: 20,
min: 5,
acquire: 60000,
idle: 10000
},
define: {
timestamps: true,
underscored: true,
paranoid: true
},
dialectOptions: {
ssl: process.env.DB_SSL === 'true' ? {
require: true,
rejectUnauthorized: false
} : false
}
}
};

112
src/config/email.js Normal file
View File

@ -0,0 +1,112 @@
const nodemailer = require('nodemailer');
// Create transporter
const createTransporter = () => {
return nodemailer.createTransporter({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
},
tls: {
rejectUnauthorized: false
}
});
};
// Email templates
const emailTemplates = {
welcome: (name, loginUrl) => ({
subject: 'Welcome to Cloudtopiaa Reseller Portal',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #2c3e50;">Welcome to Cloudtopiaa Reseller Portal</h2>
<p>Hello ${name},</p>
<p>Welcome to the Cloudtopiaa Reseller Portal! Your account has been successfully created.</p>
<p>You can now access your dashboard and start managing your cloud services.</p>
<a href="${loginUrl}" style="background-color: #3498db; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">Login to Portal</a>
<p>If you have any questions, please don't hesitate to contact our support team.</p>
<p>Best regards,<br>Cloudtopiaa Team</p>
</div>
`
}),
passwordReset: (name, resetUrl, otp) => ({
subject: 'Password Reset Request - Cloudtopiaa Reseller Portal',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #e74c3c;">Password Reset Request</h2>
<p>Hello ${name},</p>
<p>We received a request to reset your password for your Cloudtopiaa Reseller Portal account.</p>
<p>Your OTP code is: <strong style="font-size: 24px; color: #2c3e50;">${otp}</strong></p>
<p>This code will expire in 5 minutes.</p>
<p>Alternatively, you can click the link below to reset your password:</p>
<a href="${resetUrl}" style="background-color: #e74c3c; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">Reset Password</a>
<p>If you didn't request this password reset, please ignore this email.</p>
<p>Best regards,<br>Cloudtopiaa Team</p>
</div>
`
}),
emailVerification: (name, verificationUrl, otp) => ({
subject: 'Email Verification - Cloudtopiaa Reseller Portal',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #27ae60;">Email Verification</h2>
<p>Hello ${name},</p>
<p>Please verify your email address to complete your registration.</p>
<p>Your verification code is: <strong style="font-size: 24px; color: #2c3e50;">${otp}</strong></p>
<p>This code will expire in 5 minutes.</p>
<p>Alternatively, you can click the link below to verify your email:</p>
<a href="${verificationUrl}" style="background-color: #27ae60; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">Verify Email</a>
<p>Best regards,<br>Cloudtopiaa Team</p>
</div>
`
}),
mfaSetup: (name, qrCodeUrl, backupCodes) => ({
subject: 'Two-Factor Authentication Setup - Cloudtopiaa Reseller Portal',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #9b59b6;">Two-Factor Authentication Setup</h2>
<p>Hello ${name},</p>
<p>You have successfully enabled two-factor authentication for your account.</p>
<p>Please save these backup codes in a secure location:</p>
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin: 15px 0;">
${backupCodes.map(code => `<p style="margin: 5px 0; font-family: monospace;">${code}</p>`).join('')}
</div>
<p>These codes can be used to access your account if you lose access to your authenticator app.</p>
<p>Best regards,<br>Cloudtopiaa Team</p>
</div>
`
})
};
// Send email function
const sendEmail = async (to, template, data = {}) => {
try {
const transporter = createTransporter();
const emailContent = emailTemplates[template](data.name, data.url, data.otp, data.backupCodes);
const mailOptions = {
from: `${process.env.FROM_NAME} <${process.env.FROM_EMAIL}>`,
to: to,
subject: emailContent.subject,
html: emailContent.html
};
const result = await transporter.sendMail(mailOptions);
console.log('✅ Email sent successfully:', result.messageId);
return { success: true, messageId: result.messageId };
} catch (error) {
console.error('❌ Email sending failed:', error);
return { success: false, error: error.message };
}
};
module.exports = {
sendEmail,
emailTemplates
};

51
src/config/redis.js Normal file
View File

@ -0,0 +1,51 @@
const redis = require('redis');
const redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || undefined,
db: process.env.REDIS_DB || 0,
retry_strategy: (options) => {
if (options.error && options.error.code === 'ECONNREFUSED') {
console.error('❌ Redis server connection refused');
return new Error('Redis server connection refused');
}
if (options.total_retry_time > 1000 * 60 * 60) {
console.error('❌ Redis retry time exhausted');
return new Error('Retry time exhausted');
}
if (options.attempt > 10) {
console.error('❌ Redis connection attempts exceeded');
return undefined;
}
// Reconnect after
return Math.min(options.attempt * 100, 3000);
}
});
redisClient.on('connect', () => {
console.log('🔗 Redis client connected');
});
redisClient.on('ready', () => {
console.log('✅ Redis client ready');
});
redisClient.on('error', (err) => {
console.error('❌ Redis client error:', err);
});
redisClient.on('end', () => {
console.log('🔚 Redis client disconnected');
});
// Connect to Redis
(async () => {
try {
await redisClient.connect();
} catch (error) {
console.error('❌ Failed to connect to Redis:', error);
}
})();
module.exports = redisClient;

View File

@ -0,0 +1,279 @@
const { Product, Reseller, User, Customer, Invoice, WalletTransaction, AuditLog } = require('../models');
const { Op } = require('sequelize');
// Create new product (admin only)
const createProduct = async (req, res) => {
try {
const {
name,
description,
category,
subcategory,
sku,
basePrice,
currency = 'INR',
billingType,
billingCycle,
unit,
specifications = {},
features = [],
tierPricing = {
bronze: { margin: 20 },
silver: { margin: 25 },
gold: { margin: 30 },
platinum: { margin: 35 },
diamond: { margin: 40 }
},
availability = { regions: [], zones: [] },
tags = []
} = req.body;
const product = await Product.create({
name,
description,
category,
subcategory,
sku,
basePrice,
currency,
billingType,
billingCycle,
unit,
specifications,
features,
tierPricing,
availability,
tags,
createdBy: req.user.id
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'PRODUCT_CREATED',
resource: 'product',
resourceId: product.id,
newValues: product.toJSON(),
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.status(201).json({
success: true,
message: 'Product created successfully',
data: { product }
});
} catch (error) {
console.error('Create product error:', error);
res.status(500).json({
success: false,
message: 'Failed to create product'
});
}
};
// Update product (admin only)
const updateProduct = async (req, res) => {
try {
const { productId } = req.params;
const updates = req.body;
const product = await Product.findByPk(productId);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
const oldValues = product.toJSON();
await product.update({
...updates,
updatedBy: req.user.id
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'PRODUCT_UPDATED',
resource: 'product',
resourceId: product.id,
oldValues,
newValues: updates,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Product updated successfully',
data: { product }
});
} catch (error) {
console.error('Update product error:', error);
res.status(500).json({
success: false,
message: 'Failed to update product'
});
}
};
// Get all products (admin view)
const getAllProducts = async (req, res) => {
try {
const { page = 1, limit = 20, category, status, search } = req.query;
const offset = (page - 1) * limit;
const where = {};
if (category) where.category = category;
if (status) where.status = status;
if (search) {
where[Op.or] = [
{ name: { [Op.iLike]: `%${search}%` } },
{ description: { [Op.iLike]: `%${search}%` } },
{ sku: { [Op.iLike]: `%${search}%` } }
];
}
const { count, rows: products } = await Product.findAndCountAll({
where,
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'firstName', 'lastName']
},
{
model: User,
as: 'updater',
attributes: ['id', 'firstName', 'lastName']
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: {
products,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get all products error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch products'
});
}
};
// Get system statistics
const getSystemStats = async (req, res) => {
try {
const [
totalResellers,
activeResellers,
pendingResellers,
totalCustomers,
activeCustomers,
totalProducts,
activeProducts,
totalRevenue,
monthlyRevenue
] = await Promise.all([
Reseller.count(),
Reseller.count({ where: { status: 'active' } }),
Reseller.count({ where: { status: 'pending_approval' } }),
Customer.count(),
Customer.count({ where: { status: 'active' } }),
Product.count(),
Product.count({ where: { status: 'active' } }),
Invoice.sum('totalAmount', { where: { status: 'paid' } }),
Invoice.sum('totalAmount', {
where: {
status: 'paid',
paidAt: {
[Op.gte]: new Date(new Date().getFullYear(), new Date().getMonth(), 1)
}
}
})
]);
const stats = {
resellers: {
total: totalResellers,
active: activeResellers,
pending: pendingResellers
},
customers: {
total: totalCustomers,
active: activeCustomers
},
products: {
total: totalProducts,
active: activeProducts
},
revenue: {
total: parseFloat(totalRevenue || 0),
monthly: parseFloat(monthlyRevenue || 0)
}
};
res.json({
success: true,
data: { stats }
});
} catch (error) {
console.error('Get system stats error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch system statistics'
});
}
};
// Get recent activity
const getRecentActivity = async (req, res) => {
try {
const { limit = 20 } = req.query;
const recentActivity = await AuditLog.findAll({
include: [
{
model: User,
as: 'user',
attributes: ['id', 'firstName', 'lastName', 'email']
}
],
limit: parseInt(limit),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: { recentActivity }
});
} catch (error) {
console.error('Get recent activity error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch recent activity'
});
}
};
module.exports = {
createProduct,
updateProduct,
getAllProducts,
getSystemStats,
getRecentActivity
};

View File

@ -0,0 +1,350 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const { User, UserSession, AuditLog } = require('../models');
const { sendEmail } = require('../config/email');
const redisClient = require('../config/redis');
// Generate JWT tokens
const generateTokens = (userId) => {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '15m' }
);
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
);
return { accessToken, refreshToken };
};
// Register new user
const register = async (req, res) => {
try {
const { email, password, firstName, lastName, phone, role = 'read_only' } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(409).json({
success: false,
message: 'User already exists with this email'
});
}
// Generate email verification token
const emailVerificationToken = crypto.randomBytes(32).toString('hex');
const emailVerificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
// Create user
const user = await User.create({
email,
password,
firstName,
lastName,
phone,
role,
emailVerificationToken,
emailVerificationExpires
});
// Send verification email
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${emailVerificationToken}`;
await sendEmail(email, 'emailVerification', {
name: `${firstName} ${lastName}`,
url: verificationUrl,
otp: emailVerificationToken.substring(0, 6).toUpperCase()
});
// Log audit
await AuditLog.create({
userId: user.id,
action: 'USER_REGISTERED',
resource: 'user',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.status(201).json({
success: true,
message: 'User registered successfully. Please check your email for verification.',
data: {
user: user.toJSON()
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'Registration failed',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// Login user
const login = async (req, res) => {
try {
const { email, password, mfaToken } = req.body;
// Find user
const user = await User.findOne({
where: { email },
include: ['reseller']
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Check if account is locked
if (user.isLocked()) {
return res.status(423).json({
success: false,
message: 'Account is temporarily locked due to multiple failed login attempts'
});
}
// Validate password
const isValidPassword = await user.validatePassword(password);
if (!isValidPassword) {
await user.incrementLoginAttempts();
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Check if email is verified
if (!user.emailVerified) {
return res.status(401).json({
success: false,
message: 'Please verify your email before logging in'
});
}
// Check account status
if (user.status !== 'active') {
return res.status(401).json({
success: false,
message: 'Account is not active'
});
}
// Check MFA if enabled
if (user.mfaEnabled) {
if (!mfaToken) {
return res.status(200).json({
success: true,
requiresMFA: true,
message: 'MFA token required'
});
}
const isMfaValid = user.verifyMfaToken(mfaToken);
if (!isMfaValid) {
await user.incrementLoginAttempts();
return res.status(401).json({
success: false,
message: 'Invalid MFA token'
});
}
}
// Generate tokens
const { accessToken, refreshToken } = generateTokens(user.id);
// Create session
const session = await UserSession.create({
userId: user.id,
refreshToken,
deviceInfo: {
userAgent: req.get('User-Agent'),
platform: req.get('sec-ch-ua-platform')
},
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
});
// Reset login attempts and update last login
await user.resetLoginAttempts();
// Log audit
await AuditLog.create({
userId: user.id,
action: 'USER_LOGIN',
resource: 'user',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Login successful',
data: {
user: user.toJSON(),
accessToken,
refreshToken,
expiresIn: process.env.JWT_EXPIRES_IN || '15m'
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Login failed',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
// Refresh token
const refreshToken = async (req, res) => {
try {
const { refreshToken } = req.body;
const session = await UserSession.findOne({
where: {
refreshToken,
isActive: true
},
include: ['user']
});
if (!session || session.isExpired()) {
return res.status(401).json({
success: false,
message: 'Invalid or expired refresh token'
});
}
// Generate new tokens
const { accessToken, refreshToken: newRefreshToken } = generateTokens(session.userId);
// Update session
await session.update({
refreshToken: newRefreshToken,
lastUsedAt: new Date()
});
res.json({
success: true,
data: {
accessToken,
refreshToken: newRefreshToken,
expiresIn: process.env.JWT_EXPIRES_IN || '15m'
}
});
} catch (error) {
console.error('Token refresh error:', error);
res.status(500).json({
success: false,
message: 'Token refresh failed'
});
}
};
// Logout
const logout = async (req, res) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token) {
// Blacklist the access token
const decoded = jwt.decode(token);
if (decoded && decoded.exp) {
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redisClient.setEx(`blacklist:${token}`, ttl, 'true');
}
}
}
// Deactivate refresh token session
const { refreshToken } = req.body;
if (refreshToken) {
await UserSession.update(
{ isActive: false },
{ where: { refreshToken } }
);
}
// Log audit
await AuditLog.create({
userId: req.user?.id,
action: 'USER_LOGOUT',
resource: 'user',
resourceId: req.user?.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Logout successful'
});
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({
success: false,
message: 'Logout failed'
});
}
};
// Setup MFA
const setupMFA = async (req, res) => {
try {
const user = req.user;
if (user.mfaEnabled) {
return res.status(400).json({
success: false,
message: 'MFA is already enabled'
});
}
const secret = user.generateMfaSecret();
const backupCodes = user.generateMfaBackupCodes();
await user.save();
// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
res.json({
success: true,
data: {
secret: secret.base32,
qrCode: qrCodeUrl,
backupCodes
}
});
} catch (error) {
console.error('MFA setup error:', error);
res.status(500).json({
success: false,
message: 'MFA setup failed'
});
}
};
module.exports = {
register,
login,
refreshToken,
logout,
setupMFA
};

View File

@ -0,0 +1,489 @@
const { Wallet, WalletTransaction, Invoice, InvoiceItem, Customer, AuditLog } = require('../models');
const { Op } = require('sequelize');
const crypto = require('crypto');
// Get wallet details
const getWallet = async (req, res) => {
try {
const wallet = await Wallet.findOne({
where: { resellerId: req.user.resellerId },
include: [
{
model: WalletTransaction,
as: 'transactions',
limit: 10,
order: [['createdAt', 'DESC']]
}
]
});
if (!wallet) {
return res.status(404).json({
success: false,
message: 'Wallet not found'
});
}
const walletData = wallet.toJSON();
walletData.availableBalance = wallet.getAvailableBalance();
walletData.isLowBalance = wallet.isLowBalance();
walletData.shouldAutoRecharge = wallet.shouldAutoRecharge();
res.json({
success: true,
data: { wallet: walletData }
});
} catch (error) {
console.error('Get wallet error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch wallet details'
});
}
};
// Add funds to wallet
const addFunds = async (req, res) => {
try {
const { amount, paymentMethod = 'manual', paymentReference, description } = req.body;
const wallet = await Wallet.findOne({
where: { resellerId: req.user.resellerId }
});
if (!wallet) {
return res.status(404).json({
success: false,
message: 'Wallet not found'
});
}
const transactionId = `ADD_${Date.now()}_${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
const balanceBefore = parseFloat(wallet.balance);
const addAmount = parseFloat(amount);
const balanceAfter = balanceBefore + addAmount;
// Create transaction record
const transaction = await WalletTransaction.create({
walletId: wallet.id,
transactionId,
type: 'credit',
category: 'payment',
amount: addAmount,
balanceBefore,
balanceAfter,
status: 'completed',
description: description || `Funds added via ${paymentMethod}`,
paymentGateway: paymentMethod,
gatewayTransactionId: paymentReference,
processedAt: new Date(),
processedBy: req.user.id
});
// Update wallet balance
await wallet.addFunds(addAmount, transaction.id);
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'FUNDS_ADDED',
resource: 'wallet',
resourceId: wallet.id,
newValues: { amount: addAmount, transactionId },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Funds added successfully',
data: {
transaction,
newBalance: balanceAfter
}
});
} catch (error) {
console.error('Add funds error:', error);
res.status(500).json({
success: false,
message: 'Failed to add funds'
});
}
};
// Get wallet transactions
const getTransactions = async (req, res) => {
try {
const { page = 1, limit = 20, type, category, status, startDate, endDate } = req.query;
const offset = (page - 1) * limit;
const wallet = await Wallet.findOne({
where: { resellerId: req.user.resellerId }
});
if (!wallet) {
return res.status(404).json({
success: false,
message: 'Wallet not found'
});
}
const where = { walletId: wallet.id };
if (type) where.type = type;
if (category) where.category = category;
if (status) where.status = status;
if (startDate && endDate) {
where.createdAt = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
const { count, rows: transactions } = await WalletTransaction.findAndCountAll({
where,
include: [
{
model: require('../models').User,
as: 'processor',
attributes: ['id', 'firstName', 'lastName', 'email']
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: {
transactions,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get transactions error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch transactions'
});
}
};
// Get invoices
const getInvoices = async (req, res) => {
try {
const { page = 1, limit = 20, status, customerId, startDate, endDate } = req.query;
const offset = (page - 1) * limit;
const where = { resellerId: req.user.resellerId };
if (status) where.status = status;
if (customerId) where.customerId = customerId;
if (startDate && endDate) {
where.createdAt = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
const { count, rows: invoices } = await Invoice.findAndCountAll({
where,
include: [
{
model: Customer,
as: 'customer',
attributes: ['id', 'companyName', 'contactPerson', 'email']
},
{
model: require('../models').User,
as: 'creator',
attributes: ['id', 'firstName', 'lastName']
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
// Add calculated fields
const invoicesWithCalc = invoices.map(invoice => {
const invoiceData = invoice.toJSON();
invoiceData.isOverdue = invoice.isOverdue();
invoiceData.daysOverdue = invoice.getDaysOverdue();
invoiceData.outstandingAmount = invoice.getOutstandingAmount();
return invoiceData;
});
res.json({
success: true,
data: {
invoices: invoicesWithCalc,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get invoices error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch invoices'
});
}
};
// Get invoice details
const getInvoice = async (req, res) => {
try {
const { invoiceId } = req.params;
const invoice = await Invoice.findOne({
where: {
id: invoiceId,
resellerId: req.user.resellerId
},
include: [
{
model: Customer,
as: 'customer'
},
{
model: require('../models').InvoiceItem,
as: 'items',
include: [
{
model: require('../models').Product,
as: 'product',
attributes: ['id', 'name', 'sku']
}
]
}
]
});
if (!invoice) {
return res.status(404).json({
success: false,
message: 'Invoice not found'
});
}
const invoiceData = invoice.toJSON();
invoiceData.isOverdue = invoice.isOverdue();
invoiceData.daysOverdue = invoice.getDaysOverdue();
invoiceData.outstandingAmount = invoice.getOutstandingAmount();
res.json({
success: true,
data: { invoice: invoiceData }
});
} catch (error) {
console.error('Get invoice error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch invoice'
});
}
};
// Mark invoice as paid
const markInvoicePaid = async (req, res) => {
try {
const { invoiceId } = req.params;
const { amount, paymentMethod, paymentReference, notes } = req.body;
const invoice = await Invoice.findOne({
where: {
id: invoiceId,
resellerId: req.user.resellerId
}
});
if (!invoice) {
return res.status(404).json({
success: false,
message: 'Invoice not found'
});
}
await invoice.markAsPaid(amount, paymentMethod, paymentReference);
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'INVOICE_PAID',
resource: 'invoice',
resourceId: invoice.id,
newValues: { amount, paymentMethod, paymentReference, notes },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Invoice marked as paid',
data: { invoice }
});
} catch (error) {
console.error('Mark invoice paid error:', error);
res.status(500).json({
success: false,
message: 'Failed to mark invoice as paid'
});
}
};
// Download invoice
const downloadInvoice = async (req, res) => {
try {
const { invoiceId } = req.params;
const invoice = await Invoice.findOne({
where: {
id: invoiceId,
resellerId: req.user.resellerId
}
});
if (!invoice) {
return res.status(404).json({
success: false,
message: 'Invoice not found'
});
}
// Increment download count
await invoice.incrementDownload();
// For now, return the invoice data (PDF generation would be implemented separately)
res.json({
success: true,
message: 'Invoice download initiated',
data: {
downloadUrl: `/api/billing/invoices/${invoiceId}/pdf`,
invoice
}
});
} catch (error) {
console.error('Download invoice error:', error);
res.status(500).json({
success: false,
message: 'Failed to download invoice'
});
}
};
// Get billing summary
const getBillingSummary = async (req, res) => {
try {
const { period = 'month' } = req.query;
const wallet = await Wallet.findOne({
where: { resellerId: req.user.resellerId }
});
if (!wallet) {
return res.status(404).json({
success: false,
message: 'Wallet not found'
});
}
// Calculate date range based on period
let startDate;
const endDate = new Date();
switch (period) {
case 'week':
startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
break;
case 'month':
startDate = new Date();
startDate.setMonth(startDate.getMonth() - 1);
break;
case 'quarter':
startDate = new Date();
startDate.setMonth(startDate.getMonth() - 3);
break;
case 'year':
startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
break;
default:
startDate = new Date();
startDate.setMonth(startDate.getMonth() - 1);
}
// Get transaction summary
const transactionSummary = await WalletTransaction.findAll({
where: {
walletId: wallet.id,
createdAt: { [Op.between]: [startDate, endDate] }
},
attributes: [
'type',
'category',
[require('sequelize').fn('SUM', require('sequelize').col('amount')), 'totalAmount'],
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'count']
],
group: ['type', 'category']
});
// Get invoice summary
const invoiceSummary = await Invoice.findAll({
where: {
resellerId: req.user.resellerId,
createdAt: { [Op.between]: [startDate, endDate] }
},
attributes: [
'status',
[require('sequelize').fn('SUM', require('sequelize').col('total_amount')), 'totalAmount'],
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'count']
],
group: ['status']
});
res.json({
success: true,
data: {
wallet: {
balance: wallet.balance,
availableBalance: wallet.getAvailableBalance(),
reservedAmount: wallet.reservedAmount,
totalSpent: wallet.totalSpent,
totalAdded: wallet.totalAdded
},
transactionSummary,
invoiceSummary,
period: {
startDate,
endDate,
period
}
}
});
} catch (error) {
console.error('Get billing summary error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch billing summary'
});
}
};
module.exports = {
getWallet,
addFunds,
getTransactions,
getInvoices,
getInvoice,
markInvoicePaid,
downloadInvoice,
getBillingSummary
};

View File

@ -0,0 +1,443 @@
const { Customer, CustomerService, UsageRecord, Invoice, AuditLog, Product } = require('../models');
const { Op } = require('sequelize');
// Get all customers for the reseller
const getCustomers = async (req, res) => {
try {
const { page = 1, limit = 20, status, search, sortBy = 'createdAt', sortOrder = 'DESC' } = req.query;
const offset = (page - 1) * limit;
const where = { resellerId: req.user.resellerId };
if (status) where.status = status;
if (search) {
where[Op.or] = [
{ companyName: { [Op.iLike]: `%${search}%` } },
{ contactPerson: { [Op.iLike]: `%${search}%` } },
{ email: { [Op.iLike]: `%${search}%` } }
];
}
const { count, rows: customers } = await Customer.findAndCountAll({
where,
include: [
{
model: CustomerService,
as: 'services',
attributes: ['id', 'status', 'serviceName'],
required: false
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [[sortBy, sortOrder.toUpperCase()]]
});
// Add service counts and usage summary
const customersWithStats = await Promise.all(customers.map(async (customer) => {
const customerData = customer.toJSON();
// Count active services
customerData.activeServicesCount = customer.services?.filter(s => s.status === 'active').length || 0;
customerData.totalServicesCount = customer.services?.length || 0;
// Get current month usage
const currentMonth = new Date();
currentMonth.setDate(1);
currentMonth.setHours(0, 0, 0, 0);
const monthlyUsage = await UsageRecord.sum('totalCost', {
where: {
customerId: customer.id,
usageDate: { [Op.gte]: currentMonth },
billingStatus: { [Op.in]: ['pending', 'billed'] }
}
});
customerData.monthlyUsage = parseFloat(monthlyUsage || 0);
return customerData;
}));
res.json({
success: true,
data: {
customers: customersWithStats,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get customers error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch customers'
});
}
};
// Get customer details
const getCustomer = async (req, res) => {
try {
const { customerId } = req.params;
const customer = await Customer.findOne({
where: {
id: customerId,
resellerId: req.user.resellerId
},
include: [
{
model: CustomerService,
as: 'services',
include: [
{
model: Product,
as: 'product',
attributes: ['id', 'name', 'category', 'sku']
}
]
},
{
model: Invoice,
as: 'invoices',
limit: 10,
order: [['createdAt', 'DESC']]
}
]
});
if (!customer) {
return res.status(404).json({
success: false,
message: 'Customer not found'
});
}
// Get usage summary for last 6 months
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
const usageSummary = await UsageRecord.findAll({
where: {
customerId: customer.id,
usageDate: { [Op.gte]: sixMonthsAgo }
},
attributes: [
[require('sequelize').fn('DATE_TRUNC', 'month', require('sequelize').col('usage_date')), 'month'],
[require('sequelize').fn('SUM', require('sequelize').col('total_cost')), 'totalCost'],
[require('sequelize').fn('SUM', require('sequelize').col('quantity')), 'totalQuantity']
],
group: [require('sequelize').fn('DATE_TRUNC', 'month', require('sequelize').col('usage_date'))],
order: [[require('sequelize').fn('DATE_TRUNC', 'month', require('sequelize').col('usage_date')), 'ASC']]
});
const customerData = customer.toJSON();
customerData.usageSummary = usageSummary;
res.json({
success: true,
data: { customer: customerData }
});
} catch (error) {
console.error('Get customer error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch customer'
});
}
};
// Create new customer
const createCustomer = async (req, res) => {
try {
const {
companyName,
contactPerson,
email,
phone,
address,
industry,
companySize,
billingAddress,
paymentMethod = 'invoice',
creditLimit = 0,
taxId,
gstNumber,
notes
} = req.body;
const customer = await Customer.create({
resellerId: req.user.resellerId,
companyName,
contactPerson,
email,
phone,
address,
industry,
companySize,
billingAddress: billingAddress || address,
paymentMethod,
creditLimit,
taxId,
gstNumber,
notes,
createdBy: req.user.id
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'CUSTOMER_CREATED',
resource: 'customer',
resourceId: customer.id,
newValues: customer.toJSON(),
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.status(201).json({
success: true,
message: 'Customer created successfully',
data: { customer }
});
} catch (error) {
console.error('Create customer error:', error);
res.status(500).json({
success: false,
message: 'Failed to create customer'
});
}
};
// Update customer
const updateCustomer = async (req, res) => {
try {
const { customerId } = req.params;
const updates = req.body;
const customer = await Customer.findOne({
where: {
id: customerId,
resellerId: req.user.resellerId
}
});
if (!customer) {
return res.status(404).json({
success: false,
message: 'Customer not found'
});
}
const oldValues = customer.toJSON();
await customer.update(updates);
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'CUSTOMER_UPDATED',
resource: 'customer',
resourceId: customer.id,
oldValues,
newValues: updates,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Customer updated successfully',
data: { customer }
});
} catch (error) {
console.error('Update customer error:', error);
res.status(500).json({
success: false,
message: 'Failed to update customer'
});
}
};
// Get customer usage details
const getCustomerUsage = async (req, res) => {
try {
const { customerId } = req.params;
const { startDate, endDate, serviceId, metricType } = req.query;
// Verify customer belongs to reseller
const customer = await Customer.findOne({
where: {
id: customerId,
resellerId: req.user.resellerId
}
});
if (!customer) {
return res.status(404).json({
success: false,
message: 'Customer not found'
});
}
const where = { customerId };
if (startDate && endDate) {
where.usageDate = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
if (serviceId) where.serviceId = serviceId;
if (metricType) where.metricType = metricType;
const usageRecords = await UsageRecord.findAll({
where,
include: [
{
model: CustomerService,
as: 'service',
attributes: ['id', 'serviceName', 'status']
},
{
model: Product,
as: 'product',
attributes: ['id', 'name', 'category']
}
],
order: [['usageDate', 'DESC'], ['usageHour', 'DESC']]
});
// Group usage by service and metric
const usageSummary = usageRecords.reduce((acc, record) => {
const key = `${record.serviceId}_${record.metricType}`;
if (!acc[key]) {
acc[key] = {
serviceId: record.serviceId,
serviceName: record.service.serviceName,
metricType: record.metricType,
unit: record.unit,
totalQuantity: 0,
totalCost: 0,
records: []
};
}
acc[key].totalQuantity += parseFloat(record.quantity);
acc[key].totalCost += parseFloat(record.totalCost);
acc[key].records.push(record);
return acc;
}, {});
res.json({
success: true,
data: {
usageRecords,
usageSummary: Object.values(usageSummary)
}
});
} catch (error) {
console.error('Get customer usage error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch customer usage'
});
}
};
// Assign service to customer
const assignService = async (req, res) => {
try {
const { customerId } = req.params;
const {
productId,
serviceName,
configuration = {},
specifications = {},
region,
zone,
pricing,
billingCycle = 'monthly',
contractPeriod
} = req.body;
// Verify customer belongs to reseller
const customer = await Customer.findOne({
where: {
id: customerId,
resellerId: req.user.resellerId
}
});
if (!customer) {
return res.status(404).json({
success: false,
message: 'Customer not found'
});
}
// Verify product exists
const product = await Product.findByPk(productId);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
// Generate unique service instance ID
const serviceInstanceId = `${product.sku}_${customerId.slice(-8)}_${Date.now()}`;
const customerService = await CustomerService.create({
customerId,
productId,
serviceInstanceId,
serviceName,
configuration,
specifications,
region,
zone,
pricing,
billingCycle,
contractPeriod,
contractEndDate: contractPeriod ?
new Date(Date.now() + contractPeriod * 30 * 24 * 60 * 60 * 1000) : null
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'SERVICE_ASSIGNED',
resource: 'customer_service',
resourceId: customerService.id,
newValues: customerService.toJSON(),
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.status(201).json({
success: true,
message: 'Service assigned successfully',
data: { customerService }
});
} catch (error) {
console.error('Assign service error:', error);
res.status(500).json({
success: false,
message: 'Failed to assign service'
});
}
};
module.exports = {
getCustomers,
getCustomer,
createCustomer,
updateCustomer,
getCustomerUsage,
assignService
};

View File

@ -0,0 +1,408 @@
const { Customer, CustomerService, UsageRecord, Invoice, WalletTransaction, Wallet, Product } = require('../models');
const { Op } = require('sequelize');
// Get dashboard overview
const getDashboardOverview = async (req, res) => {
try {
const resellerId = req.user.resellerId;
// Get date ranges
const now = new Date();
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const currentYear = new Date(now.getFullYear(), 0, 1);
// Get wallet info
const wallet = await Wallet.findOne({
where: { resellerId }
});
// Get KPI data
const [
totalCustomers,
activeCustomers,
totalServices,
activeServices,
monthlyRevenue,
lastMonthRevenue,
yearlyRevenue,
pendingInvoices,
overdueInvoices
] = await Promise.all([
// Total customers
Customer.count({ where: { resellerId } }),
// Active customers
Customer.count({ where: { resellerId, status: 'active' } }),
// Total services
CustomerService.count({
include: [{ model: Customer, as: 'customer', where: { resellerId } }]
}),
// Active services
CustomerService.count({
where: { status: 'active' },
include: [{ model: Customer, as: 'customer', where: { resellerId } }]
}),
// Current month revenue
Invoice.sum('totalAmount', {
where: {
resellerId,
status: 'paid',
paidAt: { [Op.gte]: currentMonth }
}
}),
// Last month revenue
Invoice.sum('totalAmount', {
where: {
resellerId,
status: 'paid',
paidAt: { [Op.between]: [lastMonth, currentMonth] }
}
}),
// Yearly revenue
Invoice.sum('totalAmount', {
where: {
resellerId,
status: 'paid',
paidAt: { [Op.gte]: currentYear }
}
}),
// Pending invoices count
Invoice.count({
where: {
resellerId,
status: { [Op.in]: ['sent', 'overdue'] }
}
}),
// Overdue invoices count
Invoice.count({
where: {
resellerId,
status: 'overdue'
}
})
]);
// Calculate growth percentages
const revenueGrowth = lastMonthRevenue > 0
? ((monthlyRevenue - lastMonthRevenue) / lastMonthRevenue * 100).toFixed(1)
: 0;
const kpis = {
totalRevenue: parseFloat(yearlyRevenue || 0),
monthlyRevenue: parseFloat(monthlyRevenue || 0),
revenueGrowth: parseFloat(revenueGrowth),
totalCustomers,
activeCustomers,
customerGrowth: 0, // Would need historical data
totalServices,
activeServices,
walletBalance: wallet ? parseFloat(wallet.balance) : 0,
pendingInvoices,
overdueInvoices
};
res.json({
success: true,
data: { kpis }
});
} catch (error) {
console.error('Get dashboard overview error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch dashboard overview'
});
}
};
// Get revenue chart data
const getRevenueChart = async (req, res) => {
try {
const { period = 'month', range = 12 } = req.query;
const resellerId = req.user.resellerId;
let dateFormat, dateInterval;
switch (period) {
case 'day':
dateFormat = 'YYYY-MM-DD';
dateInterval = 'day';
break;
case 'week':
dateFormat = 'YYYY-"W"WW';
dateInterval = 'week';
break;
case 'month':
dateFormat = 'YYYY-MM';
dateInterval = 'month';
break;
case 'quarter':
dateFormat = 'YYYY-"Q"Q';
dateInterval = 'quarter';
break;
default:
dateFormat = 'YYYY-MM';
dateInterval = 'month';
}
const startDate = new Date();
startDate.setMonth(startDate.getMonth() - parseInt(range));
const revenueData = await Invoice.findAll({
where: {
resellerId,
status: 'paid',
paidAt: { [Op.gte]: startDate }
},
attributes: [
[require('sequelize').fn('DATE_TRUNC', dateInterval, require('sequelize').col('paid_at')), 'period'],
[require('sequelize').fn('SUM', require('sequelize').col('total_amount')), 'revenue'],
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'invoiceCount']
],
group: [require('sequelize').fn('DATE_TRUNC', dateInterval, require('sequelize').col('paid_at'))],
order: [[require('sequelize').fn('DATE_TRUNC', dateInterval, require('sequelize').col('paid_at')), 'ASC']]
});
res.json({
success: true,
data: { revenueData }
});
} catch (error) {
console.error('Get revenue chart error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch revenue chart data'
});
}
};
// Get service distribution chart data
const getServiceDistribution = async (req, res) => {
try {
const resellerId = req.user.resellerId;
const serviceDistribution = await CustomerService.findAll({
where: { status: 'active' },
include: [
{
model: Customer,
as: 'customer',
where: { resellerId },
attributes: []
},
{
model: Product,
as: 'product',
attributes: ['category', 'name']
}
],
attributes: [
[require('sequelize').col('product.category'), 'category'],
[require('sequelize').fn('COUNT', require('sequelize').col('CustomerService.id')), 'count']
],
group: [require('sequelize').col('product.category')],
order: [[require('sequelize').fn('COUNT', require('sequelize').col('CustomerService.id')), 'DESC']]
});
res.json({
success: true,
data: { serviceDistribution }
});
} catch (error) {
console.error('Get service distribution error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch service distribution data'
});
}
};
// Get recent customers
const getRecentCustomers = async (req, res) => {
try {
const { limit = 5 } = req.query;
const resellerId = req.user.resellerId;
const recentCustomers = await Customer.findAll({
where: { resellerId },
include: [
{
model: CustomerService,
as: 'services',
attributes: ['id', 'status'],
required: false
}
],
limit: parseInt(limit),
order: [['createdAt', 'DESC']]
});
// Add service counts
const customersWithStats = recentCustomers.map(customer => {
const customerData = customer.toJSON();
customerData.activeServicesCount = customer.services?.filter(s => s.status === 'active').length || 0;
customerData.totalServicesCount = customer.services?.length || 0;
return customerData;
});
res.json({
success: true,
data: { customers: customersWithStats }
});
} catch (error) {
console.error('Get recent customers error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch recent customers'
});
}
};
// Get recent transactions
const getRecentTransactions = async (req, res) => {
try {
const { limit = 5 } = req.query;
const resellerId = req.user.resellerId;
const wallet = await Wallet.findOne({
where: { resellerId }
});
if (!wallet) {
return res.json({
success: true,
data: { transactions: [] }
});
}
const recentTransactions = await WalletTransaction.findAll({
where: { walletId: wallet.id },
limit: parseInt(limit),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: { transactions: recentTransactions }
});
} catch (error) {
console.error('Get recent transactions error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch recent transactions'
});
}
};
// Get usage trends
const getUsageTrends = async (req, res) => {
try {
const { period = 'month', range = 6 } = req.query;
const resellerId = req.user.resellerId;
const startDate = new Date();
startDate.setMonth(startDate.getMonth() - parseInt(range));
const usageTrends = await UsageRecord.findAll({
where: {
usageDate: { [Op.gte]: startDate }
},
include: [
{
model: Customer,
as: 'customer',
where: { resellerId },
attributes: []
}
],
attributes: [
[require('sequelize').fn('DATE_TRUNC', 'month', require('sequelize').col('usage_date')), 'month'],
'metricType',
[require('sequelize').fn('SUM', require('sequelize').col('quantity')), 'totalQuantity'],
[require('sequelize').fn('SUM', require('sequelize').col('total_cost')), 'totalCost']
],
group: [
require('sequelize').fn('DATE_TRUNC', 'month', require('sequelize').col('usage_date')),
'metricType'
],
order: [
[require('sequelize').fn('DATE_TRUNC', 'month', require('sequelize').col('usage_date')), 'ASC'],
'metricType'
]
});
res.json({
success: true,
data: { usageTrends }
});
} catch (error) {
console.error('Get usage trends error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch usage trends'
});
}
};
// Export dashboard data
const exportDashboardData = async (req, res) => {
try {
const { format = 'json', period = 'month' } = req.query;
const resellerId = req.user.resellerId;
// Get comprehensive dashboard data
const [overview, revenueChart, serviceDistribution, recentCustomers, recentTransactions] = await Promise.all([
getDashboardOverview(req, { json: () => {} }),
getRevenueChart(req, { json: () => {} }),
getServiceDistribution(req, { json: () => {} }),
getRecentCustomers(req, { json: () => {} }),
getRecentTransactions(req, { json: () => {} })
]);
const exportData = {
generatedAt: new Date(),
period,
resellerId,
overview,
revenueChart,
serviceDistribution,
recentCustomers,
recentTransactions
};
if (format === 'csv') {
// Convert to CSV format (simplified)
const csvData = 'Dashboard Export\n' + JSON.stringify(exportData, null, 2);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="dashboard-export-${Date.now()}.csv"`);
res.send(csvData);
} else {
res.json({
success: true,
data: exportData
});
}
} catch (error) {
console.error('Export dashboard data error:', error);
res.status(500).json({
success: false,
message: 'Failed to export dashboard data'
});
}
};
module.exports = {
getDashboardOverview,
getRevenueChart,
getServiceDistribution,
getRecentCustomers,
getRecentTransactions,
getUsageTrends,
exportDashboardData
};

View File

@ -0,0 +1,379 @@
const { KnowledgeArticle, User, AuditLog } = require('../models');
const { Op } = require('sequelize');
// Get knowledge articles
const getKnowledgeArticles = async (req, res) => {
try {
const { page = 1, limit = 20, type, category, search, featured } = req.query;
const offset = (page - 1) * limit;
const userTier = req.user?.reseller?.tier || 'bronze';
const where = { status: 'published' };
// Filter by type and category
if (type) where.type = type;
if (category) where.category = category;
if (featured === 'true') where.featured = true;
// Search functionality
if (search) {
where[Op.or] = [
{ title: { [Op.iLike]: `%${search}%` } },
{ content: { [Op.iLike]: `%${search}%` } },
{ excerpt: { [Op.iLike]: `%${search}%` } },
{ tags: { [Op.contains]: [search] } }
];
}
// Access level filtering
if (req.user) {
where[Op.or] = [
{ accessLevel: 'public' },
{ accessLevel: 'reseller_only' },
{
accessLevel: 'tier_specific',
tierAccess: { [Op.contains]: [userTier] }
}
];
} else {
where.accessLevel = 'public';
}
const { count, rows: articles } = await KnowledgeArticle.findAndCountAll({
where,
include: [
{
model: User,
as: 'author',
attributes: ['id', 'firstName', 'lastName']
}
],
attributes: { exclude: ['content'] }, // Exclude content from list view
limit: parseInt(limit),
offset: parseInt(offset),
order: [
['featured', 'DESC'],
['viewCount', 'DESC'],
['publishedAt', 'DESC']
]
});
res.json({
success: true,
data: {
articles,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get knowledge articles error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch knowledge articles'
});
}
};
// Get article by slug
const getArticleBySlug = async (req, res) => {
try {
const { slug } = req.params;
const userTier = req.user?.reseller?.tier || 'bronze';
const article = await KnowledgeArticle.findOne({
where: { slug, status: 'published' },
include: [
{
model: User,
as: 'author',
attributes: ['id', 'firstName', 'lastName']
},
{
model: User,
as: 'reviewer',
attributes: ['id', 'firstName', 'lastName']
}
]
});
if (!article) {
return res.status(404).json({
success: false,
message: 'Article not found'
});
}
// Check access permissions
if (req.user) {
if (!article.isAccessibleBy(userTier)) {
return res.status(403).json({
success: false,
message: 'Access denied for this article'
});
}
} else if (article.accessLevel !== 'public') {
return res.status(403).json({
success: false,
message: 'Access denied for this article'
});
}
// Increment view count
await article.increment('viewCount');
// Get related articles
let relatedArticles = [];
if (article.relatedArticles && article.relatedArticles.length > 0) {
relatedArticles = await KnowledgeArticle.findAll({
where: {
id: { [Op.in]: article.relatedArticles },
status: 'published'
},
attributes: ['id', 'title', 'slug', 'excerpt', 'type', 'category'],
limit: 5
});
}
res.json({
success: true,
data: {
article,
relatedArticles
}
});
} catch (error) {
console.error('Get article by slug error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch article'
});
}
};
// Get article categories
const getArticleCategories = async (req, res) => {
try {
const { type } = req.query;
const where = { status: 'published' };
if (type) where.type = type;
const categories = await KnowledgeArticle.findAll({
attributes: [
'category',
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'count']
],
where,
group: ['category'],
order: [['category', 'ASC']]
});
res.json({
success: true,
data: { categories }
});
} catch (error) {
console.error('Get article categories error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch article categories'
});
}
};
// Search articles
const searchArticles = async (req, res) => {
try {
const { q: query, type, category, limit = 10 } = req.query;
const userTier = req.user?.reseller?.tier || 'bronze';
if (!query || query.trim().length < 2) {
return res.status(400).json({
success: false,
message: 'Search query must be at least 2 characters'
});
}
const where = {
status: 'published',
[Op.or] = [
{ title: { [Op.iLike]: `%${query}%` } },
{ content: { [Op.iLike]: `%${query}%` } },
{ excerpt: { [Op.iLike]: `%${query}%` } },
{ tags: { [Op.contains]: [query] } }
]
};
// Filter by type and category
if (type) where.type = type;
if (category) where.category = category;
// Access level filtering
if (req.user) {
where[Op.and] = [
{
[Op.or]: [
{ accessLevel: 'public' },
{ accessLevel: 'reseller_only' },
{
accessLevel: 'tier_specific',
tierAccess: { [Op.contains]: [userTier] }
}
]
}
];
} else {
where.accessLevel = 'public';
}
const articles = await KnowledgeArticle.findAll({
where,
attributes: ['id', 'title', 'slug', 'excerpt', 'type', 'category', 'difficulty'],
limit: parseInt(limit),
order: [
['featured', 'DESC'],
['viewCount', 'DESC']
]
});
res.json({
success: true,
data: {
articles,
query,
total: articles.length
}
});
} catch (error) {
console.error('Search articles error:', error);
res.status(500).json({
success: false,
message: 'Failed to search articles'
});
}
};
// Get featured articles
const getFeaturedArticles = async (req, res) => {
try {
const { limit = 5 } = req.query;
const userTier = req.user?.reseller?.tier || 'bronze';
const where = {
status: 'published',
featured: true
};
// Access level filtering
if (req.user) {
where[Op.or] = [
{ accessLevel: 'public' },
{ accessLevel: 'reseller_only' },
{
accessLevel: 'tier_specific',
tierAccess: { [Op.contains]: [userTier] }
}
];
} else {
where.accessLevel = 'public';
}
const articles = await KnowledgeArticle.findAll({
where,
attributes: ['id', 'title', 'slug', 'excerpt', 'type', 'category', 'difficulty', 'viewCount'],
limit: parseInt(limit),
order: [['viewCount', 'DESC']]
});
res.json({
success: true,
data: { articles }
});
} catch (error) {
console.error('Get featured articles error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch featured articles'
});
}
};
// Admin: Create knowledge article
const createKnowledgeArticle = async (req, res) => {
try {
const {
title,
content,
excerpt,
type,
category,
subcategory,
difficulty = 'beginner',
accessLevel = 'public',
tierAccess = [],
featured = false,
tags = [],
relatedArticles = []
} = req.body;
// Generate slug from title
const slug = title.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
// Check if slug already exists
const existingArticle = await KnowledgeArticle.findOne({ where: { slug } });
if (existingArticle) {
return res.status(400).json({
success: false,
message: 'Article with this title already exists'
});
}
const article = await KnowledgeArticle.create({
title,
slug,
content,
excerpt,
type,
category,
subcategory,
difficulty,
accessLevel,
tierAccess,
featured,
tags,
relatedArticles,
createdBy: req.user.id,
status: 'draft'
});
res.status(201).json({
success: true,
message: 'Knowledge article created successfully',
data: { article }
});
} catch (error) {
console.error('Create knowledge article error:', error);
res.status(500).json({
success: false,
message: 'Failed to create knowledge article'
});
}
};
module.exports = {
getKnowledgeArticles,
getArticleBySlug,
getArticleCategories,
searchArticles,
getFeaturedArticles,
createKnowledgeArticle
};

View File

@ -0,0 +1,359 @@
const { LegalDocument, LegalAcceptance, User, Reseller, AuditLog } = require('../models');
const { Op } = require('sequelize');
const path = require('path');
const fs = require('fs').promises;
// Get all legal documents
const getLegalDocuments = async (req, res) => {
try {
const { type, status = 'active' } = req.query;
const where = {};
if (type) where.type = type;
if (status !== 'all') where.status = status;
const documents = await LegalDocument.findAll({
where,
attributes: ['id', 'title', 'type', 'version', 'status', 'effectiveDate', 'expiryDate', 'requiresAcceptance', 'acceptanceType'],
order: [['type', 'ASC'], ['createdAt', 'DESC']]
});
res.json({
success: true,
data: { documents }
});
} catch (error) {
console.error('Get legal documents error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch legal documents'
});
}
};
// Get document details
const getDocumentDetails = async (req, res) => {
try {
const { documentId } = req.params;
const document = await LegalDocument.findByPk(documentId, {
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'firstName', 'lastName']
}
]
});
if (!document) {
return res.status(404).json({
success: false,
message: 'Document not found'
});
}
// Check if user has accepted this document
let userAcceptance = null;
if (req.user) {
userAcceptance = await LegalAcceptance.findOne({
where: {
documentId: document.id,
userId: req.user.id,
resellerId: req.user.resellerId
}
});
}
res.json({
success: true,
data: {
document,
userAcceptance: userAcceptance ? {
id: userAcceptance.id,
acceptedAt: userAcceptance.acceptedAt,
status: userAcceptance.status,
acceptanceMethod: userAcceptance.acceptanceMethod
} : null
}
});
} catch (error) {
console.error('Get document details error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch document details'
});
}
};
// Accept document
const acceptDocument = async (req, res) => {
try {
const { documentId } = req.params;
const { acceptanceMethod = 'click_through', signatureData } = req.body;
const document = await LegalDocument.findByPk(documentId);
if (!document) {
return res.status(404).json({
success: false,
message: 'Document not found'
});
}
if (!document.isActive()) {
return res.status(400).json({
success: false,
message: 'Document is not active'
});
}
// Check if already accepted
const existingAcceptance = await LegalAcceptance.findOne({
where: {
documentId,
userId: req.user.id,
resellerId: req.user.resellerId
}
});
if (existingAcceptance && existingAcceptance.isValid()) {
return res.status(400).json({
success: false,
message: 'Document already accepted'
});
}
// Create or update acceptance
const acceptanceData = {
documentId,
userId: req.user.id,
resellerId: req.user.resellerId,
acceptanceMethod,
acceptedAt: new Date(),
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
signatureData: signatureData || null,
status: 'accepted'
};
let acceptance;
if (existingAcceptance) {
acceptance = await existingAcceptance.update(acceptanceData);
} else {
acceptance = await LegalAcceptance.create(acceptanceData);
}
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'LEGAL_DOCUMENT_ACCEPTED',
resource: 'legal_document',
resourceId: document.id,
newValues: { acceptanceId: acceptance.id },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Document accepted successfully',
data: { acceptance }
});
} catch (error) {
console.error('Accept document error:', error);
res.status(500).json({
success: false,
message: 'Failed to accept document'
});
}
};
// Upload compliance document
const uploadComplianceDocument = async (req, res) => {
try {
const { documentId } = req.params;
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
const document = await LegalDocument.findByPk(documentId);
if (!document) {
return res.status(404).json({
success: false,
message: 'Document not found'
});
}
// Create acceptance with uploaded file
const acceptance = await LegalAcceptance.create({
documentId,
userId: req.user.id,
resellerId: req.user.resellerId,
acceptanceMethod: 'upload',
acceptedAt: new Date(),
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
uploadedFile: req.file.filename,
status: 'pending_verification'
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'COMPLIANCE_DOCUMENT_UPLOADED',
resource: 'legal_document',
resourceId: document.id,
newValues: {
acceptanceId: acceptance.id,
fileName: req.file.originalname
},
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Compliance document uploaded successfully',
data: { acceptance }
});
} catch (error) {
console.error('Upload compliance document error:', error);
res.status(500).json({
success: false,
message: 'Failed to upload compliance document'
});
}
};
// Download document
const downloadDocument = async (req, res) => {
try {
const { documentId } = req.params;
const document = await LegalDocument.findByPk(documentId);
if (!document) {
return res.status(404).json({
success: false,
message: 'Document not found'
});
}
if (!document.filePath) {
return res.status(404).json({
success: false,
message: 'Document file not found'
});
}
const filePath = path.join(__dirname, '../../uploads/legal', document.filePath);
try {
await fs.access(filePath);
} catch (error) {
return res.status(404).json({
success: false,
message: 'Document file not found on server'
});
}
res.setHeader('Content-Type', document.mimeType || 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename="${document.title}.pdf"`);
res.sendFile(filePath);
} catch (error) {
console.error('Download document error:', error);
res.status(500).json({
success: false,
message: 'Failed to download document'
});
}
};
// Get user acceptances
const getUserAcceptances = async (req, res) => {
try {
const acceptances = await LegalAcceptance.findAll({
where: {
userId: req.user.id,
resellerId: req.user.resellerId
},
include: [
{
model: LegalDocument,
as: 'document',
attributes: ['id', 'title', 'type', 'version', 'requiresAcceptance']
}
],
order: [['acceptedAt', 'DESC']]
});
res.json({
success: true,
data: { acceptances }
});
} catch (error) {
console.error('Get user acceptances error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch user acceptances'
});
}
};
// Admin: Create legal document
const createLegalDocument = async (req, res) => {
try {
const {
title,
type,
version,
content,
requiresAcceptance = false,
acceptanceType = 'none',
effectiveDate,
expiryDate,
category,
tags = []
} = req.body;
const document = await LegalDocument.create({
title,
type,
version,
content,
requiresAcceptance,
acceptanceType,
effectiveDate: effectiveDate ? new Date(effectiveDate) : null,
expiryDate: expiryDate ? new Date(expiryDate) : null,
category,
tags,
createdBy: req.user.id,
status: 'draft'
});
res.status(201).json({
success: true,
message: 'Legal document created successfully',
data: { document }
});
} catch (error) {
console.error('Create legal document error:', error);
res.status(500).json({
success: false,
message: 'Failed to create legal document'
});
}
};
module.exports = {
getLegalDocuments,
getDocumentDetails,
acceptDocument,
uploadComplianceDocument,
downloadDocument,
getUserAcceptances,
createLegalDocument
};

View File

@ -0,0 +1,333 @@
const { MarketingAsset, AssetDownload, User, Reseller, AuditLog } = require('../models');
const { Op } = require('sequelize');
const path = require('path');
const fs = require('fs').promises;
// Get marketing assets
const getMarketingAssets = async (req, res) => {
try {
const { page = 1, limit = 20, type, category, search } = req.query;
const offset = (page - 1) * limit;
const userTier = req.user?.reseller?.tier || 'bronze';
const where = { status: 'active' };
// Filter by type and category
if (type) where.type = type;
if (category) where.category = category;
// Search functionality
if (search) {
where[Op.or] = [
{ title: { [Op.iLike]: `%${search}%` } },
{ description: { [Op.iLike]: `%${search}%` } },
{ tags: { [Op.contains]: [search] } }
];
}
// Access level filtering
where[Op.or] = [
{ accessLevel: 'public' },
{ accessLevel: 'reseller_only' },
{
accessLevel: 'tier_specific',
tierAccess: { [Op.contains]: [userTier] }
}
];
const { count, rows: assets } = await MarketingAsset.findAndCountAll({
where,
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'firstName', 'lastName']
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: {
assets,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get marketing assets error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch marketing assets'
});
}
};
// Get asset categories
const getAssetCategories = async (req, res) => {
try {
const categories = await MarketingAsset.findAll({
attributes: [
'category',
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'count']
],
where: { status: 'active' },
group: ['category'],
order: [['category', 'ASC']]
});
res.json({
success: true,
data: { categories }
});
} catch (error) {
console.error('Get asset categories error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch asset categories'
});
}
};
// Download asset
const downloadAsset = async (req, res) => {
try {
const { assetId } = req.params;
const { purpose } = req.body;
const userTier = req.user?.reseller?.tier || 'bronze';
const asset = await MarketingAsset.findByPk(assetId);
if (!asset) {
return res.status(404).json({
success: false,
message: 'Asset not found'
});
}
// Check access permissions
if (!asset.isAccessibleBy(userTier)) {
return res.status(403).json({
success: false,
message: 'Access denied for this asset'
});
}
// Check if asset is expired
if (asset.isExpired()) {
return res.status(410).json({
success: false,
message: 'Asset has expired'
});
}
const filePath = path.join(__dirname, '../../uploads/marketing', asset.filePath);
try {
await fs.access(filePath);
} catch (error) {
return res.status(404).json({
success: false,
message: 'Asset file not found on server'
});
}
// Log download
await AssetDownload.create({
assetId: asset.id,
userId: req.user.id,
resellerId: req.user.resellerId,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
purpose
});
// Update download count
await asset.increment('downloadCount');
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'MARKETING_ASSET_DOWNLOADED',
resource: 'marketing_asset',
resourceId: asset.id,
newValues: { purpose },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.setHeader('Content-Type', asset.mimeType);
res.setHeader('Content-Disposition', `attachment; filename="${asset.fileName}"`);
res.sendFile(filePath);
} catch (error) {
console.error('Download asset error:', error);
res.status(500).json({
success: false,
message: 'Failed to download asset'
});
}
};
// Get asset details
const getAssetDetails = async (req, res) => {
try {
const { assetId } = req.params;
const userTier = req.user?.reseller?.tier || 'bronze';
const asset = await MarketingAsset.findByPk(assetId, {
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'firstName', 'lastName']
},
{
model: User,
as: 'updater',
attributes: ['id', 'firstName', 'lastName']
}
]
});
if (!asset) {
return res.status(404).json({
success: false,
message: 'Asset not found'
});
}
// Check access permissions
if (!asset.isAccessibleBy(userTier)) {
return res.status(403).json({
success: false,
message: 'Access denied for this asset'
});
}
res.json({
success: true,
data: { asset }
});
} catch (error) {
console.error('Get asset details error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch asset details'
});
}
};
// Get download history
const getDownloadHistory = async (req, res) => {
try {
const { page = 1, limit = 20 } = req.query;
const offset = (page - 1) * limit;
const { count, rows: downloads } = await AssetDownload.findAndCountAll({
where: {
userId: req.user.id,
resellerId: req.user.resellerId
},
include: [
{
model: MarketingAsset,
as: 'asset',
attributes: ['id', 'title', 'type', 'category', 'fileName']
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['downloadedAt', 'DESC']]
});
res.json({
success: true,
data: {
downloads,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get download history error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch download history'
});
}
};
// Admin: Create marketing asset
const createMarketingAsset = async (req, res) => {
try {
const {
title,
description,
type,
category,
accessLevel = 'reseller_only',
tierAccess = [],
isEditable = false,
editableFormat,
tags = [],
expiryDate
} = req.body;
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
const asset = await MarketingAsset.create({
title,
description,
type,
category,
filePath: req.file.filename,
fileName: req.file.originalname,
fileSize: req.file.size,
mimeType: req.file.mimetype,
accessLevel,
tierAccess,
isEditable,
editableFormat,
tags,
expiryDate: expiryDate ? new Date(expiryDate) : null,
createdBy: req.user.id,
status: 'active'
});
res.status(201).json({
success: true,
message: 'Marketing asset created successfully',
data: { asset }
});
} catch (error) {
console.error('Create marketing asset error:', error);
res.status(500).json({
success: false,
message: 'Failed to create marketing asset'
});
}
};
module.exports = {
getMarketingAssets,
getAssetCategories,
downloadAsset,
getAssetDetails,
getDownloadHistory,
createMarketingAsset
};

View File

@ -0,0 +1,399 @@
const { Order, OrderItem, Customer, Product, Reseller, Invoice, AuditLog } = require('../models');
const { Op } = require('sequelize');
// Get all orders for reseller
const getOrders = async (req, res) => {
try {
const { page = 1, limit = 20, status, customerId, startDate, endDate } = req.query;
const resellerId = req.user.resellerId;
const offset = (page - 1) * limit;
const where = { resellerId };
if (status) where.status = status;
if (customerId) where.customerId = customerId;
if (startDate && endDate) {
where.createdAt = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
const { count, rows: orders } = await Order.findAndCountAll({
where,
include: [
{
model: Customer,
as: 'customer',
attributes: ['id', 'companyName', 'contactPerson', 'email']
},
{
model: OrderItem,
as: 'items',
include: [
{
model: Product,
as: 'product',
attributes: ['id', 'name', 'category', 'sku']
}
]
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: {
orders,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get orders error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch orders'
});
}
};
// Get order details
const getOrderDetails = async (req, res) => {
try {
const { orderId } = req.params;
const resellerId = req.user.resellerId;
const order = await Order.findOne({
where: { id: orderId, resellerId },
include: [
{
model: Customer,
as: 'customer',
attributes: ['id', 'companyName', 'contactPerson', 'email', 'phone', 'address']
},
{
model: OrderItem,
as: 'items',
include: [
{
model: Product,
as: 'product',
attributes: ['id', 'name', 'description', 'category', 'sku', 'basePrice']
}
]
},
{
model: Invoice,
as: 'invoices',
attributes: ['id', 'invoiceNumber', 'status', 'totalAmount', 'issueDate', 'dueDate']
}
]
});
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found'
});
}
res.json({
success: true,
data: { order }
});
} catch (error) {
console.error('Get order details error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch order details'
});
}
};
// Create new order
const createOrder = async (req, res) => {
try {
const { customerId, items, notes, type = 'new_service' } = req.body;
const resellerId = req.user.resellerId;
// Verify customer belongs to reseller
const customer = await Customer.findOne({
where: { id: customerId, resellerId }
});
if (!customer) {
return res.status(404).json({
success: false,
message: 'Customer not found'
});
}
// Generate order number
const orderCount = await Order.count({ where: { resellerId } });
const orderNumber = `ORD-${resellerId.slice(-8).toUpperCase()}-${String(orderCount + 1).padStart(4, '0')}`;
// Calculate totals
let subtotal = 0;
const orderItems = [];
for (const item of items) {
const product = await Product.findByPk(item.productId);
if (!product) {
return res.status(400).json({
success: false,
message: `Product ${item.productId} not found`
});
}
// Get reseller pricing
const reseller = await Reseller.findByPk(resellerId);
const unitPrice = product.calculateResellerPrice(reseller.tier, item.customPrice);
const totalPrice = unitPrice * item.quantity;
orderItems.push({
productId: item.productId,
quantity: item.quantity,
unitPrice,
totalPrice,
configuration: item.configuration || {},
specifications: item.specifications || {}
});
subtotal += totalPrice;
}
const taxAmount = subtotal * 0.18; // 18% GST
const totalAmount = subtotal + taxAmount;
// Create order
const order = await Order.create({
orderNumber,
resellerId,
customerId,
type,
subtotal,
taxAmount,
totalAmount,
notes,
status: 'pending'
});
// Create order items
for (const item of orderItems) {
await OrderItem.create({
orderId: order.id,
...item
});
}
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'ORDER_CREATED',
resource: 'order',
resourceId: order.id,
newValues: order.toJSON(),
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
// Fetch complete order with items
const completeOrder = await Order.findByPk(order.id, {
include: [
{
model: Customer,
as: 'customer',
attributes: ['id', 'companyName', 'contactPerson']
},
{
model: OrderItem,
as: 'items',
include: [
{
model: Product,
as: 'product',
attributes: ['id', 'name', 'sku']
}
]
}
]
});
res.status(201).json({
success: true,
message: 'Order created successfully',
data: { order: completeOrder }
});
} catch (error) {
console.error('Create order error:', error);
res.status(500).json({
success: false,
message: 'Failed to create order'
});
}
};
// Update order status
const updateOrderStatus = async (req, res) => {
try {
const { orderId } = req.params;
const { status, notes } = req.body;
const resellerId = req.user.resellerId;
const order = await Order.findOne({
where: { id: orderId, resellerId }
});
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found'
});
}
const oldStatus = order.status;
await order.update({
status,
notes: notes || order.notes
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'ORDER_STATUS_UPDATED',
resource: 'order',
resourceId: order.id,
oldValues: { status: oldStatus },
newValues: { status },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Order status updated successfully',
data: { order }
});
} catch (error) {
console.error('Update order status error:', error);
res.status(500).json({
success: false,
message: 'Failed to update order status'
});
}
};
// Get pending orders
const getPendingOrders = async (req, res) => {
try {
const resellerId = req.user.resellerId;
const pendingOrders = await Order.findAll({
where: {
resellerId,
status: ['pending', 'draft']
},
include: [
{
model: Customer,
as: 'customer',
attributes: ['id', 'companyName', 'contactPerson']
},
{
model: OrderItem,
as: 'items',
include: [
{
model: Product,
as: 'product',
attributes: ['id', 'name', 'category']
}
]
}
],
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: { orders: pendingOrders }
});
} catch (error) {
console.error('Get pending orders error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch pending orders'
});
}
};
// Cancel order
const cancelOrder = async (req, res) => {
try {
const { orderId } = req.params;
const { reason } = req.body;
const resellerId = req.user.resellerId;
const order = await Order.findOne({
where: { id: orderId, resellerId }
});
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found'
});
}
if (!['draft', 'pending'].includes(order.status)) {
return res.status(400).json({
success: false,
message: 'Order cannot be cancelled in current status'
});
}
await order.update({
status: 'cancelled',
notes: `${order.notes || ''}\nCancellation reason: ${reason}`
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'ORDER_CANCELLED',
resource: 'order',
resourceId: order.id,
newValues: { status: 'cancelled', reason },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Order cancelled successfully',
data: { order }
});
} catch (error) {
console.error('Cancel order error:', error);
res.status(500).json({
success: false,
message: 'Failed to cancel order'
});
}
};
module.exports = {
getOrders,
getOrderDetails,
createOrder,
updateOrderStatus,
getPendingOrders,
cancelOrder
};

View File

@ -0,0 +1,229 @@
const { Product, ResellerPricing, Reseller, AuditLog } = require('../models');
const { Op } = require('sequelize');
// Get all products with reseller-specific pricing
const getProducts = async (req, res) => {
try {
const { page = 1, limit = 20, category, status, search } = req.query;
const offset = (page - 1) * limit;
const where = { status: status || 'active' };
if (category) where.category = category;
if (search) {
where[Op.or] = [
{ name: { [Op.iLike]: `%${search}%` } },
{ description: { [Op.iLike]: `%${search}%` } },
{ sku: { [Op.iLike]: `%${search}%` } }
];
}
const { count, rows: products } = await Product.findAndCountAll({
where,
include: [
{
model: ResellerPricing,
as: 'resellerPricing',
where: { resellerId: req.user.resellerId, isActive: true },
required: false
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['name', 'ASC']]
});
// Calculate pricing for each product
const reseller = await Reseller.findByPk(req.user.resellerId);
const productsWithPricing = products.map(product => {
const productData = product.toJSON();
// Get custom pricing if exists
const customPricing = productData.resellerPricing?.[0];
if (customPricing) {
productData.finalPrice = customPricing.calculateFinalPrice(productData.basePrice);
productData.margin = customPricing.customMargin;
productData.pricingType = customPricing.pricingType;
} else {
// Use tier-based pricing
productData.finalPrice = product.calculateResellerPrice(reseller.tier);
productData.margin = product.getMarginForTier(reseller.tier);
productData.pricingType = 'tier_based';
}
productData.profit = productData.finalPrice - productData.basePrice;
productData.profitMargin = ((productData.profit / productData.basePrice) * 100).toFixed(2);
return productData;
});
res.json({
success: true,
data: {
products: productsWithPricing,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get products error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch products'
});
}
};
// Get product details
const getProduct = async (req, res) => {
try {
const { productId } = req.params;
const product = await Product.findByPk(productId, {
include: [
{
model: ResellerPricing,
as: 'resellerPricing',
where: { resellerId: req.user.resellerId, isActive: true },
required: false
}
]
});
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
const reseller = await Reseller.findByPk(req.user.resellerId);
const productData = product.toJSON();
// Calculate pricing
const customPricing = productData.resellerPricing?.[0];
if (customPricing) {
productData.finalPrice = customPricing.calculateFinalPrice(productData.basePrice);
productData.margin = customPricing.customMargin;
productData.pricingType = customPricing.pricingType;
} else {
productData.finalPrice = product.calculateResellerPrice(reseller.tier);
productData.margin = product.getMarginForTier(reseller.tier);
productData.pricingType = 'tier_based';
}
productData.profit = productData.finalPrice - productData.basePrice;
productData.profitMargin = ((productData.profit / productData.basePrice) * 100).toFixed(2);
res.json({
success: true,
data: { product: productData }
});
} catch (error) {
console.error('Get product error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch product'
});
}
};
// Update custom pricing for a product
const updateProductPricing = async (req, res) => {
try {
const { productId } = req.params;
const { customMargin, customPrice, pricingType = 'margin' } = req.body;
const product = await Product.findByPk(productId);
if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found'
});
}
// Check if custom pricing already exists
let resellerPricing = await ResellerPricing.findOne({
where: {
resellerId: req.user.resellerId,
productId: productId,
isActive: true
}
});
const pricingData = {
resellerId: req.user.resellerId,
productId: productId,
pricingType,
customMargin: pricingType === 'margin' ? customMargin : null,
customPrice: pricingType === 'fixed_price' ? customPrice : null,
isActive: true,
effectiveFrom: new Date()
};
if (resellerPricing) {
await resellerPricing.update(pricingData);
} else {
resellerPricing = await ResellerPricing.create(pricingData);
}
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'PRODUCT_PRICING_UPDATED',
resource: 'reseller_pricing',
resourceId: resellerPricing.id,
newValues: pricingData,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Product pricing updated successfully',
data: { resellerPricing }
});
} catch (error) {
console.error('Update product pricing error:', error);
res.status(500).json({
success: false,
message: 'Failed to update product pricing'
});
}
};
// Get product categories
const getCategories = async (req, res) => {
try {
const categories = await Product.findAll({
attributes: ['category'],
group: ['category'],
where: { status: 'active' }
});
const categoryList = categories.map(item => item.category);
res.json({
success: true,
data: { categories: categoryList }
});
} catch (error) {
console.error('Get categories error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch categories'
});
}
};
module.exports = {
getProducts,
getProduct,
updateProductPricing,
getCategories
};

View File

@ -0,0 +1,356 @@
const { Instance, InstanceSnapshot, InstanceEvent, Customer, Product, AuditLog } = require('../models');
const { Op } = require('sequelize');
// Get all instances for reseller
const getInstances = async (req, res) => {
try {
const { page = 1, limit = 20, status, type, customerId, region } = req.query;
const resellerId = req.user.resellerId;
const offset = (page - 1) * limit;
const where = { resellerId };
if (status) where.status = status;
if (type) where.type = type;
if (customerId) where.customerId = customerId;
if (region) where.region = region;
const { count, rows: instances } = await Instance.findAndCountAll({
where,
include: [
{
model: Customer,
as: 'customer',
attributes: ['id', 'companyName', 'contactPerson']
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: {
instances,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get instances error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch instances'
});
}
};
// Create new instance
const createInstance = async (req, res) => {
try {
const {
customerId,
name,
template,
size,
region,
zone,
specifications,
configuration = {},
tags = []
} = req.body;
const resellerId = req.user.resellerId;
// Verify customer belongs to reseller
const customer = await Customer.findOne({
where: { id: customerId, resellerId }
});
if (!customer) {
return res.status(404).json({
success: false,
message: 'Customer not found'
});
}
// Create instance record
const instance = await Instance.create({
customerId,
resellerId,
name,
template,
size,
region,
zone,
specifications,
configuration,
tags,
status: 'pending'
});
// Log creation event
await InstanceEvent.create({
instanceId: instance.id,
eventType: 'created',
status: 'completed',
message: 'Instance created successfully',
triggeredBy: req.user.id,
source: 'user',
startedAt: new Date(),
completedAt: new Date()
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'INSTANCE_CREATED',
resource: 'instance',
resourceId: instance.id,
newValues: instance.toJSON(),
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
// TODO: Integrate with Cloudtopiaa IaaS API to actually provision the instance
// This would be an async operation that updates the instance status
res.status(201).json({
success: true,
message: 'Instance creation initiated',
data: { instance }
});
} catch (error) {
console.error('Create instance error:', error);
res.status(500).json({
success: false,
message: 'Failed to create instance'
});
}
};
// Start instance
const startInstance = async (req, res) => {
try {
const { instanceId } = req.params;
const resellerId = req.user.resellerId;
const instance = await Instance.findOne({
where: { id: instanceId, resellerId }
});
if (!instance) {
return res.status(404).json({
success: false,
message: 'Instance not found'
});
}
if (!instance.canStart()) {
return res.status(400).json({
success: false,
message: 'Instance cannot be started in current state'
});
}
// Update instance status
await instance.update({ status: 'starting' });
// Log event
await InstanceEvent.create({
instanceId: instance.id,
eventType: 'started',
status: 'in_progress',
message: 'Starting instance',
triggeredBy: req.user.id,
source: 'user',
startedAt: new Date()
});
// TODO: Call Cloudtopiaa IaaS API to start the instance
// This would be an async operation
res.json({
success: true,
message: 'Instance start initiated',
data: { instance }
});
} catch (error) {
console.error('Start instance error:', error);
res.status(500).json({
success: false,
message: 'Failed to start instance'
});
}
};
// Stop instance
const stopInstance = async (req, res) => {
try {
const { instanceId } = req.params;
const resellerId = req.user.resellerId;
const instance = await Instance.findOne({
where: { id: instanceId, resellerId }
});
if (!instance) {
return res.status(404).json({
success: false,
message: 'Instance not found'
});
}
if (!instance.canStop()) {
return res.status(400).json({
success: false,
message: 'Instance cannot be stopped in current state'
});
}
// Update instance status
await instance.update({ status: 'stopping' });
// Log event
await InstanceEvent.create({
instanceId: instance.id,
eventType: 'stopped',
status: 'in_progress',
message: 'Stopping instance',
triggeredBy: req.user.id,
source: 'user',
startedAt: new Date()
});
res.json({
success: true,
message: 'Instance stop initiated',
data: { instance }
});
} catch (error) {
console.error('Stop instance error:', error);
res.status(500).json({
success: false,
message: 'Failed to stop instance'
});
}
};
// Create snapshot
const createSnapshot = async (req, res) => {
try {
const { instanceId } = req.params;
const { name, description, type = 'manual' } = req.body;
const resellerId = req.user.resellerId;
const instance = await Instance.findOne({
where: { id: instanceId, resellerId }
});
if (!instance) {
return res.status(404).json({
success: false,
message: 'Instance not found'
});
}
// Create snapshot record
const snapshot = await InstanceSnapshot.create({
instanceId,
name,
description,
type,
status: 'creating'
});
// Log event
await InstanceEvent.create({
instanceId: instance.id,
eventType: 'snapshot_created',
status: 'in_progress',
message: `Creating snapshot: ${name}`,
triggeredBy: req.user.id,
source: 'user',
startedAt: new Date()
});
res.status(201).json({
success: true,
message: 'Snapshot creation initiated',
data: { snapshot }
});
} catch (error) {
console.error('Create snapshot error:', error);
res.status(500).json({
success: false,
message: 'Failed to create snapshot'
});
}
};
// Get instance events
const getInstanceEvents = async (req, res) => {
try {
const { instanceId } = req.params;
const { page = 1, limit = 20 } = req.query;
const resellerId = req.user.resellerId;
const offset = (page - 1) * limit;
// Verify instance belongs to reseller
const instance = await Instance.findOne({
where: { id: instanceId, resellerId }
});
if (!instance) {
return res.status(404).json({
success: false,
message: 'Instance not found'
});
}
const { count, rows: events } = await InstanceEvent.findAndCountAll({
where: { instanceId },
include: [
{
model: require('../models').User,
as: 'user',
attributes: ['id', 'firstName', 'lastName']
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: {
events,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get instance events error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch instance events'
});
}
};
module.exports = {
getInstances,
createInstance,
startInstance,
stopInstance,
createSnapshot,
getInstanceEvents
};

View File

@ -0,0 +1,279 @@
const { Customer, CustomerService, UsageRecord, Invoice, WalletTransaction, Wallet, Product, Commission } = require('../models');
const { Op } = require('sequelize');
// Get sales report
const getSalesReport = async (req, res) => {
try {
const { startDate, endDate, groupBy = 'month', format = 'json' } = req.query;
const resellerId = req.user.resellerId;
const dateFilter = {};
if (startDate && endDate) {
dateFilter.createdAt = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
// Sales by period
const salesData = await Invoice.findAll({
where: {
resellerId,
status: 'paid',
...dateFilter
},
attributes: [
[require('sequelize').fn('DATE_TRUNC', groupBy, require('sequelize').col('paid_at')), 'period'],
[require('sequelize').fn('SUM', require('sequelize').col('total_amount')), 'totalSales'],
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'invoiceCount'],
[require('sequelize').fn('AVG', require('sequelize').col('total_amount')), 'averageInvoice']
],
group: [require('sequelize').fn('DATE_TRUNC', groupBy, require('sequelize').col('paid_at'))],
order: [[require('sequelize').fn('DATE_TRUNC', groupBy, require('sequelize').col('paid_at')), 'ASC']]
});
// Top customers by revenue
const topCustomers = await Invoice.findAll({
where: {
resellerId,
status: 'paid',
...dateFilter
},
include: [
{
model: Customer,
as: 'customer',
attributes: ['id', 'companyName', 'contactPerson']
}
],
attributes: [
'customerId',
[require('sequelize').fn('SUM', require('sequelize').col('total_amount')), 'totalRevenue'],
[require('sequelize').fn('COUNT', require('sequelize').col('Invoice.id')), 'invoiceCount']
],
group: ['customerId', 'customer.id', 'customer.company_name', 'customer.contact_person'],
order: [[require('sequelize').fn('SUM', require('sequelize').col('total_amount')), 'DESC']],
limit: 10
});
res.json({
success: true,
data: {
salesData,
topCustomers,
reportPeriod: { startDate, endDate },
generatedAt: new Date()
}
});
} catch (error) {
console.error('Get sales report error:', error);
res.status(500).json({
success: false,
message: 'Failed to generate sales report'
});
}
};
// Get usage report
const getUsageReport = async (req, res) => {
try {
const { startDate, endDate, customerId, productId } = req.query;
const resellerId = req.user.resellerId;
const where = {};
if (startDate && endDate) {
where.usageDate = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
const customerWhere = { resellerId };
if (customerId) {
customerWhere.id = customerId;
}
if (productId) {
where.productId = productId;
}
const usageData = await UsageRecord.findAll({
where,
include: [
{
model: Customer,
as: 'customer',
where: customerWhere,
attributes: ['id', 'companyName']
},
{
model: Product,
as: 'product',
attributes: ['id', 'name', 'category']
},
{
model: CustomerService,
as: 'service',
attributes: ['id', 'serviceName']
}
],
order: [['usageDate', 'DESC']]
});
// Aggregate by customer and product
const usageSummary = usageData.reduce((acc, record) => {
const key = `${record.customerId}_${record.productId}`;
if (!acc[key]) {
acc[key] = {
customer: record.customer,
product: record.product,
totalQuantity: 0,
totalCost: 0,
records: []
};
}
acc[key].totalQuantity += parseFloat(record.quantity);
acc[key].totalCost += parseFloat(record.totalCost);
acc[key].records.push(record);
return acc;
}, {});
res.json({
success: true,
data: {
usageData,
usageSummary: Object.values(usageSummary),
reportPeriod: { startDate, endDate },
generatedAt: new Date()
}
});
} catch (error) {
console.error('Get usage report error:', error);
res.status(500).json({
success: false,
message: 'Failed to generate usage report'
});
}
};
// Get commission report
const getCommissionReport = async (req, res) => {
try {
const { startDate, endDate, status = 'all' } = req.query;
const resellerId = req.user.resellerId;
const where = { resellerId };
if (startDate && endDate) {
where.createdAt = {
[Op.between]: [new Date(startDate), new Date(endDate)]
};
}
if (status !== 'all') {
where.status = status;
}
const commissions = await Commission.findAll({
where,
include: [
{
model: Customer,
as: 'customer',
attributes: ['id', 'companyName']
},
{
model: Invoice,
as: 'invoice',
attributes: ['id', 'invoiceNumber', 'totalAmount']
}
],
order: [['createdAt', 'DESC']]
});
// Calculate totals
const totals = commissions.reduce((acc, commission) => {
acc.totalCommission += parseFloat(commission.commissionAmount);
acc.totalBase += parseFloat(commission.baseAmount);
if (commission.status === 'paid') {
acc.paidCommission += parseFloat(commission.commissionAmount);
} else if (commission.status === 'pending') {
acc.pendingCommission += parseFloat(commission.commissionAmount);
}
return acc;
}, {
totalCommission: 0,
totalBase: 0,
paidCommission: 0,
pendingCommission: 0
});
res.json({
success: true,
data: {
commissions,
totals,
reportPeriod: { startDate, endDate },
generatedAt: new Date()
}
});
} catch (error) {
console.error('Get commission report error:', error);
res.status(500).json({
success: false,
message: 'Failed to generate commission report'
});
}
};
// Export report data
const exportReport = async (req, res) => {
try {
const { reportType, format = 'csv', ...params } = req.query;
let reportData;
switch (reportType) {
case 'sales':
reportData = await getSalesReport({ ...req, query: params }, { json: () => {} });
break;
case 'usage':
reportData = await getUsageReport({ ...req, query: params }, { json: () => {} });
break;
case 'commission':
reportData = await getCommissionReport({ ...req, query: params }, { json: () => {} });
break;
default:
return res.status(400).json({
success: false,
message: 'Invalid report type'
});
}
if (format === 'csv') {
// Convert to CSV (simplified implementation)
const csvData = `${reportType.toUpperCase()} Report\nGenerated: ${new Date()}\n\n${JSON.stringify(reportData, null, 2)}`;
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="${reportType}-report-${Date.now()}.csv"`);
res.send(csvData);
} else {
res.json({
success: true,
data: reportData
});
}
} catch (error) {
console.error('Export report error:', error);
res.status(500).json({
success: false,
message: 'Failed to export report'
});
}
};
module.exports = {
getSalesReport,
getUsageReport,
getCommissionReport,
exportReport
};

View File

@ -0,0 +1,421 @@
const { Course, CourseEnrollment, Certificate, User, Reseller, AuditLog } = require('../models');
const { Op } = require('sequelize');
const crypto = require('crypto');
// Get available courses
const getCourses = async (req, res) => {
try {
const { page = 1, limit = 20, category, level, search } = req.query;
const offset = (page - 1) * limit;
const userTier = req.user?.reseller?.tier || 'bronze';
const where = { status: 'active' };
// Filter by category and level
if (category) where.category = category;
if (level) where.level = level;
// Search functionality
if (search) {
where[Op.or] = [
{ title: { [Op.iLike]: `%${search}%` } },
{ description: { [Op.iLike]: `%${search}%` } },
{ tags: { [Op.contains]: [search] } }
];
}
// Access level filtering
where[Op.or] = [
{ accessLevel: 'public' },
{ accessLevel: 'reseller_only' },
{
accessLevel: 'tier_specific',
tierAccess: { [Op.contains]: [userTier] }
}
];
const { count, rows: courses } = await Course.findAndCountAll({
where,
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'firstName', 'lastName']
}
],
attributes: { exclude: ['videoPath', 'materials'] }, // Exclude large fields from list
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
// Check enrollment status for each course
const coursesWithEnrollment = await Promise.all(
courses.map(async (course) => {
const enrollment = await CourseEnrollment.findOne({
where: {
courseId: course.id,
userId: req.user.id
}
});
return {
...course.toJSON(),
enrollment: enrollment ? {
id: enrollment.id,
status: enrollment.status,
progress: enrollment.progress,
enrolledAt: enrollment.enrolledAt,
completedAt: enrollment.completedAt
} : null
};
})
);
res.json({
success: true,
data: {
courses: coursesWithEnrollment,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get courses error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch courses'
});
}
};
// Get course details
const getCourseDetails = async (req, res) => {
try {
const { courseId } = req.params;
const userTier = req.user?.reseller?.tier || 'bronze';
const course = await Course.findByPk(courseId, {
include: [
{
model: User,
as: 'creator',
attributes: ['id', 'firstName', 'lastName']
}
]
});
if (!course) {
return res.status(404).json({
success: false,
message: 'Course not found'
});
}
// Check access permissions
if (!course.isAccessibleBy(userTier)) {
return res.status(403).json({
success: false,
message: 'Access denied for this course'
});
}
// Get user's enrollment
const enrollment = await CourseEnrollment.findOne({
where: {
courseId: course.id,
userId: req.user.id
}
});
res.json({
success: true,
data: {
course,
enrollment: enrollment ? {
id: enrollment.id,
status: enrollment.status,
progress: enrollment.progress,
enrolledAt: enrollment.enrolledAt,
startedAt: enrollment.startedAt,
completedAt: enrollment.completedAt,
timeSpent: enrollment.timeSpent,
attempts: enrollment.attempts,
bestScore: enrollment.bestScore,
moduleProgress: enrollment.moduleProgress
} : null
}
});
} catch (error) {
console.error('Get course details error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch course details'
});
}
};
// Enroll in course
const enrollInCourse = async (req, res) => {
try {
const { courseId } = req.params;
const userTier = req.user?.reseller?.tier || 'bronze';
const course = await Course.findByPk(courseId);
if (!course) {
return res.status(404).json({
success: false,
message: 'Course not found'
});
}
// Check access permissions
if (!course.isAccessibleBy(userTier)) {
return res.status(403).json({
success: false,
message: 'Access denied for this course'
});
}
// Check if already enrolled
const existingEnrollment = await CourseEnrollment.findOne({
where: {
courseId: course.id,
userId: req.user.id
}
});
if (existingEnrollment) {
return res.status(400).json({
success: false,
message: 'Already enrolled in this course'
});
}
// Create enrollment
const enrollment = await CourseEnrollment.create({
courseId: course.id,
userId: req.user.id,
resellerId: req.user.resellerId,
status: 'enrolled'
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'COURSE_ENROLLED',
resource: 'course',
resourceId: course.id,
newValues: { enrollmentId: enrollment.id },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.status(201).json({
success: true,
message: 'Successfully enrolled in course',
data: { enrollment }
});
} catch (error) {
console.error('Enroll in course error:', error);
res.status(500).json({
success: false,
message: 'Failed to enroll in course'
});
}
};
// Update course progress
const updateCourseProgress = async (req, res) => {
try {
const { courseId } = req.params;
const { moduleId, moduleProgress, timeSpent = 0 } = req.body;
const enrollment = await CourseEnrollment.findOne({
where: {
courseId,
userId: req.user.id
}
});
if (!enrollment) {
return res.status(404).json({
success: false,
message: 'Enrollment not found'
});
}
// Update progress
await enrollment.updateProgress(moduleId, moduleProgress);
// Update time spent
await enrollment.update({
timeSpent: enrollment.timeSpent + timeSpent,
status: enrollment.progress >= 100 ? 'completed' : 'in_progress',
completedAt: enrollment.progress >= 100 ? new Date() : null
});
res.json({
success: true,
message: 'Progress updated successfully',
data: { enrollment }
});
} catch (error) {
console.error('Update course progress error:', error);
res.status(500).json({
success: false,
message: 'Failed to update course progress'
});
}
};
// Get user certificates
const getUserCertificates = async (req, res) => {
try {
const { page = 1, limit = 20 } = req.query;
const offset = (page - 1) * limit;
const { count, rows: certificates } = await Certificate.findAndCountAll({
where: {
userId: req.user.id,
resellerId: req.user.resellerId
},
include: [
{
model: Course,
as: 'course',
attributes: ['id', 'title', 'category', 'level']
}
],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['issuedAt', 'DESC']]
});
res.json({
success: true,
data: {
certificates,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get user certificates error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch certificates'
});
}
};
// Issue certificate
const issueCertificate = async (req, res) => {
try {
const { enrollmentId } = req.params;
const { score, grade } = req.body;
const enrollment = await CourseEnrollment.findByPk(enrollmentId, {
include: [
{
model: Course,
as: 'course'
}
]
});
if (!enrollment) {
return res.status(404).json({
success: false,
message: 'Enrollment not found'
});
}
if (!enrollment.isCompleted()) {
return res.status(400).json({
success: false,
message: 'Course must be completed to issue certificate'
});
}
// Check if certificate already exists
const existingCertificate = await Certificate.findOne({
where: { enrollmentId: enrollment.id }
});
if (existingCertificate) {
return res.status(400).json({
success: false,
message: 'Certificate already issued for this enrollment'
});
}
// Generate certificate number and verification code
const certificateNumber = `CERT-${Date.now()}-${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
const verificationCode = crypto.randomBytes(16).toString('hex');
// Calculate expiry date if course has validity period
let expiresAt = null;
if (enrollment.course.validityPeriod) {
expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + enrollment.course.validityPeriod);
}
// Create certificate
const certificate = await Certificate.create({
certificateNumber,
courseId: enrollment.courseId,
userId: enrollment.userId,
resellerId: enrollment.resellerId,
enrollmentId: enrollment.id,
score,
grade,
expiresAt,
verificationCode,
issuedBy: req.user.id
});
// TODO: Generate PDF certificate file
// This would involve creating a PDF using the certificate template
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'CERTIFICATE_ISSUED',
resource: 'certificate',
resourceId: certificate.id,
newValues: { certificateNumber, courseId: enrollment.courseId },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.status(201).json({
success: true,
message: 'Certificate issued successfully',
data: { certificate }
});
} catch (error) {
console.error('Issue certificate error:', error);
res.status(500).json({
success: false,
message: 'Failed to issue certificate'
});
}
};
module.exports = {
getCourses,
getCourseDetails,
enrollInCourse,
updateCourseProgress,
getUserCertificates,
issueCertificate
};

191
src/middleware/auth.js Normal file
View File

@ -0,0 +1,191 @@
const jwt = require('jsonwebtoken');
const { User, UserSession } = require('../models');
const redisClient = require('../config/redis');
// Authenticate JWT token
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required'
});
}
// Check if token is blacklisted
const isBlacklisted = await redisClient.get(`blacklist:${token}`);
if (isBlacklisted) {
return res.status(401).json({
success: false,
message: 'Token has been invalidated'
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user from database
const user = await User.findByPk(decoded.userId, {
include: ['reseller']
});
if (!user) {
return res.status(401).json({
success: false,
message: 'User not found'
});
}
if (user.status !== 'active') {
return res.status(401).json({
success: false,
message: 'Account is not active'
});
}
req.user = user;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expired'
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Invalid token'
});
}
console.error('Auth middleware error:', error);
return res.status(500).json({
success: false,
message: 'Authentication error'
});
}
};
// Authorize based on roles
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Insufficient permissions'
});
}
next();
};
};
// Verify MFA token
const verifyMFA = async (req, res, next) => {
try {
if (!req.user.mfaEnabled) {
return next();
}
const mfaToken = req.headers['x-mfa-token'];
if (!mfaToken) {
return res.status(401).json({
success: false,
message: 'MFA token required'
});
}
const isValid = req.user.verifyMfaToken(mfaToken);
if (!isValid) {
return res.status(401).json({
success: false,
message: 'Invalid MFA token'
});
}
next();
} catch (error) {
console.error('MFA verification error:', error);
return res.status(500).json({
success: false,
message: 'MFA verification error'
});
}
};
// Check if user account is locked
const checkAccountLock = async (req, res, next) => {
try {
if (req.user && req.user.isLocked()) {
return res.status(423).json({
success: false,
message: 'Account is temporarily locked due to multiple failed login attempts'
});
}
next();
} catch (error) {
console.error('Account lock check error:', error);
return res.status(500).json({
success: false,
message: 'Account verification error'
});
}
};
// Refresh token validation
const validateRefreshToken = async (req, res, next) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
success: false,
message: 'Refresh token required'
});
}
const session = await UserSession.findOne({
where: {
refreshToken,
isActive: true
},
include: ['user']
});
if (!session || session.isExpired()) {
return res.status(401).json({
success: false,
message: 'Invalid or expired refresh token'
});
}
req.session = session;
req.user = session.user;
next();
} catch (error) {
console.error('Refresh token validation error:', error);
return res.status(500).json({
success: false,
message: 'Token validation error'
});
}
};
module.exports = {
authenticateToken,
authorize,
verifyMFA,
checkAccountLock,
validateRefreshToken
};

View File

@ -0,0 +1,77 @@
const { ValidationError, UniqueConstraintError, ForeignKeyConstraintError } = require('sequelize');
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
console.error('Error:', err);
// Sequelize validation error
if (err instanceof ValidationError) {
const message = err.errors.map(e => e.message).join(', ');
error = {
statusCode: 400,
message: `Validation Error: ${message}`
};
}
// Sequelize unique constraint error
if (err instanceof UniqueConstraintError) {
const field = err.errors[0].path;
error = {
statusCode: 409,
message: `${field} already exists`
};
}
// Sequelize foreign key constraint error
if (err instanceof ForeignKeyConstraintError) {
error = {
statusCode: 400,
message: 'Invalid reference to related resource'
};
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
error = {
statusCode: 401,
message: 'Invalid token'
};
}
if (err.name === 'TokenExpiredError') {
error = {
statusCode: 401,
message: 'Token expired'
};
}
// Multer errors (file upload)
if (err.code === 'LIMIT_FILE_SIZE') {
error = {
statusCode: 413,
message: 'File too large'
};
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
error = {
statusCode: 400,
message: 'Unexpected file field'
};
}
// Default error
const statusCode = error.statusCode || err.statusCode || 500;
const message = error.message || 'Internal Server Error';
res.status(statusCode).json({
success: false,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
module.exports = errorHandler;

151
src/middleware/upload.js Normal file
View File

@ -0,0 +1,151 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
// Ensure upload directories exist
const ensureUploadDirs = () => {
const dirs = [
'uploads/documents',
'uploads/avatars',
'uploads/kyc',
'uploads/temp'
];
dirs.forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
};
ensureUploadDirs();
// File filter function
const fileFilter = (allowedTypes) => {
return (req, file, cb) => {
const fileExtension = path.extname(file.originalname).toLowerCase();
const allowedExtensions = allowedTypes.map(type => `.${type}`);
if (allowedExtensions.includes(fileExtension)) {
cb(null, true);
} else {
cb(new Error(`File type not allowed. Allowed types: ${allowedTypes.join(', ')}`), false);
}
};
};
// Storage configuration
const createStorage = (uploadPath) => {
return multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadPath);
},
filename: (req, file, cb) => {
const uniqueSuffix = uuidv4();
const fileExtension = path.extname(file.originalname);
cb(null, `${uniqueSuffix}${fileExtension}`);
}
});
};
// Document upload (KYC, contracts, etc.)
const documentUpload = multer({
storage: createStorage('uploads/documents'),
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024, // 5MB
files: 10
},
fileFilter: fileFilter(['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png'])
});
// Avatar upload
const avatarUpload = multer({
storage: createStorage('uploads/avatars'),
limits: {
fileSize: 2 * 1024 * 1024, // 2MB
files: 1
},
fileFilter: fileFilter(['jpg', 'jpeg', 'png', 'gif'])
});
// KYC document upload
const kycUpload = multer({
storage: createStorage('uploads/kyc'),
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024, // 5MB
files: 5
},
fileFilter: fileFilter(['pdf', 'jpg', 'jpeg', 'png'])
});
// Generic file upload
const genericUpload = multer({
storage: createStorage('uploads/temp'),
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024, // 5MB
files: 5
},
fileFilter: fileFilter(['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png', 'txt', 'csv', 'xlsx'])
});
// File validation middleware
const validateFile = (req, res, next) => {
if (!req.file && !req.files) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
next();
};
// File cleanup utility
const cleanupFile = (filePath) => {
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
console.log(`File deleted: ${filePath}`);
}
} catch (error) {
console.error(`Error deleting file ${filePath}:`, error);
}
};
// File info extractor
const extractFileInfo = (file) => {
return {
originalName: file.originalname,
filename: file.filename,
path: file.path,
size: file.size,
mimetype: file.mimetype,
uploadedAt: new Date()
};
};
// Multiple file info extractor
const extractMultipleFileInfo = (files) => {
if (Array.isArray(files)) {
return files.map(extractFileInfo);
}
// Handle multer's field-based file object
const result = {};
Object.keys(files).forEach(fieldName => {
result[fieldName] = files[fieldName].map(extractFileInfo);
});
return result;
};
module.exports = {
documentUpload,
avatarUpload,
kycUpload,
genericUpload,
validateFile,
cleanupFile,
extractFileInfo,
extractMultipleFileInfo,
ensureUploadDirs
};

View File

@ -0,0 +1,119 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('users', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: Sequelize.STRING,
allowNull: false
},
firstName: {
type: Sequelize.STRING,
allowNull: false
},
lastName: {
type: Sequelize.STRING,
allowNull: false
},
phone: {
type: Sequelize.STRING,
allowNull: true
},
role: {
type: Sequelize.ENUM('reseller', 'reseller_admin', 'customer', 'support', 'admin'),
allowNull: false,
defaultValue: 'reseller'
},
status: {
type: Sequelize.ENUM('pending', 'active', 'suspended', 'inactive'),
allowNull: false,
defaultValue: 'pending'
},
emailVerified: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
emailVerificationToken: {
type: Sequelize.STRING,
allowNull: true
},
passwordResetToken: {
type: Sequelize.STRING,
allowNull: true
},
passwordResetExpires: {
type: Sequelize.DATE,
allowNull: true
},
mfaEnabled: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
mfaSecret: {
type: Sequelize.STRING,
allowNull: true
},
mfaBackupCodes: {
type: Sequelize.JSON,
defaultValue: []
},
refreshToken: {
type: Sequelize.TEXT,
allowNull: true
},
refreshTokenExpires: {
type: Sequelize.DATE,
allowNull: true
},
loginAttempts: {
type: Sequelize.INTEGER,
defaultValue: 0
},
lockUntil: {
type: Sequelize.DATE,
allowNull: true
},
lastLogin: {
type: Sequelize.DATE,
allowNull: true
},
avatar: {
type: Sequelize.STRING,
allowNull: true
},
preferences: {
type: Sequelize.JSON,
defaultValue: {}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('users', ['email']);
await queryInterface.addIndex('users', ['role']);
await queryInterface.addIndex('users', ['status']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('users');
}
};

View File

@ -0,0 +1,140 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('resellers', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
userId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
companyName: {
type: Sequelize.STRING,
allowNull: false
},
businessType: {
type: Sequelize.ENUM('individual', 'partnership', 'private_limited', 'public_limited', 'llp', 'other'),
allowNull: false
},
registrationNumber: {
type: Sequelize.STRING,
allowNull: true
},
taxId: {
type: Sequelize.STRING,
allowNull: true
},
website: {
type: Sequelize.STRING,
allowNull: true
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
address: {
type: Sequelize.JSON,
defaultValue: {}
},
contactPerson: {
type: Sequelize.STRING,
allowNull: false
},
contactEmail: {
type: Sequelize.STRING,
allowNull: false
},
contactPhone: {
type: Sequelize.STRING,
allowNull: false
},
tier: {
type: Sequelize.ENUM('bronze', 'silver', 'gold', 'platinum', 'diamond'),
allowNull: false,
defaultValue: 'bronze'
},
commissionRate: {
type: Sequelize.DECIMAL(5, 2),
allowNull: false,
defaultValue: 10.00
},
status: {
type: Sequelize.ENUM('pending_approval', 'active', 'suspended', 'inactive', 'rejected'),
allowNull: false,
defaultValue: 'pending_approval'
},
kycStatus: {
type: Sequelize.ENUM('pending', 'submitted', 'under_review', 'approved', 'rejected'),
allowNull: false,
defaultValue: 'pending'
},
kycDocuments: {
type: Sequelize.JSON,
defaultValue: []
},
kycNotes: {
type: Sequelize.TEXT,
allowNull: true
},
approvedBy: {
type: Sequelize.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
approvedAt: {
type: Sequelize.DATE,
allowNull: true
},
rejectedAt: {
type: Sequelize.DATE,
allowNull: true
},
rejectionReason: {
type: Sequelize.TEXT,
allowNull: true
},
billingAddress: {
type: Sequelize.JSON,
defaultValue: {}
},
bankDetails: {
type: Sequelize.JSON,
defaultValue: {}
},
metadata: {
type: Sequelize.JSON,
defaultValue: {}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('resellers', ['user_id']);
await queryInterface.addIndex('resellers', ['tier']);
await queryInterface.addIndex('resellers', ['status']);
await queryInterface.addIndex('resellers', ['kyc_status']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('resellers');
}
};

View File

@ -0,0 +1,131 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('products', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
name: {
type: Sequelize.STRING,
allowNull: false
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
category: {
type: Sequelize.ENUM('compute', 'storage', 'networking', 'database', 'security', 'analytics', 'ai_ml', 'other'),
allowNull: false
},
subcategory: {
type: Sequelize.STRING,
allowNull: true
},
sku: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
basePrice: {
type: Sequelize.DECIMAL(10, 2),
allowNull: false
},
currency: {
type: Sequelize.STRING,
allowNull: false,
defaultValue: 'INR'
},
billingType: {
type: Sequelize.ENUM('one_time', 'recurring', 'usage_based', 'tiered'),
allowNull: false
},
billingCycle: {
type: Sequelize.ENUM('hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'),
allowNull: true
},
unit: {
type: Sequelize.STRING,
allowNull: true
},
specifications: {
type: Sequelize.JSON,
defaultValue: {}
},
features: {
type: Sequelize.JSON,
defaultValue: []
},
tierPricing: {
type: Sequelize.JSON,
defaultValue: {
bronze: { margin: 20 },
silver: { margin: 25 },
gold: { margin: 30 },
platinum: { margin: 35 },
diamond: { margin: 40 }
}
},
status: {
type: Sequelize.ENUM('active', 'inactive', 'deprecated', 'coming_soon'),
allowNull: false,
defaultValue: 'active'
},
availability: {
type: Sequelize.JSON,
defaultValue: {
regions: [],
zones: []
}
},
minimumCommitment: {
type: Sequelize.JSON,
allowNull: true
},
tags: {
type: Sequelize.JSON,
defaultValue: []
},
metadata: {
type: Sequelize.JSON,
defaultValue: {}
},
createdBy: {
type: Sequelize.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
updatedBy: {
type: Sequelize.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('products', ['sku'], { unique: true });
await queryInterface.addIndex('products', ['category']);
await queryInterface.addIndex('products', ['status']);
await queryInterface.addIndex('products', ['billing_type']);
await queryInterface.addIndex('products', ['name']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('products');
}
};

View File

@ -0,0 +1,103 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('customers', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
resellerId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
companyName: {
type: Sequelize.STRING,
allowNull: false
},
contactPerson: {
type: Sequelize.STRING,
allowNull: false
},
email: {
type: Sequelize.STRING,
allowNull: false
},
phone: {
type: Sequelize.STRING,
allowNull: false
},
address: {
type: Sequelize.JSON,
defaultValue: {}
},
billingAddress: {
type: Sequelize.JSON,
defaultValue: {}
},
taxId: {
type: Sequelize.STRING,
allowNull: true
},
businessType: {
type: Sequelize.STRING,
allowNull: true
},
industry: {
type: Sequelize.STRING,
allowNull: true
},
website: {
type: Sequelize.STRING,
allowNull: true
},
status: {
type: Sequelize.ENUM('active', 'inactive', 'suspended', 'pending'),
allowNull: false,
defaultValue: 'active'
},
paymentTerms: {
type: Sequelize.ENUM('prepaid', 'postpaid', 'credit'),
allowNull: false,
defaultValue: 'prepaid'
},
creditLimit: {
type: Sequelize.DECIMAL(12, 2),
allowNull: true,
defaultValue: 0
},
notes: {
type: Sequelize.TEXT,
allowNull: true
},
metadata: {
type: Sequelize.JSON,
defaultValue: {}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('customers', ['reseller_id']);
await queryInterface.addIndex('customers', ['email']);
await queryInterface.addIndex('customers', ['status']);
await queryInterface.addIndex('customers', ['payment_terms']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('customers');
}
};

View File

@ -0,0 +1,85 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('wallets', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
customerId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'customers',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
balance: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
currency: {
type: Sequelize.STRING,
allowNull: false,
defaultValue: 'INR'
},
type: {
type: Sequelize.ENUM('prepaid', 'postpaid', 'credit'),
allowNull: false,
defaultValue: 'prepaid'
},
creditLimit: {
type: Sequelize.DECIMAL(12, 2),
allowNull: true,
defaultValue: 0.00
},
lowBalanceThreshold: {
type: Sequelize.DECIMAL(12, 2),
allowNull: true,
defaultValue: 100.00
},
autoRecharge: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
autoRechargeAmount: {
type: Sequelize.DECIMAL(12, 2),
allowNull: true
},
status: {
type: Sequelize.ENUM('active', 'suspended', 'frozen'),
allowNull: false,
defaultValue: 'active'
},
lastTransaction: {
type: Sequelize.DATE,
allowNull: true
},
metadata: {
type: Sequelize.JSON,
defaultValue: {}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('wallets', ['customer_id']);
await queryInterface.addIndex('wallets', ['type']);
await queryInterface.addIndex('wallets', ['status']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('wallets');
}
};

View File

@ -0,0 +1,124 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('invoices', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
invoiceNumber: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
resellerId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
}
},
customerId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'customers',
key: 'id'
}
},
orderId: {
type: Sequelize.UUID,
allowNull: true,
references: {
model: 'orders',
key: 'id'
}
},
issueDate: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW
},
dueDate: {
type: Sequelize.DATE,
allowNull: false
},
subtotal: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
taxAmount: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
discountAmount: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
totalAmount: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
currency: {
type: Sequelize.STRING,
allowNull: false,
defaultValue: 'INR'
},
status: {
type: Sequelize.ENUM('draft', 'sent', 'paid', 'overdue', 'cancelled', 'refunded'),
allowNull: false,
defaultValue: 'draft'
},
paidAt: {
type: Sequelize.DATE,
allowNull: true
},
paymentMethod: {
type: Sequelize.STRING,
allowNull: true
},
paymentReference: {
type: Sequelize.STRING,
allowNull: true
},
notes: {
type: Sequelize.TEXT,
allowNull: true
},
terms: {
type: Sequelize.TEXT,
allowNull: true
},
metadata: {
type: Sequelize.JSON,
defaultValue: {}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('invoices', ['invoice_number'], { unique: true });
await queryInterface.addIndex('invoices', ['reseller_id']);
await queryInterface.addIndex('invoices', ['customer_id']);
await queryInterface.addIndex('invoices', ['status']);
await queryInterface.addIndex('invoices', ['issue_date']);
await queryInterface.addIndex('invoices', ['due_date']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('invoices');
}
};

View File

@ -0,0 +1,95 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('orders', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true
},
orderNumber: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
resellerId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
}
},
customerId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'customers',
key: 'id'
}
},
status: {
type: Sequelize.ENUM('draft', 'pending', 'confirmed', 'processing', 'completed', 'cancelled', 'refunded'),
allowNull: false,
defaultValue: 'draft'
},
type: {
type: Sequelize.ENUM('new_service', 'upgrade', 'downgrade', 'renewal', 'addon'),
allowNull: false,
defaultValue: 'new_service'
},
subtotal: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
taxAmount: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
discountAmount: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
totalAmount: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0.00
},
currency: {
type: Sequelize.STRING,
allowNull: false,
defaultValue: 'INR'
},
notes: {
type: Sequelize.TEXT,
allowNull: true
},
metadata: {
type: Sequelize.JSON,
defaultValue: {}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
await queryInterface.addIndex('orders', ['order_number'], { unique: true });
await queryInterface.addIndex('orders', ['reseller_id']);
await queryInterface.addIndex('orders', ['customer_id']);
await queryInterface.addIndex('orders', ['status']);
await queryInterface.addIndex('orders', ['type']);
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('orders');
}
};

View File

@ -0,0 +1,90 @@
module.exports = (sequelize, DataTypes) => {
const AssetDownload = sequelize.define('AssetDownload', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
assetId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'marketing_assets',
key: 'id'
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
resellerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'resellers',
key: 'id'
}
},
downloadedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true
},
userAgent: {
type: DataTypes.TEXT,
allowNull: true
},
purpose: {
type: DataTypes.STRING,
allowNull: true // e.g., 'client_presentation', 'proposal', 'marketing_campaign'
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'asset_downloads',
indexes: [
{
fields: ['asset_id']
},
{
fields: ['user_id']
},
{
fields: ['reseller_id']
},
{
fields: ['downloaded_at']
}
]
});
// Class methods
AssetDownload.associate = function(models) {
AssetDownload.belongsTo(models.MarketingAsset, {
foreignKey: 'assetId',
as: 'asset'
});
AssetDownload.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
AssetDownload.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
};
return AssetDownload;
};

78
src/models/AuditLog.js Normal file
View File

@ -0,0 +1,78 @@
module.exports = (sequelize, DataTypes) => {
const AuditLog = sequelize.define('AuditLog', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
action: {
type: DataTypes.STRING,
allowNull: false
},
resource: {
type: DataTypes.STRING,
allowNull: false
},
resourceId: {
type: DataTypes.STRING,
allowNull: true
},
oldValues: {
type: DataTypes.JSON,
allowNull: true
},
newValues: {
type: DataTypes.JSON,
allowNull: true
},
ipAddress: {
type: DataTypes.INET,
allowNull: true
},
userAgent: {
type: DataTypes.TEXT,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'audit_logs',
indexes: [
{
fields: ['user_id']
},
{
fields: ['action']
},
{
fields: ['resource']
},
{
fields: ['resource_id']
},
{
fields: ['created_at']
}
]
});
// Class methods
AuditLog.associate = function(models) {
AuditLog.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return AuditLog;
};

194
src/models/Certificate.js Normal file
View File

@ -0,0 +1,194 @@
module.exports = (sequelize, DataTypes) => {
const Certificate = sequelize.define('Certificate', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
certificateNumber: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
courseId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'courses',
key: 'id'
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
resellerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'resellers',
key: 'id'
}
},
enrollmentId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'course_enrollments',
key: 'id'
}
},
issuedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
expiresAt: {
type: DataTypes.DATE,
allowNull: true
},
score: {
type: DataTypes.INTEGER,
allowNull: true
},
grade: {
type: DataTypes.ENUM('A+', 'A', 'B+', 'B', 'C+', 'C', 'Pass', 'Fail'),
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'expired', 'revoked', 'suspended'),
allowNull: false,
defaultValue: 'active'
},
certificatePath: {
type: DataTypes.STRING,
allowNull: true // Path to generated PDF certificate
},
badgeData: {
type: DataTypes.JSON,
defaultValue: {} // Badge/credential data for digital badges
},
verificationCode: {
type: DataTypes.STRING,
allowNull: false // Unique code for certificate verification
},
issuedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
revokedAt: {
type: DataTypes.DATE,
allowNull: true
},
revokedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
revocationReason: {
type: DataTypes.TEXT,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'certificates',
indexes: [
{
unique: true,
fields: ['certificate_number']
},
{
fields: ['course_id']
},
{
fields: ['user_id']
},
{
fields: ['reseller_id']
},
{
fields: ['status']
},
{
fields: ['issued_at']
},
{
fields: ['expires_at']
},
{
fields: ['verification_code']
}
]
});
// Instance methods
Certificate.prototype.isValid = function() {
if (this.status !== 'active') return false;
if (this.expiresAt && new Date() > this.expiresAt) return false;
return true;
};
Certificate.prototype.isExpired = function() {
return this.expiresAt && new Date() > this.expiresAt;
};
Certificate.prototype.getCertificateUrl = function() {
if (this.certificatePath) {
return `/uploads/certificates/${this.certificatePath}`;
}
return null;
};
Certificate.prototype.getVerificationUrl = function() {
return `/verify-certificate/${this.verificationCode}`;
};
// Class methods
Certificate.associate = function(models) {
Certificate.belongsTo(models.Course, {
foreignKey: 'courseId',
as: 'course'
});
Certificate.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
Certificate.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
Certificate.belongsTo(models.CourseEnrollment, {
foreignKey: 'enrollmentId',
as: 'enrollment'
});
Certificate.belongsTo(models.User, {
foreignKey: 'issuedBy',
as: 'issuer'
});
Certificate.belongsTo(models.User, {
foreignKey: 'revokedBy',
as: 'revoker'
});
};
return Certificate;
};

128
src/models/Commission.js Normal file
View File

@ -0,0 +1,128 @@
module.exports = (sequelize, DataTypes) => {
const Commission = sequelize.define('Commission', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resellerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
}
},
customerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'customers',
key: 'id'
}
},
orderId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'orders',
key: 'id'
}
},
invoiceId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'invoices',
key: 'id'
}
},
commissionType: {
type: DataTypes.ENUM('sale', 'recurring', 'bonus', 'penalty'),
allowNull: false,
defaultValue: 'sale'
},
baseAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
commissionRate: {
type: DataTypes.DECIMAL(5, 2),
allowNull: false
},
commissionAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
currency: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'INR'
},
status: {
type: DataTypes.ENUM('pending', 'approved', 'paid', 'disputed', 'cancelled'),
allowNull: false,
defaultValue: 'pending'
},
period: {
type: DataTypes.JSON,
allowNull: false
},
paidAt: {
type: DataTypes.DATE,
allowNull: true
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'commissions',
indexes: [
{
fields: ['reseller_id']
},
{
fields: ['customer_id']
},
{
fields: ['status']
},
{
fields: ['commission_type']
},
{
fields: ['created_at']
}
]
});
// Class methods
Commission.associate = function(models) {
Commission.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
Commission.belongsTo(models.Customer, {
foreignKey: 'customerId',
as: 'customer'
});
Commission.belongsTo(models.Order, {
foreignKey: 'orderId',
as: 'order'
});
Commission.belongsTo(models.Invoice, {
foreignKey: 'invoiceId',
as: 'invoice'
});
};
return Commission;
};

194
src/models/Course.js Normal file
View File

@ -0,0 +1,194 @@
module.exports = (sequelize, DataTypes) => {
const Course = sequelize.define('Course', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
category: {
type: DataTypes.ENUM('product_training', 'sales_training', 'technical_certification', 'compliance', 'onboarding', 'other'),
allowNull: false
},
level: {
type: DataTypes.ENUM('beginner', 'intermediate', 'advanced'),
allowNull: false,
defaultValue: 'beginner'
},
duration: {
type: DataTypes.INTEGER,
allowNull: true // Duration in minutes
},
estimatedHours: {
type: DataTypes.DECIMAL(4, 2),
allowNull: true
},
thumbnailPath: {
type: DataTypes.STRING,
allowNull: true
},
videoPath: {
type: DataTypes.STRING,
allowNull: true
},
materials: {
type: DataTypes.JSON,
defaultValue: [] // Array of material objects with type, title, path
},
prerequisites: {
type: DataTypes.JSON,
defaultValue: [] // Array of prerequisite course IDs
},
learningObjectives: {
type: DataTypes.JSON,
defaultValue: []
},
accessLevel: {
type: DataTypes.ENUM('public', 'reseller_only', 'tier_specific', 'admin_only'),
allowNull: false,
defaultValue: 'reseller_only'
},
tierAccess: {
type: DataTypes.JSON,
defaultValue: [] // Array of tiers that can access this course
},
isRequired: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
hasCertificate: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
certificateTemplate: {
type: DataTypes.STRING,
allowNull: true // Path to certificate template
},
passingScore: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 80 // Percentage
},
maxAttempts: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 3
},
validityPeriod: {
type: DataTypes.INTEGER,
allowNull: true // Validity in days
},
status: {
type: DataTypes.ENUM('draft', 'active', 'archived', 'maintenance'),
allowNull: false,
defaultValue: 'draft'
},
tags: {
type: DataTypes.JSON,
defaultValue: []
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
updatedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'courses',
indexes: [
{
fields: ['category']
},
{
fields: ['level']
},
{
fields: ['status']
},
{
fields: ['access_level']
},
{
fields: ['is_required']
},
{
fields: ['has_certificate']
}
]
});
// Instance methods
Course.prototype.isAccessibleBy = function(userTier) {
if (this.accessLevel === 'public') return true;
if (this.accessLevel === 'reseller_only') return true;
if (this.accessLevel === 'tier_specific') {
return this.tierAccess.includes(userTier);
}
return false;
};
Course.prototype.getThumbnailUrl = function() {
if (this.thumbnailPath) {
return `/uploads/courses/thumbnails/${this.thumbnailPath}`;
}
return null;
};
Course.prototype.getVideoUrl = function() {
if (this.videoPath) {
return `/uploads/courses/videos/${this.videoPath}`;
}
return null;
};
// Class methods
Course.associate = function(models) {
Course.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
Course.belongsTo(models.User, {
foreignKey: 'updatedBy',
as: 'updater'
});
Course.hasMany(models.CourseEnrollment, {
foreignKey: 'courseId',
as: 'enrollments'
});
Course.hasMany(models.CourseModule, {
foreignKey: 'courseId',
as: 'modules'
});
Course.hasMany(models.Certificate, {
foreignKey: 'courseId',
as: 'certificates'
});
};
return Course;
};

View File

@ -0,0 +1,173 @@
module.exports = (sequelize, DataTypes) => {
const CourseEnrollment = sequelize.define('CourseEnrollment', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
courseId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'courses',
key: 'id'
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
resellerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'resellers',
key: 'id'
}
},
enrolledAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
startedAt: {
type: DataTypes.DATE,
allowNull: true
},
completedAt: {
type: DataTypes.DATE,
allowNull: true
},
lastAccessedAt: {
type: DataTypes.DATE,
allowNull: true
},
status: {
type: DataTypes.ENUM('enrolled', 'in_progress', 'completed', 'failed', 'expired', 'cancelled'),
allowNull: false,
defaultValue: 'enrolled'
},
progress: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0, // Percentage 0-100
validate: {
min: 0,
max: 100
}
},
timeSpent: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0 // Time in minutes
},
attempts: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
bestScore: {
type: DataTypes.INTEGER,
allowNull: true // Best quiz/test score
},
currentScore: {
type: DataTypes.INTEGER,
allowNull: true // Current/latest score
},
moduleProgress: {
type: DataTypes.JSON,
defaultValue: {} // Object tracking progress per module
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
expiresAt: {
type: DataTypes.DATE,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'course_enrollments',
indexes: [
{
fields: ['course_id']
},
{
fields: ['user_id']
},
{
fields: ['reseller_id']
},
{
fields: ['status']
},
{
fields: ['enrolled_at']
},
{
fields: ['completed_at']
},
{
unique: true,
fields: ['course_id', 'user_id']
}
]
});
// Instance methods
CourseEnrollment.prototype.isCompleted = function() {
return this.status === 'completed';
};
CourseEnrollment.prototype.isExpired = function() {
return this.expiresAt && new Date() > this.expiresAt;
};
CourseEnrollment.prototype.canRetake = function(maxAttempts) {
return !maxAttempts || this.attempts < maxAttempts;
};
CourseEnrollment.prototype.updateProgress = function(moduleId, moduleProgress) {
const progress = { ...this.moduleProgress };
progress[moduleId] = moduleProgress;
// Calculate overall progress
const moduleKeys = Object.keys(progress);
const totalProgress = moduleKeys.reduce((sum, key) => sum + (progress[key] || 0), 0);
const overallProgress = moduleKeys.length > 0 ? Math.round(totalProgress / moduleKeys.length) : 0;
return this.update({
moduleProgress: progress,
progress: overallProgress,
lastAccessedAt: new Date()
});
};
// Class methods
CourseEnrollment.associate = function(models) {
CourseEnrollment.belongsTo(models.Course, {
foreignKey: 'courseId',
as: 'course'
});
CourseEnrollment.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
CourseEnrollment.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
};
return CourseEnrollment;
};

246
src/models/Customer.js Normal file
View File

@ -0,0 +1,246 @@
module.exports = (sequelize, DataTypes) => {
const Customer = sequelize.define('Customer', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resellerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
}
},
companyName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [2, 100]
}
},
contactPerson: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [2, 100]
}
},
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isEmail: true
}
},
phone: {
type: DataTypes.STRING,
allowNull: true,
validate: {
is: /^[\+]?[1-9][\d]{0,15}$/
}
},
address: {
type: DataTypes.JSON,
allowNull: false,
validate: {
hasRequiredFields(value) {
if (!value.street || !value.city || !value.state || !value.country || !value.zipCode) {
throw new Error('Address must include street, city, state, country, and zipCode');
}
}
}
},
industry: {
type: DataTypes.STRING,
allowNull: true
},
companySize: {
type: DataTypes.ENUM('startup', 'small', 'medium', 'large', 'enterprise'),
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_setup'),
allowNull: false,
defaultValue: 'pending_setup'
},
billingAddress: {
type: DataTypes.JSON,
allowNull: true
},
paymentMethod: {
type: DataTypes.ENUM('credit_card', 'bank_transfer', 'invoice', 'prepaid'),
allowNull: false,
defaultValue: 'invoice'
},
creditLimit: {
type: DataTypes.DECIMAL(12, 2),
allowNull: true,
defaultValue: 0
},
currentBalance: {
type: DataTypes.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0
},
currency: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'INR'
},
taxId: {
type: DataTypes.STRING,
allowNull: true
},
gstNumber: {
type: DataTypes.STRING,
allowNull: true,
validate: {
is: /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/
}
},
contractDetails: {
type: DataTypes.JSON,
defaultValue: {}
},
tags: {
type: DataTypes.JSON,
defaultValue: []
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
onboardingStatus: {
type: DataTypes.ENUM('not_started', 'in_progress', 'completed'),
allowNull: false,
defaultValue: 'not_started'
},
lastActivity: {
type: DataTypes.DATE,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'customers',
indexes: [
{
fields: ['reseller_id']
},
{
fields: ['email']
},
{
fields: ['status']
},
{
fields: ['company_name']
},
{
fields: ['onboarding_status']
},
{
fields: ['last_activity']
}
]
});
// Instance methods
Customer.prototype.updateBalance = async function(amount, operation = 'add') {
const currentBalance = parseFloat(this.currentBalance) || 0;
const changeAmount = parseFloat(amount);
let newBalance;
if (operation === 'add') {
newBalance = currentBalance + changeAmount;
} else if (operation === 'subtract') {
newBalance = currentBalance - changeAmount;
} else {
newBalance = changeAmount;
}
return this.update({
currentBalance: newBalance,
lastActivity: new Date()
});
};
Customer.prototype.isWithinCreditLimit = function(amount) {
const totalOwed = parseFloat(this.currentBalance) + parseFloat(amount);
return totalOwed <= parseFloat(this.creditLimit);
};
Customer.prototype.getOutstandingBalance = function() {
return Math.max(0, parseFloat(this.currentBalance));
};
Customer.prototype.isActive = function() {
return this.status === 'active';
};
Customer.prototype.updateActivity = async function() {
return this.update({ lastActivity: new Date() });
};
Customer.prototype.addTag = async function(tag) {
const currentTags = this.tags || [];
if (!currentTags.includes(tag)) {
currentTags.push(tag);
return this.update({ tags: currentTags });
}
return this;
};
Customer.prototype.removeTag = async function(tag) {
const currentTags = this.tags || [];
const updatedTags = currentTags.filter(t => t !== tag);
return this.update({ tags: updatedTags });
};
// Class methods
Customer.associate = function(models) {
Customer.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
Customer.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
Customer.hasMany(models.Order, {
foreignKey: 'customerId',
as: 'orders'
});
Customer.hasMany(models.CustomerService, {
foreignKey: 'customerId',
as: 'services'
});
Customer.hasMany(models.Invoice, {
foreignKey: 'customerId',
as: 'invoices'
});
Customer.hasMany(models.UsageRecord, {
foreignKey: 'customerId',
as: 'usageRecords'
});
};
return Customer;
};

View File

@ -0,0 +1,265 @@
module.exports = (sequelize, DataTypes) => {
const CustomerService = sequelize.define('CustomerService', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
customerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'customers',
key: 'id'
}
},
productId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'products',
key: 'id'
}
},
serviceInstanceId: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
serviceName: {
type: DataTypes.STRING,
allowNull: false
},
status: {
type: DataTypes.ENUM('provisioning', 'active', 'suspended', 'terminated', 'error'),
allowNull: false,
defaultValue: 'provisioning'
},
configuration: {
type: DataTypes.JSON,
defaultValue: {}
},
specifications: {
type: DataTypes.JSON,
defaultValue: {}
},
region: {
type: DataTypes.STRING,
allowNull: true
},
zone: {
type: DataTypes.STRING,
allowNull: true
},
pricing: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: {
basePrice: 0,
finalPrice: 0,
currency: 'INR'
}
},
billingCycle: {
type: DataTypes.ENUM('hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'),
allowNull: false,
defaultValue: 'monthly'
},
nextBillingDate: {
type: DataTypes.DATE,
allowNull: true
},
lastBilledDate: {
type: DataTypes.DATE,
allowNull: true
},
activatedAt: {
type: DataTypes.DATE,
allowNull: true
},
suspendedAt: {
type: DataTypes.DATE,
allowNull: true
},
terminatedAt: {
type: DataTypes.DATE,
allowNull: true
},
autoRenewal: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
contractPeriod: {
type: DataTypes.INTEGER,
allowNull: true // in months
},
contractEndDate: {
type: DataTypes.DATE,
allowNull: true
},
usageMetrics: {
type: DataTypes.JSON,
defaultValue: {}
},
limits: {
type: DataTypes.JSON,
defaultValue: {}
},
alerts: {
type: DataTypes.JSON,
defaultValue: []
},
tags: {
type: DataTypes.JSON,
defaultValue: []
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'customer_services',
indexes: [
{
unique: true,
fields: ['service_instance_id']
},
{
fields: ['customer_id']
},
{
fields: ['product_id']
},
{
fields: ['status']
},
{
fields: ['region']
},
{
fields: ['next_billing_date']
},
{
fields: ['activated_at']
}
]
});
// Instance methods
CustomerService.prototype.activate = async function() {
return this.update({
status: 'active',
activatedAt: new Date()
});
};
CustomerService.prototype.suspend = async function(reason = null) {
const updates = {
status: 'suspended',
suspendedAt: new Date()
};
if (reason) {
updates.notes = (this.notes || '') + `\nSuspended: ${reason}`;
}
return this.update(updates);
};
CustomerService.prototype.terminate = async function(reason = null) {
const updates = {
status: 'terminated',
terminatedAt: new Date()
};
if (reason) {
updates.notes = (this.notes || '') + `\nTerminated: ${reason}`;
}
return this.update(updates);
};
CustomerService.prototype.updateUsage = async function(metrics) {
const currentMetrics = this.usageMetrics || {};
const updatedMetrics = { ...currentMetrics, ...metrics };
return this.update({
usageMetrics: updatedMetrics,
updatedAt: new Date()
});
};
CustomerService.prototype.isActive = function() {
return this.status === 'active';
};
CustomerService.prototype.isExpired = function() {
return this.contractEndDate && new Date() > new Date(this.contractEndDate);
};
CustomerService.prototype.getDaysUntilExpiry = function() {
if (!this.contractEndDate) return null;
const now = new Date();
const expiry = new Date(this.contractEndDate);
const diffTime = expiry - now;
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
};
CustomerService.prototype.calculateNextBillingDate = function() {
const lastBilled = this.lastBilledDate || this.activatedAt || new Date();
const nextBilling = new Date(lastBilled);
switch (this.billingCycle) {
case 'hourly':
nextBilling.setHours(nextBilling.getHours() + 1);
break;
case 'daily':
nextBilling.setDate(nextBilling.getDate() + 1);
break;
case 'weekly':
nextBilling.setDate(nextBilling.getDate() + 7);
break;
case 'monthly':
nextBilling.setMonth(nextBilling.getMonth() + 1);
break;
case 'quarterly':
nextBilling.setMonth(nextBilling.getMonth() + 3);
break;
case 'yearly':
nextBilling.setFullYear(nextBilling.getFullYear() + 1);
break;
}
return nextBilling;
};
// Class methods
CustomerService.associate = function(models) {
CustomerService.belongsTo(models.Customer, {
foreignKey: 'customerId',
as: 'customer'
});
CustomerService.belongsTo(models.Product, {
foreignKey: 'productId',
as: 'product'
});
CustomerService.hasMany(models.UsageRecord, {
foreignKey: 'serviceId',
as: 'usageRecords'
});
CustomerService.hasMany(models.ServiceAlert, {
foreignKey: 'serviceId',
as: 'serviceAlerts'
});
};
return CustomerService;
};

198
src/models/Instance.js Normal file
View File

@ -0,0 +1,198 @@
module.exports = (sequelize, DataTypes) => {
const Instance = sequelize.define('Instance', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
customerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'customers',
key: 'id'
}
},
resellerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
}
},
name: {
type: DataTypes.STRING,
allowNull: false
},
instanceId: {
type: DataTypes.STRING,
allowNull: true, // External provider instance ID
unique: true
},
type: {
type: DataTypes.ENUM('compute', 'storage', 'network', 'database'),
allowNull: false,
defaultValue: 'compute'
},
template: {
type: DataTypes.STRING,
allowNull: true // OS template or image
},
size: {
type: DataTypes.STRING,
allowNull: false // e.g., 't2.micro', 'small', 'medium'
},
region: {
type: DataTypes.STRING,
allowNull: false
},
zone: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.ENUM('pending', 'creating', 'running', 'stopped', 'stopping', 'starting', 'failed', 'terminated'),
allowNull: false,
defaultValue: 'pending'
},
publicIp: {
type: DataTypes.STRING,
allowNull: true
},
privateIp: {
type: DataTypes.STRING,
allowNull: true
},
specifications: {
type: DataTypes.JSON,
defaultValue: {
cpu: 1,
memory: 1024,
storage: 20,
bandwidth: 100
}
},
configuration: {
type: DataTypes.JSON,
defaultValue: {}
},
credentials: {
type: DataTypes.JSON,
defaultValue: {} // Encrypted credentials
},
monitoring: {
type: DataTypes.JSON,
defaultValue: {
enabled: true,
alerts: []
}
},
backups: {
type: DataTypes.JSON,
defaultValue: {
enabled: false,
schedule: null,
retention: 7
}
},
tags: {
type: DataTypes.JSON,
defaultValue: []
},
provisionedAt: {
type: DataTypes.DATE,
allowNull: true
},
lastStarted: {
type: DataTypes.DATE,
allowNull: true
},
lastStopped: {
type: DataTypes.DATE,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'instances',
indexes: [
{
fields: ['customer_id']
},
{
fields: ['reseller_id']
},
{
fields: ['status']
},
{
fields: ['type']
},
{
fields: ['region']
},
{
unique: true,
fields: ['instance_id'],
where: {
instance_id: {
[sequelize.Sequelize.Op.ne]: null
}
}
}
]
});
// Instance methods
Instance.prototype.isRunning = function() {
return this.status === 'running';
};
Instance.prototype.canStart = function() {
return ['stopped', 'failed'].includes(this.status);
};
Instance.prototype.canStop = function() {
return ['running'].includes(this.status);
};
Instance.prototype.canTerminate = function() {
return !['terminated', 'creating'].includes(this.status);
};
// Class methods
Instance.associate = function(models) {
Instance.belongsTo(models.Customer, {
foreignKey: 'customerId',
as: 'customer'
});
Instance.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
Instance.hasMany(models.InstanceSnapshot, {
foreignKey: 'instanceId',
as: 'snapshots'
});
Instance.hasMany(models.InstanceEvent, {
foreignKey: 'instanceId',
as: 'events'
});
Instance.hasMany(models.UsageRecord, {
foreignKey: 'resourceId',
as: 'usageRecords',
scope: {
resourceType: 'instance'
}
});
};
return Instance;
};

109
src/models/InstanceEvent.js Normal file
View File

@ -0,0 +1,109 @@
module.exports = (sequelize, DataTypes) => {
const InstanceEvent = sequelize.define('InstanceEvent', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
instanceId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'instances',
key: 'id'
}
},
eventType: {
type: DataTypes.ENUM(
'created', 'started', 'stopped', 'restarted', 'terminated',
'snapshot_created', 'snapshot_deleted', 'backup_created',
'configuration_changed', 'status_changed', 'error', 'maintenance'
),
allowNull: false
},
status: {
type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'failed'),
allowNull: false,
defaultValue: 'pending'
},
message: {
type: DataTypes.TEXT,
allowNull: true
},
details: {
type: DataTypes.JSON,
defaultValue: {}
},
triggeredBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
source: {
type: DataTypes.ENUM('user', 'system', 'webhook', 'scheduler'),
allowNull: false,
defaultValue: 'user'
},
startedAt: {
type: DataTypes.DATE,
allowNull: true
},
completedAt: {
type: DataTypes.DATE,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'instance_events',
indexes: [
{
fields: ['instance_id']
},
{
fields: ['event_type']
},
{
fields: ['status']
},
{
fields: ['source']
},
{
fields: ['created_at']
}
]
});
// Instance methods
InstanceEvent.prototype.isCompleted = function() {
return ['completed', 'failed'].includes(this.status);
};
InstanceEvent.prototype.getDuration = function() {
if (this.startedAt && this.completedAt) {
return this.completedAt - this.startedAt;
}
return null;
};
// Class methods
InstanceEvent.associate = function(models) {
InstanceEvent.belongsTo(models.Instance, {
foreignKey: 'instanceId',
as: 'instance'
});
InstanceEvent.belongsTo(models.User, {
foreignKey: 'triggeredBy',
as: 'user'
});
};
return InstanceEvent;
};

View File

@ -0,0 +1,101 @@
module.exports = (sequelize, DataTypes) => {
const InstanceSnapshot = sequelize.define('InstanceSnapshot', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
instanceId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'instances',
key: 'id'
}
},
snapshotId: {
type: DataTypes.STRING,
allowNull: true, // External provider snapshot ID
unique: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
status: {
type: DataTypes.ENUM('creating', 'available', 'deleting', 'failed'),
allowNull: false,
defaultValue: 'creating'
},
size: {
type: DataTypes.BIGINT,
allowNull: true // Size in bytes
},
type: {
type: DataTypes.ENUM('manual', 'automatic', 'backup'),
allowNull: false,
defaultValue: 'manual'
},
retentionDays: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 30
},
expiresAt: {
type: DataTypes.DATE,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'instance_snapshots',
indexes: [
{
fields: ['instance_id']
},
{
fields: ['status']
},
{
fields: ['type']
},
{
fields: ['expires_at']
},
{
unique: true,
fields: ['snapshot_id'],
where: {
snapshot_id: {
[sequelize.Sequelize.Op.ne]: null
}
}
}
]
});
// Instance methods
InstanceSnapshot.prototype.isExpired = function() {
return this.expiresAt && new Date() > this.expiresAt;
};
InstanceSnapshot.prototype.canDelete = function() {
return ['available', 'failed'].includes(this.status);
};
// Class methods
InstanceSnapshot.associate = function(models) {
InstanceSnapshot.belongsTo(models.Instance, {
foreignKey: 'instanceId',
as: 'instance'
});
};
return InstanceSnapshot;
};

291
src/models/Invoice.js Normal file
View File

@ -0,0 +1,291 @@
module.exports = (sequelize, DataTypes) => {
const Invoice = sequelize.define('Invoice', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
invoiceNumber: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
resellerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
}
},
customerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'customers',
key: 'id'
}
},
invoiceType: {
type: DataTypes.ENUM('service', 'commission', 'adjustment', 'refund'),
allowNull: false,
defaultValue: 'service'
},
status: {
type: DataTypes.ENUM('draft', 'sent', 'paid', 'overdue', 'cancelled', 'refunded'),
allowNull: false,
defaultValue: 'draft'
},
billingPeriod: {
type: DataTypes.JSON,
allowNull: false,
validate: {
hasRequiredFields(value) {
if (!value.startDate || !value.endDate) {
throw new Error('Billing period must include startDate and endDate');
}
}
}
},
subtotal: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00
},
taxAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00
},
discountAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00
},
totalAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00
},
currency: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'INR'
},
taxDetails: {
type: DataTypes.JSON,
defaultValue: {
cgst: 0,
sgst: 0,
igst: 0,
totalTax: 0
}
},
billingAddress: {
type: DataTypes.JSON,
allowNull: false
},
shippingAddress: {
type: DataTypes.JSON,
allowNull: true
},
dueDate: {
type: DataTypes.DATE,
allowNull: false
},
paidAt: {
type: DataTypes.DATE,
allowNull: true
},
paidAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00
},
paymentMethod: {
type: DataTypes.STRING,
allowNull: true
},
paymentReference: {
type: DataTypes.STRING,
allowNull: true
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
terms: {
type: DataTypes.TEXT,
allowNull: true
},
filePath: {
type: DataTypes.STRING,
allowNull: true
},
downloadCount: {
type: DataTypes.INTEGER,
defaultValue: 0
},
lastDownloadAt: {
type: DataTypes.DATE,
allowNull: true
},
sentAt: {
type: DataTypes.DATE,
allowNull: true
},
remindersSent: {
type: DataTypes.INTEGER,
defaultValue: 0
},
lastReminderAt: {
type: DataTypes.DATE,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'invoices',
indexes: [
{
unique: true,
fields: ['invoice_number']
},
{
fields: ['reseller_id']
},
{
fields: ['customer_id']
},
{
fields: ['status']
},
{
fields: ['invoice_type']
},
{
fields: ['due_date']
},
{
fields: ['created_at']
}
]
});
// Instance methods
Invoice.prototype.calculateTotals = function() {
const subtotal = parseFloat(this.subtotal) || 0;
const taxAmount = parseFloat(this.taxAmount) || 0;
const discountAmount = parseFloat(this.discountAmount) || 0;
const total = subtotal + taxAmount - discountAmount;
return parseFloat(total.toFixed(2));
};
Invoice.prototype.markAsPaid = async function(amount, paymentMethod, paymentReference) {
const paidAmount = parseFloat(amount);
const totalAmount = parseFloat(this.totalAmount);
const updates = {
paidAmount: paidAmount,
paidAt: new Date(),
paymentMethod,
paymentReference
};
if (paidAmount >= totalAmount) {
updates.status = 'paid';
}
return this.update(updates);
};
Invoice.prototype.isOverdue = function() {
return this.status !== 'paid' && new Date() > new Date(this.dueDate);
};
Invoice.prototype.getDaysOverdue = function() {
if (!this.isOverdue()) return 0;
const now = new Date();
const dueDate = new Date(this.dueDate);
const diffTime = now - dueDate;
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
};
Invoice.prototype.getOutstandingAmount = function() {
const totalAmount = parseFloat(this.totalAmount) || 0;
const paidAmount = parseFloat(this.paidAmount) || 0;
return Math.max(0, totalAmount - paidAmount);
};
Invoice.prototype.generateInvoiceNumber = function() {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const timestamp = Date.now().toString().slice(-6);
return `INV-${year}${month}-${timestamp}`;
};
Invoice.prototype.markAsSent = async function() {
return this.update({
status: 'sent',
sentAt: new Date()
});
};
Invoice.prototype.addReminder = async function() {
return this.update({
remindersSent: this.remindersSent + 1,
lastReminderAt: new Date()
});
};
Invoice.prototype.incrementDownload = async function() {
return this.update({
downloadCount: this.downloadCount + 1,
lastDownloadAt: new Date()
});
};
// Class methods
Invoice.associate = function(models) {
Invoice.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
Invoice.belongsTo(models.Customer, {
foreignKey: 'customerId',
as: 'customer'
});
Invoice.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
Invoice.hasMany(models.InvoiceItem, {
foreignKey: 'invoiceId',
as: 'items'
});
Invoice.hasMany(models.WalletTransaction, {
foreignKey: 'reference.invoiceId',
as: 'transactions'
});
};
return Invoice;
};

112
src/models/InvoiceItem.js Normal file
View File

@ -0,0 +1,112 @@
module.exports = (sequelize, DataTypes) => {
const InvoiceItem = sequelize.define('InvoiceItem', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
invoiceId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'invoices',
key: 'id'
}
},
productId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'products',
key: 'id'
}
},
serviceId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'customer_services',
key: 'id'
}
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
quantity: {
type: DataTypes.DECIMAL(15, 4),
allowNull: false,
defaultValue: 1
},
unit: {
type: DataTypes.STRING,
allowNull: true
},
unitPrice: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
lineTotal: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
discountAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0
},
taxAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'invoice_items',
indexes: [
{
fields: ['invoice_id']
},
{
fields: ['product_id']
},
{
fields: ['service_id']
}
]
});
// Instance methods
InvoiceItem.prototype.calculateLineTotal = function() {
const quantity = parseFloat(this.quantity) || 0;
const unitPrice = parseFloat(this.unitPrice) || 0;
const discountAmount = parseFloat(this.discountAmount) || 0;
const taxAmount = parseFloat(this.taxAmount) || 0;
const subtotal = quantity * unitPrice;
return subtotal - discountAmount + taxAmount;
};
// Class methods
InvoiceItem.associate = function(models) {
InvoiceItem.belongsTo(models.Invoice, {
foreignKey: 'invoiceId',
as: 'invoice'
});
InvoiceItem.belongsTo(models.Product, {
foreignKey: 'productId',
as: 'product'
});
InvoiceItem.belongsTo(models.CustomerService, {
foreignKey: 'serviceId',
as: 'service'
});
};
return InvoiceItem;
};

View File

@ -0,0 +1,207 @@
module.exports = (sequelize, DataTypes) => {
const KnowledgeArticle = sequelize.define('KnowledgeArticle', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
slug: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
content: {
type: DataTypes.TEXT,
allowNull: false
},
excerpt: {
type: DataTypes.TEXT,
allowNull: true
},
type: {
type: DataTypes.ENUM('help_article', 'api_documentation', 'tutorial', 'faq', 'troubleshooting', 'best_practices'),
allowNull: false
},
category: {
type: DataTypes.STRING,
allowNull: false
},
subcategory: {
type: DataTypes.STRING,
allowNull: true
},
difficulty: {
type: DataTypes.ENUM('beginner', 'intermediate', 'advanced'),
allowNull: false,
defaultValue: 'beginner'
},
estimatedReadTime: {
type: DataTypes.INTEGER,
allowNull: true // Reading time in minutes
},
viewCount: {
type: DataTypes.INTEGER,
defaultValue: 0
},
helpfulCount: {
type: DataTypes.INTEGER,
defaultValue: 0
},
notHelpfulCount: {
type: DataTypes.INTEGER,
defaultValue: 0
},
status: {
type: DataTypes.ENUM('draft', 'published', 'archived', 'under_review'),
allowNull: false,
defaultValue: 'draft'
},
featured: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
accessLevel: {
type: DataTypes.ENUM('public', 'reseller_only', 'tier_specific', 'admin_only'),
allowNull: false,
defaultValue: 'public'
},
tierAccess: {
type: DataTypes.JSON,
defaultValue: []
},
tags: {
type: DataTypes.JSON,
defaultValue: []
},
relatedArticles: {
type: DataTypes.JSON,
defaultValue: [] // Array of related article IDs
},
attachments: {
type: DataTypes.JSON,
defaultValue: [] // Array of attachment objects
},
lastReviewedAt: {
type: DataTypes.DATE,
allowNull: true
},
publishedAt: {
type: DataTypes.DATE,
allowNull: true
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
updatedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
reviewedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'knowledge_articles',
indexes: [
{
unique: true,
fields: ['slug']
},
{
fields: ['type']
},
{
fields: ['category']
},
{
fields: ['status']
},
{
fields: ['access_level']
},
{
fields: ['featured']
},
{
fields: ['published_at']
},
{
fields: ['view_count']
}
]
});
// Instance methods
KnowledgeArticle.prototype.isAccessibleBy = function(userTier) {
if (this.accessLevel === 'public') return true;
if (this.accessLevel === 'reseller_only') return true;
if (this.accessLevel === 'tier_specific') {
return this.tierAccess.includes(userTier);
}
return false;
};
KnowledgeArticle.prototype.isPublished = function() {
return this.status === 'published' && this.publishedAt;
};
KnowledgeArticle.prototype.getReadingTime = function() {
if (this.estimatedReadTime) return this.estimatedReadTime;
// Calculate based on content length (average 200 words per minute)
const wordCount = this.content.split(/\s+/).length;
return Math.ceil(wordCount / 200);
};
KnowledgeArticle.prototype.getHelpfulnessRatio = function() {
const total = this.helpfulCount + this.notHelpfulCount;
if (total === 0) return 0;
return (this.helpfulCount / total) * 100;
};
// Class methods
KnowledgeArticle.associate = function(models) {
KnowledgeArticle.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'author'
});
KnowledgeArticle.belongsTo(models.User, {
foreignKey: 'updatedBy',
as: 'updater'
});
KnowledgeArticle.belongsTo(models.User, {
foreignKey: 'reviewedBy',
as: 'reviewer'
});
KnowledgeArticle.hasMany(models.ArticleFeedback, {
foreignKey: 'articleId',
as: 'feedback'
});
};
return KnowledgeArticle;
};

View File

@ -0,0 +1,162 @@
module.exports = (sequelize, DataTypes) => {
const LegalAcceptance = sequelize.define('LegalAcceptance', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
documentId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'legal_documents',
key: 'id'
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
resellerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'resellers',
key: 'id'
}
},
acceptanceMethod: {
type: DataTypes.ENUM('click_through', 'signature', 'upload', 'email', 'physical'),
allowNull: false
},
acceptedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true
},
userAgent: {
type: DataTypes.TEXT,
allowNull: true
},
signatureData: {
type: DataTypes.JSON,
allowNull: true // For digital signatures
},
uploadedFile: {
type: DataTypes.STRING,
allowNull: true // Path to uploaded compliance document
},
status: {
type: DataTypes.ENUM('accepted', 'pending_verification', 'verified', 'rejected', 'expired'),
allowNull: false,
defaultValue: 'accepted'
},
verifiedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
verifiedAt: {
type: DataTypes.DATE,
allowNull: true
},
rejectionReason: {
type: DataTypes.TEXT,
allowNull: true
},
expiresAt: {
type: DataTypes.DATE,
allowNull: true
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'legal_acceptances',
indexes: [
{
fields: ['document_id']
},
{
fields: ['user_id']
},
{
fields: ['reseller_id']
},
{
fields: ['status']
},
{
fields: ['accepted_at']
},
{
fields: ['expires_at']
},
{
unique: true,
fields: ['document_id', 'user_id', 'reseller_id']
}
]
});
// Instance methods
LegalAcceptance.prototype.isValid = function() {
return this.status === 'accepted' || this.status === 'verified';
};
LegalAcceptance.prototype.isExpired = function() {
return this.expiresAt && new Date() > this.expiresAt;
};
LegalAcceptance.prototype.requiresVerification = function() {
return this.status === 'pending_verification';
};
LegalAcceptance.prototype.getUploadUrl = function() {
if (this.uploadedFile) {
return `/uploads/legal/compliance/${this.uploadedFile}`;
}
return null;
};
// Class methods
LegalAcceptance.associate = function(models) {
LegalAcceptance.belongsTo(models.LegalDocument, {
foreignKey: 'documentId',
as: 'document'
});
LegalAcceptance.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
LegalAcceptance.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
LegalAcceptance.belongsTo(models.User, {
foreignKey: 'verifiedBy',
as: 'verifier'
});
};
return LegalAcceptance;
};

150
src/models/LegalDocument.js Normal file
View File

@ -0,0 +1,150 @@
module.exports = (sequelize, DataTypes) => {
const LegalDocument = sequelize.define('LegalDocument', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
type: {
type: DataTypes.ENUM('terms_of_service', 'privacy_policy', 'sla', 'compliance', 'agreement', 'other'),
allowNull: false
},
version: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: '1.0'
},
content: {
type: DataTypes.TEXT,
allowNull: true
},
filePath: {
type: DataTypes.STRING,
allowNull: true
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: true
},
mimeType: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.ENUM('draft', 'active', 'archived', 'superseded'),
allowNull: false,
defaultValue: 'draft'
},
effectiveDate: {
type: DataTypes.DATE,
allowNull: true
},
expiryDate: {
type: DataTypes.DATE,
allowNull: true
},
requiresAcceptance: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
acceptanceType: {
type: DataTypes.ENUM('click_through', 'signature', 'upload', 'none'),
allowNull: false,
defaultValue: 'none'
},
category: {
type: DataTypes.STRING,
allowNull: true
},
tags: {
type: DataTypes.JSON,
defaultValue: []
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
updatedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'legal_documents',
indexes: [
{
fields: ['type']
},
{
fields: ['status']
},
{
fields: ['effective_date']
},
{
fields: ['requires_acceptance']
},
{
unique: true,
fields: ['type', 'version'],
where: {
status: 'active'
}
}
]
});
// Instance methods
LegalDocument.prototype.isActive = function() {
const now = new Date();
return this.status === 'active' &&
(!this.effectiveDate || this.effectiveDate <= now) &&
(!this.expiryDate || this.expiryDate > now);
};
LegalDocument.prototype.isExpired = function() {
return this.expiryDate && new Date() > this.expiryDate;
};
LegalDocument.prototype.getFileUrl = function() {
if (this.filePath) {
return `/uploads/legal/${this.filePath}`;
}
return null;
};
// Class methods
LegalDocument.associate = function(models) {
LegalDocument.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
LegalDocument.belongsTo(models.User, {
foreignKey: 'updatedBy',
as: 'updater'
});
LegalDocument.hasMany(models.LegalAcceptance, {
foreignKey: 'documentId',
as: 'acceptances'
});
};
return LegalDocument;
};

View File

@ -0,0 +1,167 @@
module.exports = (sequelize, DataTypes) => {
const MarketingAsset = sequelize.define('MarketingAsset', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
type: {
type: DataTypes.ENUM('logo', 'brochure', 'presentation', 'video', 'image', 'document', 'template', 'other'),
allowNull: false
},
category: {
type: DataTypes.ENUM('sales_collateral', 'pitch_deck', 'email_template', 'brand_assets', 'product_sheets', 'case_studies', 'other'),
allowNull: false
},
filePath: {
type: DataTypes.STRING,
allowNull: false
},
fileName: {
type: DataTypes.STRING,
allowNull: false
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: false
},
mimeType: {
type: DataTypes.STRING,
allowNull: false
},
thumbnailPath: {
type: DataTypes.STRING,
allowNull: true
},
downloadCount: {
type: DataTypes.INTEGER,
defaultValue: 0
},
isEditable: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
editableFormat: {
type: DataTypes.STRING,
allowNull: true // e.g., 'pptx', 'docx', 'figma'
},
accessLevel: {
type: DataTypes.ENUM('public', 'reseller_only', 'tier_specific', 'admin_only'),
allowNull: false,
defaultValue: 'reseller_only'
},
tierAccess: {
type: DataTypes.JSON,
defaultValue: [] // Array of tiers that can access this asset
},
tags: {
type: DataTypes.JSON,
defaultValue: []
},
version: {
type: DataTypes.STRING,
defaultValue: '1.0'
},
status: {
type: DataTypes.ENUM('active', 'archived', 'draft'),
allowNull: false,
defaultValue: 'active'
},
expiryDate: {
type: DataTypes.DATE,
allowNull: true
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
updatedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'marketing_assets',
indexes: [
{
fields: ['type']
},
{
fields: ['category']
},
{
fields: ['status']
},
{
fields: ['access_level']
},
{
fields: ['created_at']
}
]
});
// Instance methods
MarketingAsset.prototype.isAccessibleBy = function(userTier) {
if (this.accessLevel === 'public') return true;
if (this.accessLevel === 'reseller_only') return true;
if (this.accessLevel === 'tier_specific') {
return this.tierAccess.includes(userTier);
}
return false;
};
MarketingAsset.prototype.isExpired = function() {
return this.expiryDate && new Date() > this.expiryDate;
};
MarketingAsset.prototype.getDownloadUrl = function() {
return `/uploads/marketing/${this.filePath}`;
};
MarketingAsset.prototype.getThumbnailUrl = function() {
if (this.thumbnailPath) {
return `/uploads/marketing/thumbnails/${this.thumbnailPath}`;
}
return null;
};
// Class methods
MarketingAsset.associate = function(models) {
MarketingAsset.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
MarketingAsset.belongsTo(models.User, {
foreignKey: 'updatedBy',
as: 'updater'
});
MarketingAsset.hasMany(models.AssetDownload, {
foreignKey: 'assetId',
as: 'downloads'
});
};
return MarketingAsset;
};

126
src/models/Order.js Normal file
View File

@ -0,0 +1,126 @@
module.exports = (sequelize, DataTypes) => {
const Order = sequelize.define('Order', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
orderNumber: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
resellerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
}
},
customerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'customers',
key: 'id'
}
},
status: {
type: DataTypes.ENUM('pending', 'confirmed', 'processing', 'completed', 'cancelled', 'failed'),
allowNull: false,
defaultValue: 'pending'
},
orderType: {
type: DataTypes.ENUM('new_service', 'upgrade', 'downgrade', 'renewal', 'addon'),
allowNull: false,
defaultValue: 'new_service'
},
subtotal: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0
},
taxAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0
},
discountAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0
},
totalAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0
},
currency: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'INR'
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'orders',
indexes: [
{
unique: true,
fields: ['order_number']
},
{
fields: ['reseller_id']
},
{
fields: ['customer_id']
},
{
fields: ['status']
},
{
fields: ['order_type']
}
]
});
// Class methods
Order.associate = function(models) {
Order.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
Order.belongsTo(models.Customer, {
foreignKey: 'customerId',
as: 'customer'
});
Order.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
Order.hasMany(models.OrderItem, {
foreignKey: 'orderId',
as: 'items'
});
};
return Order;
};

75
src/models/OrderItem.js Normal file
View File

@ -0,0 +1,75 @@
module.exports = (sequelize, DataTypes) => {
const OrderItem = sequelize.define('OrderItem', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
orderId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'orders',
key: 'id'
}
},
productId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'products',
key: 'id'
}
},
quantity: {
type: DataTypes.DECIMAL(15, 4),
allowNull: false,
defaultValue: 1
},
unitPrice: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
lineTotal: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
configuration: {
type: DataTypes.JSON,
defaultValue: {}
},
specifications: {
type: DataTypes.JSON,
defaultValue: {}
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'order_items',
indexes: [
{
fields: ['order_id']
},
{
fields: ['product_id']
}
]
});
// Class methods
OrderItem.associate = function(models) {
OrderItem.belongsTo(models.Order, {
foreignKey: 'orderId',
as: 'order'
});
OrderItem.belongsTo(models.Product, {
foreignKey: 'productId',
as: 'product'
});
};
return OrderItem;
};

188
src/models/Product.js Normal file
View File

@ -0,0 +1,188 @@
module.exports = (sequelize, DataTypes) => {
const Product = sequelize.define('Product', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [2, 100]
}
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
category: {
type: DataTypes.ENUM('compute', 'storage', 'networking', 'database', 'security', 'analytics', 'ai_ml', 'other'),
allowNull: false
},
subcategory: {
type: DataTypes.STRING,
allowNull: true
},
sku: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
basePrice: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
validate: {
min: 0
}
},
currency: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'INR'
},
billingType: {
type: DataTypes.ENUM('one_time', 'recurring', 'usage_based', 'tiered'),
allowNull: false
},
billingCycle: {
type: DataTypes.ENUM('hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'),
allowNull: true
},
unit: {
type: DataTypes.STRING,
allowNull: true // e.g., 'GB', 'vCPU', 'instance', 'request'
},
specifications: {
type: DataTypes.JSON,
defaultValue: {}
},
features: {
type: DataTypes.JSON,
defaultValue: []
},
tierPricing: {
type: DataTypes.JSON,
defaultValue: {
bronze: { margin: 20 },
silver: { margin: 25 },
gold: { margin: 30 },
platinum: { margin: 35 },
diamond: { margin: 40 }
}
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'deprecated', 'coming_soon'),
allowNull: false,
defaultValue: 'active'
},
availability: {
type: DataTypes.JSON,
defaultValue: {
regions: [],
zones: []
}
},
minimumCommitment: {
type: DataTypes.JSON,
allowNull: true
},
tags: {
type: DataTypes.JSON,
defaultValue: []
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
updatedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'products',
indexes: [
{
unique: true,
fields: ['sku']
},
{
fields: ['category']
},
{
fields: ['status']
},
{
fields: ['billing_type']
},
{
fields: ['name']
}
]
});
// Instance methods
Product.prototype.calculateResellerPrice = function(resellerTier, customMargin = null) {
let margin = customMargin;
if (!margin && this.tierPricing[resellerTier]) {
margin = this.tierPricing[resellerTier].margin;
}
if (!margin) {
margin = 20; // Default margin
}
const markupAmount = (this.basePrice * margin) / 100;
return parseFloat(this.basePrice) + markupAmount;
};
Product.prototype.getMarginForTier = function(tier) {
return this.tierPricing[tier]?.margin || 20;
};
Product.prototype.isAvailableInRegion = function(region) {
return this.availability.regions.length === 0 || this.availability.regions.includes(region);
};
Product.prototype.isActive = function() {
return this.status === 'active';
};
// Class methods
Product.associate = function(models) {
Product.belongsTo(models.User, {
foreignKey: 'createdBy',
as: 'creator'
});
Product.belongsTo(models.User, {
foreignKey: 'updatedBy',
as: 'updater'
});
Product.hasMany(models.ResellerPricing, {
foreignKey: 'productId',
as: 'resellerPricing'
});
Product.hasMany(models.OrderItem, {
foreignKey: 'productId',
as: 'orderItems'
});
};
return Product;
};

246
src/models/Reseller.js Normal file
View File

@ -0,0 +1,246 @@
module.exports = (sequelize, DataTypes) => {
const Reseller = sequelize.define('Reseller', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
companyName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [2, 100]
}
},
companyType: {
type: DataTypes.ENUM('individual', 'partnership', 'private_limited', 'public_limited', 'llp'),
allowNull: false
},
registrationNumber: {
type: DataTypes.STRING,
allowNull: true,
unique: true
},
gstNumber: {
type: DataTypes.STRING,
allowNull: true,
validate: {
is: /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/
}
},
panNumber: {
type: DataTypes.STRING,
allowNull: true,
validate: {
is: /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/
}
},
address: {
type: DataTypes.JSON,
allowNull: false,
validate: {
hasRequiredFields(value) {
if (!value.street || !value.city || !value.state || !value.country || !value.zipCode) {
throw new Error('Address must include street, city, state, country, and zipCode');
}
}
}
},
contactEmail: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isEmail: true
}
},
contactPhone: {
type: DataTypes.STRING,
allowNull: false,
validate: {
is: /^[\+]?[1-9][\d]{0,15}$/
}
},
website: {
type: DataTypes.STRING,
allowNull: true,
validate: {
isUrl: true
}
},
tier: {
type: DataTypes.ENUM('bronze', 'silver', 'gold', 'platinum', 'diamond'),
allowNull: false,
defaultValue: 'bronze'
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_approval', 'rejected'),
allowNull: false,
defaultValue: 'pending_approval'
},
commissionRate: {
type: DataTypes.DECIMAL(5, 2),
allowNull: false,
defaultValue: 10.00,
validate: {
min: 0,
max: 100
}
},
marginSettings: {
type: DataTypes.JSON,
defaultValue: {
defaultMargin: 20,
serviceMargins: {}
}
},
billingDetails: {
type: DataTypes.JSON,
defaultValue: {
billingCycle: 'monthly',
paymentTerms: 30,
currency: 'INR'
}
},
kycStatus: {
type: DataTypes.ENUM('pending', 'submitted', 'approved', 'rejected', 'expired'),
allowNull: false,
defaultValue: 'pending'
},
kycDocuments: {
type: DataTypes.JSON,
defaultValue: []
},
approvedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
approvedAt: {
type: DataTypes.DATE,
allowNull: true
},
rejectionReason: {
type: DataTypes.TEXT,
allowNull: true
},
contractStartDate: {
type: DataTypes.DATE,
allowNull: true
},
contractEndDate: {
type: DataTypes.DATE,
allowNull: true
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'resellers',
indexes: [
{
unique: true,
fields: ['registration_number']
},
{
unique: true,
fields: ['gst_number']
},
{
fields: ['tier']
},
{
fields: ['status']
},
{
fields: ['kyc_status']
},
{
fields: ['contact_email']
}
]
});
// Instance methods
Reseller.prototype.calculateCommission = function(amount, serviceType = null) {
let rate = this.commissionRate;
if (serviceType && this.marginSettings.serviceMargins[serviceType]) {
rate = this.marginSettings.serviceMargins[serviceType];
}
return (amount * rate) / 100;
};
Reseller.prototype.isActive = function() {
return this.status === 'active' && this.kycStatus === 'approved';
};
Reseller.prototype.canUpgradeTier = function() {
const tierOrder = ['bronze', 'silver', 'gold', 'platinum', 'diamond'];
const currentIndex = tierOrder.indexOf(this.tier);
return currentIndex < tierOrder.length - 1;
};
Reseller.prototype.getNextTier = function() {
const tierOrder = ['bronze', 'silver', 'gold', 'platinum', 'diamond'];
const currentIndex = tierOrder.indexOf(this.tier);
return currentIndex < tierOrder.length - 1 ? tierOrder[currentIndex + 1] : null;
};
Reseller.prototype.updateKycStatus = async function(status, documents = null, rejectionReason = null) {
const updates = { kycStatus: status };
if (documents) {
updates.kycDocuments = documents;
}
if (status === 'rejected' && rejectionReason) {
updates.rejectionReason = rejectionReason;
}
if (status === 'approved') {
updates.approvedAt = new Date();
updates.status = 'active';
}
return this.update(updates);
};
// Class methods
Reseller.associate = function(models) {
Reseller.hasMany(models.User, {
foreignKey: 'resellerId',
as: 'users'
});
Reseller.belongsTo(models.User, {
foreignKey: 'approvedBy',
as: 'approver'
});
Reseller.hasMany(models.Customer, {
foreignKey: 'resellerId',
as: 'customers'
});
Reseller.hasMany(models.Order, {
foreignKey: 'resellerId',
as: 'orders'
});
Reseller.hasMany(models.Commission, {
foreignKey: 'resellerId',
as: 'commissions'
});
};
return Reseller;
};

View File

@ -0,0 +1,176 @@
module.exports = (sequelize, DataTypes) => {
const ResellerPricing = sequelize.define('ResellerPricing', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resellerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'resellers',
key: 'id'
}
},
productId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'products',
key: 'id'
}
},
customMargin: {
type: DataTypes.DECIMAL(5, 2),
allowNull: true,
validate: {
min: 0,
max: 100
}
},
customPrice: {
type: DataTypes.DECIMAL(10, 2),
allowNull: true,
validate: {
min: 0
}
},
pricingType: {
type: DataTypes.ENUM('margin', 'fixed_price'),
allowNull: false,
defaultValue: 'margin'
},
minimumQuantity: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 1
},
maximumQuantity: {
type: DataTypes.INTEGER,
allowNull: true
},
volumeDiscounts: {
type: DataTypes.JSON,
defaultValue: []
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
effectiveFrom: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
effectiveTo: {
type: DataTypes.DATE,
allowNull: true
},
notes: {
type: DataTypes.TEXT,
allowNull: true
},
approvedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
approvedAt: {
type: DataTypes.DATE,
allowNull: true
}
}, {
tableName: 'reseller_pricing',
indexes: [
{
unique: true,
fields: ['reseller_id', 'product_id'],
where: {
is_active: true
}
},
{
fields: ['reseller_id']
},
{
fields: ['product_id']
},
{
fields: ['is_active']
},
{
fields: ['effective_from']
},
{
fields: ['effective_to']
}
]
});
// Instance methods
ResellerPricing.prototype.calculateFinalPrice = function(basePrice, quantity = 1) {
let finalPrice;
if (this.pricingType === 'fixed_price' && this.customPrice) {
finalPrice = this.customPrice;
} else {
const margin = this.customMargin || 20;
finalPrice = basePrice + (basePrice * margin / 100);
}
// Apply volume discounts
if (this.volumeDiscounts && this.volumeDiscounts.length > 0) {
const applicableDiscount = this.volumeDiscounts
.filter(discount => quantity >= discount.minQuantity)
.sort((a, b) => b.minQuantity - a.minQuantity)[0];
if (applicableDiscount) {
finalPrice = finalPrice * (1 - applicableDiscount.discountPercent / 100);
}
}
return parseFloat(finalPrice.toFixed(2));
};
ResellerPricing.prototype.isEffective = function(date = new Date()) {
const checkDate = new Date(date);
const from = new Date(this.effectiveFrom);
const to = this.effectiveTo ? new Date(this.effectiveTo) : null;
return checkDate >= from && (!to || checkDate <= to);
};
ResellerPricing.prototype.getVolumeDiscount = function(quantity) {
if (!this.volumeDiscounts || this.volumeDiscounts.length === 0) {
return null;
}
return this.volumeDiscounts
.filter(discount => quantity >= discount.minQuantity)
.sort((a, b) => b.minQuantity - a.minQuantity)[0] || null;
};
// Class methods
ResellerPricing.associate = function(models) {
ResellerPricing.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
ResellerPricing.belongsTo(models.Product, {
foreignKey: 'productId',
as: 'product'
});
ResellerPricing.belongsTo(models.User, {
foreignKey: 'approvedBy',
as: 'approver'
});
};
return ResellerPricing;
};

View File

@ -0,0 +1,88 @@
module.exports = (sequelize, DataTypes) => {
const ServiceAlert = sequelize.define('ServiceAlert', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
serviceId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'customer_services',
key: 'id'
}
},
alertType: {
type: DataTypes.ENUM('usage_threshold', 'billing_threshold', 'service_down', 'maintenance', 'expiry_warning'),
allowNull: false
},
severity: {
type: DataTypes.ENUM('low', 'medium', 'high', 'critical'),
allowNull: false,
defaultValue: 'medium'
},
title: {
type: DataTypes.STRING,
allowNull: false
},
message: {
type: DataTypes.TEXT,
allowNull: false
},
status: {
type: DataTypes.ENUM('active', 'acknowledged', 'resolved', 'dismissed'),
allowNull: false,
defaultValue: 'active'
},
threshold: {
type: DataTypes.JSON,
allowNull: true
},
currentValue: {
type: DataTypes.JSON,
allowNull: true
},
acknowledgedAt: {
type: DataTypes.DATE,
allowNull: true
},
resolvedAt: {
type: DataTypes.DATE,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'service_alerts',
indexes: [
{
fields: ['service_id']
},
{
fields: ['alert_type']
},
{
fields: ['severity']
},
{
fields: ['status']
},
{
fields: ['created_at']
}
]
});
// Class methods
ServiceAlert.associate = function(models) {
ServiceAlert.belongsTo(models.CustomerService, {
foreignKey: 'serviceId',
as: 'service'
});
};
return ServiceAlert;
};

179
src/models/UsageRecord.js Normal file
View File

@ -0,0 +1,179 @@
module.exports = (sequelize, DataTypes) => {
const UsageRecord = sequelize.define('UsageRecord', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
customerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'customers',
key: 'id'
}
},
serviceId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'customer_services',
key: 'id'
}
},
productId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'products',
key: 'id'
}
},
usageDate: {
type: DataTypes.DATEONLY,
allowNull: false
},
usageHour: {
type: DataTypes.INTEGER,
allowNull: true,
validate: {
min: 0,
max: 23
}
},
metricType: {
type: DataTypes.STRING,
allowNull: false // e.g., 'cpu_hours', 'storage_gb', 'bandwidth_gb', 'requests'
},
quantity: {
type: DataTypes.DECIMAL(15, 4),
allowNull: false,
defaultValue: 0
},
unit: {
type: DataTypes.STRING,
allowNull: false // e.g., 'hours', 'GB', 'requests', 'instances'
},
unitPrice: {
type: DataTypes.DECIMAL(10, 4),
allowNull: false,
defaultValue: 0
},
totalCost: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0
},
currency: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'INR'
},
region: {
type: DataTypes.STRING,
allowNull: true
},
zone: {
type: DataTypes.STRING,
allowNull: true
},
resourceId: {
type: DataTypes.STRING,
allowNull: true
},
tags: {
type: DataTypes.JSON,
defaultValue: {}
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
},
billingStatus: {
type: DataTypes.ENUM('pending', 'billed', 'disputed', 'refunded'),
allowNull: false,
defaultValue: 'pending'
},
billedAt: {
type: DataTypes.DATE,
allowNull: true
},
invoiceId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'invoices',
key: 'id'
}
}
}, {
tableName: 'usage_records',
indexes: [
{
fields: ['customer_id']
},
{
fields: ['service_id']
},
{
fields: ['product_id']
},
{
fields: ['usage_date']
},
{
fields: ['metric_type']
},
{
fields: ['billing_status']
},
{
fields: ['resource_id']
},
{
unique: true,
fields: ['service_id', 'usage_date', 'usage_hour', 'metric_type']
}
]
});
// Instance methods
UsageRecord.prototype.calculateCost = function() {
const quantity = parseFloat(this.quantity) || 0;
const unitPrice = parseFloat(this.unitPrice) || 0;
return parseFloat((quantity * unitPrice).toFixed(2));
};
UsageRecord.prototype.markAsBilled = async function(invoiceId) {
return this.update({
billingStatus: 'billed',
billedAt: new Date(),
invoiceId: invoiceId
});
};
// Class methods
UsageRecord.associate = function(models) {
UsageRecord.belongsTo(models.Customer, {
foreignKey: 'customerId',
as: 'customer'
});
UsageRecord.belongsTo(models.CustomerService, {
foreignKey: 'serviceId',
as: 'service'
});
UsageRecord.belongsTo(models.Product, {
foreignKey: 'productId',
as: 'product'
});
UsageRecord.belongsTo(models.Invoice, {
foreignKey: 'invoiceId',
as: 'invoice'
});
};
return UsageRecord;
};

260
src/models/User.js Normal file
View File

@ -0,0 +1,260 @@
const bcrypt = require('bcryptjs');
const speakeasy = require('speakeasy');
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [8, 128]
}
},
firstName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [2, 50]
}
},
lastName: {
type: DataTypes.STRING,
allowNull: false,
validate: {
len: [2, 50]
}
},
phone: {
type: DataTypes.STRING,
allowNull: true,
validate: {
is: /^[\+]?[1-9][\d]{0,15}$/
}
},
role: {
type: DataTypes.ENUM('reseller_admin', 'sales_agent', 'support_agent', 'read_only'),
allowNull: false,
defaultValue: 'read_only'
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'pending_verification'),
allowNull: false,
defaultValue: 'pending_verification'
},
emailVerified: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
emailVerificationToken: {
type: DataTypes.STRING,
allowNull: true
},
emailVerificationExpires: {
type: DataTypes.DATE,
allowNull: true
},
passwordResetToken: {
type: DataTypes.STRING,
allowNull: true
},
passwordResetExpires: {
type: DataTypes.DATE,
allowNull: true
},
mfaEnabled: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
mfaSecret: {
type: DataTypes.STRING,
allowNull: true
},
mfaBackupCodes: {
type: DataTypes.JSON,
allowNull: true
},
lastLogin: {
type: DataTypes.DATE,
allowNull: true
},
loginAttempts: {
type: DataTypes.INTEGER,
defaultValue: 0
},
lockUntil: {
type: DataTypes.DATE,
allowNull: true
},
avatar: {
type: DataTypes.STRING,
allowNull: true
},
timezone: {
type: DataTypes.STRING,
defaultValue: 'UTC'
},
language: {
type: DataTypes.STRING,
defaultValue: 'en'
},
preferences: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'users',
indexes: [
{
unique: true,
fields: ['email']
},
{
fields: ['role']
},
{
fields: ['status']
},
{
fields: ['email_verification_token']
},
{
fields: ['password_reset_token']
}
],
hooks: {
beforeCreate: async (user) => {
if (user.password) {
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS) || 12);
user.password = await bcrypt.hash(user.password, salt);
}
},
beforeUpdate: async (user) => {
if (user.changed('password')) {
const salt = await bcrypt.genSalt(parseInt(process.env.BCRYPT_ROUNDS) || 12);
user.password = await bcrypt.hash(user.password, salt);
}
}
}
});
// Instance methods
User.prototype.validatePassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
User.prototype.generateMfaSecret = function() {
const secret = speakeasy.generateSecret({
name: `Cloudtopiaa Reseller Portal (${this.email})`,
issuer: 'Cloudtopiaa'
});
this.mfaSecret = secret.base32;
return secret;
};
User.prototype.verifyMfaToken = function(token) {
if (!this.mfaSecret) return false;
return speakeasy.totp.verify({
secret: this.mfaSecret,
encoding: 'base32',
token: token,
window: 2
});
};
User.prototype.generateMfaBackupCodes = function() {
const codes = [];
for (let i = 0; i < 10; i++) {
codes.push(Math.random().toString(36).substring(2, 10).toUpperCase());
}
this.mfaBackupCodes = codes;
return codes;
};
User.prototype.verifyMfaBackupCode = function(code) {
if (!this.mfaBackupCodes || !Array.isArray(this.mfaBackupCodes)) {
return false;
}
const index = this.mfaBackupCodes.indexOf(code.toUpperCase());
if (index > -1) {
this.mfaBackupCodes.splice(index, 1);
return true;
}
return false;
};
User.prototype.isLocked = function() {
return !!(this.lockUntil && this.lockUntil > Date.now());
};
User.prototype.incrementLoginAttempts = async function() {
// If we have a previous lock that has expired, restart at 1
if (this.lockUntil && this.lockUntil < Date.now()) {
return this.update({
loginAttempts: 1,
lockUntil: null
});
}
const updates = { loginAttempts: this.loginAttempts + 1 };
// Lock account after 5 failed attempts for 2 hours
if (this.loginAttempts + 1 >= 5 && !this.isLocked()) {
updates.lockUntil = Date.now() + 2 * 60 * 60 * 1000; // 2 hours
}
return this.update(updates);
};
User.prototype.resetLoginAttempts = async function() {
return this.update({
loginAttempts: 0,
lockUntil: null,
lastLogin: new Date()
});
};
User.prototype.toJSON = function() {
const values = Object.assign({}, this.get());
delete values.password;
delete values.mfaSecret;
delete values.mfaBackupCodes;
delete values.emailVerificationToken;
delete values.passwordResetToken;
return values;
};
// Class methods
User.associate = function(models) {
User.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
User.hasMany(models.UserSession, {
foreignKey: 'userId',
as: 'sessions'
});
User.hasMany(models.AuditLog, {
foreignKey: 'userId',
as: 'auditLogs'
});
};
return User;
};

86
src/models/UserSession.js Normal file
View File

@ -0,0 +1,86 @@
module.exports = (sequelize, DataTypes) => {
const UserSession = sequelize.define('UserSession', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
refreshToken: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
deviceInfo: {
type: DataTypes.JSON,
defaultValue: {}
},
ipAddress: {
type: DataTypes.INET,
allowNull: true
},
userAgent: {
type: DataTypes.TEXT,
allowNull: true
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
expiresAt: {
type: DataTypes.DATE,
allowNull: false
},
lastUsedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'user_sessions',
indexes: [
{
unique: true,
fields: ['refresh_token']
},
{
fields: ['user_id']
},
{
fields: ['is_active']
},
{
fields: ['expires_at']
}
]
});
// Instance methods
UserSession.prototype.isExpired = function() {
return this.expiresAt < new Date();
};
UserSession.prototype.updateLastUsed = async function() {
return this.update({ lastUsedAt: new Date() });
};
UserSession.prototype.deactivate = async function() {
return this.update({ isActive: false });
};
// Class methods
UserSession.associate = function(models) {
UserSession.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return UserSession;
};

207
src/models/Wallet.js Normal file
View File

@ -0,0 +1,207 @@
module.exports = (sequelize, DataTypes) => {
const Wallet = sequelize.define('Wallet', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resellerId: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
references: {
model: 'resellers',
key: 'id'
}
},
balance: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00,
validate: {
min: 0
}
},
currency: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'INR'
},
walletType: {
type: DataTypes.ENUM('prepaid', 'postpaid', 'hybrid'),
allowNull: false,
defaultValue: 'postpaid'
},
creditLimit: {
type: DataTypes.DECIMAL(15, 2),
allowNull: true,
defaultValue: 0.00
},
availableCredit: {
type: DataTypes.DECIMAL(15, 2),
allowNull: true,
defaultValue: 0.00
},
reservedAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00
},
totalSpent: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00
},
totalAdded: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0.00
},
lowBalanceThreshold: {
type: DataTypes.DECIMAL(15, 2),
allowNull: true,
defaultValue: 1000.00
},
autoRecharge: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
autoRechargeAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: true
},
autoRechargeThreshold: {
type: DataTypes.DECIMAL(15, 2),
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'suspended', 'frozen'),
allowNull: false,
defaultValue: 'active'
},
lastTransactionAt: {
type: DataTypes.DATE,
allowNull: true
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'wallets',
indexes: [
{
unique: true,
fields: ['reseller_id']
},
{
fields: ['status']
},
{
fields: ['wallet_type']
},
{
fields: ['balance']
}
]
});
// Instance methods
Wallet.prototype.getAvailableBalance = function() {
const balance = parseFloat(this.balance) || 0;
const reserved = parseFloat(this.reservedAmount) || 0;
const available = balance - reserved;
if (this.walletType === 'postpaid' || this.walletType === 'hybrid') {
const creditLimit = parseFloat(this.creditLimit) || 0;
return available + creditLimit;
}
return Math.max(0, available);
};
Wallet.prototype.canDeduct = function(amount) {
const availableBalance = this.getAvailableBalance();
return availableBalance >= parseFloat(amount);
};
Wallet.prototype.addFunds = async function(amount, transactionId = null) {
const addAmount = parseFloat(amount);
const currentBalance = parseFloat(this.balance) || 0;
const currentTotalAdded = parseFloat(this.totalAdded) || 0;
return this.update({
balance: currentBalance + addAmount,
totalAdded: currentTotalAdded + addAmount,
lastTransactionAt: new Date()
});
};
Wallet.prototype.deductFunds = async function(amount, transactionId = null) {
const deductAmount = parseFloat(amount);
if (!this.canDeduct(deductAmount)) {
throw new Error('Insufficient funds');
}
const currentBalance = parseFloat(this.balance) || 0;
const currentTotalSpent = parseFloat(this.totalSpent) || 0;
return this.update({
balance: currentBalance - deductAmount,
totalSpent: currentTotalSpent + deductAmount,
lastTransactionAt: new Date()
});
};
Wallet.prototype.reserveFunds = async function(amount) {
const reserveAmount = parseFloat(amount);
if (!this.canDeduct(reserveAmount)) {
throw new Error('Insufficient funds to reserve');
}
const currentReserved = parseFloat(this.reservedAmount) || 0;
return this.update({
reservedAmount: currentReserved + reserveAmount
});
};
Wallet.prototype.releaseFunds = async function(amount) {
const releaseAmount = parseFloat(amount);
const currentReserved = parseFloat(this.reservedAmount) || 0;
return this.update({
reservedAmount: Math.max(0, currentReserved - releaseAmount)
});
};
Wallet.prototype.isLowBalance = function() {
const threshold = parseFloat(this.lowBalanceThreshold) || 0;
return parseFloat(this.balance) <= threshold;
};
Wallet.prototype.shouldAutoRecharge = function() {
if (!this.autoRecharge || !this.autoRechargeThreshold) {
return false;
}
return parseFloat(this.balance) <= parseFloat(this.autoRechargeThreshold);
};
// Class methods
Wallet.associate = function(models) {
Wallet.belongsTo(models.Reseller, {
foreignKey: 'resellerId',
as: 'reseller'
});
Wallet.hasMany(models.WalletTransaction, {
foreignKey: 'walletId',
as: 'transactions'
});
};
return Wallet;
};

View File

@ -0,0 +1,242 @@
module.exports = (sequelize, DataTypes) => {
const WalletTransaction = sequelize.define('WalletTransaction', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
walletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'wallets',
key: 'id'
}
},
transactionId: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
type: {
type: DataTypes.ENUM('credit', 'debit', 'reserve', 'release', 'refund', 'adjustment'),
allowNull: false
},
category: {
type: DataTypes.ENUM('payment', 'service_charge', 'commission', 'refund', 'adjustment', 'penalty', 'bonus'),
allowNull: false
},
amount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
validate: {
min: 0
}
},
currency: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'INR'
},
balanceBefore: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
balanceAfter: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
status: {
type: DataTypes.ENUM('pending', 'completed', 'failed', 'cancelled', 'reversed'),
allowNull: false,
defaultValue: 'pending'
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
reference: {
type: DataTypes.JSON,
defaultValue: {}
},
paymentGateway: {
type: DataTypes.STRING,
allowNull: true
},
gatewayTransactionId: {
type: DataTypes.STRING,
allowNull: true
},
gatewayResponse: {
type: DataTypes.JSON,
defaultValue: {}
},
processedAt: {
type: DataTypes.DATE,
allowNull: true
},
processedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
reversalTransactionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'wallet_transactions',
key: 'id'
}
},
parentTransactionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'wallet_transactions',
key: 'id'
}
},
taxAmount: {
type: DataTypes.DECIMAL(10, 2),
allowNull: true,
defaultValue: 0.00
},
taxDetails: {
type: DataTypes.JSON,
defaultValue: {}
},
metadata: {
type: DataTypes.JSON,
defaultValue: {}
}
}, {
tableName: 'wallet_transactions',
indexes: [
{
unique: true,
fields: ['transaction_id']
},
{
fields: ['wallet_id']
},
{
fields: ['type']
},
{
fields: ['status']
},
{
fields: ['category']
},
{
fields: ['payment_gateway']
},
{
fields: ['gateway_transaction_id']
},
{
fields: ['processed_at']
},
{
fields: ['created_at']
}
]
});
// Instance methods
WalletTransaction.prototype.markCompleted = async function() {
return this.update({
status: 'completed',
processedAt: new Date()
});
};
WalletTransaction.prototype.markFailed = async function(reason = null) {
const updates = {
status: 'failed',
processedAt: new Date()
};
if (reason) {
updates.metadata = {
...this.metadata,
failureReason: reason
};
}
return this.update(updates);
};
WalletTransaction.prototype.reverse = async function(reason = null) {
const { WalletTransaction } = require('../models');
// Create reversal transaction
const reversalTransaction = await WalletTransaction.create({
walletId: this.walletId,
transactionId: `REV_${this.transactionId}_${Date.now()}`,
type: this.type === 'credit' ? 'debit' : 'credit',
category: 'adjustment',
amount: this.amount,
currency: this.currency,
balanceBefore: this.balanceAfter,
balanceAfter: this.balanceBefore,
status: 'completed',
description: `Reversal of transaction ${this.transactionId}${reason ? `: ${reason}` : ''}`,
parentTransactionId: this.id,
processedAt: new Date()
});
// Update original transaction
await this.update({
status: 'reversed',
reversalTransactionId: reversalTransaction.id
});
return reversalTransaction;
};
WalletTransaction.prototype.isReversible = function() {
return this.status === 'completed' && !this.reversalTransactionId;
};
WalletTransaction.prototype.calculateTax = function(taxRate = 18) {
if (this.category === 'service_charge' || this.category === 'commission') {
const taxAmount = (parseFloat(this.amount) * taxRate) / 100;
return parseFloat(taxAmount.toFixed(2));
}
return 0;
};
// Class methods
WalletTransaction.associate = function(models) {
WalletTransaction.belongsTo(models.Wallet, {
foreignKey: 'walletId',
as: 'wallet'
});
WalletTransaction.belongsTo(models.User, {
foreignKey: 'processedBy',
as: 'processor'
});
WalletTransaction.belongsTo(WalletTransaction, {
foreignKey: 'reversalTransactionId',
as: 'reversalTransaction'
});
WalletTransaction.belongsTo(WalletTransaction, {
foreignKey: 'parentTransactionId',
as: 'parentTransaction'
});
WalletTransaction.hasOne(WalletTransaction, {
foreignKey: 'parentTransactionId',
as: 'childTransaction'
});
};
return WalletTransaction;
};

41
src/models/index.js Normal file
View File

@ -0,0 +1,41 @@
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/database.js')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js' &&
file.indexOf('.test.js') === -1
);
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

60
src/routes/admin.js Normal file
View File

@ -0,0 +1,60 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const adminController = require('../controllers/adminController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Product validation
const productValidation = [
body('name')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Product name must be 2-100 characters'),
body('category')
.isIn(['compute', 'storage', 'networking', 'database', 'security', 'analytics', 'ai_ml', 'other'])
.withMessage('Invalid product category'),
body('sku')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('SKU must be 2-50 characters'),
body('basePrice')
.isFloat({ min: 0 })
.withMessage('Base price must be a positive number'),
body('billingType')
.isIn(['one_time', 'recurring', 'usage_based', 'tiered'])
.withMessage('Invalid billing type'),
body('billingCycle')
.optional()
.isIn(['hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'])
.withMessage('Invalid billing cycle')
];
// All admin routes require admin role
router.use(authorize('reseller_admin'));
// Product management
router.post('/products', productValidation, handleValidationErrors, adminController.createProduct);
router.put('/products/:productId', productValidation, handleValidationErrors, adminController.updateProduct);
router.get('/products', adminController.getAllProducts);
// System statistics
router.get('/stats', adminController.getSystemStats);
// Recent activity
router.get('/activity', adminController.getRecentActivity);
module.exports = router;

112
src/routes/auth.js Normal file
View File

@ -0,0 +1,112 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const authController = require('../controllers/authController');
const { authenticateToken, validateRefreshToken } = require('../middleware/auth');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Registration validation
const registerValidation = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Valid email is required'),
body('password')
.isLength({ min: 8 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.withMessage('Password must be at least 8 characters with uppercase, lowercase, number and special character'),
body('firstName')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('First name must be 2-50 characters'),
body('lastName')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Last name must be 2-50 characters'),
body('phone')
.optional()
.matches(/^[\+]?[1-9][\d]{0,15}$/)
.withMessage('Invalid phone number format'),
body('role')
.optional()
.isIn(['reseller_admin', 'sales_agent', 'support_agent', 'read_only'])
.withMessage('Invalid role')
];
// Login validation
const loginValidation = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Valid email is required'),
body('password')
.notEmpty()
.withMessage('Password is required'),
body('mfaToken')
.optional()
.isLength({ min: 6, max: 6 })
.isNumeric()
.withMessage('MFA token must be 6 digits')
];
// Password reset validation
const forgotPasswordValidation = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Valid email is required')
];
const resetPasswordValidation = [
body('token')
.notEmpty()
.withMessage('Reset token is required'),
body('newPassword')
.isLength({ min: 8 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.withMessage('Password must be at least 8 characters with uppercase, lowercase, number and special character')
];
// Email verification validation
const verifyEmailValidation = [
body('token')
.notEmpty()
.withMessage('Verification token is required')
];
// MFA validation
const mfaTokenValidation = [
body('token')
.isLength({ min: 6, max: 6 })
.isNumeric()
.withMessage('MFA token must be 6 digits')
];
// Routes
router.post('/register', registerValidation, handleValidationErrors, authController.register);
router.post('/login', loginValidation, handleValidationErrors, authController.login);
router.post('/refresh-token', validateRefreshToken, authController.refreshToken);
router.post('/logout', authController.logout);
router.post('/verify-email', verifyEmailValidation, handleValidationErrors, authController.verifyEmail);
router.post('/forgot-password', forgotPasswordValidation, handleValidationErrors, authController.forgotPassword);
router.post('/reset-password', resetPasswordValidation, handleValidationErrors, authController.resetPassword);
// MFA routes (protected)
router.post('/setup-mfa', authenticateToken, authController.setupMFA);
router.post('/verify-mfa', authenticateToken, mfaTokenValidation, handleValidationErrors, authController.verifyMFA);
router.post('/disable-mfa', authenticateToken, mfaTokenValidation, handleValidationErrors, authController.disableMFA);
module.exports = router;

72
src/routes/billing.js Normal file
View File

@ -0,0 +1,72 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const billingController = require('../controllers/billingController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Add funds validation
const addFundsValidation = [
body('amount')
.isFloat({ min: 1 })
.withMessage('Amount must be greater than 0'),
body('paymentMethod')
.optional()
.isString()
.withMessage('Payment method must be a string'),
body('paymentReference')
.optional()
.isString()
.withMessage('Payment reference must be a string'),
body('description')
.optional()
.isString()
.withMessage('Description must be a string')
];
// Mark invoice paid validation
const markInvoicePaidValidation = [
body('amount')
.isFloat({ min: 0 })
.withMessage('Amount must be a positive number'),
body('paymentMethod')
.notEmpty()
.withMessage('Payment method is required'),
body('paymentReference')
.optional()
.isString()
.withMessage('Payment reference must be a string'),
body('notes')
.optional()
.isString()
.withMessage('Notes must be a string')
];
// Wallet routes
router.get('/wallet', billingController.getWallet);
router.post('/wallet/add-funds', addFundsValidation, handleValidationErrors, billingController.addFunds);
router.get('/wallet/transactions', billingController.getTransactions);
// Invoice routes
router.get('/invoices', billingController.getInvoices);
router.get('/invoices/:invoiceId', billingController.getInvoice);
router.put('/invoices/:invoiceId/paid', markInvoicePaidValidation, handleValidationErrors, billingController.markInvoicePaid);
router.get('/invoices/:invoiceId/download', billingController.downloadInvoice);
// Billing summary
router.get('/summary', billingController.getBillingSummary);
module.exports = router;

129
src/routes/customers.js Normal file
View File

@ -0,0 +1,129 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const customerController = require('../controllers/customerController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Customer validation
const customerValidation = [
body('companyName')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Company name must be 2-100 characters'),
body('contactPerson')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Contact person must be 2-100 characters'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Valid email is required'),
body('phone')
.optional()
.matches(/^[\+]?[1-9][\d]{0,15}$/)
.withMessage('Invalid phone number format'),
body('address')
.isObject()
.withMessage('Address must be an object'),
body('address.street')
.notEmpty()
.withMessage('Street address is required'),
body('address.city')
.notEmpty()
.withMessage('City is required'),
body('address.state')
.notEmpty()
.withMessage('State is required'),
body('address.country')
.notEmpty()
.withMessage('Country is required'),
body('address.zipCode')
.notEmpty()
.withMessage('Zip code is required'),
body('companySize')
.optional()
.isIn(['startup', 'small', 'medium', 'large', 'enterprise'])
.withMessage('Invalid company size'),
body('paymentMethod')
.optional()
.isIn(['credit_card', 'bank_transfer', 'invoice', 'prepaid'])
.withMessage('Invalid payment method'),
body('creditLimit')
.optional()
.isFloat({ min: 0 })
.withMessage('Credit limit must be a positive number'),
body('gstNumber')
.optional()
.matches(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/)
.withMessage('Invalid GST number format')
];
// Service assignment validation
const serviceAssignmentValidation = [
body('productId')
.isUUID()
.withMessage('Valid product ID is required'),
body('serviceName')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Service name must be 2-100 characters'),
body('configuration')
.optional()
.isObject()
.withMessage('Configuration must be an object'),
body('specifications')
.optional()
.isObject()
.withMessage('Specifications must be an object'),
body('pricing')
.isObject()
.withMessage('Pricing information is required'),
body('pricing.basePrice')
.isFloat({ min: 0 })
.withMessage('Base price must be a positive number'),
body('pricing.finalPrice')
.isFloat({ min: 0 })
.withMessage('Final price must be a positive number'),
body('billingCycle')
.optional()
.isIn(['hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'])
.withMessage('Invalid billing cycle'),
body('contractPeriod')
.optional()
.isInt({ min: 1 })
.withMessage('Contract period must be a positive integer (months)')
];
// Get all customers
router.get('/', customerController.getCustomers);
// Get customer details
router.get('/:customerId', customerController.getCustomer);
// Create new customer
router.post('/', customerValidation, handleValidationErrors, customerController.createCustomer);
// Update customer
router.put('/:customerId', customerValidation, handleValidationErrors, customerController.updateCustomer);
// Get customer usage details
router.get('/:customerId/usage', customerController.getCustomerUsage);
// Assign service to customer
router.post('/:customerId/services', serviceAssignmentValidation, handleValidationErrors, customerController.assignService);
module.exports = router;

75
src/routes/dashboard.js Normal file
View File

@ -0,0 +1,75 @@
const express = require('express');
const { query, validationResult } = require('express-validator');
const dashboardController = require('../controllers/dashboardController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Chart data validation
const chartValidation = [
query('period')
.optional()
.isIn(['day', 'week', 'month', 'quarter'])
.withMessage('Period must be day, week, month, or quarter'),
query('range')
.optional()
.isInt({ min: 1, max: 24 })
.withMessage('Range must be between 1 and 24')
];
// Export validation
const exportValidation = [
query('format')
.optional()
.isIn(['json', 'csv'])
.withMessage('Format must be json or csv'),
query('period')
.optional()
.isIn(['day', 'week', 'month', 'quarter'])
.withMessage('Period must be day, week, month, or quarter')
];
// Dashboard overview
router.get('/overview', dashboardController.getDashboardOverview);
// Revenue chart data
router.get('/revenue-chart', chartValidation, handleValidationErrors, dashboardController.getRevenueChart);
// Service distribution chart
router.get('/service-distribution', dashboardController.getServiceDistribution);
// Recent customers
router.get('/recent-customers', [
query('limit')
.optional()
.isInt({ min: 1, max: 20 })
.withMessage('Limit must be between 1 and 20')
], handleValidationErrors, dashboardController.getRecentCustomers);
// Recent transactions
router.get('/recent-transactions', [
query('limit')
.optional()
.isInt({ min: 1, max: 20 })
.withMessage('Limit must be between 1 and 20')
], handleValidationErrors, dashboardController.getRecentTransactions);
// Usage trends
router.get('/usage-trends', chartValidation, handleValidationErrors, dashboardController.getUsageTrends);
// Export dashboard data
router.get('/export', exportValidation, handleValidationErrors, dashboardController.exportDashboardData);
module.exports = router;

127
src/routes/legal.js Normal file
View File

@ -0,0 +1,127 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const { uploadLegal } = require('../middleware/upload');
const legalController = require('../controllers/legalController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Document validation
const documentValidation = [
body('title')
.trim()
.isLength({ min: 2, max: 200 })
.withMessage('Title must be 2-200 characters'),
body('type')
.isIn(['terms_of_service', 'privacy_policy', 'sla', 'compliance', 'agreement', 'other'])
.withMessage('Invalid document type'),
body('version')
.trim()
.isLength({ min: 1, max: 20 })
.withMessage('Version must be 1-20 characters'),
body('content')
.optional()
.trim()
.notEmpty()
.withMessage('Content cannot be empty'),
body('requiresAcceptance')
.optional()
.isBoolean()
.withMessage('Requires acceptance must be boolean'),
body('acceptanceType')
.optional()
.isIn(['click_through', 'signature', 'upload', 'none'])
.withMessage('Invalid acceptance type'),
body('effectiveDate')
.optional()
.isISO8601()
.withMessage('Effective date must be valid ISO 8601 date'),
body('expiryDate')
.optional()
.isISO8601()
.withMessage('Expiry date must be valid ISO 8601 date'),
body('category')
.optional()
.trim()
.isLength({ max: 50 })
.withMessage('Category must be less than 50 characters'),
body('tags')
.optional()
.isArray()
.withMessage('Tags must be an array')
];
// Acceptance validation
const acceptanceValidation = [
body('acceptanceMethod')
.optional()
.isIn(['click_through', 'signature', 'upload', 'email', 'physical'])
.withMessage('Invalid acceptance method'),
body('signatureData')
.optional()
.isObject()
.withMessage('Signature data must be an object')
];
// UUID parameter validation
const uuidValidation = [
param('documentId')
.isUUID()
.withMessage('Document ID must be a valid UUID')
];
// Query validation
const queryValidation = [
query('type')
.optional()
.isIn(['terms_of_service', 'privacy_policy', 'sla', 'compliance', 'agreement', 'other'])
.withMessage('Invalid document type'),
query('status')
.optional()
.isIn(['all', 'draft', 'active', 'archived', 'superseded'])
.withMessage('Invalid status')
];
// Public routes (no authentication required)
router.get('/', queryValidation, handleValidationErrors, legalController.getLegalDocuments);
router.get('/:documentId', uuidValidation, handleValidationErrors, legalController.getDocumentDetails);
router.get('/:documentId/download', uuidValidation, handleValidationErrors, legalController.downloadDocument);
// Protected routes (authentication required)
router.use(authorize('reseller'));
// Document acceptance
router.post('/:documentId/accept', [
...uuidValidation,
...acceptanceValidation
], handleValidationErrors, legalController.acceptDocument);
// Compliance document upload
router.post('/:documentId/upload', [
...uuidValidation,
uploadLegal.single('complianceDocument')
], handleValidationErrors, legalController.uploadComplianceDocument);
// User acceptances
router.get('/user/acceptances', legalController.getUserAcceptances);
// Admin routes (admin only)
router.post('/admin/documents', [
authorize('reseller_admin'),
...documentValidation
], handleValidationErrors, legalController.createLegalDocument);
module.exports = router;

126
src/routes/marketing.js Normal file
View File

@ -0,0 +1,126 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const { uploadMarketing } = require('../middleware/upload');
const marketingController = require('../controllers/marketingController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Asset validation
const assetValidation = [
body('title')
.trim()
.isLength({ min: 2, max: 200 })
.withMessage('Title must be 2-200 characters'),
body('type')
.isIn(['logo', 'brochure', 'presentation', 'video', 'image', 'document', 'template', 'other'])
.withMessage('Invalid asset type'),
body('category')
.isIn(['sales_collateral', 'pitch_deck', 'email_template', 'brand_assets', 'product_sheets', 'case_studies', 'other'])
.withMessage('Invalid asset category'),
body('description')
.optional()
.trim()
.isLength({ max: 1000 })
.withMessage('Description must be less than 1000 characters'),
body('accessLevel')
.optional()
.isIn(['public', 'reseller_only', 'tier_specific', 'admin_only'])
.withMessage('Invalid access level'),
body('tierAccess')
.optional()
.isArray()
.withMessage('Tier access must be an array'),
body('isEditable')
.optional()
.isBoolean()
.withMessage('Is editable must be boolean'),
body('tags')
.optional()
.isArray()
.withMessage('Tags must be an array'),
body('expiryDate')
.optional()
.isISO8601()
.withMessage('Expiry date must be valid ISO 8601 date')
];
// Download validation
const downloadValidation = [
body('purpose')
.optional()
.trim()
.isLength({ max: 255 })
.withMessage('Purpose must be less than 255 characters')
];
// UUID parameter validation
const uuidValidation = [
param('assetId')
.isUUID()
.withMessage('Asset ID must be a valid UUID')
];
// Query validation
const queryValidation = [
query('page')
.optional()
.isInt({ min: 1 })
.withMessage('Page must be a positive integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Limit must be between 1 and 100'),
query('type')
.optional()
.isIn(['logo', 'brochure', 'presentation', 'video', 'image', 'document', 'template', 'other'])
.withMessage('Invalid asset type'),
query('category')
.optional()
.isIn(['sales_collateral', 'pitch_deck', 'email_template', 'brand_assets', 'product_sheets', 'case_studies', 'other'])
.withMessage('Invalid asset category'),
query('search')
.optional()
.trim()
.isLength({ min: 2 })
.withMessage('Search query must be at least 2 characters')
];
// Public routes (no authentication required for some assets)
router.get('/', queryValidation, handleValidationErrors, marketingController.getMarketingAssets);
router.get('/categories', marketingController.getAssetCategories);
// Protected routes (authentication required)
router.use(authorize('reseller'));
// Asset management
router.get('/:assetId', uuidValidation, handleValidationErrors, marketingController.getAssetDetails);
router.post('/:assetId/download', [
...uuidValidation,
...downloadValidation
], handleValidationErrors, marketingController.downloadAsset);
// Download history
router.get('/user/downloads', marketingController.getDownloadHistory);
// Admin routes (admin only)
router.post('/admin/assets', [
authorize('reseller_admin'),
uploadMarketing.single('assetFile'),
...assetValidation
], handleValidationErrors, marketingController.createMarketingAsset);
module.exports = router;

132
src/routes/orders.js Normal file
View File

@ -0,0 +1,132 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const ordersController = require('../controllers/ordersController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Order creation validation
const orderValidation = [
body('customerId')
.isUUID()
.withMessage('Customer ID must be a valid UUID'),
body('items')
.isArray({ min: 1 })
.withMessage('Items must be a non-empty array'),
body('items.*.productId')
.isUUID()
.withMessage('Product ID must be a valid UUID'),
body('items.*.quantity')
.isInt({ min: 1 })
.withMessage('Quantity must be a positive integer'),
body('items.*.customPrice')
.optional()
.isFloat({ min: 0 })
.withMessage('Custom price must be a positive number'),
body('items.*.configuration')
.optional()
.isObject()
.withMessage('Configuration must be an object'),
body('items.*.specifications')
.optional()
.isObject()
.withMessage('Specifications must be an object'),
body('type')
.optional()
.isIn(['new_service', 'upgrade', 'downgrade', 'renewal', 'addon'])
.withMessage('Invalid order type'),
body('notes')
.optional()
.trim()
.isLength({ max: 1000 })
.withMessage('Notes must be less than 1000 characters')
];
// Status update validation
const statusUpdateValidation = [
body('status')
.isIn(['draft', 'pending', 'confirmed', 'processing', 'completed', 'cancelled', 'refunded'])
.withMessage('Invalid order status'),
body('notes')
.optional()
.trim()
.isLength({ max: 1000 })
.withMessage('Notes must be less than 1000 characters')
];
// Cancel order validation
const cancelValidation = [
body('reason')
.trim()
.isLength({ min: 5, max: 500 })
.withMessage('Cancellation reason must be 5-500 characters')
];
// UUID parameter validation
const uuidValidation = [
param('orderId')
.isUUID()
.withMessage('Order ID must be a valid UUID')
];
// Query validation
const queryValidation = [
query('page')
.optional()
.isInt({ min: 1 })
.withMessage('Page must be a positive integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Limit must be between 1 and 100'),
query('status')
.optional()
.isIn(['draft', 'pending', 'confirmed', 'processing', 'completed', 'cancelled', 'refunded'])
.withMessage('Invalid status'),
query('customerId')
.optional()
.isUUID()
.withMessage('Customer ID must be a valid UUID'),
query('startDate')
.optional()
.isISO8601()
.withMessage('Start date must be valid ISO 8601 date'),
query('endDate')
.optional()
.isISO8601()
.withMessage('End date must be valid ISO 8601 date')
];
// All routes require authentication
router.use(authorize('reseller'));
// Order management routes
router.get('/', queryValidation, handleValidationErrors, ordersController.getOrders);
router.post('/', orderValidation, handleValidationErrors, ordersController.createOrder);
router.get('/pending', ordersController.getPendingOrders);
// Individual order routes
router.get('/:orderId', uuidValidation, handleValidationErrors, ordersController.getOrderDetails);
router.put('/:orderId/status', [
...uuidValidation,
...statusUpdateValidation
], handleValidationErrors, ordersController.updateOrderStatus);
router.post('/:orderId/cancel', [
...uuidValidation,
...cancelValidation
], handleValidationErrors, ordersController.cancelOrder);
module.exports = router;

45
src/routes/products.js Normal file
View File

@ -0,0 +1,45 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const productController = require('../controllers/productController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Get all products with reseller pricing
router.get('/', productController.getProducts);
// Get product categories
router.get('/categories', productController.getCategories);
// Get specific product details
router.get('/:productId', productController.getProduct);
// Update custom pricing for a product
router.put('/:productId/pricing', [
body('pricingType')
.isIn(['margin', 'fixed_price'])
.withMessage('Pricing type must be margin or fixed_price'),
body('customMargin')
.if(body('pricingType').equals('margin'))
.isFloat({ min: 0, max: 100 })
.withMessage('Custom margin must be between 0 and 100'),
body('customPrice')
.if(body('pricingType').equals('fixed_price'))
.isFloat({ min: 0 })
.withMessage('Custom price must be greater than 0')
], handleValidationErrors, productController.updateProductPricing);
module.exports = router;

139
src/routes/provisioning.js Normal file
View File

@ -0,0 +1,139 @@
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const provisioningController = require('../controllers/provisioningController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Instance creation validation
const instanceValidation = [
body('customerId')
.isUUID()
.withMessage('Customer ID must be a valid UUID'),
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Instance name must be 2-50 characters'),
body('template')
.trim()
.notEmpty()
.withMessage('Template is required'),
body('size')
.trim()
.notEmpty()
.withMessage('Instance size is required'),
body('region')
.trim()
.notEmpty()
.withMessage('Region is required'),
body('specifications')
.optional()
.isObject()
.withMessage('Specifications must be an object'),
body('configuration')
.optional()
.isObject()
.withMessage('Configuration must be an object'),
body('tags')
.optional()
.isArray()
.withMessage('Tags must be an array')
];
// Snapshot validation
const snapshotValidation = [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Snapshot name must be 2-50 characters'),
body('description')
.optional()
.trim()
.isLength({ max: 255 })
.withMessage('Description must be less than 255 characters'),
body('type')
.optional()
.isIn(['manual', 'automatic', 'backup'])
.withMessage('Invalid snapshot type')
];
// UUID parameter validation
const uuidValidation = [
param('instanceId')
.isUUID()
.withMessage('Instance ID must be a valid UUID')
];
// Query validation
const queryValidation = [
query('page')
.optional()
.isInt({ min: 1 })
.withMessage('Page must be a positive integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Limit must be between 1 and 100'),
query('status')
.optional()
.isIn(['pending', 'creating', 'running', 'stopped', 'stopping', 'starting', 'failed', 'terminated'])
.withMessage('Invalid status'),
query('type')
.optional()
.isIn(['compute', 'storage', 'network', 'database'])
.withMessage('Invalid instance type'),
query('customerId')
.optional()
.isUUID()
.withMessage('Customer ID must be a valid UUID'),
query('region')
.optional()
.trim()
.notEmpty()
.withMessage('Region cannot be empty')
];
// All routes require authentication
router.use(authorize('reseller'));
// Instance management routes
router.get('/', queryValidation, handleValidationErrors, provisioningController.getInstances);
router.post('/', instanceValidation, handleValidationErrors, provisioningController.createInstance);
// Instance control routes
router.post('/:instanceId/start', uuidValidation, handleValidationErrors, provisioningController.startInstance);
router.post('/:instanceId/stop', uuidValidation, handleValidationErrors, provisioningController.stopInstance);
// Snapshot management
router.post('/:instanceId/snapshots', [
...uuidValidation,
...snapshotValidation
], handleValidationErrors, provisioningController.createSnapshot);
// Instance events
router.get('/:instanceId/events', [
...uuidValidation,
query('page')
.optional()
.isInt({ min: 1 })
.withMessage('Page must be a positive integer'),
query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Limit must be between 1 and 100')
], handleValidationErrors, provisioningController.getInstanceEvents);
module.exports = router;

80
src/routes/reports.js Normal file
View File

@ -0,0 +1,80 @@
const express = require('express');
const { query, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const reportsController = require('../controllers/reportsController');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Date range validation
const dateRangeValidation = [
query('startDate')
.optional()
.isISO8601()
.withMessage('Start date must be a valid ISO 8601 date'),
query('endDate')
.optional()
.isISO8601()
.withMessage('End date must be a valid ISO 8601 date'),
query('groupBy')
.optional()
.isIn(['day', 'week', 'month', 'quarter', 'year'])
.withMessage('Group by must be day, week, month, quarter, or year'),
query('format')
.optional()
.isIn(['json', 'csv'])
.withMessage('Format must be json or csv')
];
// Export validation
const exportValidation = [
query('reportType')
.isIn(['sales', 'usage', 'commission'])
.withMessage('Report type must be sales, usage, or commission'),
query('format')
.optional()
.isIn(['json', 'csv', 'pdf'])
.withMessage('Format must be json, csv, or pdf')
];
// Sales report
router.get('/sales', dateRangeValidation, handleValidationErrors, reportsController.getSalesReport);
// Usage report
router.get('/usage', [
...dateRangeValidation,
query('customerId')
.optional()
.isUUID()
.withMessage('Customer ID must be a valid UUID'),
query('productId')
.optional()
.isUUID()
.withMessage('Product ID must be a valid UUID')
], handleValidationErrors, reportsController.getUsageReport);
// Commission report
router.get('/commission', [
...dateRangeValidation,
query('status')
.optional()
.isIn(['all', 'pending', 'approved', 'paid', 'disputed', 'cancelled'])
.withMessage('Invalid commission status')
], handleValidationErrors, reportsController.getCommissionReport);
// Export reports
router.get('/export', exportValidation, handleValidationErrors, reportsController.exportReport);
module.exports = router;

382
src/routes/resellers.js Normal file
View File

@ -0,0 +1,382 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const { kycUpload, extractMultipleFileInfo } = require('../middleware/upload');
const { Reseller, User, AuditLog } = require('../models');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Get reseller profile
router.get('/profile', async (req, res) => {
try {
const reseller = await Reseller.findOne({
where: { id: req.user.resellerId },
include: ['users', 'approver']
});
if (!reseller) {
return res.status(404).json({
success: false,
message: 'Reseller profile not found'
});
}
res.json({
success: true,
data: { reseller }
});
} catch (error) {
console.error('Get reseller profile error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch reseller profile'
});
}
});
// Update reseller profile
router.put('/profile', [
body('companyName')
.optional()
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('Company name must be 2-100 characters'),
body('companyType')
.optional()
.isIn(['individual', 'partnership', 'private_limited', 'public_limited', 'llp'])
.withMessage('Invalid company type'),
body('gstNumber')
.optional()
.matches(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/)
.withMessage('Invalid GST number format'),
body('panNumber')
.optional()
.matches(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
.withMessage('Invalid PAN number format'),
body('contactEmail')
.optional()
.isEmail()
.withMessage('Invalid email format'),
body('contactPhone')
.optional()
.matches(/^[\+]?[1-9][\d]{0,15}$/)
.withMessage('Invalid phone number format'),
body('website')
.optional()
.isURL()
.withMessage('Invalid website URL')
], handleValidationErrors, async (req, res) => {
try {
const {
companyName,
companyType,
registrationNumber,
gstNumber,
panNumber,
address,
contactEmail,
contactPhone,
website
} = req.body;
const reseller = await Reseller.findOne({
where: { id: req.user.resellerId }
});
if (!reseller) {
return res.status(404).json({
success: false,
message: 'Reseller profile not found'
});
}
const updates = {};
if (companyName !== undefined) updates.companyName = companyName;
if (companyType !== undefined) updates.companyType = companyType;
if (registrationNumber !== undefined) updates.registrationNumber = registrationNumber;
if (gstNumber !== undefined) updates.gstNumber = gstNumber;
if (panNumber !== undefined) updates.panNumber = panNumber;
if (address !== undefined) updates.address = address;
if (contactEmail !== undefined) updates.contactEmail = contactEmail;
if (contactPhone !== undefined) updates.contactPhone = contactPhone;
if (website !== undefined) updates.website = website;
await reseller.update(updates);
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'RESELLER_PROFILE_UPDATED',
resource: 'reseller',
resourceId: reseller.id,
newValues: updates,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Reseller profile updated successfully',
data: { reseller }
});
} catch (error) {
console.error('Update reseller profile error:', error);
res.status(500).json({
success: false,
message: 'Failed to update reseller profile'
});
}
});
// Upload KYC documents
router.post('/kyc-documents', kycUpload.fields([
{ name: 'gstCertificate', maxCount: 1 },
{ name: 'panCard', maxCount: 1 },
{ name: 'incorporationCertificate', maxCount: 1 },
{ name: 'addressProof', maxCount: 1 },
{ name: 'bankStatement', maxCount: 1 }
]), async (req, res) => {
try {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).json({
success: false,
message: 'No KYC documents uploaded'
});
}
const reseller = await Reseller.findOne({
where: { id: req.user.resellerId }
});
if (!reseller) {
return res.status(404).json({
success: false,
message: 'Reseller profile not found'
});
}
const uploadedFiles = extractMultipleFileInfo(req.files);
const currentDocuments = reseller.kycDocuments || [];
// Add new documents
Object.keys(uploadedFiles).forEach(docType => {
uploadedFiles[docType].forEach(file => {
currentDocuments.push({
type: docType,
...file,
status: 'submitted'
});
});
});
await reseller.update({
kycDocuments: currentDocuments,
kycStatus: 'submitted'
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'KYC_DOCUMENTS_UPLOADED',
resource: 'reseller',
resourceId: reseller.id,
metadata: { documentTypes: Object.keys(uploadedFiles) },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'KYC documents uploaded successfully',
data: {
uploadedFiles,
kycStatus: reseller.kycStatus
}
});
} catch (error) {
console.error('KYC upload error:', error);
res.status(500).json({
success: false,
message: 'Failed to upload KYC documents'
});
}
});
// Get all resellers (admin only)
router.get('/', authorize('reseller_admin'), async (req, res) => {
try {
const { page = 1, limit = 10, status, tier, kycStatus, search } = req.query;
const offset = (page - 1) * limit;
const where = {};
if (status) where.status = status;
if (tier) where.tier = tier;
if (kycStatus) where.kycStatus = kycStatus;
if (search) {
where[require('sequelize').Op.or] = [
{ companyName: { [require('sequelize').Op.iLike]: `%${search}%` } },
{ contactEmail: { [require('sequelize').Op.iLike]: `%${search}%` } },
{ gstNumber: { [require('sequelize').Op.iLike]: `%${search}%` } }
];
}
const { count, rows: resellers } = await Reseller.findAndCountAll({
where,
include: ['users', 'approver'],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: {
resellers,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get resellers error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch resellers'
});
}
});
// Approve/reject reseller (admin only)
router.put('/:resellerId/approval', authorize('reseller_admin'), [
body('action')
.isIn(['approve', 'reject'])
.withMessage('Action must be approve or reject'),
body('rejectionReason')
.if(body('action').equals('reject'))
.notEmpty()
.withMessage('Rejection reason is required when rejecting')
], handleValidationErrors, async (req, res) => {
try {
const { resellerId } = req.params;
const { action, rejectionReason } = req.body;
const reseller = await Reseller.findByPk(resellerId);
if (!reseller) {
return res.status(404).json({
success: false,
message: 'Reseller not found'
});
}
const updates = {
approvedBy: req.user.id,
approvedAt: new Date()
};
if (action === 'approve') {
updates.status = 'active';
updates.kycStatus = 'approved';
} else {
updates.status = 'rejected';
updates.kycStatus = 'rejected';
updates.rejectionReason = rejectionReason;
}
await reseller.update(updates);
// Log audit
await AuditLog.create({
userId: req.user.id,
action: `RESELLER_${action.toUpperCase()}D`,
resource: 'reseller',
resourceId: resellerId,
newValues: updates,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: `Reseller ${action}d successfully`,
data: { reseller }
});
} catch (error) {
console.error('Reseller approval error:', error);
res.status(500).json({
success: false,
message: 'Failed to process reseller approval'
});
}
});
// Update reseller tier (admin only)
router.put('/:resellerId/tier', authorize('reseller_admin'), [
body('tier')
.isIn(['bronze', 'silver', 'gold', 'platinum', 'diamond'])
.withMessage('Invalid tier'),
body('commissionRate')
.optional()
.isFloat({ min: 0, max: 100 })
.withMessage('Commission rate must be between 0 and 100')
], handleValidationErrors, async (req, res) => {
try {
const { resellerId } = req.params;
const { tier, commissionRate } = req.body;
const reseller = await Reseller.findByPk(resellerId);
if (!reseller) {
return res.status(404).json({
success: false,
message: 'Reseller not found'
});
}
const updates = { tier };
if (commissionRate !== undefined) {
updates.commissionRate = commissionRate;
}
await reseller.update(updates);
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'RESELLER_TIER_UPDATED',
resource: 'reseller',
resourceId: resellerId,
newValues: updates,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Reseller tier updated successfully',
data: { reseller }
});
} catch (error) {
console.error('Update reseller tier error:', error);
res.status(500).json({
success: false,
message: 'Failed to update reseller tier'
});
}
});
module.exports = router;

298
src/routes/users.js Normal file
View File

@ -0,0 +1,298 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { authorize } = require('../middleware/auth');
const { avatarUpload, extractFileInfo } = require('../middleware/upload');
const { User, AuditLog } = require('../models');
const router = express.Router();
// Validation middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
next();
};
// Get current user profile
router.get('/profile', async (req, res) => {
try {
const user = await User.findByPk(req.user.id, {
include: ['reseller']
});
res.json({
success: true,
data: { user: user.toJSON() }
});
} catch (error) {
console.error('Get profile error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch profile'
});
}
});
// Update user profile
router.put('/profile', [
body('firstName')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('First name must be 2-50 characters'),
body('lastName')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Last name must be 2-50 characters'),
body('phone')
.optional()
.matches(/^[\+]?[1-9][\d]{0,15}$/)
.withMessage('Invalid phone number format'),
body('timezone')
.optional()
.isString()
.withMessage('Invalid timezone'),
body('language')
.optional()
.isIn(['en', 'hi', 'es', 'fr'])
.withMessage('Unsupported language')
], handleValidationErrors, async (req, res) => {
try {
const { firstName, lastName, phone, timezone, language, preferences } = req.body;
const user = req.user;
const updates = {};
if (firstName !== undefined) updates.firstName = firstName;
if (lastName !== undefined) updates.lastName = lastName;
if (phone !== undefined) updates.phone = phone;
if (timezone !== undefined) updates.timezone = timezone;
if (language !== undefined) updates.language = language;
if (preferences !== undefined) updates.preferences = preferences;
await user.update(updates);
// Log audit
await AuditLog.create({
userId: user.id,
action: 'PROFILE_UPDATED',
resource: 'user',
resourceId: user.id,
newValues: updates,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Profile updated successfully',
data: { user: user.toJSON() }
});
} catch (error) {
console.error('Update profile error:', error);
res.status(500).json({
success: false,
message: 'Failed to update profile'
});
}
});
// Upload avatar
router.post('/avatar', avatarUpload.single('avatar'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No avatar file uploaded'
});
}
const user = req.user;
const fileInfo = extractFileInfo(req.file);
await user.update({
avatar: `/uploads/avatars/${fileInfo.filename}`
});
// Log audit
await AuditLog.create({
userId: user.id,
action: 'AVATAR_UPDATED',
resource: 'user',
resourceId: user.id,
metadata: { filename: fileInfo.filename },
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Avatar uploaded successfully',
data: {
avatar: user.avatar,
fileInfo
}
});
} catch (error) {
console.error('Avatar upload error:', error);
res.status(500).json({
success: false,
message: 'Failed to upload avatar'
});
}
});
// Change password
router.put('/password', [
body('currentPassword')
.notEmpty()
.withMessage('Current password is required'),
body('newPassword')
.isLength({ min: 8 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.withMessage('Password must be at least 8 characters with uppercase, lowercase, number and special character')
], handleValidationErrors, async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
const user = req.user;
// Verify current password
const isValidPassword = await user.validatePassword(currentPassword);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Current password is incorrect'
});
}
// Update password
await user.update({ password: newPassword });
// Log audit
await AuditLog.create({
userId: user.id,
action: 'PASSWORD_CHANGED',
resource: 'user',
resourceId: user.id,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'Password changed successfully'
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
message: 'Failed to change password'
});
}
});
// Get all users (admin only)
router.get('/', authorize('reseller_admin'), async (req, res) => {
try {
const { page = 1, limit = 10, role, status, search } = req.query;
const offset = (page - 1) * limit;
const where = {};
if (role) where.role = role;
if (status) where.status = status;
if (search) {
where[require('sequelize').Op.or] = [
{ firstName: { [require('sequelize').Op.iLike]: `%${search}%` } },
{ lastName: { [require('sequelize').Op.iLike]: `%${search}%` } },
{ email: { [require('sequelize').Op.iLike]: `%${search}%` } }
];
}
const { count, rows: users } = await User.findAndCountAll({
where,
include: ['reseller'],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
data: {
users: users.map(user => user.toJSON()),
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
}
});
} catch (error) {
console.error('Get users error:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch users'
});
}
});
// Update user role/status (admin only)
router.put('/:userId/status', authorize('reseller_admin'), [
body('status')
.isIn(['active', 'inactive', 'suspended'])
.withMessage('Invalid status'),
body('role')
.optional()
.isIn(['reseller_admin', 'sales_agent', 'support_agent', 'read_only'])
.withMessage('Invalid role')
], handleValidationErrors, async (req, res) => {
try {
const { userId } = req.params;
const { status, role } = req.body;
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
const updates = { status };
if (role !== undefined) updates.role = role;
await user.update(updates);
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'USER_STATUS_UPDATED',
resource: 'user',
resourceId: userId,
newValues: updates,
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
res.json({
success: true,
message: 'User status updated successfully',
data: { user: user.toJSON() }
});
} catch (error) {
console.error('Update user status error:', error);
res.status(500).json({
success: false,
message: 'Failed to update user status'
});
}
});
module.exports = router;

View File

@ -0,0 +1,30 @@
const { sequelize } = require('../models');
async function createDatabase() {
try {
console.log('🔄 Creating database tables...');
// Sync all models with database
await sequelize.sync({ force: false, alter: true });
console.log('✅ Database tables created successfully');
// Test connection
await sequelize.authenticate();
console.log('✅ Database connection verified');
} catch (error) {
console.error('❌ Database creation failed:', error);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
createDatabase().then(() => {
console.log('🎉 Database setup completed');
process.exit(0);
});
}
module.exports = createDatabase;