normalize phone
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Cascade, Collection, Entity, OneToMany, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { AdminRole } from './adminRole.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Entity({ tableName: 'admins' })
|
||||
export class Admin extends BaseEntity {
|
||||
@@ -10,8 +11,16 @@ export class Admin extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
private _phone!: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
phone!: string;
|
||||
get phone(): string {
|
||||
return this._phone;
|
||||
}
|
||||
|
||||
set phone(value: string) {
|
||||
this._phone = normalizePhone(value);
|
||||
}
|
||||
|
||||
// Add the new role property
|
||||
@OneToMany(() => AdminRole, adminRole => adminRole.admin, {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CacheService } from '../../utils/cache.service';
|
||||
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
|
||||
import { UpdateAdminDto } from '../dto/update-admin.dto';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -22,7 +23,8 @@ export class AdminService {
|
||||
) {}
|
||||
|
||||
async findByPhone(phone: string): Promise<Admin | null> {
|
||||
return this.adminRepository.findOne({ phone });
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
}
|
||||
|
||||
async findById(adminId: string, restId: string): Promise<Admin | null> {
|
||||
@@ -39,13 +41,14 @@ export class AdminService {
|
||||
|
||||
async createAdminForMyRestaurant(restId: string, dto: CreateMyRestaurantAdminDto) {
|
||||
const { phone, firstName, lastName, roleId } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let admin: Admin | null = null;
|
||||
admin = await this.adminRepository.findOne({
|
||||
phone,
|
||||
phone: normalizedPhone,
|
||||
});
|
||||
if (!admin) {
|
||||
admin = this.adminRepository.create({
|
||||
phone,
|
||||
phone: normalizedPhone,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
@@ -123,11 +126,12 @@ export class AdminService {
|
||||
admin.lastName = rest.lastName;
|
||||
}
|
||||
if (rest.phone !== undefined && rest.phone !== admin.phone) {
|
||||
const exists = await this.adminRepository.findOne({ phone: rest.phone });
|
||||
const normalizedPhone = normalizePhone(rest.phone);
|
||||
const exists = await this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
if (exists) {
|
||||
throw new ConflictException('This Phone Number is already taken');
|
||||
}
|
||||
admin.phone = rest.phone;
|
||||
admin.phone = normalizedPhone;
|
||||
}
|
||||
|
||||
// Update role if roleId is provided
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Admin } from '../entities/admin.entity';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { RestRepository } from '../../restaurants/repositories/rest.repository';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRepository extends EntityRepository<Admin> {
|
||||
@@ -14,6 +15,7 @@ export class AdminRepository extends EntityRepository<Admin> {
|
||||
}
|
||||
|
||||
async findByPhoneAndRestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
// First, find the restaurant by slug using the same repository as auth service
|
||||
const restaurant = await this.restRepository.findOne({ slug });
|
||||
if (!restaurant) {
|
||||
@@ -24,7 +26,7 @@ export class AdminRepository extends EntityRepository<Admin> {
|
||||
const adminRole = await this.em.findOne(
|
||||
AdminRole,
|
||||
{
|
||||
admin: { phone },
|
||||
admin: { phone: normalizedPhone },
|
||||
restaurant: { id: restaurant.id },
|
||||
},
|
||||
{ populate: ['admin', 'admin.roles', 'role', 'role.permissions', 'restaurant'] },
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AdminRepository } from 'src/modules/admin/repositories/admin.repository
|
||||
import { AdminLoginTransformer } from '../transformers/admin-login.transformer';
|
||||
import { UserLoginTransformer } from '../transformers/user-login.transformer';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -36,24 +37,26 @@ export class AuthService {
|
||||
|
||||
async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
|
||||
const { phone, slug } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const code = this.generateOtpCode();
|
||||
const key = isAdmin ? this.adminOtpKey(slug, phone) : this.userOtpKey(slug, phone);
|
||||
const key = isAdmin ? this.adminOtpKey(slug, normalizedPhone) : this.userOtpKey(slug, normalizedPhone);
|
||||
await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME);
|
||||
|
||||
// await this.smsService.sendOtp(phone, code);
|
||||
// await this.smsService.sendOtp(normalizedPhone, code);
|
||||
|
||||
return { code };
|
||||
}
|
||||
|
||||
async verifyOtp(phone: string, slug: string, code: string) {
|
||||
const key = this.userOtpKey(slug, phone);
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const key = this.userOtpKey(slug, normalizedPhone);
|
||||
const cachedCode = await this.cacheService.get(key);
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
|
||||
await this.cacheService.del(key);
|
||||
|
||||
const user = await this.userService.findOrCreateByPhone(phone);
|
||||
const user = await this.userService.findOrCreateByPhone(normalizedPhone);
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
@@ -69,7 +72,8 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
||||
const key = this.adminOtpKey(slug, phone);
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const key = this.adminOtpKey(slug, normalizedPhone);
|
||||
const cachedCode = await this.cacheService.get(key);
|
||||
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
@@ -77,7 +81,7 @@ export class AuthService {
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
await this.cacheService.del(key);
|
||||
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(phone, slug);
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
|
||||
@@ -235,7 +235,7 @@ export class CouponService {
|
||||
}
|
||||
}
|
||||
|
||||
generateShortCode(length = 8) {
|
||||
generateShortCode(restaurantSlug: string, length = 5) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const bytes = randomBytes(length);
|
||||
let result = '';
|
||||
@@ -243,12 +243,16 @@ export class CouponService {
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
result += chars[bytes[i] % chars.length];
|
||||
}
|
||||
|
||||
return result;
|
||||
const prefix = restaurantSlug.slice(0, 2);
|
||||
return prefix + '-' + result;
|
||||
}
|
||||
|
||||
async generateCouponCode(restId: string): Promise<string> {
|
||||
const code = this.generateShortCode(8);
|
||||
const restaurant = await this.restRepository.findOne({ id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
const code = this.generateShortCode(restaurant.slug);
|
||||
const existing = await this.couponRepository.findOne({ code, restaurant: { id: restId } });
|
||||
|
||||
if (existing) return this.generateCouponCode(restId);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Entity, Property, OneToMany, Collection, Cascade } from '@mikro-orm/cor
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { UserAddress } from './user-address.entity';
|
||||
import { Order } from 'src/modules/orders/entities/order.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Entity({ tableName: 'users' })
|
||||
export class User extends BaseEntity {
|
||||
@@ -14,8 +15,16 @@ export class User extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
private _phone!: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
phone!: string;
|
||||
get phone(): string {
|
||||
return this._phone;
|
||||
}
|
||||
|
||||
set phone(value: string) {
|
||||
this._phone = normalizePhone(value);
|
||||
}
|
||||
|
||||
@Property({ default: null, type: 'date' })
|
||||
birthDate!: Date;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CreateUserAddressDto } from './dto/create-user-address.dto';
|
||||
import { UpdateUserAddressDto } from './dto/update-user-address.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
import { normalizePhone } from '../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@@ -18,13 +19,14 @@ export class UserService {
|
||||
) {}
|
||||
|
||||
async findOrCreateByPhone(phone: string): Promise<User> {
|
||||
let user = await this.userRepository.findOne({ phone });
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let user = await this.userRepository.findOne({ phone: normalizedPhone });
|
||||
|
||||
if (!user) {
|
||||
// firstName is required on the entity; use phone as a sensible default
|
||||
const _raw = {
|
||||
phone,
|
||||
firstName: phone,
|
||||
phone: normalizedPhone,
|
||||
firstName: normalizedPhone,
|
||||
// keep dates undefined (no value) — cast through unknown to satisfy TS typing
|
||||
birthDate: undefined,
|
||||
marriageDate: undefined,
|
||||
@@ -54,7 +56,8 @@ export class UserService {
|
||||
// }
|
||||
|
||||
async findByPhone(phone: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ phone });
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.userRepository.findOne({ phone: normalizedPhone });
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
@@ -62,9 +65,10 @@ export class UserService {
|
||||
}
|
||||
|
||||
async create(phone: string): Promise<User> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const _raw = {
|
||||
phone,
|
||||
firstName: phone,
|
||||
phone: normalizedPhone,
|
||||
firstName: normalizedPhone,
|
||||
birthDate: undefined,
|
||||
marriageDate: undefined,
|
||||
wallet: 0,
|
||||
@@ -117,11 +121,15 @@ export class UserService {
|
||||
// 4. Add 'search' logic (case-insensitive)
|
||||
if (search) {
|
||||
const searchPattern = `%${search}%`;
|
||||
const normalizedSearch = normalizePhone(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
|
||||
where.$or = [
|
||||
{ firstName: { $ilike: searchPattern } },
|
||||
{ lastName: { $ilike: searchPattern } },
|
||||
{ phone: { $ilike: searchPattern } },
|
||||
...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Normalizes Iranian phone numbers to a standard format: 9120000001
|
||||
* Removes +98 prefix, leading 0, and any spaces/dashes
|
||||
* @param phone - The phone number to normalize
|
||||
* @returns Normalized phone number (e.g., 9120000001)
|
||||
*/
|
||||
export function normalizePhone(phone: string): string {
|
||||
if (!phone) {
|
||||
return phone;
|
||||
}
|
||||
|
||||
// Remove all spaces, dashes, and parentheses
|
||||
let normalized = phone.replace(/[\s\-\(\)]/g, '');
|
||||
|
||||
// Remove +98 prefix (Iran country code)
|
||||
if (normalized.startsWith('+98')) {
|
||||
normalized = normalized.substring(3);
|
||||
} else if (normalized.startsWith('0098')) {
|
||||
normalized = normalized.substring(4);
|
||||
} else if (normalized.startsWith('98')) {
|
||||
normalized = normalized.substring(2);
|
||||
}
|
||||
|
||||
// Remove leading 0
|
||||
if (normalized.startsWith('0')) {
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user