import { ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CustomerPolicy } from '../authorization/policies/customer.policy'; import { AuthUser } from '../auth/interfaces/auth-user.interface'; import { Reserve } from '../reserves/entities/reserve.entity'; import { ReserveStatus } from '../reserves/enums/reserve-status.enum'; import { Salon } from '../salons/entities/salon.entity'; import { UserRole } from '../users/enums/user-role.enum'; import { UsersService } from '../users/users.service'; import { CreateCustomerDto } from './dto/create-customer.dto'; import { CreateSalonCustomerDto } from './dto/salon-customer.dto'; import { FindSalonCustomersQueryDto } from './dto/salon-customer.dto'; import { SalonCustomerDetailDto, SalonCustomerListResponseDto, SalonCustomerSummaryDto, } from './dto/salon-customer-response.dto'; import { UpdateCustomerDto } from './dto/update-customer.dto'; import { Customer } from './entities/customer.entity'; import { SalonCustomer } from './entities/salon-customer.entity'; import { daysBetween, groupVisitsByCustomer, mapSegmentToClientStatus, parseDate, resolveSegment, sumTotalPurchase, } from './helpers/customer-segmentation.helper'; @Injectable() export class CustomersService { constructor( @InjectRepository(Customer) private readonly customersRepository: Repository, @InjectRepository(SalonCustomer) private readonly salonCustomersRepository: Repository, @InjectRepository(Salon) private readonly salonsRepository: Repository, @InjectRepository(Reserve) private readonly reservesRepository: Repository, private readonly usersService: UsersService, private readonly customerPolicy: CustomerPolicy, ) {} async create(createCustomerDto: CreateCustomerDto): Promise { const user = await this.usersService.create({ firstName: createCustomerDto.firstName, lastName: createCustomerDto.lastName, mobile: createCustomerDto.mobile, role: UserRole.CUSTOMER, }); const customer = this.customersRepository.create({ userId: user.id, dateOfBirth: createCustomerDto.dateOfBirth, address: createCustomerDto.address, }); const saved = await this.customersRepository.save(customer); return this.findOne(saved.id); } findAll(): Promise { return this.customersRepository.find({ relations: { user: true } }); } async findAllForSalon( user: AuthUser, query: FindSalonCustomersQueryDto, ): Promise { const salon = await this.resolveSalonForUser(user); const summaries = await this.buildSalonCustomerSummaries(salon.id); const filtered = this.filterSalonCustomers(summaries, query); return { customers: filtered, stats: this.buildSalonCustomerStats(summaries), }; } async findOneForSalon( id: string, user: AuthUser, ): Promise { const salon = await this.resolveSalonForUser(user); const customer = await this.findOne(id); if (!(await this.customerPolicy.canRead(user, customer))) { throw new ForbiddenException(); } const isLinked = await this.salonCustomersRepository.exists({ where: { salonId: salon.id, customerId: customer.id }, }); if (!isLinked) { throw new NotFoundException(`Customer #${id} not found in this salon`); } return this.buildSalonCustomerDetail(customer, salon.id); } async createForSalon( dto: CreateSalonCustomerDto, user: AuthUser, ): Promise { const salon = await this.resolveSalonForUser(user); const customer = await this.findOrCreateByMobile(dto); await this.linkToSalon(salon.id, customer.id); return this.buildSalonCustomerDetail(customer, salon.id); } async linkToSalon(salonId: string, customerId: string): Promise { await this.salonCustomersRepository.upsert( { salonId, customerId }, { conflictPaths: ['salonId', 'customerId'] }, ); } async findOrCreateByMobile(dto: CreateSalonCustomerDto): Promise { const existingUser = await this.usersService.findByMobile(dto.mobile); if (existingUser) { let customer = await this.customersRepository.findOne({ where: { userId: existingUser.id }, relations: { user: true }, }); if (!customer) { customer = await this.customersRepository.save( this.customersRepository.create({ userId: existingUser.id, dateOfBirth: dto.dateOfBirth ?? '2000-01-01', address: dto.address ?? '-', }), ); customer = await this.findOne(customer.id); } return customer; } return this.create({ firstName: dto.firstName, lastName: dto.lastName, mobile: dto.mobile, dateOfBirth: dto.dateOfBirth ?? '2000-01-01', address: dto.address ?? '-', }); } async findOne(id: string, user?: AuthUser): Promise { const customer = await this.customersRepository.findOne({ where: { id }, relations: { user: true }, }); if (!customer) { throw new NotFoundException(`Customer #${id} not found`); } if (user && !(await this.customerPolicy.canRead(user, customer))) { throw new ForbiddenException(); } return customer; } async update( id: string, updateCustomerDto: UpdateCustomerDto, user: AuthUser, ): Promise { const customer = await this.findOne(id); if (!this.customerPolicy.canUpdate(user, customer)) { throw new ForbiddenException(); } const { firstName, lastName, mobile, dateOfBirth, address } = updateCustomerDto; if ( firstName !== undefined || lastName !== undefined || mobile !== undefined ) { await this.usersService.update(customer.userId, { firstName, lastName, mobile, }); } if (dateOfBirth !== undefined) { customer.dateOfBirth = dateOfBirth; } if (address !== undefined) { customer.address = address; } await this.customersRepository.save(customer); return this.findOne(customer.id, user); } async remove(id: string): Promise { const customer = await this.findOne(id); await this.usersService.remove(customer.userId); } private async resolveSalonForUser(user: AuthUser): Promise { if (user.role !== UserRole.SALON) { throw new ForbiddenException(); } const salon = await this.salonsRepository.findOne({ where: { ownerId: user.id }, order: { createdAt: 'ASC' }, }); if (!salon) { throw new NotFoundException('Salon not found for this user'); } return salon; } private async buildSalonCustomerSummaries( salonId: string, ): Promise { const links = await this.salonCustomersRepository.find({ where: { salonId }, relations: { customer: { user: true } }, order: { createdAt: 'ASC' }, }); const completedReserves = await this.reservesRepository.find({ where: { salonId, status: ReserveStatus.COMPLETED }, relations: { items: { serviceSkill: true, service: true } }, order: { appointmentDate: 'ASC' }, }); const visitsByCustomer = groupVisitsByCustomer(completedReserves); const now = new Date(); return links.map((link) => this.buildSummaryFromCustomer( link.customer, link.createdAt, visitsByCustomer[link.customer.id] ?? [], now, ), ); } private async buildSalonCustomerDetail( customer: Customer, salonId: string, ): Promise { const link = await this.salonCustomersRepository.findOne({ where: { salonId, customerId: customer.id }, }); const completedReserves = await this.reservesRepository.find({ where: { salonId, customerId: customer.id, status: ReserveStatus.COMPLETED, }, relations: { items: { serviceSkill: true, service: true } }, order: { appointmentDate: 'DESC' }, }); const visits = groupVisitsByCustomer(completedReserves)[customer.id] ?? []; const summary = this.buildSummaryFromCustomer( customer, link?.createdAt ?? customer.createdAt, visits, new Date(), ); const serviceHistory = completedReserves.flatMap((reserve) => (reserve.items ?? []).map((item) => ({ id: item.id, serviceName: item.service?.title ?? 'خدمت', serviceIcon: item.service?.iconUrl ?? null, date: reserve.appointmentDate, price: reserve.totalAmount, })), ); return { ...summary, serviceHistory, }; } private buildSummaryFromCustomer( customer: Customer, membershipDate: Date, visits: ReturnType[string], referenceDate: Date, ): SalonCustomerSummaryDto { const totalPurchase = sumTotalPurchase(visits); const visitCount = visits.length; const lastVisit = visits[visits.length - 1]; const lastVisitDate = lastVisit?.appointmentDate ?? null; const daysSinceVisit = lastVisitDate ? daysBetween(parseDate(lastVisitDate), referenceDate) : null; const segment = lastVisit ? resolveSegment(daysSinceVisit ?? 0, lastVisit.renewalDays) : 'active'; return { id: customer.id, firstName: customer.user.firstName, lastName: customer.user.lastName, mobile: customer.user.mobile, dateOfBirth: customer.dateOfBirth, address: customer.address, membershipDate: membershipDate.toISOString(), totalPurchase, visitCount, lastVisitDate, daysSinceVisit, status: mapSegmentToClientStatus(segment, totalPurchase), }; } private filterSalonCustomers( customers: SalonCustomerSummaryDto[], query: FindSalonCustomersQueryDto, ): SalonCustomerSummaryDto[] { const search = query.search?.trim().toLowerCase(); return customers.filter((customer) => { const fullName = `${customer.firstName} ${customer.lastName}`.toLowerCase(); const matchesSearch = !search || fullName.includes(search) || customer.mobile.includes(search); const matchesStatus = !query.status || customer.status === query.status; return matchesSearch && matchesStatus; }); } private buildSalonCustomerStats(customers: SalonCustomerSummaryDto[]) { return { total: customers.length, active: customers.filter((customer) => customer.status === 'active') .length, atRisk: customers.filter((customer) => customer.status === 'at-risk') .length, lost: customers.filter((customer) => customer.status === 'lost').length, vip: customers.filter((customer) => customer.status === 'vip').length, }; } }