feat: add admin transaction create/update with auto payment status sync
Allow admins to edit transaction amount and status, recalculate payment totals, and mark invoices fully paid when paid transactions cover the payable amount.
This commit is contained in:
@@ -62,6 +62,43 @@ const refreshPaymentTotals = async (payment) => {
|
||||
return payment;
|
||||
};
|
||||
|
||||
const emitPaymentStatusChangedIfNeeded = (payment, previousStatus, actorId = null) => {
|
||||
if (payment.status === previousStatus) return;
|
||||
eventEmitter.emit(EVENT_NAMES.PAYMENT_STATUS_CHANGED, {
|
||||
paymentId: payment._id,
|
||||
userId: payment.user,
|
||||
oldStatus: previousStatus,
|
||||
newStatus: payment.status,
|
||||
actorId
|
||||
});
|
||||
};
|
||||
|
||||
const reconcilePendingTransactions = async (payment) => {
|
||||
const transactions = await Transaction.find({ payment: payment._id }).lean();
|
||||
const remaining = getPayableAmount(payment) - sumPaidTransactions(transactions);
|
||||
const pending = await Transaction.find({ payment: payment._id, status: 'pending' }).sort({ dueDate: 1 });
|
||||
|
||||
if (remaining <= 0) {
|
||||
await Transaction.deleteMany({ payment: payment._id, status: 'pending' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pending.length) {
|
||||
pending[0].amount = remaining;
|
||||
await pending[0].save();
|
||||
if (pending.length > 1) {
|
||||
await Transaction.deleteMany({ _id: { $in: pending.slice(1).map((row) => row._id) } });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await Transaction.create(buildTransactionPayload(payment, {
|
||||
amount: remaining,
|
||||
status: 'pending',
|
||||
dueDate: payment.dueDate || new Date()
|
||||
}));
|
||||
};
|
||||
|
||||
const buildTransactionPayload = (payment, trxData, actorId = null) => {
|
||||
const status = trxData.status === 'pending' ? 'pending' : (trxData.status === 'paid' || trxData.date ? 'paid' : 'pending');
|
||||
const paidDate = parseDate(trxData.date) || (status === 'paid' ? new Date() : undefined);
|
||||
@@ -248,15 +285,7 @@ const updatePayment = async (id, body, actorId = null) => {
|
||||
await refreshPaymentTotals(payment);
|
||||
}
|
||||
|
||||
if (body.status && body.status !== previousStatus) {
|
||||
eventEmitter.emit(EVENT_NAMES.PAYMENT_STATUS_CHANGED, {
|
||||
paymentId: payment._id,
|
||||
userId: payment.user,
|
||||
oldStatus: previousStatus,
|
||||
newStatus: payment.status,
|
||||
actorId
|
||||
});
|
||||
}
|
||||
emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
|
||||
return getPaymentById(payment._id);
|
||||
};
|
||||
@@ -274,26 +303,60 @@ const addTransaction = async (paymentId, trxData, actorId = null) => {
|
||||
const payment = await Payment.findById(paymentId);
|
||||
if (!payment) throw new AppError('PAYMENT_NOT_FOUND');
|
||||
|
||||
const previousStatus = payment.status;
|
||||
const payload = buildTransactionPayload(payment, { ...trxData, status: trxData.status || 'paid' }, actorId);
|
||||
if (!payload.amount) throw new AppError('VALIDATION_FAILED', { amount: 'Amount is required' }, 'مبلغ تراکنش الزامی است.');
|
||||
await Transaction.create(payload);
|
||||
|
||||
const remaining = getPayableAmount(payment) - sumPaidTransactions(await Transaction.find({ payment: payment._id }).lean());
|
||||
const pending = await Transaction.find({ payment: payment._id, status: 'pending' }).sort({ dueDate: 1 });
|
||||
if (remaining <= 0) {
|
||||
await Transaction.deleteMany({ payment: payment._id, status: 'pending' });
|
||||
} else if (pending.length) {
|
||||
pending[0].amount = remaining;
|
||||
await pending[0].save();
|
||||
if (pending.length > 1) {
|
||||
await Transaction.deleteMany({ _id: { $in: pending.slice(1).map((row) => row._id) } });
|
||||
}
|
||||
}
|
||||
|
||||
await reconcilePendingTransactions(payment);
|
||||
await refreshPaymentTotals(payment);
|
||||
|
||||
eventEmitter.emit(EVENT_NAMES.PAYMENT_TRANSACTION_ADDED, {
|
||||
paymentId: payment._id,
|
||||
userId: payment.user,
|
||||
actorId
|
||||
});
|
||||
emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
|
||||
return getPaymentById(paymentId);
|
||||
};
|
||||
|
||||
const updateTransaction = async (transactionId, body, actorId = null) => {
|
||||
const trx = await Transaction.findById(transactionId);
|
||||
if (!trx) throw new AppError('TRANSACTION_NOT_FOUND');
|
||||
|
||||
const payment = await Payment.findById(trx.payment);
|
||||
if (!payment) throw new AppError('PAYMENT_NOT_FOUND');
|
||||
|
||||
const previousStatus = payment.status;
|
||||
const merged = {
|
||||
amount: body.amount !== undefined ? body.amount : trx.amount,
|
||||
status: body.status !== undefined ? body.status : trx.status,
|
||||
method: body.method !== undefined ? body.method : trx.method,
|
||||
receiptNumber: body.receiptNumber !== undefined ? body.receiptNumber : trx.receiptNumber,
|
||||
notes: body.notes !== undefined ? body.notes : trx.notes,
|
||||
dueDate: body.dueDate !== undefined ? body.dueDate : trx.dueDate,
|
||||
date: body.date !== undefined ? body.date : trx.date
|
||||
};
|
||||
|
||||
const payload = buildTransactionPayload(payment, merged, actorId);
|
||||
trx.amount = payload.amount;
|
||||
trx.status = payload.status;
|
||||
trx.method = payload.method;
|
||||
trx.receiptNumber = payload.receiptNumber;
|
||||
trx.notes = payload.notes;
|
||||
trx.dueDate = payload.dueDate;
|
||||
trx.date = payload.date;
|
||||
if (actorId) trx.recordedBy = actorId;
|
||||
|
||||
await trx.save();
|
||||
await reconcilePendingTransactions(payment);
|
||||
await refreshPaymentTotals(payment);
|
||||
emitPaymentStatusChangedIfNeeded(payment, previousStatus, actorId);
|
||||
|
||||
return getPaymentById(payment._id);
|
||||
};
|
||||
|
||||
const getMyPayments = async (userId, query = {}) => {
|
||||
return getAllPayments({ ...query, userId });
|
||||
};
|
||||
@@ -306,10 +369,12 @@ module.exports = {
|
||||
deletePayment,
|
||||
searchPayments,
|
||||
addTransaction,
|
||||
updateTransaction,
|
||||
getMyPayments,
|
||||
createTransactionsForPayment,
|
||||
refreshPaymentTotals,
|
||||
buildTransactionPayload,
|
||||
reconcilePendingTransactions,
|
||||
// Aliases for older call sites
|
||||
getAll: getAllPayments,
|
||||
getOne: getPaymentById,
|
||||
|
||||
Reference in New Issue
Block a user