Compare commits

...
Author SHA1 Message Date
Abdalhamid Alhamad 3b295ea79f ZOD-344-Add QR code validation error handling and localization support
- Introduced new error handling for already used or expired QR codes in JuniorService.
- Added corresponding localization entries in Arabic and English app.json files for QR code validation messages.
2025-11-02 10:52:43 +03:00
abdalhamid99 5ffe18ede3 Merge pull request #54 from HamzaSha1/fix/verfy-email
Implement OTP generation and email verification logic in UserService
2025-10-28 16:17:51 +03:00
Abdalhamid Alhamad a3a61b4923 Implement OTP generation and email verification logic in UserService 2025-10-28 15:52:24 +03:00
abdalhamid99 39d5fc1869 Merge pull request #52 from HamzaSha1/ZOD-349-weekly-spending-data-not-displaying-in-the-child-profile-parent-view
Enhance weekly summary functionality to accept optional date range pa…
2025-10-28 11:22:52 +03:00
Abdalhamid Alhamad 05a6ad2d84 Enhance weekly summary functionality to accept optional date range parameters in CardService, TransactionService, JuniorService, and JuniorController. Update API documentation to reflect new query parameters for start and end dates. 2025-10-28 11:20:49 +03:00
abdalhamid99 5649d24724 Merge pull request #50 from HamzaSha1/ZOD-349-weekly-spending-data-not-displaying-in-the-child-profile-parent-view
git checkout -b ZOD-349-weekly-spending-data-not-displaying-in-the-child-profile-parent-view
2025-10-26 16:05:00 +03:00
Abdalhamid Alhamad bbeece9e03 git checkout -b ZOD-349-weekly-spending-data-not-displaying-in-the-child-profile-parent-view 2025-10-26 13:14:35 +03:00
abdalhamid99 596562f6dc Merge pull request #48 from HamzaSha1/feat/neoleap-integration
Feat/neoleap integration
2025-10-21 14:56:38 +03:00
abdalhamid99 10de8f69c9 Merge pull request #47 from HamzaSha1/ZOD-341-junior-a-child-can-edit-their-email-to-an-existing-email-causing-multiple-child-accounts-to-share-the-same-login
Remove duplicate email cleanup logic and add unique constraint to use…
2025-10-21 14:15:03 +03:00
Abdalhamid Alhamad 8a6b1cc900 Remove duplicate email cleanup logic and add unique constraint to user email 2025-10-21 14:10:14 +03:00
abdalhamid99 d16ae66252 Merge pull request #46 from HamzaSha1/ZOD-341-junior-a-child-can-edit-their-email-to-an-existing-email-causing-multiple-child-accounts-to-share-the-same-login
ZOD-341-Add unique constraint to user email and clean up duplicates
2025-10-21 10:51:12 +03:00
Abdalhamid Alhamad e966f95463 ZOD-341-Add unique constraint to user email and clean up duplicates 2025-10-21 10:49:43 +03:00
abdalhamid99 2714255dd1 Merge pull request #45 from HamzaSha1/ZOD-341-junior-a-child-can-edit-their-email-to-an-existing-email-causing-multiple-child-accounts-to-share-the-same-login
ZOD-341 Add email uniqueness validation to prevent duplicate emails
2025-10-20 14:31:11 +03:00
abdalhamid99 39a0b131b8 Merge pull request #44 from HamzaSha1/ZOD-341-junior-a-child-can-edit-their-email-to-an-existing-email-causing-multiple-child-accounts-to-share-the-same-login
Zod 341 junior a child can edit their email to an existing email causing multiple child accounts to share the same login
2025-10-20 14:27:40 +03:00
Abdalhamid Alhamad 4f778f7904 * ZOD-341-junior-a-child-can-edit-their-email-to-an-existing-email-causing-multiple-child-accounts-to-share-the-same-login 2025-10-20 14:25:53 +03:00
abdalhamid99 7e9bc397a9 Merge pull request #43 from HamzaSha1/ZOD-204-view-spending-from-child-login
git checkout -b ZOD-204-view-spending-from-child-login
2025-10-20 10:30:27 +03:00
abdalhamid99 7bfc14f0d9 Merge pull request #42 from HamzaSha1/ZOD-204-view-spending-from-child-login
ZOD-204-view-spending-from-child-login
2025-10-19 15:44:16 +03:00
abdalhamid99 f81714a525 Merge pull request #41 from HamzaSha1/ZOD-339-child-profile-gender-update-is-not-reflected-after-editing
ZOD-339-child-profile-gender-update-is-not-reflected-after-editing
2025-10-19 11:07:39 +03:00
abdalhamid99 f3282a680b Merge pull request #40 from HamzaSha1/ZOD-339-child-profile-gender-update-is-not-reflected-after-editing
Zod 339 child profile gender update is not reflected after editing
2025-10-19 11:02:40 +03:00
Abdalhamid Alhamad 7b57277a7f ZOD-339-child-profile-gender-update-is-not-reflected-after-editing 2025-10-19 11:01:52 +03:00
abdalhamid99 d70ab09960 Merge pull request #38 from HamzaSha1/ZOD-333-junior-incorrect-relationship-label-displayed-as-child-instead-of-daughter-or-son-in-child-confirmation-details-after-the-scan-the-qr-code
Zod 333 junior incorrect relationship label displayed as child instead of daughter or son in child confirmation details after the scan the qr code
2025-10-19 09:58:57 +03:00
11 changed files with 115 additions and 17 deletions
+2 -2
View File
@@ -163,8 +163,8 @@ export class CardService {
return finalAmount.toNumber();
}
getWeeklySummary(juniorId: string) {
return this.transactionService.getWeeklySummary(juniorId);
getWeeklySummary(juniorId: string, startDate?: Date, endDate?: Date) {
return this.transactionService.getWeeklySummary(juniorId, startDate, endDate);
}
fundIban(iban: string, amount: number) {
+16 -3
View File
@@ -84,15 +84,28 @@ export class TransactionService {
return existingTransaction;
}
async getWeeklySummary(juniorId: string) {
const startOfWeek = moment().startOf('week').toDate();
const endOfWeek = moment().endOf('week').toDate();
async getWeeklySummary(juniorId: string, startDate?: Date, endDate?: Date) {
let startOfWeek: Date;
let endOfWeek: Date;
if (startDate && endDate) {
startOfWeek = startDate;
endOfWeek = endDate;
} else {
const now = moment();
const dayOfWeek = now.day();
startOfWeek = moment().subtract(dayOfWeek, 'days').startOf('day').toDate();
endOfWeek = moment().add(6 - dayOfWeek, 'days').endOf('day').toDate();
}
const transactions = await this.transactionRepository.getTransactionsForCardWithinDateRange(
juniorId,
startOfWeek,
endOfWeek,
);
const summary = {
startOfWeek: startOfWeek,
endOfWeek: endOfWeek,
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddUniqueConstraintToUserEmail1761032305682 implements MigrationInterface {
name = 'AddUniqueConstraintToUserEmail1761032305682'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email")`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3"`);
}
}
+2 -1
View File
@@ -4,4 +4,5 @@ export * from './1754915164810-seed-default-avatar';
export * from './1757349525708-create-money-requests-table';
export * from './1757433339849-add-reservation-amount-to-account-entity';
export * from './1757915357218-add-deleted-at-column-to-junior';
export * from './1760869651296-AddMerchantInfoToTransactions';
export * from './1760869651296-AddMerchantInfoToTransactions';
export * from './1761032305682-AddUniqueConstraintToUserEmail';
+4
View File
@@ -19,6 +19,10 @@
"TOKEN_EXPIRED": "رمز المستخدم منتهي الصلاحية."
},
"QR": {
"CODE_USED_OR_EXPIRED": "تم استخدام رمز QR مسبقًا أو انتهت صلاحيته."
},
"USER": {
"PHONE_ALREADY_VERIFIED": "تم التحقق من رقم الهاتف بالفعل.",
"EMAIL_ALREADY_VERIFIED": "تم التحقق من عنوان البريد الإلكتروني بالفعل.",
+4
View File
@@ -19,6 +19,10 @@
"TOKEN_EXPIRED": "The user token has expired."
},
"QR": {
"CODE_USED_OR_EXPIRED": "The QR code has already been used or expired."
},
"USER": {
"PHONE_ALREADY_VERIFIED": "The phone number has already been verified.",
"EMAIL_ALREADY_VERIFIED": "The email address has already been verified.",
+7 -1
View File
@@ -151,11 +151,17 @@ export class JuniorController {
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse(WeeklySummaryResponseDto)
@ApiQuery({ name: 'startUtc', required: false, type: String, example: '2025-10-20T00:00:00.000Z', description: 'Start date (defaults to start of current week)' })
@ApiQuery({ name: 'endUtc', required: false, type: String, example: '2025-10-26T23:59:59.999Z', description: 'End date (defaults to end of current week)' })
async getWeeklySummary(
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
@AuthenticatedUser() user: IJwtPayload,
@Query('startUtc') startUtc?: string,
@Query('endUtc') endUtc?: string,
) {
const summary = await this.juniorService.getWeeklySummary(juniorId, user.sub);
const startDate = startUtc ? new Date(startUtc) : undefined;
const endDate = endUtc ? new Date(endUtc) : undefined;
const summary = await this.juniorService.getWeeklySummary(juniorId, user.sub, startDate, endDate);
return ResponseFactory.data(summary);
}
+13 -4
View File
@@ -5,6 +5,7 @@ import { Roles } from '~/auth/enums';
import { CardService, TransactionService } from '~/card/services';
import { NeoLeapService } from '~/common/modules/neoleap/services';
import { PageOptionsRequestDto } from '~/core/dtos';
import { ErrorCategory } from '~/core/enums';
import { setIf } from '~/core/utils';
import { CustomerService } from '~/customer/services';
import { DocumentService, OciService } from '~/document/services';
@@ -120,6 +121,7 @@ export class JuniorService {
setIf(customer, 'firstName', body.firstName);
setIf(customer, 'lastName', body.lastName);
setIf(customer, 'dateOfBirth', body.dateOfBirth as unknown as Date);
setIf(customer, 'gender', body.gender);
setIf(junior, 'relationship', body.relationship);
await Promise.all([junior.save(), customer.save(), user.save()]);
@@ -157,7 +159,14 @@ export class JuniorService {
async validateToken(token: string) {
this.logger.log(`Validating token ${token}`);
const juniorId = await this.userTokenService.validateToken(token, UserType.JUNIOR);
return this.findJuniorById(juniorId!, true);
const junior = await this.findJuniorById(juniorId!, true);
if (junior.customer?.user?.password) {
this.logger.error(`Token ${token} already used for junior ${juniorId}`);
throw new BadRequestException({ message: 'QR.CODE_USED_OR_EXPIRED', category: ErrorCategory.BUSINESS_ERROR });
}
return junior;
}
async generateToken(juniorId: string) {
@@ -211,8 +220,8 @@ export class JuniorService {
this.logger.log(`Junior ${juniorId} deleted successfully`);
}
getWeeklySummary(juniorId: string, guardianId: string) {
const doesBelong = this.doesJuniorBelongToGuardian(guardianId, juniorId);
async getWeeklySummary(juniorId: string, guardianId: string, startDate?: Date, endDate?: Date) {
const doesBelong = await this.doesJuniorBelongToGuardian(guardianId, juniorId);
if (!doesBelong) {
this.logger.error(`Junior ${juniorId} does not belong to guardian ${guardianId}`);
@@ -220,7 +229,7 @@ export class JuniorService {
}
this.logger.log(`Getting weekly summary for junior ${juniorId}`);
return this.cardService.getWeeklySummary(juniorId);
return this.cardService.getWeeklySummary(juniorId, startDate, endDate);
}
async getJuniorHome(juniorId: string, userId: string, size: number): Promise<JuniorHomeResponseDto> {
@@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsDateString, IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { Gender } from '~/customer/enums';
export class UpdateUserRequestDto {
@ApiProperty({ example: 'John' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'user.firstName' }) })
@@ -14,8 +15,23 @@ export class UpdateUserRequestDto {
@IsOptional()
lastName!: string;
@ApiPropertyOptional({ example: '[email protected]' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'user.email' }) })
@IsOptional()
email!: string;
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'user.profilePictureId' }) })
@IsOptional()
profilePictureId!: string;
@ApiPropertyOptional({ enum: Gender })
@IsEnum(Gender, { message: i18n('validation.IsEnum', { path: 'general', property: 'customer.gender' }) })
@IsOptional()
gender!: Gender;
@ApiPropertyOptional({ example: '2020-01-01' })
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
@IsOptional()
dateOfBirth!: Date;
}
+1 -1
View File
@@ -28,7 +28,7 @@ export class User extends BaseEntity {
@Column('varchar', { length: 255, name: 'last_name', nullable: false })
lastName!: string;
@Column('varchar', { length: 255, name: 'email', nullable: true })
@Column('varchar', { length: 255, name: 'email', nullable: true, unique: true })
email!: string;
@Column('varchar', { length: 255, name: 'phone_number', nullable: true })
+33 -3
View File
@@ -191,20 +191,50 @@ export class UserService {
async updateUser(userId: string, data: UpdateUserRequestDto) {
await this.validateProfilePictureId(data.profilePictureId, userId);
if (data.email) {
const userWithEmail = await this.findUser({ email: data.email });
if (userWithEmail && userWithEmail.id !== userId) {
this.logger.error(`Email ${data.email} is already taken by another user`);
throw new BadRequestException('USER.EMAIL_ALREADY_TAKEN');
}
}
this.logger.log(`Updating user ${userId} with data ${JSON.stringify(data)}`);
const { affected } = await this.userRepository.update(userId, data);
const { gender, dateOfBirth, ...userData } = data;
const { affected } = await this.userRepository.update(userId, userData);
if (affected === 0) {
this.logger.error(`User with id ${userId} not found`);
throw new BadRequestException('USER.NOT_FOUND');
}
if (gender !== undefined || dateOfBirth !== undefined) {
const customerData: Partial<{ gender: typeof gender; dateOfBirth: Date }> = {};
if (gender !== undefined) {
customerData.gender = gender;
}
if (dateOfBirth !== undefined) {
customerData.dateOfBirth = dateOfBirth;
}
await this.customerService.updateCustomer(userId, customerData);
}
}
async updateUserEmail(userId: string, email: string) {
const userWithEmail = await this.findUser({ email, isEmailVerified: true });
const userWithEmail = await this.findUser({ email });
if (userWithEmail) {
if (userWithEmail.id === userId) {
return;
this.logger.log(`Generating OTP for current email ${email} for user ${userId}`);
await this.userRepository.update(userId, { isEmailVerified: false });
return this.otpService.generateAndSendOtp({
userId,
recipient: email,
otpType: OtpType.EMAIL,
scope: OtpScope.VERIFY_EMAIL,
});
}
this.logger.error(`Email ${email} is already taken by another user`);