279 lines
8.0 KiB
TypeScript
279 lines
8.0 KiB
TypeScript
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<User> {
|
|
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>;
|
|
|
|
user = this.userRepository.create(createData);
|
|
await this.em.persistAndFlush(user);
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
// async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
|
|
// 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<User | null> {
|
|
return this.userRepository.findOne({ phone });
|
|
}
|
|
|
|
async findById(id: string): Promise<User | null> {
|
|
return this.userRepository.findOne({ id });
|
|
}
|
|
|
|
async create(phone: string): Promise<User> {
|
|
const _raw = {
|
|
phone,
|
|
firstName: phone,
|
|
birthDate: undefined,
|
|
marriageDate: undefined,
|
|
wallet: 0,
|
|
points: 0,
|
|
};
|
|
|
|
const createData = _raw as unknown as RequiredEntityData<User>;
|
|
|
|
const user = this.userRepository.create(createData);
|
|
await this.em.persistAndFlush(user);
|
|
return user;
|
|
}
|
|
|
|
async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
|
|
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<User> = { ...dto } as Partial<User>;
|
|
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<PaginatedResult<User>> {
|
|
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<User> = {
|
|
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<UserAddress[]> {
|
|
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<UserAddress> {
|
|
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<UserAddress> {
|
|
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<UserAddress> {
|
|
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<void> {
|
|
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<UserAddress> {
|
|
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;
|
|
}
|
|
}
|