佣金2
This commit is contained in:
parent
784e373c7e
commit
c79e164f87
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
175
backend/src/scripts/fixDualCurrencyCommissions.js
Normal file
175
backend/src/scripts/fixDualCurrencyCommissions.js
Normal 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();
|
||||||
|
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
1637
backend/src/tests/dualCurrencyCommission.property.test.js
Normal file
1637
backend/src/tests/dualCurrencyCommission.property.test.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user