Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/controllers/batchTransferController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { AuthRequest } from "../middleware/auth.unified.middleware";
import { getAvailableBalance } from "../utils/walletBalance";
import { resolveWalletWhere, validateFundsAvailability } from "../utils/transferValidation";
import { notifyPaymentReceived, notifyBatchTransferCompleted } from "../utils/notificationHelpers";
import { findOrCreateDMChat } from "../services/chatService";
import { postMoneyChatMessage } from "../services/moneyChatMessageService";

const {
Wallet,
Expand Down Expand Up @@ -479,7 +481,13 @@ export const createBatchTransfer = async (

totalSent += transferAmount;
successCount += 1;
results.push({ ...recipientIdentity, amount: transferAmount, status: "success", transactionId: newTransaction.id });
results.push({
...recipientIdentity,
amount: transferAmount,
status: "success",
transactionId: newTransaction.id,
referenceId: newTransaction.referenceId,
});
}

const failureCount = recipients.length - successCount;
Expand Down Expand Up @@ -538,6 +546,34 @@ export const createBatchTransfer = async (
console.error("Batch transfer notification error:", notificationError);
}

// Best-effort: post a normal money-sent message in each recipient's DM, same as an
// individual chat send would. Never let this affect the response - the transfers
// already committed above.
if (senderUserId) {
const io = req.app.get("io");
for (const result of results) {
if (result.status !== "success" || !result.receiverUserId) continue;
try {
const recipientUser = await User.findByPk(result.receiverUserId);
const senderUser = await User.findByPk(senderUserId);
const chat = await findOrCreateDMChat(database_models, senderUserId, result.receiverUserId);
await postMoneyChatMessage(io, database_models, chat.id, senderUserId, {
type: "money_transfer",
amount: result.amount,
currency: senderWallet.currency || "RWF",
senderName: senderUser ? `${senderUser.firstName} ${senderUser.lastName}` : "A user",
recipientName: recipientUser ? `${recipientUser.firstName} ${recipientUser.lastName}` : "Recipient",
note: description || "",
transactionId: result.transactionId,
referenceId: result.referenceId,
timestamp: new Date().toISOString(),
});
} catch (messageError) {
console.error(`Batch transfer chat message error (recipient ${result.receiverUserId}):`, messageError);
}
}
}

res.status(200).json({
success: true,
message: `Batch transfer processed: ${successCount} succeeded, ${failureCount} failed`,
Expand Down
79 changes: 52 additions & 27 deletions src/controllers/chatMoneyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,13 @@ export const sendMoneyInChat = async (
let recipientUser: any;
let recipientWallet: any;
let isGroupFundraising = false;
let isSharedWalletDeposit = false;
let groupDetails: any;

// Handle group chats with fundraising
// Handle group chats with fundraising or a shared wallet
if (chat.isGroup) {
const group = (chat as any).group;

if (!group) {
await dbTransaction.rollback();
res.status(404).json({
Expand All @@ -121,12 +122,12 @@ export const sendMoneyInChat = async (
return;
}

// Check if group has fundraising enabled
if (!group.hasFundraising) {
// Check if group accepts incoming money at all
if (!group.hasFundraising && !group.sharedWalletId) {
await dbTransaction.rollback();
res.status(400).json({
success: false,
message: "This group does not accept donations. Money can only be sent to fundraising groups."
message: "This group does not accept money. Money can only be sent to fundraising groups or shared wallets."
});
return;
}
Expand All @@ -150,7 +151,8 @@ export const sendMoneyInChat = async (
return;
}

isGroupFundraising = true;
isGroupFundraising = !!group.hasFundraising;
isSharedWalletDeposit = !isGroupFundraising && !!group.sharedWalletId;
groupDetails = group;
recipientWallet = group.wallet;
} else {
Expand All @@ -171,6 +173,11 @@ export const sendMoneyInChat = async (
recipientUser = recipientParticipant.get('user') as any;
}

// True whenever the recipient is a group (fundraising or shared wallet) rather
// than a DM participant - used wherever the code needs "group name" instead of
// "recipientUser's name", regardless of which kind of group wallet it is.
const isGroupRecipient = isGroupFundraising || isSharedWalletDeposit;

// Get authenticated user for PIN verification
const authenticatedUser = await models.User.findByPk(userId, {
transaction: dbTransaction
Expand Down Expand Up @@ -275,7 +282,7 @@ export const sendMoneyInChat = async (
}

// Get or verify receiver wallet based on chat type
if (!isGroupFundraising) {
if (!isGroupRecipient) {
recipientWallet = await models.Wallet.findOne({
where: { userId: recipientId, isActive: true },
lock: dbTransaction.LOCK.UPDATE,
Expand Down Expand Up @@ -339,9 +346,11 @@ export const sendMoneyInChat = async (

// Determine transaction type and description
const transactionType = isGroupFundraising ? 'donation' : 'transfer';
const description = note || (isGroupFundraising
? `Donation to ${groupDetails.name}`
: `Money sent via chat`);
const description = note || (isGroupFundraising
? `Donation to ${groupDetails.name}`
: isSharedWalletDeposit
? `Deposit to ${groupDetails.name}`
: `Money sent via chat`);

// Create transaction record
const transactionRecord = await models.Transaction.create({
Expand Down Expand Up @@ -394,8 +403,8 @@ export const sendMoneyInChat = async (
try {
console.log('Starting PDF receipt generation...');

const recipientName = isGroupFundraising
? groupDetails.name
const recipientName = isGroupRecipient
? groupDetails.name
: `${recipientUser.firstName} ${recipientUser.lastName}`;

const newBalance = parseFloat(recipientWallet.balance.toString());
Expand All @@ -416,8 +425,8 @@ export const sendMoneyInChat = async (
status: 'completed',
note: note,
isGroupDonation: isGroupFundraising,
groupName: isGroupFundraising ? groupDetails.name : undefined,
fundraisingTarget: isGroupFundraising && groupDetails.fundraisingTarget
groupName: isGroupRecipient ? groupDetails.name : undefined,
fundraisingTarget: isGroupFundraising && groupDetails.fundraisingTarget
? parseFloat(groupDetails.fundraisingTarget.toString())
: undefined,
currentProgress: isGroupFundraising ? newBalance : undefined
Expand Down Expand Up @@ -459,18 +468,18 @@ export const sendMoneyInChat = async (
console.log('Preparing message content...');

// Create well-structured message content
const recipientName = isGroupFundraising
? groupDetails.name
const recipientName = isGroupRecipient
? groupDetails.name
: `${recipientUser.firstName} ${recipientUser.lastName}`;

const messageContent = JSON.stringify({
type: isGroupFundraising ? 'group_donation' : 'money_transfer',
type: isGroupFundraising ? 'group_donation' : isSharedWalletDeposit ? 'shared_wallet_deposit' : 'money_transfer',
amount: transferAmount,
currency: 'RWF',
senderName: `${authenticatedUser.firstName} ${authenticatedUser.lastName}`,
recipientName: recipientName,
groupId: isGroupFundraising ? groupDetails.id : undefined,
groupName: isGroupFundraising ? groupDetails.name : undefined,
groupId: isGroupRecipient ? groupDetails.id : undefined,
groupName: isGroupRecipient ? groupDetails.name : undefined,
note: note || '',
transactionId: transactionRecord.id,
referenceId: transactionRecord.referenceId,
Expand Down Expand Up @@ -532,14 +541,18 @@ export const sendMoneyInChat = async (
io.to(`user_${p.userId}`).emit('new_message', broadcastMessage);

// Send notification to recipient (for DMs) or all group members (for groups)
if (isGroupFundraising || p.userId === recipientId) {
const notificationEvent = isGroupFundraising ? 'group_donation_received' : 'money_received';
if (isGroupRecipient || p.userId === recipientId) {
const notificationEvent = isGroupFundraising
? 'group_donation_received'
: isSharedWalletDeposit
? 'shared_wallet_deposit_received'
: 'money_received';
io.to(`user_${p.userId}`).emit(notificationEvent, {
amount: transferAmount,
currency: 'RWF',
from: `${authenticatedUser.firstName} ${authenticatedUser.lastName}`,
groupId: isGroupFundraising ? groupDetails.id : undefined,
groupName: isGroupFundraising ? groupDetails.name : undefined,
groupId: isGroupRecipient ? groupDetails.id : undefined,
groupName: isGroupRecipient ? groupDetails.name : undefined,
transactionId: transactionRecord.id,
referenceId: transactionRecord.referenceId,
receiptUrl: receiptUrl || '',
Expand All @@ -565,15 +578,27 @@ export const sendMoneyInChat = async (
donationAmount: transferAmount
});
}

// Emit balance update for shared wallet deposits, so the chat header badge
// (SharedWalletBalanceBadge) refreshes live instead of only on next load.
if (isSharedWalletDeposit) {
const newBalance = parseFloat(recipientWallet.balance.toString());
io.to(`group:${groupDetails.id}`).emit('shared_wallet_balance_update', {
groupId: groupDetails.id,
balance: newBalance
});
}
}

const recipientDisplayName = isGroupFundraising
? groupDetails.name
const recipientDisplayName = isGroupRecipient
? groupDetails.name
: `${recipientUser.firstName} ${recipientUser.lastName}`;

const successMessage = isGroupFundraising
? `Successfully donated RWF ${transferAmount.toLocaleString('en-US', { minimumFractionDigits: 2 })} to ${recipientDisplayName}`
: `Successfully sent RWF ${transferAmount.toLocaleString('en-US', { minimumFractionDigits: 2 })} to ${recipientDisplayName}`;
: isSharedWalletDeposit
? `Successfully added RWF ${transferAmount.toLocaleString('en-US', { minimumFractionDigits: 2 })} to ${recipientDisplayName}`
: `Successfully sent RWF ${transferAmount.toLocaleString('en-US', { minimumFractionDigits: 2 })} to ${recipientDisplayName}`;

res.status(201).json({
success: true,
Expand All @@ -589,7 +614,7 @@ export const sendMoneyInChat = async (
type: transactionType,
recipientName: recipientDisplayName,
isGroupDonation: isGroupFundraising,
groupId: isGroupFundraising ? groupDetails.id : undefined,
groupId: isGroupRecipient ? groupDetails.id : undefined,
status: 'completed',
timestamp: new Date().toISOString()
},
Expand Down
Loading
Loading