import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import { CreateAgentBookingDto, OpenShiftDto, CloseShiftDto } from './agents.dto'; import { IdDocumentType } from '@prisma/client'; function generateRef(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); } @Injectable() export class AgentsService { constructor( private prisma: PrismaService, @InjectDataSource() private readonly dataSource: DataSource, ) {} async findAll(filters: { search?: string; active?: string }) { const where: any = {}; if (filters.active !== undefined && filters.active !== '') { where.active = filters.active === 'true'; } const agents = await this.prisma.agent.findMany({ where, orderBy: { createdAt: 'desc' }, }); // Enrich with IAM user data const iamUserIds = agents.map(a => a.iamUserId).filter(Boolean) as string[]; type IamRow = { id: string; email: string; name: any; phone_number: string | null }; const iamRows: IamRow[] = iamUserIds.length > 0 ? await this.dataSource.query( `SELECT id, email, name, phone_number FROM iam.users WHERE id = ANY($1)`, [iamUserIds], ).catch(() => []) : []; const iamMap = new Map(iamRows.map(r => [r.id, r])); const items = agents .map(a => { const iam = a.iamUserId ? iamMap.get(a.iamUserId) ?? null : null; const fullName = iam?.name?.en ?? iam?.name?.am ?? null; if (filters.search) { const q = filters.search.toLowerCase(); const matches = a.agentCode.toLowerCase().includes(q) || (iam?.email ?? '').toLowerCase().includes(q) || (fullName ?? '').toLowerCase().includes(q); if (!matches) return null; } return { ...a, user: iam ? { fullName, email: iam.email, phone: iam.phone_number } : null, }; }) .filter(Boolean); return { items, total: items.length }; } async createAgentBooking(dto: CreateAgentBookingDto) { const agent = await this.prisma.agent.findUnique({ where: { id: dto.agentId } }); if (!agent || !agent.active) throw new NotFoundException('Agent not found or inactive'); const passenger = agent.iamUserId ? await this.prisma.passenger.findUnique({ where: { iamUserId: agent.iamUserId } }) : null; if (!passenger) throw new BadRequestException('Agent must have a linked passenger account'); const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId } }); if (!schedule) throw new NotFoundException('Schedule not found'); const seatIds = dto.passengers.map(p => p.seatId); const seats = await this.prisma.seat.findMany({ where: { id: { in: seatIds } } }); if (seats.length !== seatIds.length) throw new BadRequestException('Invalid seat selection'); const baseFare = 45000 * dto.passengers.length; const totalMinor = baseFare; const booking = await this.prisma.booking.create({ data: { bookingRef: generateRef(), passengerId: passenger.id, scheduleId: dto.scheduleId, status: dto.paymentMethod === 'CASH' ? 'CONFIRMED' : 'PENDING_PAYMENT', totalMinor, seats: { create: dto.passengers.map(p => ({ seat: { connect: { id: p.seatId } }, passengerName: p.fullName, idDocumentType: p.idDocumentType as IdDocumentType | undefined, idDocumentNumber: p.idDocumentNumber })) } }, include: { seats: true } }); await this.prisma.seat.updateMany({ where: { id: { in: seatIds } }, data: { status: 'BOOKED' } }); const changeGiven = dto.cashReceived ? dto.cashReceived - totalMinor : 0; await this.prisma.agentBooking.create({ data: { agentId: dto.agentId, bookingId: booking.id, paymentMethod: dto.paymentMethod, cashReceived: dto.cashReceived, changeGiven, paperTicket: dto.paperTicket ?? false } }); const commissionAmount = Math.floor(totalMinor * agent.commissionRate / 100); await this.prisma.agentCommission.create({ data: { agentId: dto.agentId, bookingId: booking.id, amountMinor: commissionAmount, rate: agent.commissionRate } }); return { booking, commission: commissionAmount }; } async openShift(dto: OpenShiftDto) { const agent = await this.prisma.agent.findUnique({ where: { id: dto.agentId } }); if (!agent) throw new NotFoundException('Agent not found'); const openShift = await this.prisma.agentShift.findFirst({ where: { agentId: dto.agentId, closedAt: null } }); if (openShift) throw new BadRequestException('Shift already open'); return this.prisma.agentShift.create({ data: { agentId: dto.agentId, openingBalance: dto.openingBalance ?? 0 } }); } async closeShift(dto: CloseShiftDto) { const shift = await this.prisma.agentShift.findUnique({ where: { id: dto.shiftId } }); if (!shift) throw new NotFoundException('Shift not found'); if (shift.closedAt) throw new BadRequestException('Shift already closed'); return this.prisma.agentShift.update({ where: { id: dto.shiftId }, data: { closedAt: new Date(), closingBalance: dto.closingBalance, notes: dto.notes, reconciled: true } }); } async getCommissions(agentId: string, dateFrom?: Date, dateTo?: Date) { return this.prisma.agentCommission.findMany({ where: { agentId, createdAt: { gte: dateFrom, lte: dateTo } }, orderBy: { createdAt: 'desc' } }); } async getShifts(agentId: string) { return this.prisma.agentShift.findMany({ where: { agentId }, orderBy: { openedAt: 'desc' }, take: 20 }); } async getMe(iamUserId: string) { const agent = await this.prisma.agent.findUnique({ where: { iamUserId } }); if (!agent) throw new NotFoundException('No agent profile found for this user'); return agent; } async createAgent(dto: { iamUserId: string; agentCode?: string; commissionRate?: number }) { const existing = await this.prisma.agent.findUnique({ where: { iamUserId: dto.iamUserId } }); if (existing) throw new BadRequestException('An agent profile already exists for this user'); const agentCode = dto.agentCode || `AG${String(Date.now()).slice(-4)}`; return this.prisma.agent.create({ data: { iamUserId: dto.iamUserId, agentCode, commissionRate: dto.commissionRate ?? 5, active: true, }, }); } async updateAgent(id: string, dto: { agentCode?: string; commissionRate?: number; active?: boolean }) { const agent = await this.prisma.agent.findUnique({ where: { id } }); if (!agent) throw new NotFoundException('Agent not found'); return this.prisma.agent.update({ where: { id }, data: { ...(dto.agentCode !== undefined && { agentCode: dto.agentCode }), ...(dto.commissionRate !== undefined && { commissionRate: dto.commissionRate }), ...(dto.active !== undefined && { active: dto.active }), }, }); } }