Compare commits

...

8 Commits

Author SHA1 Message Date
47b825c4b2 Merge pull request #89 from Zod-Alkhair/feature/notification-system-fcm-registration
feat: enhance card service and transaction notification for shared ac…
2026-01-20 16:28:42 +03:00
f5c3b03264 feat: enhance card service and transaction notification for shared account handling
- Updated CardService to differentiate between shared and separate accounts during card control updates, optimizing balance allocation.
- Enhanced TransactionNotificationListener to accurately reflect balance based on account structure for internal transfers and top-ups.
- Improved logging for better traceability of account operations and balance calculations.
2026-01-20 16:27:32 +03:00
6a250efd5e Merge pull request #88 from Zod-Alkhair/feature/notification-system-fcm-registration
feat: enhance transaction notification listener for internal transfer…
2026-01-20 15:21:45 +03:00
a09b84e475 feat: enhance transaction notification listener for internal transfer support
- Updated TransactionNotificationListener to differentiate between internal transfers and external top-ups for child accounts.
- Added new notification scopes and messages for internal transfers from parent to child.
- Improved balance retrieval logic to ensure accurate account balances are displayed in notifications.
- Enhanced localization support by adding relevant keys for internal transfer notifications in both English and Arabic.
2026-01-20 15:20:58 +03:00
604cb7ce25 Merge pull request #87 from Zod-Alkhair/feature/notification-system-fcm-registration
fix the messages
2026-01-20 14:42:03 +03:00
4305c4b75f fix the messages 2026-01-20 14:40:16 +03:00
ef5572440c Merge pull request #86 from Zod-Alkhair/fix/spending-history-junior-id-query
fix: correct junior ID to customer ID mapping in transaction queries
2026-01-20 12:50:34 +03:00
64623c7cea fix: correct junior ID to customer ID mapping in transaction queries
Fixed spending history and related transaction queries that were incorrectly
using juniorId as customerId. The queries now properly join through the
Customer -> Junior relationship to filter by junior ID.

Affected methods:
- getTransactionsForCardWithinDateRange (spending history)
- findTransfersToJunior (transfers list)
- countTransfersToJunior (transfers count)
- findTransactionById (transaction details)

This fixes the spending history endpoint which was returning empty results
due to ID mismatch between Junior entity ID and Customer entity ID.

Performance impact: Minimal (~1-2ms overhead from additional joins on
indexed foreign keys). The queries now return correct results instead of
0 results.
2026-01-20 12:39:10 +03:00
7 changed files with 274 additions and 102 deletions

View File

@ -92,7 +92,9 @@ export class TransactionRepository {
return this.transactionRepository return this.transactionRepository
.createQueryBuilder('transaction') .createQueryBuilder('transaction')
.innerJoinAndSelect('transaction.card', 'card') .innerJoinAndSelect('transaction.card', 'card')
.where('card.customerId = :juniorId', { juniorId }) .innerJoin('card.customer', 'customer')
.innerJoin('customer.junior', 'junior')
.where('junior.id = :juniorId', { juniorId })
.andWhere('transaction.transactionScope = :scope', { scope: TransactionScope.CARD }) .andWhere('transaction.transactionScope = :scope', { scope: TransactionScope.CARD })
.andWhere('transaction.transactionType = :type', { type: TransactionType.EXTERNAL }) .andWhere('transaction.transactionType = :type', { type: TransactionType.EXTERNAL })
.andWhere('transaction.transactionDate BETWEEN :startDate AND :endDate', { startDate, endDate }) .andWhere('transaction.transactionDate BETWEEN :startDate AND :endDate', { startDate, endDate })
@ -153,7 +155,9 @@ export class TransactionRepository {
.createQueryBuilder('tx') .createQueryBuilder('tx')
.innerJoinAndSelect('tx.card', 'card') .innerJoinAndSelect('tx.card', 'card')
.innerJoinAndSelect('card.account', 'account') .innerJoinAndSelect('card.account', 'account')
.where('card.customerId = :juniorId', { juniorId }) .innerJoin('card.customer', 'customer')
.innerJoin('customer.junior', 'junior')
.where('junior.id = :juniorId', { juniorId })
.andWhere('tx.transactionScope = :scope', { scope: TransactionScope.CARD }) .andWhere('tx.transactionScope = :scope', { scope: TransactionScope.CARD })
.andWhere('tx.transactionType = :type', { type: TransactionType.INTERNAL }) .andWhere('tx.transactionType = :type', { type: TransactionType.INTERNAL })
.orderBy('tx.transactionDate', 'DESC') .orderBy('tx.transactionDate', 'DESC')
@ -166,7 +170,9 @@ export class TransactionRepository {
return this.transactionRepository return this.transactionRepository
.createQueryBuilder('tx') .createQueryBuilder('tx')
.innerJoin('tx.card', 'card') .innerJoin('tx.card', 'card')
.where('card.customerId = :juniorId', { juniorId }) .innerJoin('card.customer', 'customer')
.innerJoin('customer.junior', 'junior')
.where('junior.id = :juniorId', { juniorId })
.andWhere('tx.transactionScope = :scope', { scope: TransactionScope.CARD }) .andWhere('tx.transactionScope = :scope', { scope: TransactionScope.CARD })
.andWhere('tx.transactionType = :type', { type: TransactionType.INTERNAL }) .andWhere('tx.transactionType = :type', { type: TransactionType.INTERNAL })
.getCount(); .getCount();
@ -176,8 +182,10 @@ export class TransactionRepository {
return this.transactionRepository return this.transactionRepository
.createQueryBuilder('tx') .createQueryBuilder('tx')
.innerJoinAndSelect('tx.card', 'card') .innerJoinAndSelect('tx.card', 'card')
.innerJoin('card.customer', 'customer')
.innerJoin('customer.junior', 'junior')
.where('tx.id = :transactionId', { transactionId }) .where('tx.id = :transactionId', { transactionId })
.andWhere('card.customerId = :juniorId', { juniorId }) .andWhere('junior.id = :juniorId', { juniorId })
.getOne(); .getOne();
} }
} }

View File

@ -240,16 +240,37 @@ export class CardService {
this.logger.debug(`Updating card control - cardReference: ${card.cardReference}, finalAmount: ${finalAmountNumber}`); this.logger.debug(`Updating card control - cardReference: ${card.cardReference}, finalAmount: ${finalAmountNumber}`);
// Check if child and parent share the same account
const isSharedAccount = card.parentId && card.account.id === fundingAccount.id;
this.logger.debug(
`Account structure - Child account: ${card.account.id}, Parent account: ${fundingAccount.id}, ` +
`Shared: ${isSharedAccount ? 'YES' : 'NO'}`
);
// First, ensure all external operations succeed before creating transaction // First, ensure all external operations succeed before creating transaction
await Promise.all([ if (isSharedAccount) {
this.neoleapService.updateCardControl(card.cardReference, finalAmountNumber), // Shared account: Only update card limit and reserved balance
this.updateCardLimit(card.id, finalAmountNumber), // Money is already in the shared account, just allocate it to the child
this.accountService.increaseReservedBalance(fundingAccount, amount), this.logger.debug(`Shared account detected - only updating card limit and reserved balance`);
// Increase child account balance await Promise.all([
this.accountService.creditAccountBalance(card.account.accountReference, amount), this.neoleapService.updateCardControl(card.cardReference, finalAmountNumber),
// Decrease parent account balance (only if parent is funding) this.updateCardLimit(card.id, finalAmountNumber),
card.parentId ? this.accountService.decreaseAccountBalance(fundingAccount.accountReference, amount) : Promise.resolve(), this.accountService.increaseReservedBalance(fundingAccount, amount),
]); ]);
} else {
// Separate accounts: Transfer money from parent to child
this.logger.debug(`Separate accounts - transferring money from parent to child`);
await Promise.all([
this.neoleapService.updateCardControl(card.cardReference, finalAmountNumber),
this.updateCardLimit(card.id, finalAmountNumber),
this.accountService.increaseReservedBalance(fundingAccount, amount),
// Increase child account balance
this.accountService.creditAccountBalance(card.account.accountReference, amount),
// Decrease parent account balance
this.accountService.decreaseAccountBalance(fundingAccount.accountReference, amount),
]);
}
// Only create transaction and emit event after all operations succeed // Only create transaction and emit event after all operations succeed
await this.transactionService.createInternalChildTransaction(card.id, amount); await this.transactionService.createInternalChildTransaction(card.id, amount);

View File

@ -6,10 +6,14 @@ export enum NotificationScope {
OTP = 'OTP', OTP = 'OTP',
USER_INVITED = 'USER_INVITED', USER_INVITED = 'USER_INVITED',
// Transaction notifications - Top-up // Transaction notifications - Top-up (external funds)
CHILD_TOP_UP = 'CHILD_TOP_UP', CHILD_TOP_UP = 'CHILD_TOP_UP',
PARENT_TOP_UP_CONFIRMATION = 'PARENT_TOP_UP_CONFIRMATION', PARENT_TOP_UP_CONFIRMATION = 'PARENT_TOP_UP_CONFIRMATION',
// Transaction notifications - Internal Transfer (parent to child)
CHILD_INTERNAL_TRANSFER = 'CHILD_INTERNAL_TRANSFER',
PARENT_INTERNAL_TRANSFER = 'PARENT_INTERNAL_TRANSFER',
// Transaction notifications - Spending // Transaction notifications - Spending
CHILD_SPENDING = 'CHILD_SPENDING', CHILD_SPENDING = 'CHILD_SPENDING',
PARENT_SPENDING_ALERT = 'PARENT_SPENDING_ALERT', PARENT_SPENDING_ALERT = 'PARENT_SPENDING_ALERT',

View File

@ -139,14 +139,39 @@ export class MoneyRequestNotificationListener {
const currency = getCurrency(accountCurrency, null, 'SAR'); const currency = getCurrency(accountCurrency, null, 'SAR');
const formattedAmount = formatCurrencyAmount(amount, currency); const formattedAmount = formatCurrencyAmount(amount, currency);
const locale = this.getUserLocale(parentUser);
this.logger.debug( this.logger.debug(
`Notifying parent (user ${parentUser.id}): ${childName} requested ${formattedAmount} ${currency} - ${reason}` `Notifying parent (user ${parentUser.id}): ${childName} requested ${formattedAmount} ${currency} for ${reason}`
); );
let title: string;
let message: string;
try {
title = this.i18n.t('app.NOTIFICATION.MONEY_REQUEST_CREATED_TITLE', { lang: locale });
message = this.i18n.t('app.NOTIFICATION.MONEY_REQUEST_CREATED_MESSAGE', {
lang: locale,
args: {
childName: childName,
amount: formattedAmount,
currency: currency,
reason: reason,
},
});
} catch (i18nError: any) {
this.logger.error(
`[MoneyRequestNotificationListener] i18n error for parent ${parentUser.id}: ${i18nError?.message || 'Unknown i18n error'}. Falling back to English.`,
i18nError?.stack
);
title = 'Money Request';
message = `${childName} has requested ${formattedAmount} ${currency} for ${reason}.`;
}
await this.notificationFactory.send({ await this.notificationFactory.send({
userId: parentUser.id, userId: parentUser.id,
title: 'Money Request', title,
message: `${childName} requested ${formattedAmount} ${currency}. Reason: ${reason}`, message,
scope: NotificationScope.MONEY_REQUEST_CREATED, scope: NotificationScope.MONEY_REQUEST_CREATED,
preferences: this.getUserPreferences(parentUser), preferences: this.getUserPreferences(parentUser),
data: { data: {

View File

@ -97,36 +97,86 @@ export class TransactionNotificationListener {
return; return;
} }
const scope = isTopUp // Determine scope: internal transfer (parent to child) vs external top-up
? NotificationScope.CHILD_TOP_UP let scope: NotificationScope;
: NotificationScope.CHILD_SPENDING; if (isTopUp) {
scope = isChildSpending
? NotificationScope.CHILD_INTERNAL_TRANSFER // Parent transferring to child
: NotificationScope.CHILD_TOP_UP; // External top-up
} else {
scope = NotificationScope.CHILD_SPENDING;
}
const locale = this.getUserLocale(user); const locale = this.getUserLocale(user);
const amount = transaction.transactionAmount; const amount = transaction.transactionAmount;
const merchant = transaction.merchantName || 'merchant'; const merchant = transaction.merchantName || 'merchant';
// For child top-up notifications, show card.limit (newAmount) instead of account balance // For child notifications, show the appropriate balance based on account structure
// card.limit represents the total spending limit on the card after the transfer
let balance = 0; let balance = 0;
let accountCurrency: string | undefined; let accountCurrency: string | undefined;
if (isTopUp) { if (isTopUp && isChildSpending) {
// For top-up: show card limit (newAmount from transfer response) // Internal transfer: For shared accounts, show card limit (child's spending power)
// For separate accounts, show child's account balance
try { try {
// Reload card to get updated limit // Reload card to get updated data
const cardWithUpdatedLimit = await this.cardService.getCardById(card.id); const cardWithUpdatedBalance = await this.cardService.getCardById(card.id);
balance = cardWithUpdatedLimit.limit || card.limit || 0;
accountCurrency = cardWithUpdatedLimit.account?.currency || card.account?.currency; // Check if child has parent (shared account scenario)
this.logger.debug( if (cardWithUpdatedBalance.parentId) {
`[Child Top-Up Notification] Using card limit (newAmount) - limit: ${balance} ${accountCurrency}` // Likely shared account - use card limit as the child's "balance"
); balance = cardWithUpdatedBalance.limit || card.limit || 0;
accountCurrency = cardWithUpdatedBalance.account?.currency || card.account?.currency;
this.logger.debug(
`[Child Internal Transfer] Shared account - using card limit: ${balance} ${accountCurrency}`
);
} else {
// Separate account - use child's account balance
if (cardWithUpdatedBalance?.account?.accountReference) {
const account = await this.accountService.getAccountByReferenceNumber(
cardWithUpdatedBalance.account.accountReference
);
balance = account.balance;
accountCurrency = account.currency;
this.logger.debug(
`[Child Internal Transfer] Separate account - using account balance: ${balance} ${accountCurrency}`
);
} else {
balance = cardWithUpdatedBalance.account?.balance || card.account?.balance || 0;
accountCurrency = cardWithUpdatedBalance.account?.currency || card.account?.currency;
}
}
} catch (error: any) { } catch (error: any) {
this.logger.warn( this.logger.warn(
`[Child Top-Up Notification] Could not reload card: ${error?.message}. Using card limit from event.` `[Child Internal Transfer] Could not fetch balance: ${error?.message}. Using card limit.`
); );
balance = card.limit || 0; balance = card.limit || 0;
accountCurrency = card.account?.currency; accountCurrency = card.account?.currency;
} }
} else if (isTopUp) {
// External top-up: show child's account balance
try {
const cardWithUpdatedBalance = await this.cardService.getCardById(card.id);
if (cardWithUpdatedBalance?.account?.accountReference) {
const account = await this.accountService.getAccountByReferenceNumber(
cardWithUpdatedBalance.account.accountReference
);
balance = account.balance;
accountCurrency = account.currency;
this.logger.debug(
`[Child Top-Up Notification] Fetched account by reference - balance: ${balance} ${accountCurrency}`
);
} else {
balance = cardWithUpdatedBalance.account?.balance || card.account?.balance || 0;
accountCurrency = cardWithUpdatedBalance.account?.currency || card.account?.currency;
}
} catch (error: any) {
this.logger.warn(
`[Child Top-Up Notification] Could not fetch account: ${error?.message}. Using card balance.`
);
balance = card.account?.balance || 0;
accountCurrency = card.account?.currency;
}
} else { } else {
// For spending: show account balance // For spending: show account balance
try { try {
@ -176,36 +226,44 @@ export class TransactionNotificationListener {
let message: string; let message: string;
try { try {
title = isTopUp if (isTopUp) {
? this.i18n.t('app.NOTIFICATION.CHILD_TOP_UP_TITLE', { lang: locale }) // Internal transfer or external top-up
: this.i18n.t('app.NOTIFICATION.CHILD_SPENDING_TITLE', { lang: locale }); const titleKey = isChildSpending
? 'app.NOTIFICATION.CHILD_INTERNAL_TRANSFER_TITLE'
message = isTopUp : 'app.NOTIFICATION.CHILD_TOP_UP_TITLE';
? this.i18n.t('app.NOTIFICATION.CHILD_TOP_UP_MESSAGE', { const messageKey = isChildSpending
lang: locale, ? 'app.NOTIFICATION.CHILD_INTERNAL_TRANSFER_MESSAGE'
args: { : 'app.NOTIFICATION.CHILD_TOP_UP_MESSAGE';
amount: formattedAmount,
currency: currency, title = this.i18n.t(titleKey, { lang: locale });
balance: formattedBalance, message = this.i18n.t(messageKey, {
}, lang: locale,
}) args: {
: this.i18n.t('app.NOTIFICATION.CHILD_SPENDING_MESSAGE', { amount: formattedAmount,
lang: locale, currency: currency,
args: { balance: formattedBalance,
amount: formattedAmount, },
currency: currency, });
merchant: merchant, } else {
balance: formattedBalance, // Spending
}, title = this.i18n.t('app.NOTIFICATION.CHILD_SPENDING_TITLE', { lang: locale });
}); message = this.i18n.t('app.NOTIFICATION.CHILD_SPENDING_MESSAGE', {
lang: locale,
args: {
amount: formattedAmount,
currency: currency,
merchant: merchant,
},
});
}
} catch (i18nError: any) { } catch (i18nError: any) {
console.error(`[TransactionNotificationListener] i18n error:`, i18nError); console.error(`[TransactionNotificationListener] i18n error:`, i18nError);
this.logger.error(`i18n translation failed: ${i18nError?.message}`, i18nError?.stack); this.logger.error(`i18n translation failed: ${i18nError?.message}`, i18nError?.stack);
// Fallback to English without i18n // Fallback to English without i18n
title = isTopUp ? 'Card Topped Up' : 'Purchase Successful'; title = isTopUp ? 'Funds Credited' : 'Purchase Successful';
message = isTopUp message = isTopUp
? `You received ${formattedAmount} ${currency}. Total balance: ${formattedBalance} ${currency}` ? `${formattedAmount} ${currency} has been added to your card. Total balance: ${formattedBalance} ${currency}`
: `You spent ${formattedAmount} ${currency} at ${merchant}. Balance: ${formattedBalance} ${currency}`; : `You spent ${formattedAmount} ${currency} at ${merchant}`;
} }
this.logger.debug( this.logger.debug(
@ -263,29 +321,80 @@ export class TransactionNotificationListener {
const amount = transaction.transactionAmount; const amount = transaction.transactionAmount;
const merchant = transaction.merchantName || 'a merchant'; const merchant = transaction.merchantName || 'a merchant';
// Get parent's account balance (not child's balance) - reload to get fresh balance // Get parent's available balance (balance - reserved_balance) - reload to get fresh balance
let parentAccountBalance = 0; let parentAccountBalance = 0;
let parentAccountReservedBalance = 0;
let parentAccountCurrency: string | undefined; let parentAccountCurrency: string | undefined;
let availableBalance = 0;
try { try {
if (card.parentId) { if (card.parentId) {
// Always reload parent account to get fresh balance after transaction // Get parent's card to access their account reference
const parentAccount = await this.accountService.getAccountByCustomerId(card.parentId); const parentCard = await this.cardService.getCardByCustomerId(card.parentId);
parentAccountBalance = parentAccount.balance; if (parentCard?.account?.accountReference) {
parentAccountCurrency = parentAccount.currency; // Fetch by reference number to get fresh balance from database
this.logger.debug(`Fetched parent account balance: ${parentAccountBalance}, currency: ${parentAccountCurrency}`); const parentAccount = await this.accountService.getAccountByReferenceNumber(
parentCard.account.accountReference
);
parentAccountBalance = parentAccount.balance;
parentAccountReservedBalance = parentAccount.reservedBalance;
availableBalance = parentAccountBalance - parentAccountReservedBalance;
parentAccountCurrency = parentAccount.currency;
this.logger.debug(
`[Parent Spending] Fetched parent account by reference - balance: ${parentAccountBalance}, reserved: ${parentAccountReservedBalance}, available: ${availableBalance} ${parentAccountCurrency}`
);
} else {
// Fallback: try by customer ID
const parentAccount = await this.accountService.getAccountByCustomerId(card.parentId);
parentAccountBalance = parentAccount.balance;
parentAccountReservedBalance = parentAccount.reservedBalance;
availableBalance = parentAccountBalance - parentAccountReservedBalance;
parentAccountCurrency = parentAccount.currency;
this.logger.debug(
`[Parent Spending] Fetched parent account by customer ID - balance: ${parentAccountBalance}, reserved: ${parentAccountReservedBalance}, available: ${availableBalance} ${parentAccountCurrency}`
);
}
} else { } else {
const parentCustomer = customer?.junior?.guardian?.customer; const parentCustomer = customer?.junior?.guardian?.customer;
if (parentCustomer?.cards?.[0]?.account) { if (parentCustomer?.id) {
// Reload to get fresh balance try {
const parentAccount = await this.accountService.getAccountByCustomerId(parentCustomer.id); const parentCard = await this.cardService.getCardByCustomerId(parentCustomer.id);
parentAccountBalance = parentAccount.balance; if (parentCard?.account?.accountReference) {
parentAccountCurrency = parentAccount.currency; const parentAccount = await this.accountService.getAccountByReferenceNumber(
this.logger.debug(`Fetched parent account balance via customer: ${parentAccountBalance}, currency: ${parentAccountCurrency}`); parentCard.account.accountReference
);
parentAccountBalance = parentAccount.balance;
parentAccountReservedBalance = parentAccount.reservedBalance;
availableBalance = parentAccountBalance - parentAccountReservedBalance;
parentAccountCurrency = parentAccount.currency;
this.logger.debug(
`[Parent Spending] Fetched parent account via customer relation (by reference) - balance: ${parentAccountBalance}, reserved: ${parentAccountReservedBalance}, available: ${availableBalance} ${parentAccountCurrency}`
);
} else {
const parentAccount = await this.accountService.getAccountByCustomerId(parentCustomer.id);
parentAccountBalance = parentAccount.balance;
parentAccountReservedBalance = parentAccount.reservedBalance;
availableBalance = parentAccountBalance - parentAccountReservedBalance;
parentAccountCurrency = parentAccount.currency;
this.logger.debug(
`[Parent Spending] Fetched parent account via customer relation - balance: ${parentAccountBalance}, reserved: ${parentAccountReservedBalance}, available: ${availableBalance} ${parentAccountCurrency}`
);
}
} catch (error: any) {
this.logger.warn(
`[Parent Spending] Could not fetch parent account via customer: ${error?.message}. Using child account balance as fallback.`
);
availableBalance = card.account?.balance || 0;
parentAccountCurrency = card.account?.currency;
}
} else {
availableBalance = card.account?.balance || 0;
parentAccountCurrency = card.account?.currency;
} }
} }
} catch (error: any) { } catch (error: any) {
this.logger.warn(`Could not fetch parent account for parent notification: ${error?.message}, using child account balance as fallback`); this.logger.warn(`[Parent Spending] Could not fetch parent account: ${error?.message}, using child account balance as fallback`);
parentAccountBalance = card.account?.balance || 0; availableBalance = card.account?.balance || 0;
parentAccountCurrency = card.account?.currency; parentAccountCurrency = card.account?.currency;
} }
@ -297,11 +406,12 @@ export class TransactionNotificationListener {
); );
this.logger.debug( this.logger.debug(
`[Parent Spending Notification] Parent account currency: ${parentAccountCurrency}, Account currency: ${accountCurrency}, Transaction currency: ${transaction.transactionCurrency}, Final currency: ${currency}, Parent balance: ${parentAccountBalance}, Amount: ${amount}` `[Parent Spending Notification] Parent account currency: ${parentAccountCurrency}, Account currency: ${accountCurrency}, Transaction currency: ${transaction.transactionCurrency}, Final currency: ${currency}, Parent available balance: ${availableBalance}, Amount: ${amount}`
); );
const formattedAmount = formatCurrencyAmount(amount, currency); const formattedAmount = formatCurrencyAmount(amount, currency);
const formattedBalance = formatCurrencyAmount(parentAccountBalance, currency); // Use available balance for parent spending notification
const formattedBalance = formatCurrencyAmount(availableBalance, currency);
this.logger.debug( this.logger.debug(
`Notifying parent (user ${parentUser.id}): ${childName} spent ${formattedAmount} ${currency} at ${merchant}` `Notifying parent (user ${parentUser.id}): ${childName} spent ${formattedAmount} ${currency} at ${merchant}`
@ -325,8 +435,8 @@ export class TransactionNotificationListener {
} catch (i18nError: any) { } catch (i18nError: any) {
console.error(`[TransactionNotificationListener] i18n error in parent spending:`, i18nError); console.error(`[TransactionNotificationListener] i18n error in parent spending:`, i18nError);
this.logger.error(`i18n translation failed: ${i18nError?.message}`, i18nError?.stack); this.logger.error(`i18n translation failed: ${i18nError?.message}`, i18nError?.stack);
title = 'Child Spending Alert'; title = 'Spending Alert';
message = `${childName} spent ${formattedAmount} ${currency} at ${merchant}. Balance: ${formattedBalance} ${currency}`; message = `${childName} spent ${formattedAmount} ${currency} at ${merchant}. Remaining balance: ${formattedBalance} ${currency}`;
} }
await this.notificationFactory.send({ await this.notificationFactory.send({
@ -360,7 +470,7 @@ export class TransactionNotificationListener {
} }
/** /**
* Notify parent when they top up their child's card * Notify parent when they transfer money to their child's card (internal transfer)
* This is a confirmation notification for the parent * This is a confirmation notification for the parent
*/ */
private async notifyParentOfTopUp(transaction: Transaction, card: Card): Promise<void> { private async notifyParentOfTopUp(transaction: Transaction, card: Card): Promise<void> {
@ -479,15 +589,15 @@ export class TransactionNotificationListener {
const formattedBalance = formatCurrencyAmount(balance, currency); const formattedBalance = formatCurrencyAmount(balance, currency);
this.logger.debug( this.logger.debug(
`Notifying parent (user ${parentUser.id}): Transferred ${formattedAmount} ${currency} to ${childName}, parent balance: ${formattedBalance} ${currency}` `Notifying parent (user ${parentUser.id}): Transferred ${formattedAmount} ${currency} to ${childName}, child balance: ${formattedBalance} ${currency}`
); );
let title: string; let title: string;
let message: string; let message: string;
try { try {
title = this.i18n.t('app.NOTIFICATION.PARENT_TOP_UP_TITLE', { lang: locale }); title = this.i18n.t('app.NOTIFICATION.PARENT_INTERNAL_TRANSFER_TITLE', { lang: locale });
message = this.i18n.t('app.NOTIFICATION.PARENT_TOP_UP_MESSAGE', { message = this.i18n.t('app.NOTIFICATION.PARENT_INTERNAL_TRANSFER_MESSAGE', {
lang: locale, lang: locale,
args: { args: {
amount: formattedAmount, amount: formattedAmount,
@ -497,17 +607,17 @@ export class TransactionNotificationListener {
}, },
}); });
} catch (i18nError: any) { } catch (i18nError: any) {
console.error(`[TransactionNotificationListener] i18n error in parent top-up:`, i18nError); console.error(`[TransactionNotificationListener] i18n error in parent internal transfer:`, i18nError);
this.logger.error(`i18n translation failed: ${i18nError?.message}`, i18nError?.stack); this.logger.error(`i18n translation failed: ${i18nError?.message}`, i18nError?.stack);
title = 'Top-Up Confirmation'; title = 'Internal Transfer Completed';
message = `You transferred ${formattedAmount} ${currency} to ${childName}. Balance: ${formattedBalance} ${currency}`; message = `${formattedAmount} ${currency} has been transferred to ${childName}'s card. ${childName}'s balance is ${formattedBalance} ${currency}`;
} }
await this.notificationFactory.send({ await this.notificationFactory.send({
userId: parentUser.id, userId: parentUser.id,
title, title,
message, message,
scope: NotificationScope.PARENT_TOP_UP_CONFIRMATION, scope: NotificationScope.PARENT_INTERNAL_TRANSFER,
preferences: this.getUserPreferences(parentUser), preferences: this.getUserPreferences(parentUser),
data: { data: {
transactionId: transaction.id, transactionId: transaction.id,

View File

@ -112,17 +112,19 @@
"NOT_FOUND": "لم يتم العثور على البطاقة." "NOT_FOUND": "لم يتم العثور على البطاقة."
}, },
"NOTIFICATION": { "NOTIFICATION": {
"CHILD_TOP_UP_TITLE": "تم شحن البطاقة", "CHILD_TOP_UP_TITLE": "تم إضافة رصيد",
"CHILD_TOP_UP_MESSAGE": "لقد استلمت {amount} {currency}. الرصيد الإجمالي: {balance} {currency}", "CHILD_TOP_UP_MESSAGE": "تمت إضافة {amount} {currency} إلى بطاقتك. إجمالي الرصيد: {balance} {currency}",
"CHILD_SPENDING_TITLE": "تمت العملية بنجاح", "CHILD_INTERNAL_TRANSFER_TITLE": "تم إضافة رصيد",
"CHILD_SPENDING_MESSAGE": "لقد أنفقت {amount} {currency} في {merchant}. الرصيد: {balance} {currency}", "CHILD_INTERNAL_TRANSFER_MESSAGE": "تمت إضافة {amount} {currency} إلى بطاقتك. إجمالي الرصيد: {balance} {currency}",
"PARENT_TOP_UP_TITLE": "تأكيد الشحن", "PARENT_INTERNAL_TRANSFER_TITLE": "اكتمل التحويل",
"PARENT_TOP_UP_MESSAGE": "لقد قمت بتحويل {amount} {currency} إلى {childName}. الرصيد: {balance} {currency}", "PARENT_INTERNAL_TRANSFER_MESSAGE": "تم تحويل {amount} {currency} إلى بطاقة {childName}. رصيد {childName}: {balance} {currency}",
"PARENT_SPENDING_TITLE": "تنبيه إنفاق الطفل", "CHILD_SPENDING_TITLE": "عملية شراء ناجحة",
"PARENT_SPENDING_MESSAGE": "أنفق {childName} {amount} {currency} في {merchant}. الرصيد: {balance} {currency}", "CHILD_SPENDING_MESSAGE": "قمت بإنفاق {amount} {currency} في {merchant}",
"PARENT_SPENDING_TITLE": "تنبيه صرف",
"PARENT_SPENDING_MESSAGE": "قام {childName} بإنفاق {amount} {currency} في {merchant}. الرصيد المتبقي: {balance} {currency}",
"YOUR_CHILD": "طفلك", "YOUR_CHILD": "طفلك",
"MONEY_REQUEST_CREATED_TITLE": "طلب مال", "MONEY_REQUEST_CREATED_TITLE": "طلب مبلغ مالي",
"MONEY_REQUEST_CREATED_MESSAGE": "طلب {childName} مبلغ {amount} {currency}. السبب: {reason}", "MONEY_REQUEST_CREATED_MESSAGE": "طلب {childName} مبلغ {amount} {currency} لـ {reason}",
"MONEY_REQUEST_APPROVED_TITLE": "تمت الموافقة على طلب المال", "MONEY_REQUEST_APPROVED_TITLE": "تمت الموافقة على طلب المال",
"MONEY_REQUEST_APPROVED_MESSAGE": "تمت الموافقة على طلبك بمبلغ {amount} {currency}. تمت إضافة المال إلى حسابك.", "MONEY_REQUEST_APPROVED_MESSAGE": "تمت الموافقة على طلبك بمبلغ {amount} {currency}. تمت إضافة المال إلى حسابك.",
"MONEY_REQUEST_DECLINED_TITLE": "تم رفض طلب المال", "MONEY_REQUEST_DECLINED_TITLE": "تم رفض طلب المال",

View File

@ -111,17 +111,19 @@
"NOT_FOUND": "The card was not found." "NOT_FOUND": "The card was not found."
}, },
"NOTIFICATION": { "NOTIFICATION": {
"CHILD_TOP_UP_TITLE": "Card Topped Up", "CHILD_TOP_UP_TITLE": "Funds Credited",
"CHILD_TOP_UP_MESSAGE": "You received {amount} {currency}. Total balance: {balance} {currency}", "CHILD_TOP_UP_MESSAGE": "{amount} {currency} has been added to your card. Total balance: {balance} {currency}",
"CHILD_INTERNAL_TRANSFER_TITLE": "Funds Credited",
"CHILD_INTERNAL_TRANSFER_MESSAGE": "{amount} {currency} has been added to your card. Total balance: {balance} {currency}",
"PARENT_INTERNAL_TRANSFER_TITLE": "Internal Transfer Completed",
"PARENT_INTERNAL_TRANSFER_MESSAGE": "{amount} {currency} has been transferred to {childName}'s card. {childName}'s balance is {balance} {currency}",
"CHILD_SPENDING_TITLE": "Purchase Successful", "CHILD_SPENDING_TITLE": "Purchase Successful",
"CHILD_SPENDING_MESSAGE": "You spent {amount} {currency} at {merchant}. Balance: {balance} {currency}", "CHILD_SPENDING_MESSAGE": "You spent {amount} {currency} at {merchant}",
"PARENT_TOP_UP_TITLE": "Top-Up Confirmation", "PARENT_SPENDING_TITLE": "Spending Alert",
"PARENT_TOP_UP_MESSAGE": "You transferred {amount} {currency} to {childName}. Balance: {balance} {currency}", "PARENT_SPENDING_MESSAGE": "{childName} spent {amount} {currency} at {merchant}. Remaining balance: {balance} {currency}",
"PARENT_SPENDING_TITLE": "Child Spending Alert",
"PARENT_SPENDING_MESSAGE": "{childName} spent {amount} {currency} at {merchant}. Balance: {balance} {currency}",
"YOUR_CHILD": "Your child", "YOUR_CHILD": "Your child",
"MONEY_REQUEST_CREATED_TITLE": "Money Request", "MONEY_REQUEST_CREATED_TITLE": "Money Request",
"MONEY_REQUEST_CREATED_MESSAGE": "{childName} requested {amount} {currency}. Reason: {reason}", "MONEY_REQUEST_CREATED_MESSAGE": "{childName} has requested {amount} {currency} for {reason}.",
"MONEY_REQUEST_APPROVED_TITLE": "Money Request Approved", "MONEY_REQUEST_APPROVED_TITLE": "Money Request Approved",
"MONEY_REQUEST_APPROVED_MESSAGE": "Your request for {amount} {currency} has been approved. The money has been added to your account.", "MONEY_REQUEST_APPROVED_MESSAGE": "Your request for {amount} {currency} has been approved. The money has been added to your account.",
"MONEY_REQUEST_DECLINED_TITLE": "Money Request Declined", "MONEY_REQUEST_DECLINED_TITLE": "Money Request Declined",