user module
This commit is contained in:
@@ -1,71 +1,46 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { FilterQuery, RequiredEntityData } from '@mikro-orm/core';
|
||||
import { RequiredEntityData } from '@mikro-orm/core';
|
||||
import { User } from '../entities/user.entity';
|
||||
import { UserAddress } from '../entities/user-address.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { FindUsersDto } from '../dto/find-user.dto';
|
||||
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
|
||||
import { CreateWalletTransactionDto } from '../dto/create-wallet-transaction.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';
|
||||
import { normalizePhone } from '../../util/phone.util';
|
||||
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||
import { WalletTransactionRepository } from '../repositories/wallet-transaction.repository';
|
||||
import { WalletService } from './wallet.service';
|
||||
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
|
||||
import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly restaurantRepository: RestRepository,
|
||||
private readonly walletTransactionRepository: WalletTransactionRepository,
|
||||
private readonly walletService: WalletService,
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async findOrCreateByPhone(phone: string): Promise<User> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let user = await this.userRepository.findOne({ phone: normalizedPhone });
|
||||
async create(dto: CreateUserDto): Promise<User> {
|
||||
const normalizedPhone = normalizePhone(dto.phone)
|
||||
|
||||
if (!user) {
|
||||
// firstName is required on the entity; use phone as a sensible default
|
||||
const _raw = {
|
||||
phone: normalizedPhone,
|
||||
firstName: '[نام]',
|
||||
// keep dates undefined (no value) — cast through unknown to satisfy TS typing
|
||||
birthDate: undefined,
|
||||
marriageDate: undefined,
|
||||
wallet: 0,
|
||||
points: 0,
|
||||
};
|
||||
const currentUser = await this.userRepository.findOne({ phone: normalizedPhone });
|
||||
|
||||
const createData = _raw as unknown as RequiredEntityData<User>;
|
||||
|
||||
user = this.userRepository.create(createData);
|
||||
await this.em.persistAndFlush(user);
|
||||
if (currentUser) {
|
||||
return currentUser
|
||||
}
|
||||
|
||||
const createData: RequiredEntityData<User> = {
|
||||
phone: normalizedPhone,
|
||||
firstName: dto.firstName,
|
||||
lastName: dto.lastName,
|
||||
gender: dto.gender,
|
||||
maxCredit: dto.maxCredit
|
||||
};
|
||||
|
||||
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.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> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.userRepository.findOne({ phone: normalizedPhone });
|
||||
@@ -74,107 +49,24 @@ export class UserService {
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ id });
|
||||
}
|
||||
// todo : transaction code here
|
||||
async create(phone: string, restId: string): Promise<User> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const _raw = {
|
||||
phone: normalizedPhone,
|
||||
firstName: normalizedPhone,
|
||||
birthDate: undefined,
|
||||
marriageDate: undefined,
|
||||
};
|
||||
// get registerscore from restaurant
|
||||
const restaurant = await this.restaurantRepository.findOne({ id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant not found.`);
|
||||
}
|
||||
const points = Number(restaurant.score?.registerScore || 0);
|
||||
const createData = _raw as unknown as RequiredEntityData<User>;
|
||||
|
||||
const user = this.userRepository.create(createData);
|
||||
const newWalletTransaction = this.walletTransactionRepository.create({
|
||||
user: user,
|
||||
restaurant: restaurant,
|
||||
balance: 0,
|
||||
amount: points,
|
||||
type: WalletTransactionType.CREDIT,
|
||||
reason: WalletTransactionReason.DEPOSIT
|
||||
});
|
||||
await this.em.persistAndFlush([user, newWalletTransaction]);
|
||||
return user;
|
||||
}
|
||||
async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
|
||||
|
||||
async updateUser(userId: string, restId: 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);
|
||||
}
|
||||
// get restuarant
|
||||
this.em.assign(user, assignData);
|
||||
this.em.assign(user, dto);
|
||||
|
||||
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}%`;
|
||||
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 } }] : []),
|
||||
];
|
||||
}
|
||||
|
||||
// 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 findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
||||
return this.userRepository.findAllPaginated(dto)
|
||||
}
|
||||
|
||||
async getUserAddresses(userId: string): Promise<UserAddress[]> {
|
||||
@@ -188,201 +80,124 @@ export class UserService {
|
||||
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.`);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
// // 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,
|
||||
});
|
||||
// // 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;
|
||||
}
|
||||
// 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 },
|
||||
});
|
||||
// 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}.`);
|
||||
}
|
||||
// if (!address) {
|
||||
// throw new NotFoundException(`Address with ID ${addressId} not found for user ${userId}.`);
|
||||
// }
|
||||
|
||||
return address;
|
||||
}
|
||||
// return address;
|
||||
// }
|
||||
|
||||
async updateUserAddress(userId: string, addressId: string, dto: UpdateUserAddressDto): Promise<UserAddress> {
|
||||
const address = await this.em.findOne(UserAddress, {
|
||||
id: addressId,
|
||||
user: { id: userId },
|
||||
});
|
||||
// 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 (!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();
|
||||
}
|
||||
// // 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();
|
||||
// // Update address fields
|
||||
// this.em.assign(address, dto);
|
||||
// await this.em.flush();
|
||||
|
||||
return address;
|
||||
}
|
||||
// return address;
|
||||
// }
|
||||
|
||||
async deleteUserAddress(userId: string, addressId: string): Promise<void> {
|
||||
const address = await this.em.findOne(UserAddress, {
|
||||
id: addressId,
|
||||
user: { id: userId },
|
||||
});
|
||||
// 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}.`);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// // 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 },
|
||||
});
|
||||
// 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}.`);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
// // 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();
|
||||
// // Set this address as default
|
||||
// address.isDefault = true;
|
||||
// await this.em.flush();
|
||||
|
||||
return address;
|
||||
}
|
||||
// return address;
|
||||
// }
|
||||
|
||||
async convertScoreToWallet(userId: string, restId: string) {
|
||||
return this.em.transactional(async em => {
|
||||
const user = await em.findOne(User, { id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
}
|
||||
const restaurant = await em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(`Restaurant with ID ${restId} not found.`);
|
||||
}
|
||||
let walletTransaction = await em.findOne(WalletTransaction, { user: { id: userId }, restaurant: { id: restId } },
|
||||
{ orderBy: { createdAt: 'DESC' } });
|
||||
|
||||
if (!walletTransaction) {
|
||||
walletTransaction = em.create(WalletTransaction,
|
||||
{
|
||||
user: user,
|
||||
restaurant,
|
||||
balance: 0,
|
||||
amount: 0,
|
||||
type: WalletTransactionType.CREDIT,
|
||||
reason: WalletTransactionReason.DEPOSIT
|
||||
});
|
||||
await em.persistAndFlush(walletTransaction);
|
||||
}
|
||||
// Determine how many points to convert
|
||||
const pointsToConvert = walletTransaction.balance;
|
||||
|
||||
// Validate points to convert
|
||||
if (pointsToConvert <= 0) {
|
||||
throw new BadRequestException('Points to convert must be greater than 0.');
|
||||
}
|
||||
|
||||
if (!restaurant.score) {
|
||||
throw new BadRequestException('Restaurant score not found.');
|
||||
}
|
||||
if (!restaurant.score.purchaseScore) {
|
||||
throw new BadRequestException('Restaurant purchase score not found.');
|
||||
}
|
||||
if (!restaurant.score.purchaseAmount) {
|
||||
throw new BadRequestException('Restaurant purchase amount not found.');
|
||||
}
|
||||
const walletIncreaseAmount =
|
||||
(Number(pointsToConvert) * Number(restaurant.score.purchaseAmount)) / Number(restaurant.score.purchaseScore);
|
||||
// Convert points to wallet (1:1 ratio - can be made configurable)
|
||||
|
||||
const newWalletTransaction = em.create(WalletTransaction, {
|
||||
user: user,
|
||||
restaurant: restaurant,
|
||||
amount: walletIncreaseAmount,
|
||||
type: WalletTransactionType.CREDIT,
|
||||
reason: WalletTransactionReason.CONVERT_SCORE_TO_WALLET,
|
||||
balance: walletTransaction.balance + walletIncreaseAmount,
|
||||
});
|
||||
// Update user's wallet and points
|
||||
walletTransaction.balance = walletTransaction.balance + walletIncreaseAmount;
|
||||
em.persist([newWalletTransaction]);
|
||||
|
||||
await em.flush();
|
||||
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
getUserWallet(userId: string, restId: string): Promise<WalletTransaction | null> {
|
||||
return this.walletTransactionRepository.findOne({ user: { id: userId }, restaurant: { id: restId } },
|
||||
{ orderBy: { createdAt: 'DESC' } });
|
||||
}
|
||||
|
||||
getUserWalletTransactions(userId: string, restId: string, dto: FindWalletTransactionsDto) {
|
||||
return this.walletService.getUserWalletTransactions(userId, restId, dto);
|
||||
}
|
||||
|
||||
async createWalletTransaction(userId: string, restId: string, dto: CreateWalletTransactionDto) {
|
||||
return this.em.transactional(async em => {
|
||||
return this.walletService.createTransaction(em, userId, restId, dto);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user