const User = require('../models/User'); const Commission = require('../models/Commission'); const PaymentOrder = require('../models/PaymentOrder'); const { sequelize } = require('../config/database'); const commissionConfigService = require('./commissionConfigService'); /** * Commission Service * Handles commission calculation and management */ /** * Calculate and record commission for a payment order * @param {string} paymentOrderId - Payment order ID * @returns {Object|null} Commission record or null if no commission generated */ const calculateCommission = async (paymentOrderId) => { // Get payment order with user info const paymentOrder = await PaymentOrder.findByPk(paymentOrderId, { include: [{ model: User, as: 'user' }], }); if (!paymentOrder) { throw new Error('Payment order not found'); } if (paymentOrder.status !== 'active') { return null; // Don't calculate commission for cancelled orders } // Check if user was invited const inviteeUser = paymentOrder.user; if (!inviteeUser || !inviteeUser.invitedBy) { return null; // User was not invited, no commission } // Get current commission rate const commissionRate = await commissionConfigService.getCommissionRate(); // Calculate commission amount - prioritize RMB, then Peso, then legacy amount // Commission is calculated based on whichever currency amount is available let paymentAmount = 0; let currency = 'RMB'; if (paymentOrder.amountRmb && parseFloat(paymentOrder.amountRmb) > 0) { paymentAmount = parseFloat(paymentOrder.amountRmb); currency = 'RMB'; } else if (paymentOrder.amountPeso && parseFloat(paymentOrder.amountPeso) > 0) { paymentAmount = parseFloat(paymentOrder.amountPeso); currency = 'PHP'; } else if (paymentOrder.amount && parseFloat(paymentOrder.amount) > 0) { paymentAmount = parseFloat(paymentOrder.amount); currency = 'RMB'; } if (paymentAmount <= 0) { return null; // No valid payment amount } const commissionAmount = paymentAmount * commissionRate; // Start transaction const transaction = await sequelize.transaction(); try { // Create commission record with currency const commission = await Commission.create({ inviterId: inviteeUser.invitedBy, inviteeId: inviteeUser.id, paymentOrderId: paymentOrder.id, paymentAmount: paymentAmount, commissionRate: commissionRate, commissionAmount: commissionAmount, currency: currency, status: 'credited', }, { transaction }); // Update inviter's balance based on currency const inviter = await User.findByPk(inviteeUser.invitedBy, { transaction }); if (inviter) { if (currency === 'PHP') { inviter.balancePeso = parseFloat(inviter.balancePeso || 0) + commissionAmount; } else { inviter.balance = parseFloat(inviter.balance) + commissionAmount; } await inviter.save({ transaction }); } await transaction.commit(); return commission; } catch (error) { await transaction.rollback(); throw error; } }; /** * Get commission records for an inviter * @param {string} inviterId - Inviter user ID * @param {Object} options - Query options (page, limit) * @returns {Object} Commission records with pagination */ const getCommissionsByInviter = async (inviterId, options = {}) => { const { page = 1, limit = 20 } = options; const offset = (page - 1) * limit; const { count, rows } = await Commission.findAndCountAll({ where: { inviterId, status: 'credited' }, include: [ { model: User, as: 'invitee', attributes: ['id', 'uid', 'nickname', 'avatar'], }, { model: PaymentOrder, as: 'paymentOrder', attributes: ['id', 'orderNo', 'serviceContent', 'paymentTime'], }, ], order: [['createdAt', 'DESC']], limit: parseInt(limit), offset: parseInt(offset), }); const records = rows.map(commission => ({ id: commission.id, invitee: commission.invitee ? { id: commission.invitee.id, uid: commission.invitee.uid, nickname: commission.invitee.nickname, avatar: commission.invitee.avatar, } : null, paymentOrder: commission.paymentOrder ? { id: commission.paymentOrder.id, orderNo: commission.paymentOrder.orderNo, serviceContent: commission.paymentOrder.serviceContent, paymentTime: commission.paymentOrder.paymentTime, } : null, paymentAmount: parseFloat(commission.paymentAmount).toFixed(2), currency: commission.currency || 'RMB', commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`, commissionAmount: parseFloat(commission.commissionAmount).toFixed(2), createdAt: commission.createdAt, })); return { records, pagination: { page: parseInt(page), limit: parseInt(limit), total: count, totalPages: Math.ceil(count / limit), }, }; }; /** * Get invited users with their commission details (grouped by user) * @param {string} inviterId - Inviter user ID * @param {Object} options - Query options (page, limit) * @returns {Object} Invited users with commission details */ const getInvitedUsersWithCommissions = async (inviterId, options = {}) => { const { page = 1, limit = 20 } = options; const offset = (page - 1) * limit; // Get all invited users const { count, rows: invitedUsers } = await User.findAndCountAll({ where: { invitedBy: inviterId }, attributes: ['id', 'uid', 'nickname', 'avatar', 'createdAt'], order: [['createdAt', 'DESC']], limit: parseInt(limit), offset: parseInt(offset), }); // Get commission records for each invited user const usersWithCommissions = await Promise.all( invitedUsers.map(async (user) => { // Get all commissions for this invitee const commissions = await Commission.findAll({ where: { inviterId, inviteeId: user.id, status: 'credited' }, include: [ { model: PaymentOrder, as: 'paymentOrder', attributes: ['id', 'orderNo', 'serviceContent', 'paymentTime', 'amountRmb', 'amountPeso'], }, ], order: [['createdAt', 'DESC']], }); // Calculate totals by currency let totalCommissionRmb = 0; let totalCommissionPeso = 0; let totalPaymentRmb = 0; let totalPaymentPeso = 0; const orders = commissions.map(c => { const amount = parseFloat(c.commissionAmount); const paymentAmount = parseFloat(c.paymentAmount); if (c.currency === 'PHP') { totalCommissionPeso += amount; totalPaymentPeso += paymentAmount; } else { totalCommissionRmb += amount; totalPaymentRmb += paymentAmount; } return { id: c.id, orderNo: c.paymentOrder?.orderNo || '-', serviceContent: c.paymentOrder?.serviceContent || '-', paymentTime: c.paymentOrder?.paymentTime || c.createdAt, paymentAmount: paymentAmount.toFixed(2), commissionAmount: amount.toFixed(2), currency: c.currency || 'RMB', createdAt: c.createdAt, }; }); return { user: { id: user.id, uid: user.uid, nickname: user.nickname, avatar: user.avatar, registeredAt: user.createdAt, }, orderCount: orders.length, orders, totalPaymentRmb: totalPaymentRmb.toFixed(2), totalPaymentPeso: totalPaymentPeso.toFixed(2), totalCommissionRmb: totalCommissionRmb.toFixed(2), totalCommissionPeso: totalCommissionPeso.toFixed(2), }; }) ); return { records: usersWithCommissions, pagination: { page: parseInt(page), limit: parseInt(limit), total: count, totalPages: Math.ceil(count / limit), }, }; }; /** * Generate invitation code * @returns {string} Invitation code */ const generateInvitationCode = () => { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; let code = ''; for (let i = 0; i < 6; i++) { code += chars.charAt(Math.floor(Math.random() * chars.length)); } return code; }; /** * Get commission statistics for an inviter * @param {string} inviterId - Inviter user ID * @returns {Object} Commission statistics */ const getCommissionStats = async (inviterId) => { const user = await User.findByPk(inviterId); if (!user) { throw new Error('User not found'); } // If user doesn't have invitation code, generate one let invitationCode = user.invitationCode; if (!invitationCode) { invitationCode = generateInvitationCode(); await user.update({ invitationCode }); } // Get total invites count const totalInvites = await User.count({ where: { invitedBy: inviterId }, }); // Get commission statistics by currency const commissions = await Commission.findAll({ where: { inviterId, status: 'credited' }, attributes: ['commissionAmount', 'currency'], }); // Calculate total commission by currency let totalCommissionRmb = 0; let totalCommissionPeso = 0; commissions.forEach(c => { const amount = parseFloat(c.commissionAmount); if (c.currency === 'PHP') { totalCommissionPeso += amount; } else { totalCommissionRmb += amount; } }); // Get paid invites count (invitees who have made payments) const paidInvitesCount = await Commission.count({ where: { inviterId, status: 'credited' }, distinct: true, col: 'invitee_id', }); return { totalInvites, paidInvites: paidInvitesCount, totalCommission: totalCommissionRmb.toFixed(2), totalCommissionRmb: totalCommissionRmb.toFixed(2), totalCommissionPeso: totalCommissionPeso.toFixed(2), commissionCount: commissions.length, availableBalance: parseFloat(user.balance).toFixed(2), availableBalanceRmb: parseFloat(user.balance).toFixed(2), availableBalancePeso: parseFloat(user.balancePeso || 0).toFixed(2), invitationCode: invitationCode, }; }; /** * Get all commission records (for admin) * @param {Object} options - Query options (page, limit, inviterId, inviteeId) * @returns {Object} Commission records with pagination */ const getAllCommissions = async (options = {}) => { const { page = 1, limit = 20, inviterId, inviteeId, status } = options; const offset = (page - 1) * limit; const where = {}; if (inviterId) where.inviterId = inviterId; if (inviteeId) where.inviteeId = inviteeId; if (status) where.status = status; const { count, rows } = await Commission.findAndCountAll({ where, include: [ { model: User, as: 'inviter', attributes: ['id', 'uid', 'nickname'], }, { model: User, as: 'invitee', attributes: ['id', 'uid', 'nickname'], }, { model: PaymentOrder, as: 'paymentOrder', attributes: ['id', 'orderNo', 'amount'], }, ], order: [['createdAt', 'DESC']], limit: parseInt(limit), offset: parseInt(offset), }); const records = rows.map(commission => ({ id: commission.id, inviter: commission.inviter, invitee: commission.invitee, paymentOrder: commission.paymentOrder, paymentAmount: parseFloat(commission.paymentAmount).toFixed(2), commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`, commissionAmount: parseFloat(commission.commissionAmount).toFixed(2), status: commission.status, createdAt: commission.createdAt, })); return { records, pagination: { page: parseInt(page), limit: parseInt(limit), total: count, totalPages: Math.ceil(count / limit), }, }; }; /** * Get platform commission statistics (for admin) * @returns {Object} Platform commission statistics */ const getPlatformCommissionStats = async () => { // Total commission paid const totalPaid = await Commission.sum('commissionAmount', { where: { status: 'credited' }, }) || 0; // Total commission count const totalCount = await Commission.count({ where: { status: 'credited' }, }); // Get user balance statistics (pending withdrawal) const users = await User.findAll({ where: sequelize.where( sequelize.cast(sequelize.col('balance'), 'DECIMAL(10,2)'), { [require('sequelize').Op.gt]: 0 } ), attributes: ['balance'], }); const pendingWithdrawal = users.reduce( (sum, u) => sum + parseFloat(u.balance), 0 ); return { totalCommissionPaid: parseFloat(totalPaid).toFixed(2), totalCommissionCount: totalCount, pendingWithdrawal: pendingWithdrawal.toFixed(2), }; }; module.exports = { calculateCommission, getCommissionsByInviter, getInvitedUsersWithCommissions, getCommissionStats, getAllCommissions, getPlatformCommissionStats, };