Merge pull request #88 from Zod-Alkhair/feature/notification-system-fcm-registration

feat: enhance transaction notification listener for internal transfer…
This commit is contained in:
Majdalkilany0
2026-01-20 15:21:45 +03:00
committed by GitHub
4 changed files with 90 additions and 55 deletions

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

@ -97,34 +97,52 @@ 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 child's account balance (available balance)
// 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) {
// For top-up: show card limit (newAmount from transfer response) // For top-up: show child's account balance after the transfer
try { try {
// Reload card to get updated limit // Reload card to get account reference with fresh balance
const cardWithUpdatedLimit = await this.cardService.getCardById(card.id); const cardWithUpdatedBalance = await this.cardService.getCardById(card.id);
balance = cardWithUpdatedLimit.limit || card.limit || 0; if (cardWithUpdatedBalance?.account?.accountReference) {
accountCurrency = cardWithUpdatedLimit.account?.currency || card.account?.currency; // Fetch by reference number to get fresh balance from database
this.logger.debug( const account = await this.accountService.getAccountByReferenceNumber(
`[Child Top-Up Notification] Using card limit (newAmount) - limit: ${balance} ${accountCurrency}` cardWithUpdatedBalance.account.accountReference
); );
balance = account.balance;
accountCurrency = account.currency;
this.logger.debug(
`[Child Top-Up Notification] Fetched account by reference - balance: ${balance} ${accountCurrency}`
);
} else {
// Fallback: use card's account balance
balance = cardWithUpdatedBalance.account?.balance || card.account?.balance || 0;
accountCurrency = cardWithUpdatedBalance.account?.currency || card.account?.currency;
this.logger.debug(
`[Child Top-Up Notification] Using card account balance - balance: ${balance} ${accountCurrency}`
);
}
} 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 Top-Up Notification] Could not fetch account by reference: ${error?.message}. Using card account balance.`
); );
balance = card.limit || 0; balance = card.account?.balance || 0;
accountCurrency = card.account?.currency; accountCurrency = card.account?.currency;
} }
} else { } else {
@ -176,27 +194,36 @@ 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 {
}, // 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);
@ -411,7 +438,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> {
@ -537,8 +564,8 @@ export class TransactionNotificationListener {
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,
@ -548,9 +575,9 @@ 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 = 'Transfer Completed'; title = 'Internal Transfer Completed';
message = `${formattedAmount} ${currency} has been transferred to ${childName}'s card. ${childName}'s balance is ${formattedBalance} ${currency}`; message = `${formattedAmount} ${currency} has been transferred to ${childName}'s card. ${childName}'s balance is ${formattedBalance} ${currency}`;
} }
@ -558,7 +585,7 @@ export class TransactionNotificationListener {
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}", "CHILD_INTERNAL_TRANSFER_MESSAGE": "تمت إضافة {amount} {currency} إلى بطاقتك. إجمالي الرصيد: {balance} {currency}",
"PARENT_TOP_UP_TITLE": "اكتمل التحويل", "PARENT_INTERNAL_TRANSFER_TITLE": "اكتمل التحويل",
"PARENT_TOP_UP_MESSAGE": "تم تحويل {amount} {currency} إلى بطاقة {childName}. رصيد {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

@ -113,10 +113,12 @@
"NOTIFICATION": { "NOTIFICATION": {
"CHILD_TOP_UP_TITLE": "Funds Credited", "CHILD_TOP_UP_TITLE": "Funds Credited",
"CHILD_TOP_UP_MESSAGE": "{amount} {currency} has been added to your card. 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}", "CHILD_SPENDING_MESSAGE": "You spent {amount} {currency} at {merchant}",
"PARENT_TOP_UP_TITLE": "Transfer Completed",
"PARENT_TOP_UP_MESSAGE": "{amount} {currency} has been transferred to {childName}'s card. {childName}'s balance is {balance} {currency}",
"PARENT_SPENDING_TITLE": "Spending Alert", "PARENT_SPENDING_TITLE": "Spending Alert",
"PARENT_SPENDING_MESSAGE": "{childName} spent {amount} {currency} at {merchant}. Remaining balance: {balance} {currency}", "PARENT_SPENDING_MESSAGE": "{childName} spent {amount} {currency} at {merchant}. Remaining balance: {balance} {currency}",
"YOUR_CHILD": "Your child", "YOUR_CHILD": "Your child",