import { Injectable, NotFoundException } from '@nestjs/common'; import { FilterQuery, RequiredEntityData } from '@mikro-orm/core'; import { User } from './entities/user.entity'; import { UserAddress } from './entities/user-address.entity'; import { EntityManager } from '@mikro-orm/postgresql'; import { UpdateUserDto } from './dto/update-user.dto'; import { FindUsersDto } from './dto/find-user.dto'; 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'; @Injectable() export class UserService { constructor( private readonly userRepository: UserRepository, private readonly em: EntityManager, ) {} async findOrCreateByPhone(phone: string): Promise { let user = await this.userRepository.findOne({ phone }); if (!user) { // firstName is required on the entity; use phone as a sensible default const _raw = { phone, firstName: phone, // keep dates undefined (no value) — cast through unknown to satisfy TS typing birthDate: undefined, marriageDate: undefined, wallet: 0, points: 0, }; const createData = _raw as unknown as RequiredEntityData; user = this.userRepository.create(createData); await this.em.persistAndFlush(user); } return user; } // async updateUser(userId: string, dto: UpdateUserDto): Promise { // const user = await this.em.findOneOrFail(User, userId, {}).catch(() => { // throw new NotFoundException(`User with ID ${userId} not found.`); // }); // this.em.assign(user, { ...dto }); // await this.em.flush(); // return user; // } async findByPhone(phone: string): Promise { return this.userRepository.findOne({ phone }); } async findById(id: string): Promise { return this.userRepository.findOne({ id }); } async create(phone: string): Promise { const _raw = { phone, firstName: phone, birthDate: undefined, marriageDate: undefined, wallet: 0, points: 0, }; const createData = _raw as unknown as RequiredEntityData; const user = this.userRepository.create(createData); await this.em.persistAndFlush(user); return user; } async updateUser(userId: string, dto: UpdateUserDto): Promise { const user = await this.userRepository.findOne({ id: userId }); if (!user) { throw new NotFoundException(`User with ID ${userId} not found.`); } // Normalize date strings into Date objects if provided const assignData: Partial = { ...dto } as Partial; if (dto.birthDate) { assignData.birthDate = new Date(dto.birthDate); } if (dto.marriageDate) { assignData.marriageDate = new Date(dto.marriageDate); } this.em.assign(user, assignData); await this.em.flush(); return user; } async findAll(restId: string, dto: FindUsersDto): Promise> { const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto; // 1. Calculate pagination const offset = (page - 1) * limit; // 2. Build the 'where' filter query const where: FilterQuery = { orders: { restaurant: { id: restId, }, }, }; // 4. Add 'search' logic (case-insensitive) if (search) { const searchPattern = `%${search}%`; // $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default) where.$or = [ { firstName: { $ilike: searchPattern } }, { lastName: { $ilike: searchPattern } }, { phone: { $ilike: searchPattern } }, ]; } // 5. Execute the query using findAndCount const [users, total] = await this.userRepository.findAndCount(where, { limit, offset, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, }); // 6. Calculate total pages const totalPages = Math.ceil(total / limit); // 7. Return the paginated result return { data: users, meta: { total, page, limit, totalPages, }, }; } async getUserAddresses(userId: string): Promise { const user = await this.userRepository.findOne({ id: userId }, { populate: ['addresses'] }); if (!user) { throw new NotFoundException(`User with ID ${userId} not found.`); } // Load addresses if not already loaded await user.addresses.loadItems(); return user.addresses.getItems(); } async createUserAddress(userId: string, dto: CreateUserAddressDto): Promise { const user = await this.userRepository.findOne({ id: userId }); if (!user) { throw new NotFoundException(`User with ID ${userId} not found.`); } // If setting as default, unset other default addresses if (dto.isDefault) { const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true }); for (const address of existingAddresses) { address.isDefault = false; } await this.em.flush(); } // Create new address const address = this.em.create(UserAddress, { user, title: dto.title, address: dto.address, city: dto.city, province: dto.province, postalCode: dto.postalCode, latitude: dto.latitude, longitude: dto.longitude, phone: dto.phone, isDefault: dto.isDefault ?? false, }); await this.em.persistAndFlush(address); return address; } async getUserAddress(userId: string, addressId: string): Promise { const address = await this.em.findOne(UserAddress, { id: addressId, user: { id: userId }, }); if (!address) { throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); } return address; } async updateUserAddress(userId: string, addressId: string, dto: UpdateUserAddressDto): Promise { const address = await this.em.findOne(UserAddress, { id: addressId, user: { id: userId }, }); if (!address) { throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); } // If setting as default, unset other default addresses if (dto.isDefault === true) { const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true, id: { $ne: addressId }, }); for (const existingAddress of existingAddresses) { existingAddress.isDefault = false; } await this.em.flush(); } // Update address fields this.em.assign(address, dto); await this.em.flush(); return address; } async deleteUserAddress(userId: string, addressId: string): Promise { const address = await this.em.findOne(UserAddress, { id: addressId, user: { id: userId }, }); if (!address) { throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); } // Soft delete the address address.deletedAt = new Date(); await this.em.persistAndFlush(address); } async setDefaultAddress(userId: string, addressId: string): Promise { const address = await this.em.findOne(UserAddress, { id: addressId, user: { id: userId }, }); if (!address) { throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`); } // Unset all other default addresses const existingAddresses = await this.em.find(UserAddress, { user: { id: userId }, isDefault: true, id: { $ne: addressId }, }); for (const existingAddress of existingAddresses) { existingAddress.isDefault = false; } // Set this address as default address.isDefault = true; await this.em.flush(); return address; } }