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',
USER_INVITED = 'USER_INVITED',
// Transaction notifications - Top-up
// Transaction notifications - Top-up (external funds)
CHILD_TOP_UP = 'CHILD_TOP_UP',
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
CHILD_SPENDING = 'CHILD_SPENDING',
PARENT_SPENDING_ALERT = 'PARENT_SPENDING_ALERT',

View File

@ -97,34 +97,52 @@ export class TransactionNotificationListener {
return;
}
const scope = isTopUp
? NotificationScope.CHILD_TOP_UP
: NotificationScope.CHILD_SPENDING;
// Determine scope: internal transfer (parent to child) vs external top-up
let scope: NotificationScope;
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 amount = transaction.transactionAmount;
const merchant = transaction.merchantName || 'merchant';
// For child top-up notifications, show card.limit (newAmount) instead of account balance
// card.limit represents the total spending limit on the card after the transfer
// For child notifications, show the child's account balance (available balance)
let balance = 0;
let accountCurrency: string | undefined;
if (isTopUp) {
// For top-up: show card limit (newAmount from transfer response)
// For top-up: show child's account balance after the transfer
try {
// Reload card to get updated limit
const cardWithUpdatedLimit = await this.cardService.getCardById(card.id);
balance = cardWithUpdatedLimit.limit || card.limit || 0;
accountCurrency = cardWithUpdatedLimit.account?.currency || card.account?.currency;
this.logger.debug(
`[Child Top-Up Notification] Using card limit (newAmount) - limit: ${balance} ${accountCurrency}`
);
// Reload card to get account reference with fresh balance
const cardWithUpdatedBalance = await this.cardService.getCardById(card.id);
if (cardWithUpdatedBalance?.account?.accountReference) {
// Fetch by reference number to get fresh balance from database
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 {
// 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) {
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;
}
} else {
@ -176,27 +194,36 @@ export class TransactionNotificationListener {
let message: string;
try {
title = isTopUp
? this.i18n.t('app.NOTIFICATION.CHILD_TOP_UP_TITLE', { lang: locale })
: this.i18n.t('app.NOTIFICATION.CHILD_SPENDING_TITLE', { lang: locale });
message = isTopUp
? this.i18n.t('app.NOTIFICATION.CHILD_TOP_UP_MESSAGE', {
lang: locale,
args: {
amount: formattedAmount,
currency: currency,
balance: formattedBalance,
},
})
: this.i18n.t('app.NOTIFICATION.CHILD_SPENDING_MESSAGE', {
lang: locale,
args: {
amount: formattedAmount,
currency: currency,
merchant: merchant,
},
});
if (isTopUp) {
// Internal transfer or external top-up
const titleKey = isChildSpending
? 'app.NOTIFICATION.CHILD_INTERNAL_TRANSFER_TITLE'
: 'app.NOTIFICATION.CHILD_TOP_UP_TITLE';
const messageKey = isChildSpending
? 'app.NOTIFICATION.CHILD_INTERNAL_TRANSFER_MESSAGE'
: 'app.NOTIFICATION.CHILD_TOP_UP_MESSAGE';
title = this.i18n.t(titleKey, { lang: locale });
message = this.i18n.t(messageKey, {
lang: locale,
args: {
amount: formattedAmount,
currency: currency,
balance: formattedBalance,
},
});
} 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) {
console.error(`[TransactionNotificationListener] i18n error:`, i18nError);
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
*/
private async notifyParentOfTopUp(transaction: Transaction, card: Card): Promise<void> {
@ -537,8 +564,8 @@ export class TransactionNotificationListener {
let message: string;
try {
title = this.i18n.t('app.NOTIFICATION.PARENT_TOP_UP_TITLE', { lang: locale });
message = this.i18n.t('app.NOTIFICATION.PARENT_TOP_UP_MESSAGE', {
title = this.i18n.t('app.NOTIFICATION.PARENT_INTERNAL_TRANSFER_TITLE', { lang: locale });
message = this.i18n.t('app.NOTIFICATION.PARENT_INTERNAL_TRANSFER_MESSAGE', {
lang: locale,
args: {
amount: formattedAmount,
@ -548,9 +575,9 @@ export class TransactionNotificationListener {
},
});
} 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);
title = 'Transfer Completed';
title = 'Internal Transfer Completed';
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,
title,
message,
scope: NotificationScope.PARENT_TOP_UP_CONFIRMATION,
scope: NotificationScope.PARENT_INTERNAL_TRANSFER,
preferences: this.getUserPreferences(parentUser),
data: {
transactionId: transaction.id,

View File

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

View File

@ -113,10 +113,12 @@
"NOTIFICATION": {
"CHILD_TOP_UP_TITLE": "Funds Credited",
"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_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_MESSAGE": "{childName} spent {amount} {currency} at {merchant}. Remaining balance: {balance} {currency}",
"YOUR_CHILD": "Your child",