appointment_system/backend/src/services/adminWithdrawalService.js
2025-12-24 16:26:43 +08:00

311 lines
8.7 KiB
JavaScript

const User = require('../models/User');
const Withdrawal = require('../models/Withdrawal');
const { sequelize } = require('../config/database');
const { Op } = require('sequelize');
const logger = require('../config/logger');
/**
* Admin Withdrawal Service
* Handles withdrawal management operations for admin panel
*/
/**
* Get paginated withdrawal list with search and filters
* @param {Object} options - Query options
* @returns {Promise<Object>} Withdrawal list with pagination
*/
const getWithdrawalList = async (options = {}) => {
const {
page = 1,
limit = 20,
status = '',
search = '',
sortBy = 'createdAt',
sortOrder = 'DESC',
} = options;
const offset = (page - 1) * limit;
// Build where clause
const where = {};
// Filter by status
if (status) {
where.status = status;
}
// Search by withdrawal number or user info
let userIds = [];
if (search) {
// Search for users matching the search term
const users = await User.findAll({
where: {
[Op.or]: [
{ nickname: { [Op.like]: `%${search}%` } },
{ realName: { [Op.like]: `%${search}%` } },
{ phone: { [Op.like]: `%${search}%` } },
],
},
attributes: ['id'],
});
userIds = users.map(u => u.id);
// Also search by withdrawal number
where[Op.or] = [
{ withdrawalNo: { [Op.like]: `%${search}%` } },
...(userIds.length > 0 ? [{ userId: { [Op.in]: userIds } }] : []),
];
}
// Query withdrawals with user information
const { count, rows: withdrawals } = await Withdrawal.findAndCountAll({
where,
limit: parseInt(limit),
offset,
order: [[sortBy, sortOrder]],
include: [
{
model: User,
as: 'user',
attributes: ['id', 'nickname', 'realName', 'phone', 'whatsapp', 'wechatId'],
},
],
});
// Format response
const formattedWithdrawals = withdrawals.map(withdrawal => ({
id: withdrawal.id,
withdrawalNo: withdrawal.withdrawalNo,
amount: parseFloat(withdrawal.amount).toFixed(2),
paymentMethod: withdrawal.paymentMethod,
paymentDetails: withdrawal.paymentDetails,
status: withdrawal.status,
reviewedBy: withdrawal.reviewedBy,
reviewedAt: withdrawal.reviewedAt,
rejectionReason: withdrawal.rejectionReason,
completedAt: withdrawal.completedAt,
createdAt: withdrawal.createdAt,
updatedAt: withdrawal.updatedAt,
user: withdrawal.user ? {
id: withdrawal.user.id,
nickname: withdrawal.user.nickname,
realName: withdrawal.user.realName,
phone: withdrawal.user.phone,
whatsapp: withdrawal.user.whatsapp,
wechatId: withdrawal.user.wechatId,
} : null,
}));
return {
withdrawals: formattedWithdrawals,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(count / limit),
},
};
};
/**
* Approve withdrawal request
* @param {String} withdrawalId - Withdrawal ID
* @param {String} adminId - Admin ID performing the action
* @param {String} notes - Optional notes
* @returns {Promise<Object>} Updated withdrawal
*/
const approveWithdrawal = async (withdrawalId, adminId, notes = '') => {
const transaction = await sequelize.transaction();
try {
// Get withdrawal with lock
const withdrawal = await Withdrawal.findByPk(withdrawalId, {
transaction,
lock: transaction.LOCK.UPDATE,
include: [
{
model: User,
as: 'user',
attributes: ['id', 'balance', 'balancePeso'],
},
],
});
if (!withdrawal) {
throw new Error('Withdrawal not found');
}
// Check if withdrawal is in waiting status
if (withdrawal.status !== 'waiting') {
throw new Error(`Cannot approve withdrawal with status: ${withdrawal.status}`);
}
// Update withdrawal status (balance was already deducted when user submitted the request)
await withdrawal.update({
status: 'completed',
reviewedBy: adminId,
reviewedAt: new Date(),
completedAt: new Date(),
}, { transaction });
await transaction.commit();
logger.info(`Withdrawal ${withdrawal.withdrawalNo} approved by admin ${adminId}`);
// Get current user balance for response
const user = withdrawal.user;
const currency = withdrawal.currency || 'CNY';
const currentBalance = currency === 'PHP'
? parseFloat(user?.balancePeso || 0).toFixed(2)
: parseFloat(user?.balance || 0).toFixed(2);
return {
id: withdrawal.id,
withdrawalNo: withdrawal.withdrawalNo,
amount: parseFloat(withdrawal.amount).toFixed(2),
currency: currency,
status: withdrawal.status,
reviewedBy: withdrawal.reviewedBy,
reviewedAt: withdrawal.reviewedAt,
completedAt: withdrawal.completedAt,
userBalance: currentBalance,
};
} catch (error) {
await transaction.rollback();
logger.error('Approve withdrawal error:', error);
throw error;
}
};
/**
* Reject withdrawal request
* @param {String} withdrawalId - Withdrawal ID
* @param {String} adminId - Admin ID performing the action
* @param {String} reason - Rejection reason (required)
* @returns {Promise<Object>} Updated withdrawal
*/
const rejectWithdrawal = async (withdrawalId, adminId, reason) => {
if (!reason || reason.trim() === '') {
throw new Error('Rejection reason is required');
}
const transaction = await sequelize.transaction();
try {
// Get withdrawal with lock
const withdrawal = await Withdrawal.findByPk(withdrawalId, {
transaction,
lock: transaction.LOCK.UPDATE,
});
if (!withdrawal) {
throw new Error('Withdrawal not found');
}
// Check if withdrawal is in waiting status
if (withdrawal.status !== 'waiting') {
throw new Error(`Cannot reject withdrawal with status: ${withdrawal.status}`);
}
// Get user with lock to refund the balance
const user = await User.findByPk(withdrawal.userId, {
transaction,
lock: transaction.LOCK.UPDATE,
});
if (!user) {
throw new Error('User not found');
}
// Refund the withdrawal amount to user balance based on currency
const withdrawalAmount = parseFloat(withdrawal.amount);
const currency = withdrawal.currency || 'CNY';
if (currency === 'PHP') {
user.balancePeso = parseFloat(user.balancePeso || 0) + withdrawalAmount;
} else {
user.balance = parseFloat(user.balance || 0) + withdrawalAmount;
}
await user.save({ transaction });
// Update withdrawal status
await withdrawal.update({
status: 'rejected',
reviewedBy: adminId,
reviewedAt: new Date(),
rejectionReason: reason,
}, { transaction });
await transaction.commit();
logger.info(`Withdrawal ${withdrawal.withdrawalNo} rejected by admin ${adminId}, amount refunded to user`);
return {
id: withdrawal.id,
withdrawalNo: withdrawal.withdrawalNo,
amount: parseFloat(withdrawal.amount).toFixed(2),
currency: currency,
status: withdrawal.status,
reviewedBy: withdrawal.reviewedBy,
reviewedAt: withdrawal.reviewedAt,
rejectionReason: withdrawal.rejectionReason,
};
} catch (error) {
await transaction.rollback();
logger.error('Reject withdrawal error:', error);
throw error;
}
};
/**
* Get withdrawal details
* @param {String} withdrawalId - Withdrawal ID
* @returns {Promise<Object>} Withdrawal details with user info
*/
const getWithdrawalDetails = async (withdrawalId) => {
const withdrawal = await Withdrawal.findByPk(withdrawalId, {
include: [
{
model: User,
as: 'user',
attributes: ['id', 'nickname', 'realName', 'phone', 'whatsapp', 'wechatId', 'balance'],
},
],
});
if (!withdrawal) {
throw new Error('Withdrawal not found');
}
return {
id: withdrawal.id,
withdrawalNo: withdrawal.withdrawalNo,
amount: parseFloat(withdrawal.amount).toFixed(2),
paymentMethod: withdrawal.paymentMethod,
paymentDetails: withdrawal.paymentDetails,
status: withdrawal.status,
reviewedBy: withdrawal.reviewedBy,
reviewedAt: withdrawal.reviewedAt,
rejectionReason: withdrawal.rejectionReason,
completedAt: withdrawal.completedAt,
createdAt: withdrawal.createdAt,
updatedAt: withdrawal.updatedAt,
user: withdrawal.user ? {
id: withdrawal.user.id,
nickname: withdrawal.user.nickname,
realName: withdrawal.user.realName,
phone: withdrawal.user.phone,
whatsapp: withdrawal.user.whatsapp,
wechatId: withdrawal.user.wechatId,
balance: parseFloat(withdrawal.user.balance).toFixed(2),
} : null,
};
};
module.exports = {
getWithdrawalList,
approveWithdrawal,
rejectWithdrawal,
getWithdrawalDetails,
};