This commit is contained in:
18631081161 2025-12-24 15:09:56 +08:00
parent 784e373c7e
commit c79e164f87
6 changed files with 2135 additions and 128 deletions

View File

@ -2,15 +2,23 @@
<div class="commissions-container"> <div class="commissions-container">
<!-- Statistics Cards --> <!-- Statistics Cards -->
<el-row :gutter="20" class="stats-row"> <el-row :gutter="20" class="stats-row">
<el-col :span="8"> <el-col :span="6">
<el-card class="stat-card"> <el-card class="stat-card">
<div class="stat-content"> <div class="stat-content">
<div class="stat-value">¥{{ stats.totalCommissionPaid }}</div> <div class="stat-value rmb">¥{{ stats.totalCommissionPaidRmb || stats.totalCommissionPaid }}</div>
<div class="stat-label">累计佣金支出</div> <div class="stat-label">累计佣金支出(人民币)</div>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-value peso">{{ stats.totalCommissionPaidPeso || '0.00' }}</div>
<div class="stat-label">累计佣金支出(比索)</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card"> <el-card class="stat-card">
<div class="stat-content"> <div class="stat-content">
<div class="stat-value">{{ stats.totalCommissionCount }}</div> <div class="stat-value">{{ stats.totalCommissionCount }}</div>
@ -18,10 +26,11 @@
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="6">
<el-card class="stat-card"> <el-card class="stat-card">
<div class="stat-content"> <div class="stat-content">
<div class="stat-value">¥{{ stats.pendingWithdrawal }}</div> <div class="stat-value rmb">¥{{ stats.pendingWithdrawalRmb || stats.pendingWithdrawal }}</div>
<div class="stat-value peso">{{ stats.pendingWithdrawalPeso || '0.00' }}</div>
<div class="stat-label">待提现金额</div> <div class="stat-label">待提现金额</div>
</div> </div>
</el-card> </el-card>
@ -93,13 +102,20 @@
</el-table-column> </el-table-column>
<el-table-column prop="paymentAmount" label="支付金额" width="120"> <el-table-column prop="paymentAmount" label="支付金额" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="payment-amount">¥{{ row.paymentAmount }}</span> <span class="payment-amount">{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.paymentAmount }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="commissionRate" label="佣金比例" width="100" /> <el-table-column prop="commissionRate" label="佣金比例" width="100" />
<el-table-column prop="commissionAmount" label="佣金金额" width="120"> <el-table-column prop="commissionAmount" label="佣金金额" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="commission-amount">¥{{ row.commissionAmount }}</span> <span class="commission-amount">{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.commissionAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="currency" label="货币" width="80">
<template #default="{ row }">
<el-tag :type="row.currency === 'PHP' ? 'success' : 'warning'" size="small">
{{ row.currency === 'PHP' ? '比索' : '人民币' }}
</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" width="100"> <el-table-column prop="status" label="状态" width="100">
@ -271,6 +287,15 @@ onMounted(() => {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #409eff; color: #409eff;
&.rmb {
color: #e6a23c;
}
&.peso {
color: #67c23a;
font-size: 20px;
}
} }
.stat-label { .stat-label {
@ -304,6 +329,10 @@ onMounted(() => {
.payment-amount { .payment-amount {
color: #e6a23c; color: #e6a23c;
&.peso {
color: #67c23a;
}
} }
.commission-amount { .commission-amount {

View File

@ -78,9 +78,18 @@
{{ formatDate(row.paymentTime) }} {{ formatDate(row.paymentTime) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="佣金" width="120"> <el-table-column label="佣金" width="150">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.commission" class="commission">¥{{ row.commission.amount }}</span> <div v-if="row.commissions && row.commissions.length > 0" class="commissions-info">
<span
v-for="(comm, index) in row.commissions"
:key="index"
class="commission-item"
:class="{ 'peso': comm.currency === 'PHP', 'rmb': comm.currency !== 'PHP' }"
>
{{ comm.currency === 'PHP' ? '₱' : '¥' }}{{ comm.amount }}
</span>
</div>
<span v-else class="no-commission"></span> <span v-else class="no-commission"></span>
</template> </template>
</el-table-column> </el-table-column>
@ -189,17 +198,35 @@
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
<el-divider v-if="currentOrder?.commission">佣金信息</el-divider> <el-divider v-if="currentOrder?.commissions && currentOrder.commissions.length > 0">佣金信息</el-divider>
<el-descriptions v-if="currentOrder?.commission" :column="2" border> <div v-if="currentOrder?.commissions && currentOrder.commissions.length > 0">
<el-descriptions-item label="邀请人">{{ currentOrder.commission.inviter?.nickname }} (UID: {{ currentOrder.commission.inviter?.uid }})</el-descriptions-item> <el-descriptions
<el-descriptions-item label="佣金比例">{{ currentOrder.commission.commissionRate }}</el-descriptions-item> v-for="(comm, index) in currentOrder.commissions"
<el-descriptions-item label="佣金金额">¥{{ currentOrder.commission.commissionAmount }}</el-descriptions-item> :key="comm.id || index"
<el-descriptions-item label="佣金状态"> :column="2"
<el-tag :type="currentOrder.commission.status === 'credited' ? 'success' : 'danger'"> border
{{ currentOrder.commission.status === 'credited' ? '已入账' : '已取消' }} :class="{ 'commission-section': index > 0 }"
</el-tag> >
</el-descriptions-item> <el-descriptions-item label="货币类型">
</el-descriptions> <el-tag :type="comm.currency === 'PHP' ? 'primary' : 'warning'" size="small">
{{ comm.currency === 'PHP' ? '比索 (PHP)' : '人民币 (RMB)' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="佣金状态">
<el-tag :type="comm.status === 'credited' ? 'success' : 'danger'">
{{ comm.status === 'credited' ? '已入账' : '已取消' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="邀请人">{{ comm.inviter?.nickname || '-' }} (UID: {{ comm.inviter?.uid || '-' }})</el-descriptions-item>
<el-descriptions-item label="佣金比例">{{ comm.commissionRate }}</el-descriptions-item>
<el-descriptions-item label="佣金金额">
<span :class="{ 'peso-amount': comm.currency === 'PHP', 'rmb-amount': comm.currency !== 'PHP' }">
{{ comm.currency === 'PHP' ? '₱' : '¥' }}{{ comm.commissionAmount }}
</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(comm.createdAt) }}</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
@ -370,8 +397,14 @@ async function handleCreate() {
createDialogVisible.value = false createDialogVisible.value = false
fetchOrders() fetchOrders()
if (response.data.data.commission) { // Show commission info for all created commissions
ElMessage.info(`已生成佣金 ¥${response.data.data.commission.commissionAmount}`) const commissions = response.data.data.commissions
if (commissions && commissions.length > 0) {
const commissionMessages = commissions.map(c => {
const currencySymbol = c.currency === 'PHP' ? '₱' : '¥'
return `${currencySymbol}${c.commissionAmount}`
})
ElMessage.info(`已生成佣金: ${commissionMessages.join(', ')}`)
} }
} else { } else {
ElMessage.error(response.data.error?.message || '创建失败') ElMessage.error(response.data.error?.message || '创建失败')
@ -510,6 +543,24 @@ onMounted(() => {
font-weight: bold; font-weight: bold;
} }
.commissions-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.commission-item {
font-weight: bold;
&.peso {
color: #409eff;
}
&.rmb {
color: #e6a23c;
}
}
.no-commission { .no-commission {
color: #909399; color: #909399;
} }
@ -551,5 +602,21 @@ onMounted(() => {
.cancel-reason { .cancel-reason {
color: #f56c6c; color: #f56c6c;
} }
.commission-section {
margin-top: 16px;
}
.peso-amount {
color: #409eff;
font-weight: bold;
font-size: 16px;
}
.rmb-amount {
color: #e6a23c;
font-weight: bold;
font-size: 16px;
}
} }
</style> </style>

View File

@ -0,0 +1,175 @@
/**
* Script to fix dual-currency commissions for existing payment orders
*
* This script finds payment orders that have both amountRmb and amountPeso,
* but only have one commission record, and creates the missing commission.
*
* Usage: node src/scripts/fixDualCurrencyCommissions.js [--dry-run]
*/
const { sequelize } = require('../config/database');
const PaymentOrder = require('../models/PaymentOrder');
const Commission = require('../models/Commission');
const User = require('../models/User');
const commissionConfigService = require('../services/commissionConfigService');
const isDryRun = process.argv.includes('--dry-run');
async function fixDualCurrencyCommissions() {
console.log('=== Fix Dual Currency Commissions ===');
console.log(`Mode: ${isDryRun ? 'DRY RUN (no changes will be made)' : 'LIVE'}`);
console.log('');
try {
// Find all active payment orders with both currencies
const orders = await PaymentOrder.findAll({
where: {
status: 'active',
},
include: [
{
model: User,
as: 'user',
attributes: ['id', 'invitedBy'],
},
],
});
console.log(`Found ${orders.length} active payment orders`);
let fixedCount = 0;
let skippedCount = 0;
for (const order of orders) {
const amountRmb = parseFloat(order.amountRmb || 0);
const amountPeso = parseFloat(order.amountPeso || 0);
const legacyAmount = parseFloat(order.amount || 0);
// Check if order has both currencies
const hasRmb = amountRmb > 0 || legacyAmount > 0;
const hasPeso = amountPeso > 0;
if (!hasRmb && !hasPeso) {
continue; // No amounts, skip
}
// Check if user was invited
if (!order.user || !order.user.invitedBy) {
continue; // No inviter, skip
}
// Get existing commissions for this order
const existingCommissions = await Commission.findAll({
where: { paymentOrderId: order.id },
});
const existingCurrencies = existingCommissions.map(c => c.currency || 'RMB');
const hasRmbCommission = existingCurrencies.includes('RMB');
const hasPhpCommission = existingCurrencies.includes('PHP');
// Check if we need to create missing commissions
const needsRmbCommission = hasRmb && !hasRmbCommission;
const needsPhpCommission = hasPeso && !hasPhpCommission;
if (!needsRmbCommission && !needsPhpCommission) {
continue; // All commissions exist, skip
}
console.log(`\nOrder ${order.orderNo}:`);
console.log(` - amountRmb: ${amountRmb}, amountPeso: ${amountPeso}, amount: ${legacyAmount}`);
console.log(` - Existing commissions: ${existingCurrencies.join(', ') || 'none'}`);
console.log(` - Needs RMB commission: ${needsRmbCommission}`);
console.log(` - Needs PHP commission: ${needsPhpCommission}`);
if (isDryRun) {
console.log(` - [DRY RUN] Would create missing commissions`);
fixedCount++;
continue;
}
// Get commission rate (use existing rate if available, otherwise get current rate)
let commissionRate;
if (existingCommissions.length > 0) {
commissionRate = parseFloat(existingCommissions[0].commissionRate);
} else {
commissionRate = await commissionConfigService.getCommissionRate();
}
const transaction = await sequelize.transaction();
try {
const inviter = await User.findByPk(order.user.invitedBy, { transaction });
if (needsRmbCommission) {
const rmbPaymentAmount = amountRmb > 0 ? amountRmb : legacyAmount;
const rmbCommissionAmount = rmbPaymentAmount * commissionRate;
await Commission.create({
inviterId: order.user.invitedBy,
inviteeId: order.user.id,
paymentOrderId: order.id,
paymentAmount: rmbPaymentAmount,
commissionRate: commissionRate,
commissionAmount: rmbCommissionAmount,
currency: 'RMB',
status: 'credited',
}, { transaction });
if (inviter) {
inviter.balance = parseFloat(inviter.balance || 0) + rmbCommissionAmount;
}
console.log(` - Created RMB commission: ¥${rmbCommissionAmount.toFixed(2)}`);
}
if (needsPhpCommission) {
const phpCommissionAmount = amountPeso * commissionRate;
await Commission.create({
inviterId: order.user.invitedBy,
inviteeId: order.user.id,
paymentOrderId: order.id,
paymentAmount: amountPeso,
commissionRate: commissionRate,
commissionAmount: phpCommissionAmount,
currency: 'PHP',
status: 'credited',
}, { transaction });
if (inviter) {
inviter.balancePeso = parseFloat(inviter.balancePeso || 0) + phpCommissionAmount;
}
console.log(` - Created PHP commission: ₱${phpCommissionAmount.toFixed(2)}`);
}
if (inviter) {
await inviter.save({ transaction });
}
await transaction.commit();
fixedCount++;
console.log(` - ✓ Fixed successfully`);
} catch (error) {
await transaction.rollback();
console.error(` - ✗ Error: ${error.message}`);
skippedCount++;
}
}
console.log('\n=== Summary ===');
console.log(`Fixed: ${fixedCount}`);
console.log(`Skipped/Errors: ${skippedCount}`);
if (isDryRun) {
console.log('\nThis was a dry run. Run without --dry-run to apply changes.');
}
} catch (error) {
console.error('Error:', error);
} finally {
await sequelize.close();
}
}
fixDualCurrencyCommissions();

View File

@ -10,9 +10,10 @@ const commissionConfigService = require('./commissionConfigService');
*/ */
/** /**
* Calculate and record commission for a payment order * Calculate and record commissions for a payment order
* Creates separate commission records for each currency present (RMB and/or PHP)
* @param {string} paymentOrderId - Payment order ID * @param {string} paymentOrderId - Payment order ID
* @returns {Object|null} Commission record or null if no commission generated * @returns {Array<Object>|null} Array of commission records or null if no commission generated
*/ */
const calculateCommission = async (paymentOrderId) => { const calculateCommission = async (paymentOrderId) => {
// Get payment order with user info // Get payment order with user info
@ -37,57 +38,70 @@ const calculateCommission = async (paymentOrderId) => {
// Get current commission rate // Get current commission rate
const commissionRate = await commissionConfigService.getCommissionRate(); const commissionRate = await commissionConfigService.getCommissionRate();
// Calculate commission amount - prioritize RMB, then Peso, then legacy amount // Collect all currency amounts to process
// Commission is calculated based on whichever currency amount is available const currencyAmounts = [];
let paymentAmount = 0;
let currency = 'RMB';
if (paymentOrder.amountRmb && parseFloat(paymentOrder.amountRmb) > 0) { // Check RMB amount (including legacy 'amount' field)
paymentAmount = parseFloat(paymentOrder.amountRmb); const rmbAmount = parseFloat(paymentOrder.amountRmb || 0) || parseFloat(paymentOrder.amount || 0);
currency = 'RMB'; if (rmbAmount > 0) {
} else if (paymentOrder.amountPeso && parseFloat(paymentOrder.amountPeso) > 0) { currencyAmounts.push({ currency: 'RMB', paymentAmount: rmbAmount });
paymentAmount = parseFloat(paymentOrder.amountPeso);
currency = 'PHP';
} else if (paymentOrder.amount && parseFloat(paymentOrder.amount) > 0) {
paymentAmount = parseFloat(paymentOrder.amount);
currency = 'RMB';
} }
if (paymentAmount <= 0) { // Check Peso amount
return null; // No valid payment amount const pesoAmount = parseFloat(paymentOrder.amountPeso || 0);
if (pesoAmount > 0) {
currencyAmounts.push({ currency: 'PHP', paymentAmount: pesoAmount });
}
if (currencyAmounts.length === 0) {
return null; // No valid payment amounts
} }
const commissionAmount = paymentAmount * commissionRate;
// Start transaction // Start transaction
const transaction = await sequelize.transaction(); const transaction = await sequelize.transaction();
try { try {
// Create commission record with currency const commissions = [];
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 // Get inviter for balance updates
const inviter = await User.findByPk(inviteeUser.invitedBy, { transaction }); const inviter = await User.findByPk(inviteeUser.invitedBy, { transaction });
if (inviter) {
if (currency === 'PHP') { // Create commission record for each currency
inviter.balancePeso = parseFloat(inviter.balancePeso || 0) + commissionAmount; for (const { currency, paymentAmount } of currencyAmounts) {
} else { const commissionAmount = paymentAmount * commissionRate;
inviter.balance = parseFloat(inviter.balance) + commissionAmount;
// Create commission record
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 });
commissions.push(commission);
// Update inviter's balance based on currency
if (inviter) {
if (currency === 'PHP') {
inviter.balancePeso = parseFloat(inviter.balancePeso || 0) + commissionAmount;
} else {
inviter.balance = parseFloat(inviter.balance || 0) + commissionAmount;
}
} }
}
// Save inviter balance updates
if (inviter) {
await inviter.save({ transaction }); await inviter.save({ transaction });
} }
await transaction.commit(); await transaction.commit();
return commission; return commissions;
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
throw error; throw error;
@ -376,6 +390,7 @@ const getAllCommissions = async (options = {}) => {
paymentAmount: parseFloat(commission.paymentAmount).toFixed(2), paymentAmount: parseFloat(commission.paymentAmount).toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`, commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2), commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
currency: commission.currency || 'RMB',
status: commission.status, status: commission.status,
createdAt: commission.createdAt, createdAt: commission.createdAt,
})); }));
@ -396,34 +411,52 @@ const getAllCommissions = async (options = {}) => {
* @returns {Object} Platform commission statistics * @returns {Object} Platform commission statistics
*/ */
const getPlatformCommissionStats = async () => { const getPlatformCommissionStats = async () => {
// Total commission paid const Op = require('sequelize').Op;
const totalPaid = await Commission.sum('commissionAmount', {
// Total commission paid by currency
const commissions = await Commission.findAll({
where: { status: 'credited' }, where: { status: 'credited' },
}) || 0; attributes: ['commissionAmount', 'currency'],
raw: true,
});
let totalPaidRmb = 0;
let totalPaidPeso = 0;
commissions.forEach(c => {
const amount = parseFloat(c.commissionAmount) || 0;
if (c.currency === 'PHP') {
totalPaidPeso += amount;
} else {
totalPaidRmb += amount;
}
});
// Total commission count // Total commission count
const totalCount = await Commission.count({ const totalCount = await Commission.count({
where: { status: 'credited' }, where: { status: 'credited' },
}); });
// Get user balance statistics (pending withdrawal) // Get user balance statistics (pending withdrawal) - dual currency
const users = await User.findAll({ const users = await User.findAll({
where: sequelize.where( attributes: ['balance', 'balancePeso'],
sequelize.cast(sequelize.col('balance'), 'DECIMAL(10,2)'), raw: true,
{ [require('sequelize').Op.gt]: 0 }
),
attributes: ['balance'],
}); });
const pendingWithdrawal = users.reduce( let pendingWithdrawalRmb = 0;
(sum, u) => sum + parseFloat(u.balance), let pendingWithdrawalPeso = 0;
0 users.forEach(u => {
); pendingWithdrawalRmb += parseFloat(u.balance) || 0;
pendingWithdrawalPeso += parseFloat(u.balancePeso) || 0;
});
return { return {
totalCommissionPaid: parseFloat(totalPaid).toFixed(2), totalCommissionPaid: parseFloat(totalPaidRmb).toFixed(2),
totalCommissionPaidRmb: parseFloat(totalPaidRmb).toFixed(2),
totalCommissionPaidPeso: parseFloat(totalPaidPeso).toFixed(2),
totalCommissionCount: totalCount, totalCommissionCount: totalCount,
pendingWithdrawal: pendingWithdrawal.toFixed(2), pendingWithdrawal: pendingWithdrawalRmb.toFixed(2),
pendingWithdrawalRmb: pendingWithdrawalRmb.toFixed(2),
pendingWithdrawalPeso: pendingWithdrawalPeso.toFixed(2),
}; };
}; };

View File

@ -117,16 +117,28 @@ const createPaymentOrder = async (data, adminId) => {
}); });
// Calculate commission (if applicable, for any currency) // Calculate commission (if applicable, for any currency)
let commission = null; // commissionService.calculateCommission now returns an array of commissions
let commissions = null;
if (totalAmountForCommission > 0) { if (totalAmountForCommission > 0) {
try { try {
commission = await commissionService.calculateCommission(paymentOrder.id); commissions = await commissionService.calculateCommission(paymentOrder.id);
} catch (error) { } catch (error) {
// Log error but don't fail the order creation // Log error but don't fail the order creation
console.error('Commission calculation error:', error.message); console.error('Commission calculation error:', error.message);
} }
} }
// Format commissions array for response
const formattedCommissions = commissions && Array.isArray(commissions) && commissions.length > 0
? commissions.map(c => ({
id: c.id,
inviterId: c.inviterId,
commissionAmount: parseFloat(c.commissionAmount).toFixed(2),
commissionRate: `${(parseFloat(c.commissionRate) * 100).toFixed(2)}%`,
currency: c.currency || 'RMB',
}))
: null;
return { return {
paymentOrder: { paymentOrder: {
id: paymentOrder.id, id: paymentOrder.id,
@ -142,12 +154,9 @@ const createPaymentOrder = async (data, adminId) => {
status: paymentOrder.status, status: paymentOrder.status,
createdAt: paymentOrder.createdAt, createdAt: paymentOrder.createdAt,
}, },
commission: commission ? { commissions: formattedCommissions,
id: commission.id, // Keep backward compatibility with single commission field
inviterId: commission.inviterId, commission: formattedCommissions && formattedCommissions.length > 0 ? formattedCommissions[0] : null,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
} : null,
}; };
}; };
@ -232,37 +241,50 @@ const getPaymentOrders = async (options = {}) => {
totalPeso: parseFloat(statisticsResult?.totalPeso || 0).toFixed(2), totalPeso: parseFloat(statisticsResult?.totalPeso || 0).toFixed(2),
}; };
// Get commission status for each order // Get all commissions for each order (dual-currency support)
const Commission = require('../models/Commission'); const Commission = require('../models/Commission');
const orderIds = rows.map(o => o.id); const orderIds = rows.map(o => o.id);
const commissions = await Commission.findAll({ const commissions = await Commission.findAll({
where: { paymentOrderId: orderIds }, where: { paymentOrderId: orderIds },
attributes: ['paymentOrderId', 'commissionAmount', 'status'], attributes: ['paymentOrderId', 'commissionAmount', 'status', 'currency'],
});
const commissionMap = {};
commissions.forEach(c => {
commissionMap[c.paymentOrderId] = {
amount: parseFloat(c.commissionAmount).toFixed(2),
status: c.status,
};
}); });
const records = rows.map(order => ({ // Build a map of orderId -> array of commissions
id: order.id, const commissionsMap = {};
orderNo: order.orderNo, commissions.forEach(c => {
user: order.user, const orderId = c.paymentOrderId;
appointment: order.appointment, if (!commissionsMap[orderId]) {
amount: order.amount ? parseFloat(order.amount).toFixed(2) : null, commissionsMap[orderId] = [];
amountPeso: order.amountPeso ? parseFloat(order.amountPeso).toFixed(2) : null, }
amountRmb: order.amountRmb ? parseFloat(order.amountRmb).toFixed(2) : null, commissionsMap[orderId].push({
serviceContent: order.serviceContent, amount: parseFloat(c.commissionAmount).toFixed(2),
paymentTime: order.paymentTime, status: c.status,
notes: order.notes, currency: c.currency || 'RMB',
status: order.status, });
creator: order.creator, });
commission: commissionMap[order.id] || null,
createdAt: order.createdAt, const records = rows.map(order => {
})); const orderCommissions = commissionsMap[order.id] || [];
return {
id: order.id,
orderNo: order.orderNo,
user: order.user,
appointment: order.appointment,
amount: order.amount ? parseFloat(order.amount).toFixed(2) : null,
amountPeso: order.amountPeso ? parseFloat(order.amountPeso).toFixed(2) : null,
amountRmb: order.amountRmb ? parseFloat(order.amountRmb).toFixed(2) : null,
serviceContent: order.serviceContent,
paymentTime: order.paymentTime,
notes: order.notes,
status: order.status,
creator: order.creator,
// Return array of all commissions for dual-currency support
commissions: orderCommissions.length > 0 ? orderCommissions : null,
// Keep backward compatibility with single commission field (first commission)
commission: orderCommissions.length > 0 ? orderCommissions[0] : null,
createdAt: order.createdAt,
};
});
return { return {
records, records,
@ -306,9 +328,9 @@ const getPaymentOrderById = async (orderId) => {
throw new Error('Payment order not found'); throw new Error('Payment order not found');
} }
// Get commission info // Get all commission info (dual-currency support)
const Commission = require('../models/Commission'); const Commission = require('../models/Commission');
const commission = await Commission.findOne({ const commissions = await Commission.findAll({
where: { paymentOrderId: orderId }, where: { paymentOrderId: orderId },
include: [ include: [
{ {
@ -319,6 +341,19 @@ const getPaymentOrderById = async (orderId) => {
], ],
}); });
// Format commissions array
const formattedCommissions = commissions.length > 0
? commissions.map(c => ({
id: c.id,
inviter: c.inviter,
commissionAmount: parseFloat(c.commissionAmount).toFixed(2),
commissionRate: `${(parseFloat(c.commissionRate) * 100).toFixed(2)}%`,
currency: c.currency || 'RMB',
status: c.status,
createdAt: c.createdAt,
}))
: null;
return { return {
id: order.id, id: order.id,
orderNo: order.orderNo, orderNo: order.orderNo,
@ -333,14 +368,10 @@ const getPaymentOrderById = async (orderId) => {
status: order.status, status: order.status,
cancelReason: order.cancelReason, cancelReason: order.cancelReason,
creator: order.creator, creator: order.creator,
commission: commission ? { // Return array of all commissions for dual-currency support
id: commission.id, commissions: formattedCommissions,
inviter: commission.inviter, // Keep backward compatibility with single commission field (first commission)
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2), commission: formattedCommissions && formattedCommissions.length > 0 ? formattedCommissions[0] : null,
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
status: commission.status,
createdAt: commission.createdAt,
} : null,
createdAt: order.createdAt, createdAt: order.createdAt,
updatedAt: order.updatedAt, updatedAt: order.updatedAt,
}; };
@ -375,25 +406,59 @@ const cancelPaymentOrder = async (orderId, cancelReason) => {
order.cancelReason = cancelReason.trim(); order.cancelReason = cancelReason.trim();
await order.save({ transaction }); await order.save({ transaction });
// Cancel related commission and revert balance // Find ALL related commissions (dual-currency support)
const Commission = require('../models/Commission'); const Commission = require('../models/Commission');
const commission = await Commission.findOne({ const commissions = await Commission.findAll({
where: { paymentOrderId: orderId, status: 'credited' }, where: { paymentOrderId: orderId, status: 'credited' },
transaction, transaction,
}); });
if (commission) { let commissionsCancelled = 0;
// Revert inviter's balance
const inviter = await User.findByPk(commission.inviterId, { transaction }); if (commissions.length > 0) {
if (inviter) { // Group commissions by inviter to batch balance updates
inviter.balance = parseFloat(inviter.balance) - parseFloat(commission.commissionAmount); const inviterBalanceUpdates = {};
if (inviter.balance < 0) inviter.balance = 0;
await inviter.save({ transaction }); for (const commission of commissions) {
const inviterId = commission.inviterId;
const currency = commission.currency || 'RMB';
const amount = parseFloat(commission.commissionAmount);
if (!inviterBalanceUpdates[inviterId]) {
inviterBalanceUpdates[inviterId] = { rmbDeduction: 0, pesoDeduction: 0 };
}
if (currency === 'PHP') {
inviterBalanceUpdates[inviterId].pesoDeduction += amount;
} else {
inviterBalanceUpdates[inviterId].rmbDeduction += amount;
}
// Cancel commission
commission.status = 'cancelled';
await commission.save({ transaction });
commissionsCancelled++;
} }
// Cancel commission // Update inviter balances
commission.status = 'cancelled'; for (const [inviterId, deductions] of Object.entries(inviterBalanceUpdates)) {
await commission.save({ transaction }); const inviter = await User.findByPk(inviterId, { transaction });
if (inviter) {
// Deduct RMB balance (floor at zero)
if (deductions.rmbDeduction > 0) {
inviter.balance = parseFloat(inviter.balance || 0) - deductions.rmbDeduction;
if (inviter.balance < 0) inviter.balance = 0;
}
// Deduct Peso balance (floor at zero)
if (deductions.pesoDeduction > 0) {
inviter.balancePeso = parseFloat(inviter.balancePeso || 0) - deductions.pesoDeduction;
if (inviter.balancePeso < 0) inviter.balancePeso = 0;
}
await inviter.save({ transaction });
}
}
} }
await transaction.commit(); await transaction.commit();
@ -403,7 +468,8 @@ const cancelPaymentOrder = async (orderId, cancelReason) => {
orderNo: order.orderNo, orderNo: order.orderNo,
status: order.status, status: order.status,
cancelReason: order.cancelReason, cancelReason: order.cancelReason,
commissionCancelled: !!commission, commissionCancelled: commissionsCancelled > 0,
commissionsCancelledCount: commissionsCancelled,
}; };
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();

File diff suppressed because it is too large Load Diff