import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { FilterQuery, RequiredEntityData } from '@mikro-orm/core'; import { User } from '../entities/user.entity'; import { UserAddress } from '../entities/user-address.entity'; import { Shop } from ../../..shops/entities/shop.entity'; import { EntityManager } from '@mikro-orm/postgresql'; import { UpdateUserDto } from '../dto/update-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 '../../../utils/phone.util'; import { RestRepository } from ../../..shops/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'; @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 { 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: normalizedPhone, firstName: '[نام]', // 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 { const normalizedPhone = normalizePhone(phone); return this.userRepository.findOne({ phone: normalizedPhone }); } async findById(id: string): Promise { return this.userRepository.findOne({ id }); } // todo : transaction code here async create(phone: string, restId: string): Promise { const normalizedPhone = normalizePhone(phone); const _raw = { phone: normalizedPhone, firstName: normalizedPhone, birthDate: undefined, marriageDate: undefined, }; // get registerscore from shop const shop = await this.restaurantRepository.findOne({ id: restId }); if (!shop) { throw new NotFoundException(`Shop not found.`); } const points = Number(shop.score?.registerScore || 0); const createData = _raw as unknown as RequiredEntityData; const user = this.userRepository.create(createData); const newWalletTransaction = this.walletTransactionRepository.create({ user: user, shop: shop, balance: 0, amount: points, type: WalletTransactionType.CREDIT, reason: WalletTransactionReason.DEPOSIT }); await this.em.persistAndFlush([user, newWalletTransaction]); return user; } async updateUser(userId: string, restId: 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); } // get restuarant 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: { shop: { 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 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; } 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 shop = await em.findOne(Shop, { id: restId }); if (!shop) { throw new NotFoundException(`Shop with ID ${restId} not found.`); } let walletTransaction = await em.findOne(WalletTransaction, { user: { id: userId }, shop: { id: restId } }, { orderBy: { createdAt: 'DESC' } }); if (!walletTransaction) { walletTransaction = em.create(WalletTransaction, { user: user, shop, 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 (!shop.score) { throw new BadRequestException('Shop score not found.'); } if (!shop.score.purchaseScore) { throw new BadRequestException('Shop purchase score not found.'); } if (!shop.score.purchaseAmount) { throw new BadRequestException('Shop purchase amount not found.'); } const walletIncreaseAmount = (Number(pointsToConvert) * Number(shop.score.purchaseAmount)) / Number(shop.score.purchaseScore); // Convert points to wallet (1:1 ratio - can be made configurable) const newWalletTransaction = em.create(WalletTransaction, { user: user, shop: shop, 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 { return this.walletTransactionRepository.findOne({ user: { id: userId }, shop: { 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); }); } }