Compare commits

..
Author SHA1 Message Date
Abdalhamid Alhamad 2ab9554c0c feat: add profile update notification handling for child users
- Implemented logic to skip notifications for child users (roles.JUNIOR) when their profiles are updated, preventing unnecessary notifications to both child and parent.
- Enhanced logging to indicate when notifications are skipped for child users.
2026-02-01 14:44:42 +03:00
9 changed files with 15 additions and 148 deletions
@@ -92,9 +92,7 @@ export class TransactionRepository {
return this.transactionRepository
.createQueryBuilder('transaction')
.innerJoinAndSelect('transaction.card', 'card')
.innerJoin('card.customer', 'customer')
.innerJoin('customer.junior', 'junior')
.where('junior.id = :juniorId', { juniorId })
.where('card.customerId = :juniorId', { juniorId })
.andWhere('transaction.transactionScope = :scope', { scope: TransactionScope.CARD })
.andWhere('transaction.transactionType = :type', { type: TransactionType.EXTERNAL })
.andWhere('transaction.transactionDate BETWEEN :startDate AND :endDate', { startDate, endDate })
@@ -155,9 +153,7 @@ export class TransactionRepository {
.createQueryBuilder('tx')
.innerJoinAndSelect('tx.card', 'card')
.innerJoinAndSelect('card.account', 'account')
.innerJoin('card.customer', 'customer')
.innerJoin('customer.junior', 'junior')
.where('junior.id = :juniorId', { juniorId })
.where('card.customerId = :juniorId', { juniorId })
.andWhere('tx.transactionScope = :scope', { scope: TransactionScope.CARD })
.andWhere('tx.transactionType = :type', { type: TransactionType.INTERNAL })
.orderBy('tx.transactionDate', 'DESC')
@@ -170,9 +166,7 @@ export class TransactionRepository {
return this.transactionRepository
.createQueryBuilder('tx')
.innerJoin('tx.card', 'card')
.innerJoin('card.customer', 'customer')
.innerJoin('customer.junior', 'junior')
.where('junior.id = :juniorId', { juniorId })
.where('card.customerId = :juniorId', { juniorId })
.andWhere('tx.transactionScope = :scope', { scope: TransactionScope.CARD })
.andWhere('tx.transactionType = :type', { type: TransactionType.INTERNAL })
.getCount();
@@ -182,10 +176,8 @@ export class TransactionRepository {
return this.transactionRepository
.createQueryBuilder('tx')
.innerJoinAndSelect('tx.card', 'card')
.innerJoin('card.customer', 'customer')
.innerJoin('customer.junior', 'junior')
.where('tx.id = :transactionId', { transactionId })
.andWhere('junior.id = :juniorId', { juniorId })
.andWhere('card.customerId = :juniorId', { juniorId })
.getOne();
}
}
@@ -1,12 +1,9 @@
import { BadRequestException, Controller, Get, Req, UseGuards } from '@nestjs/common';
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { AccessTokenGuard } from '~/common/guards';
import { ApiDataArrayResponse, ApiLangRequestHeader } from '~/core/decorators';
import { ApiDataArrayResponse } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils';
import { DocumentMetaResponseDto } from '~/document/dtos/response';
import { HelpSupportFaqResponseDto } from '../dtos/response';
import { LookupLanguage } from '../enums';
import { LookupService } from '../services';
@Controller('lookup')
@@ -32,24 +29,4 @@ export class LookupController {
return ResponseFactory.dataArray(avatars.map((avatar) => new DocumentMetaResponseDto(avatar)));
}
@UseGuards(AccessTokenGuard)
@Get('help-support-faqs')
@ApiDataArrayResponse(HelpSupportFaqResponseDto)
@ApiLangRequestHeader()
getHelpSupportFaqs(@Req() request: Request) {
const header = request.headers['accept-language'];
const lang = Array.isArray(header) ? header[0] : header;
const normalized = (lang || '').trim().toLowerCase();
if (normalized && normalized !== LookupLanguage.EN && normalized !== LookupLanguage.AR) {
throw new BadRequestException('Accept-Language must be "en" or "ar".');
}
const faqs = this.lookupService.getHelpSupportFaqs(
normalized === LookupLanguage.AR ? LookupLanguage.AR : LookupLanguage.EN,
);
return ResponseFactory.dataArray(faqs);
}
}
@@ -1,72 +0,0 @@
[
{
"id": "transfer_failed_or_delayed",
"question_en": "What happens if a transfer fails or is delayed?",
"answer_en": "If a transfer fails, the amount will not be deducted from your balance. If a transfer is delayed, it will usually complete within a short time. If the issue continues, please contact customer support.",
"question_ar": "ماذا يحدث إذا فشل التحويل أو تأخر؟",
"answer_ar": "إذا فشل التحويل، لن يُخصم المبلغ من رصيدك. إذا تأخر التحويل، فعادةً يكتمل خلال وقت قصير. إذا استمرت المشكلة، يرجى التواصل مع دعم العملاء."
},
{
"id": "same_email_or_phone_multiple_accounts",
"question_en": "Can I use the same email or phone number for multiple accounts?",
"answer_en": "No. Each email address and phone number can be used for only one account.",
"question_ar": "هل يمكنني استخدام نفس البريد الإلكتروني أو رقم الهاتف لعدة حسابات؟",
"answer_ar": "لا. يمكن استخدام كل بريد إلكتروني ورقم هاتف لحساب واحد فقط."
},
{
"id": "update_profile_details",
"question_en": "How do I update my profile details?",
"answer_en": "Go to Profile - Edit Profile, update your details, and save the changes.",
"question_ar": "كيف يمكنني تحديث بيانات ملفي الشخصي؟",
"answer_ar": "انتقل إلى الملف الشخصي - تعديل الملف الشخصي، حدّث بياناتك ثم احفظ التغييرات."
},
{
"id": "change_email_address",
"question_en": "How do I change my email address?",
"answer_en": "You can update your email or phone number from Profile - Edit Profile. You may be asked to re-login for security reasons.",
"question_ar": "كيف أغيّر عنوان بريدي الإلكتروني؟",
"answer_ar": "يمكنك تحديث بريدك الإلكتروني أو رقم هاتفك من الملف الشخصي - تعديل الملف الشخصي. قد يُطلب منك تسجيل الدخول مرة أخرى لأسباب أمنية."
},
{
"id": "change_phone_number",
"question_en": "How do I change my phone number?",
"answer_en": "At the moment, phone numbers can’t be changed directly in the app. Please contact our support team, and they’ll assist you with updating it.",
"question_ar": "كيف أغيّر رقم هاتفي؟",
"answer_ar": "حاليًا لا يمكن تغيير أرقام الهواتف مباشرةً داخل التطبيق. يرجى التواصل مع فريق الدعم، وسيساعدونك في تحديثه."
},
{
"id": "activate_card",
"question_en": "How do I activate my card?",
"answer_en": "Transfer 20 SAR to activate your card. Once completed, your card will become active.",
"question_ar": "كيف أقوم بتفعيل بطاقتي؟",
"answer_ar": "حوّل 20 ريالًا سعوديًا لتفعيل بطاقتك. بعد إتمام التحويل، ستصبح البطاقة نشطة."
},
{
"id": "why_transfer_20_sar",
"question_en": "Why do I need to transfer 20 SAR to activate the card?",
"answer_en": "This amount is required to activate the card and enable usage.",
"question_ar": "لماذا يجب علي تحويل 20 ريالًا سعوديًا لتفعيل البطاقة؟",
"answer_ar": "هذا المبلغ مطلوب لتفعيل البطاقة وتمكين استخدامها."
},
{
"id": "where_activation_amount_go",
"question_en": "Where does the activation amount go?",
"answer_en": "The activation amount is collected by the company as part of the card activation process.",
"question_ar": "أين يذهب مبلغ التفعيل؟",
"answer_ar": "يتم تحصيل مبلغ التفعيل من قبل الشركة كجزء من عملية تفعيل البطاقة."
},
{
"id": "top_up_external_bank",
"question_en": "Can I top up from an external bank account?",
"answer_en": "Yes. You can transfer funds from your external bank to your ZOD account",
"question_ar": "هل يمكنني الشحن من حساب بنكي خارجي؟",
"answer_ar": "نعم. يمكنك تحويل الأموال من بنكك الخارجي إلى حسابك في زد."
},
{
"id": "data_security",
"question_en": "Is my data secure in the ZOD app?",
"answer_en": "Yes. We use secure systems and encryption to protect your data.",
"question_ar": "هل بياناتي آمنة في تطبيق زد؟",
"answer_ar": "نعم. نستخدم أنظمة آمنة وتشفيرًا لحماية بياناتك."
}
]
@@ -1,18 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
export class HelpSupportFaqResponseDto {
@ApiProperty()
id!: string;
@ApiProperty()
question!: string;
@ApiProperty()
answer!: string;
constructor(item: { id: string; question: string; answer: string }) {
this.id = item.id;
this.question = item.question;
this.answer = item.answer;
}
}
@@ -1 +0,0 @@
export * from './help-support-faq.response.dto';
-1
View File
@@ -1 +0,0 @@
export * from './lookup-language.enum';
@@ -1,4 +0,0 @@
export enum LookupLanguage {
EN = 'en',
AR = 'ar',
}
@@ -1,9 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { DocumentType } from '~/document/enums';
import { DocumentService, OciService } from '~/document/services';
import { HelpSupportFaqResponseDto } from '../dtos/response';
import { LookupLanguage } from '../enums';
import helpSupportFaqs from '../data/help-support-faqs.json';
@Injectable()
export class LookupService {
@@ -36,16 +33,4 @@ export class LookupService {
this.logger.log(`Default tasks logos returned successfully`);
return documents;
}
getHelpSupportFaqs(lang: LookupLanguage = LookupLanguage.EN): HelpSupportFaqResponseDto[] {
const useArabic = lang === LookupLanguage.AR;
return helpSupportFaqs.map((faq) =>
new HelpSupportFaqResponseDto({
id: faq.id,
question: useArabic ? faq.question_ar : faq.question_en,
answer: useArabic ? faq.answer_ar : faq.answer_en,
}),
);
}
}
@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { I18nService } from 'nestjs-i18n';
import { Roles } from '~/auth/enums';
import { NotificationFactory, NotificationPreferences } from '../services/notification-factory.service';
import { UserService } from '~/user/services/user.service';
import { NOTIFICATION_EVENTS } from '../constants/event-names.constant';
@@ -24,6 +25,14 @@ export class ProfileNotificationListener {
try {
const { user, updatedFields } = event;
// Do not notify when a child updates their profile (no notification to child or parent)
if (user?.roles?.includes(Roles.JUNIOR)) {
this.logger.log(
`Skipping profile updated notification for child user ${user.id} - no notification sent`
);
return;
}
this.logger.log(
`Processing profile updated notification for user ${user.id} - Updated fields: ${updatedFields.join(', ')}`
);