mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 20:10:56 +00:00
222 lines
7.7 KiB
TypeScript
222 lines
7.7 KiB
TypeScript
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<IamRow[]>(
|
|
`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 } },
|
|
scheduleId: dto.scheduleId,
|
|
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 }),
|
|
},
|
|
});
|
|
}
|
|
|
|
async deleteAgent(id: string) {
|
|
const agent = await this.prisma.agent.findUnique({ where: { id } });
|
|
if (!agent) throw new NotFoundException('Agent not found');
|
|
await this.prisma.agent.delete({ where: { id } });
|
|
return { deleted: true };
|
|
}
|
|
}
|