From cf7fd04913cfae4498873e15c8cddd77f02ca2d2 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 25 Feb 2026 16:58:36 +0330 Subject: [PATCH] phone --- src/modules/admin/entities/admin.entity.ts | 11 +--- src/modules/admin/providers/admin.service.ts | 14 ++--- src/modules/auth/dto/request-otp.dto.ts | 5 +- src/modules/auth/dto/verify-otp.dto.ts | 5 +- src/modules/auth/services/auth.service.ts | 20 +++----- .../user/controllers/users.controller.ts | 2 +- .../user/dto/create-user-address.dto.ts | 51 ------------------- src/modules/user/dto/create-user.dto.ts | 9 +++- src/modules/user/entities/user.entity.ts | 13 +---- src/modules/user/providers/user.service.ts | 23 ++++++--- .../user/repositories/user.repository.ts | 3 +- src/modules/util/phone.util.ts | 16 +++++- src/seeders/admins.seeder.ts | 5 +- src/seeders/users.seeder.ts | 8 ++- 14 files changed, 68 insertions(+), 117 deletions(-) delete mode 100644 src/modules/user/dto/create-user-address.dto.ts diff --git a/src/modules/admin/entities/admin.entity.ts b/src/modules/admin/entities/admin.entity.ts index b22166e..dd91e1e 100644 --- a/src/modules/admin/entities/admin.entity.ts +++ b/src/modules/admin/entities/admin.entity.ts @@ -1,6 +1,5 @@ import { Cascade, Entity, ManyToOne, OptionalProps, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; -import { normalizePhone } from '../../util/phone.util'; import { Role } from 'src/modules/roles/entities/role.entity'; @Entity({ tableName: 'admins' }) @@ -13,16 +12,8 @@ export class Admin extends BaseEntity { @Property({ nullable: true }) lastName?: string; - private _phone!: string; - @Property({ unique: true }) - get phone(): string { - return this._phone; - } - - set phone(value: string) { - this._phone = normalizePhone(value); - } + phone: string @ManyToOne(() => Role) role: Role diff --git a/src/modules/admin/providers/admin.service.ts b/src/modules/admin/providers/admin.service.ts index 0c8ad95..f09c435 100644 --- a/src/modules/admin/providers/admin.service.ts +++ b/src/modules/admin/providers/admin.service.ts @@ -3,7 +3,6 @@ import { Admin } from '../entities/admin.entity'; import { Role } from '../../roles/entities/role.entity'; import { EntityManager } from '@mikro-orm/postgresql'; import { UpdateAdminDto } from '../dto/update-admin.dto'; -import { normalizePhone } from '../../util/phone.util'; import { CreateAdminDto } from '../dto/create-admin.dto'; import { AdminRepository } from '../repositories/admin.repository'; import { RoleRepository } from 'src/modules/roles/respository/role.repository'; @@ -20,10 +19,9 @@ export class AdminService { async create(dto: CreateAdminDto) { const { phone, firstName, lastName, roleId } = dto; - const normalizedPhone = normalizePhone(phone); const currentAdmin = await this.adminRepository.findOne({ - phone: normalizedPhone, + phone, }, { filters: { notDeleted: false } }); if (currentAdmin && !currentAdmin.deletedAt) { @@ -42,7 +40,7 @@ export class AdminService { } const admin = this.em.create(Admin, { - phone: normalizedPhone, + phone, firstName, lastName, role, @@ -52,8 +50,7 @@ export class AdminService { } async findByPhone(phone: string): Promise { - const normalizedPhone = normalizePhone(phone); - return this.adminRepository.findOne({ phone: normalizedPhone }); + return this.adminRepository.findOne({ phone }); } async findById(adminId: string): Promise { @@ -89,12 +86,11 @@ export class AdminService { } if (rest.phone !== undefined && rest.phone !== admin.phone) { - const normalizedPhone = normalizePhone(rest.phone); - const exists = await this.adminRepository.findOne({ phone: normalizedPhone }); + const exists = await this.adminRepository.findOne({ phone: rest.phone }); if (exists) { throw new ConflictException('This Phone Number is already taken'); } - admin.phone = normalizedPhone; + admin.phone = rest.phone; } // Update role if roleId is provided diff --git a/src/modules/auth/dto/request-otp.dto.ts b/src/modules/auth/dto/request-otp.dto.ts index 8aa4ecb..f369fdf 100644 --- a/src/modules/auth/dto/request-otp.dto.ts +++ b/src/modules/auth/dto/request-otp.dto.ts @@ -1,10 +1,13 @@ import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { normalizePhoneToLocal } from 'src/modules/util/phone.util'; +import { Transform } from 'class-transformer'; export class RequestOtpDto { + @Transform(({ value }) => normalizePhoneToLocal(value)) @IsNotEmpty() @IsString() - @ApiProperty({ example: '09362532122', description: 'Mobile number' }) + @ApiProperty({ example: '09362532122', description: 'Mobile number (accepts +98, 0098, 98, 09 formats)' }) @IsMobilePhone('fa-IR') phone: string; diff --git a/src/modules/auth/dto/verify-otp.dto.ts b/src/modules/auth/dto/verify-otp.dto.ts index 77c6e88..dfbd697 100644 --- a/src/modules/auth/dto/verify-otp.dto.ts +++ b/src/modules/auth/dto/verify-otp.dto.ts @@ -1,10 +1,13 @@ import { IsNotEmpty, IsString, Length, IsMobilePhone } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { normalizePhoneToLocal } from 'src/modules/util/phone.util'; export class VerifyOtpDto { + @Transform(({ value }) => normalizePhoneToLocal(value)) @IsNotEmpty() @IsString() - @ApiProperty({ example: '09362532122', description: 'Mobile number' }) + @ApiProperty({ example: '09362532122', description: 'Mobile number (accepts +98, 0098, 98, 09 formats)' }) @IsMobilePhone('fa-IR') phone: string; diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 59adae3..63bfa50 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -8,7 +8,6 @@ import { AuthMessage } from 'src/common/enums/message.enum'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; import { AdminLoginTransformer } from '../transformers/admin-login.transformer'; import { UserLoginTransformer } from '../transformers/user-login.transformer'; -import { normalizePhone } from '../../util/phone.util'; import { OtpService } from './otp.service'; @Injectable() @@ -27,15 +26,14 @@ export class AuthService { async requestOtp(dto: RequestOtpDto, isAdmin: boolean) { const { phone } = dto; - const normalizedPhone = normalizePhone(phone); if (isAdmin) { - await this.valdateAdmin(normalizedPhone) + await this.valdateAdmin(phone) } const code = this.generateOtpCode(); - await this.otpService.set(normalizedPhone, code) + await this.otpService.set(phone, code) // await this.smsService.sendotp(normalizedPhone, code); @@ -43,16 +41,15 @@ export class AuthService { } async verifyOtp(phone: string, code: string) { - const normalizedPhone = normalizePhone(phone); - const cachedCode = await this.otpService.get(normalizedPhone); + const cachedCode = await this.otpService.get(phone); if (!cachedCode) throw new BadRequestException('OTP expired or not found'); if (cachedCode !== code) throw new BadRequestException('Invalid OTP'); - await this.otpService.delete(normalizedPhone) + await this.otpService.delete(phone) const user = await this.userService.getOrCreate({ - phone: normalizedPhone, + phone: phone, gender: true, // maxCredit: 0 }); @@ -66,16 +63,15 @@ export class AuthService { } async verifyOtpAdmin(phone: string, code: string) { - const normalizedPhone = normalizePhone(phone); - const cachedCode = await this.otpService.get(normalizedPhone); + const cachedCode = await this.otpService.get(phone); if (!cachedCode) throw new BadRequestException('OTP expired or not found'); if (cachedCode !== code) throw new BadRequestException('Invalid OTP'); - await this.otpService.delete(normalizedPhone); + await this.otpService.delete(phone); - const admin = await this.adminRepository.findOne({ phone: normalizedPhone }, { populate: ['role', 'role.permissions'] }); + const admin = await this.adminRepository.findOne({ phone }, { populate: ['role', 'role.permissions'] }); if (!admin) { throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND); diff --git a/src/modules/user/controllers/users.controller.ts b/src/modules/user/controllers/users.controller.ts index 810a24d..ba630f5 100644 --- a/src/modules/user/controllers/users.controller.ts +++ b/src/modules/user/controllers/users.controller.ts @@ -50,7 +50,7 @@ export class UsersController { return this.userService.createUserAsAdmin(dto); } - @Post('admin/users/:id') + @Get('admin/users/:id') @UseGuards(AdminAuthGuard) @Permissions(PermissionEnum.VIEW_USERS) @ApiOperation({ summary: 'find user ' }) diff --git a/src/modules/user/dto/create-user-address.dto.ts b/src/modules/user/dto/create-user-address.dto.ts deleted file mode 100644 index 0e6a454..0000000 --- a/src/modules/user/dto/create-user-address.dto.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { IsString, IsOptional, IsNumber, IsBoolean, Min, Max } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -/** - * DTO for creating a new user address. - */ -export class CreateUserAddressDto { - @ApiProperty({ description: 'Address title/label (e.g., "Home", "Work")', example: 'Home' }) - @IsString() - title!: string; - - @ApiProperty({ description: 'Street address', example: '123 Main Street' }) - @IsString() - address!: string; - - @ApiProperty({ description: 'City name', example: 'Tehran' }) - @IsString() - city!: string; - - @ApiPropertyOptional({ description: 'Province', example: 'Tehran Province' }) - @IsOptional() - @IsString() - province?: string; - - @ApiPropertyOptional({ description: 'Postal/ZIP code', example: '12345-6789' }) - @IsOptional() - @IsString() - postalCode?: string; - - @ApiProperty({ description: 'Latitude coordinate', example: 35.6892, minimum: -90, maximum: 90 }) - @IsNumber() - @Min(-90) - @Max(90) - latitude!: number; - - @ApiProperty({ description: 'Longitude coordinate', example: 51.389, minimum: -180, maximum: 180 }) - @IsNumber() - @Min(-180) - @Max(180) - longitude!: number; - - @ApiPropertyOptional({ description: 'Contact phone for this address', example: '+989123456789' }) - @IsOptional() - @IsString() - phone?: string; - - @ApiPropertyOptional({ description: 'Set as default address', example: false, default: false }) - @IsOptional() - @IsBoolean() - isDefault?: boolean; -} diff --git a/src/modules/user/dto/create-user.dto.ts b/src/modules/user/dto/create-user.dto.ts index 842c51d..5836a8e 100644 --- a/src/modules/user/dto/create-user.dto.ts +++ b/src/modules/user/dto/create-user.dto.ts @@ -1,10 +1,13 @@ -import { IsString, IsBoolean, IsNotEmpty, IsMobilePhone, IsInt, IsNumber, IsOptional } from 'class-validator'; +import { IsString, IsBoolean, IsNotEmpty, IsMobilePhone, IsInt, IsNumber, IsOptional, Min } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { normalizePhoneToLocal } from '../../util/phone.util'; export class CreateUserDto { + @Transform(({ value }) => normalizePhoneToLocal(value)) @IsNotEmpty() @IsString() - @ApiProperty({ example: '09362532122', description: 'Mobile number' }) + @ApiProperty({ example: '09362532122', description: 'Mobile number (accepts +98, 0098, 98, 09 formats)' }) @IsMobilePhone('fa-IR') phone: string; @@ -32,6 +35,8 @@ export class CreateUserDto { export class CreateUserAsAdminDto extends CreateUserDto { @ApiProperty({ description: "User's max credit in Toman", example: 100000 }) + @Transform(({ value }) => Number(value)) @IsInt() + @Min(0) maxCredit: number; } \ No newline at end of file diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index 57faadc..beeb2c4 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -2,9 +2,7 @@ import { Entity, Index, Property, OneToMany, Collection, PrimaryKey, OptionalPro import { BaseEntity } from '../../../common/entities/base.entity'; import { LearningProgress } from '../../learnings/entities/learning-progress.entity'; import { Criticism } from '../../criticisms/entities/criticism.entity'; -// import { Order } from 'src/modules/order/entities/order.entity'; -import { normalizePhone } from '../../util/phone.util'; -import { ulid } from 'ulid'; + @Entity({ tableName: 'users' }) export class User extends BaseEntity { @@ -26,16 +24,9 @@ export class User extends BaseEntity { @Property({ nullable: true }) lastName?: string; - private _phone!: string; @Property({ unique: true }) - get phone(): string { - return this._phone; - } - - set phone(value: string) { - this._phone = normalizePhone(value); - } + phone: string @Property({ default: true }) diff --git a/src/modules/user/providers/user.service.ts b/src/modules/user/providers/user.service.ts index a747aaa..e56a83a 100644 --- a/src/modules/user/providers/user.service.ts +++ b/src/modules/user/providers/user.service.ts @@ -6,7 +6,7 @@ import { CreateUserAsAdminDto, CreateUserDto } from '../dto/create-user.dto'; import { FindUsersDto } from '../dto/find-user.dto'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { UserRepository } from '../repositories/user.repository'; -import { normalizePhone } from '../../util/phone.util'; +// import { normalizePhone } from '../../util/phone.util'; import { UpdateUserDto } from '../dto/update-user.dto'; import { CreateUser } from '../interface/user'; @@ -25,13 +25,15 @@ export class UserService { throw new BadRequestException("User with this phone already exist") } - const user = await this.createUser({ ...dto, phone: normalizePhone(dto.phone) }) + const user = await this.createUser(dto) + + return user; } async getOrCreate(dto: CreateUserDto): Promise { - const normalizedPhone = normalizePhone(dto.phone) + const normalizedPhone = dto.phone const currentUser = await this.userRepository.findOne({ phone: normalizedPhone }); @@ -45,8 +47,8 @@ export class UserService { } async findByPhone(phone: string): Promise { - const normalizedPhone = normalizePhone(phone); - return this.userRepository.findOne({ phone: normalizedPhone }); + + return this.userRepository.findOne({ phone }); } async findById(id: string): Promise { @@ -61,7 +63,11 @@ export class UserService { throw new NotFoundException(`User with ID ${userId} not found.`); } - this.em.assign(user, dto); + const { address, ...rest } = dto; + this.em.assign(user, rest); + if (address !== undefined) { + user.addresse = address; + } await this.em.flush(); @@ -89,12 +95,13 @@ export class UserService { firstName: param.firstName, lastName: param.lastName, gender: param.gender, - maxCredit: param.maxCredit + maxCredit: param.maxCredit, + addresse:param.address }; const user = this.userRepository.create(createData); - await this.em.persistAndFlush([user]); + await this.em.flush(); return user } diff --git a/src/modules/user/repositories/user.repository.ts b/src/modules/user/repositories/user.repository.ts index dcc1528..1d2fe32 100644 --- a/src/modules/user/repositories/user.repository.ts +++ b/src/modules/user/repositories/user.repository.ts @@ -3,7 +3,6 @@ import { EntityManager, EntityRepository, FilterQuery } from '@mikro-orm/postgre import { User } from '../entities/user.entity'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { FindUsersDto } from '../dto/find-user.dto'; -import { normalizePhone } from 'src/modules/util/phone.util'; @Injectable() export class UserRepository extends EntityRepository { @@ -23,7 +22,7 @@ export class UserRepository extends EntityRepository { // 4. Add 'search' logic (case-insensitive) if (search) { const searchPattern = `%${search}%`; - const normalizedSearch = normalizePhone(search); + const normalizedSearch = search const normalizedSearchPattern = `%${normalizedSearch}%`; // $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default) // Search with both original and normalized phone patterns to handle various input formats diff --git a/src/modules/util/phone.util.ts b/src/modules/util/phone.util.ts index d659e29..ef003ac 100644 --- a/src/modules/util/phone.util.ts +++ b/src/modules/util/phone.util.ts @@ -4,7 +4,7 @@ * @param phone - The phone number to normalize * @returns Normalized phone number (e.g., 9120000001) */ -export function normalizePhone(phone: string): string { +export function normalizePhone1(phone: string): string { if (!phone) { return phone; } @@ -28,3 +28,17 @@ export function normalizePhone(phone: string): string { return normalized; } + +/** + * Normalizes Iranian mobile numbers to local format: 09xxxxxxxxx + * Accepts +98..., 0098..., 98..., 9..., 09... and strips spaces/dashes + * @param phone - The phone number to normalize + * @returns Normalized phone number (e.g., 09362532122) + */ +export function normalizePhoneToLocal(phone: string): string { + if (!phone || typeof phone !== 'string') { + return phone; + } + const normalized = normalizePhone1(phone); + return normalized ? `0${normalized}` : phone; +} diff --git a/src/seeders/admins.seeder.ts b/src/seeders/admins.seeder.ts index e163af9..59bb59b 100644 --- a/src/seeders/admins.seeder.ts +++ b/src/seeders/admins.seeder.ts @@ -2,8 +2,7 @@ import type { EntityManager } from '@mikro-orm/core'; import { Admin } from '../modules/admin/entities/admin.entity'; import type { Role } from '../modules/roles/entities/role.entity'; import { adminsData } from './data/admins.data'; -import { normalizePhone } from '../modules/util/phone.util'; - + export class AdminsSeeder { async run(em: EntityManager, rolesMap: Map): Promise { for (const adminData of adminsData) { @@ -15,7 +14,7 @@ export class AdminsSeeder { const admin = em.create(Admin, { - phone: normalizePhone(adminData.phone), + phone: (adminData.phone), firstName: adminData.firstName, lastName: adminData.lastName, role diff --git a/src/seeders/users.seeder.ts b/src/seeders/users.seeder.ts index 55e64fc..d66ebc8 100644 --- a/src/seeders/users.seeder.ts +++ b/src/seeders/users.seeder.ts @@ -1,7 +1,6 @@ import type { EntityManager } from '@mikro-orm/core'; import { User } from '../modules/user/entities/user.entity'; import { usersData } from './data/users.data'; -import { normalizePhone } from '../modules/util/phone.util'; export class UsersSeeder { async run(em: EntityManager): Promise> { @@ -9,11 +8,10 @@ export class UsersSeeder { // Create users for (const userData of usersData) { - const normalizedPhone = normalizePhone(userData.phone); - let user = await em.findOne(User, { phone: normalizedPhone }); + let user = await em.findOne(User, { phone: userData.phone }); if (!user) { user = em.create(User, { - phone: normalizedPhone, // Use normalized phone + phone: userData.phone, // Use normalized phone firstName: userData.firstName, lastName: userData.lastName, isActive: userData.isActive, @@ -23,7 +21,7 @@ export class UsersSeeder { }); em.persist(user); } - usersMap.set(normalizedPhone, user); // Use normalized phone as key + usersMap.set(userData.phone, user); // Use normalized phone as key } await em.flush();