338 lines
9.6 KiB
JavaScript
338 lines
9.6 KiB
JavaScript
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: {
|
|
id: commission.invitee.id,
|
|
uid: commission.invitee.uid,
|
|
nickname: commission.invitee.nickname,
|
|
avatar: commission.invitee.avatar,
|
|
},
|
|
paymentOrder: {
|
|
id: commission.paymentOrder.id,
|
|
orderNo: commission.paymentOrder.orderNo,
|
|
serviceContent: commission.paymentOrder.serviceContent,
|
|
paymentTime: commission.paymentOrder.paymentTime,
|
|
},
|
|
paymentAmount: parseFloat(commission.paymentAmount).toFixed(2),
|
|
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),
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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,
|
|
getCommissionStats,
|
|
getAllCommissions,
|
|
getPlatformCommissionStats,
|
|
};
|