mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
207 lines
6.8 KiB
TypeScript
207 lines
6.8 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectDataSource } from '@nestjs/typeorm';
|
|
import { DataSource } from 'typeorm';
|
|
import { PrismaService } from '../../common/prisma.service';
|
|
import { GenerateReportDto, ReportType } from './reports.dto';
|
|
|
|
@Injectable()
|
|
export class ReportsService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
@InjectDataSource() private dataSource: DataSource,
|
|
) {}
|
|
|
|
async generateReport(dto: GenerateReportDto) {
|
|
const dateFrom = new Date(dto.dateFrom);
|
|
dateFrom.setHours(0, 0, 0, 0);
|
|
|
|
const dateTo = new Date(dto.dateTo);
|
|
dateTo.setHours(23, 59, 59, 999);
|
|
|
|
let data: any;
|
|
switch (dto.reportType) {
|
|
case ReportType.REVENUE:
|
|
data = await this.generateRevenueReport(dateFrom, dateTo);
|
|
break;
|
|
case ReportType.OCCUPANCY:
|
|
data = await this.generateOccupancyReport(dateFrom, dateTo);
|
|
break;
|
|
case ReportType.AGENT_SALES:
|
|
data = await this.generateAgentSalesReport(dateFrom, dateTo, dto.agentId);
|
|
break;
|
|
case ReportType.CANCELLATIONS:
|
|
data = await this.generateCancellationsReport(dateFrom, dateTo);
|
|
break;
|
|
case ReportType.PAYMENT_METHODS:
|
|
data = await this.generatePaymentMethodsReport(dateFrom, dateTo);
|
|
break;
|
|
default:
|
|
data = {};
|
|
}
|
|
|
|
const report = await this.prisma.operationalReport.create({
|
|
data: {
|
|
reportType: dto.reportType,
|
|
dateFrom,
|
|
dateTo,
|
|
data
|
|
}
|
|
});
|
|
|
|
return { reportId: report.id, reportType: dto.reportType, data };
|
|
}
|
|
|
|
private async generateRevenueReport(dateFrom: Date, dateTo: Date) {
|
|
// Fetch all bookings in date range, regardless of status
|
|
const bookings = await this.prisma.booking.findMany({
|
|
where: {
|
|
createdAt: { gte: dateFrom, lte: dateTo }
|
|
},
|
|
include: { paymentIntent: true }
|
|
});
|
|
|
|
console.log(`[Reports] Revenue Report: Found ${bookings.length} bookings between ${dateFrom} and ${dateTo}`);
|
|
|
|
const totalRevenue = bookings.reduce((sum, b) => sum + b.totalMinor, 0);
|
|
const byPaymentMethod = bookings.reduce((acc, b) => {
|
|
const method = b.paymentIntent?.method ?? 'UNKNOWN';
|
|
acc[method] = (acc[method] || 0) + b.totalMinor;
|
|
return acc;
|
|
}, {} as Record<string, number>);
|
|
|
|
// Group by date for charts
|
|
const byDate = bookings.reduce((acc, b) => {
|
|
const date = b.createdAt.toISOString().split('T')[0];
|
|
if (!acc[date]) {
|
|
acc[date] = { totalMinor: 0, count: 0 };
|
|
}
|
|
acc[date].totalMinor += b.totalMinor;
|
|
acc[date].count += 1;
|
|
return acc;
|
|
}, {} as Record<string, any>);
|
|
|
|
return {
|
|
totalBookings: bookings.length,
|
|
totalRevenueMinor: totalRevenue,
|
|
totalRevenue: totalRevenue / 100,
|
|
currency: 'ETB',
|
|
byPaymentMethod,
|
|
byDate,
|
|
cancellationRate: 0
|
|
};
|
|
}
|
|
|
|
private async generateOccupancyReport(dateFrom: Date, dateTo: Date) {
|
|
const schedules = await this.prisma.trainSchedule.findMany({
|
|
where: { departureAt: { gte: dateFrom, lte: dateTo } },
|
|
include: {
|
|
coachAssignments: { include: { coach: { include: { seats: true } } } },
|
|
bookings: { include: { seats: true } },
|
|
},
|
|
});
|
|
|
|
const tripData = schedules.map(schedule => {
|
|
const totalSeats = schedule.coachAssignments.reduce((sum, a) => sum + a.coach.seats.length, 0);
|
|
const bookedSeats = schedule.bookings.reduce((sum, b) => sum + b.seats.length, 0);
|
|
const occupancyRate = totalSeats > 0 ? (bookedSeats / totalSeats) * 100 : 0;
|
|
return { scheduleId: schedule.id, departureAt: schedule.departureAt, totalSeats, bookedSeats, occupancyRate: +occupancyRate.toFixed(2) };
|
|
});
|
|
|
|
const avgOccupancy = tripData.length > 0 ? tripData.reduce((sum, t) => sum + t.occupancyRate, 0) / tripData.length : 0;
|
|
return { totalSchedules: schedules.length, averageOccupancyRate: +avgOccupancy.toFixed(2), schedules: tripData };
|
|
}
|
|
|
|
private async generateAgentSalesReport(dateFrom: Date, dateTo: Date, agentId?: string) {
|
|
const agentBookings = await this.prisma.agentBooking.findMany({
|
|
where: {
|
|
createdAt: { gte: dateFrom, lte: dateTo },
|
|
...(agentId ? { agentId } : {})
|
|
},
|
|
include: {
|
|
agent: { select: { id: true, iamUserId: true, agentCode: true } },
|
|
booking: true
|
|
}
|
|
});
|
|
|
|
const iamUserIds = [...new Set(
|
|
agentBookings.map(ab => ab.agent.iamUserId).filter(Boolean) as string[]
|
|
)];
|
|
const iamRows = iamUserIds.length > 0
|
|
? await this.dataSource.query<{ id: string; name: { en?: string; am?: string } | null }[]>(
|
|
`SELECT id, name FROM iam.users WHERE id = ANY($1)`,
|
|
[iamUserIds],
|
|
)
|
|
: [];
|
|
const iamMap = new Map(iamRows.map(r => [r.id, r]));
|
|
|
|
const byAgent = agentBookings.reduce((acc, ab) => {
|
|
const iam = ab.agent.iamUserId ? iamMap.get(ab.agent.iamUserId) : undefined;
|
|
const agentName = iam?.name?.en ?? iam?.name?.am ?? ab.agent.agentCode;
|
|
if (!acc[agentName]) {
|
|
acc[agentName] = { bookings: 0, revenueMinor: 0, cashCollected: 0 };
|
|
}
|
|
acc[agentName].bookings += 1;
|
|
acc[agentName].revenueMinor += ab.booking.totalMinor;
|
|
acc[agentName].cashCollected += ab.cashReceived ?? 0;
|
|
return acc;
|
|
}, {} as Record<string, any>);
|
|
|
|
return {
|
|
totalAgentBookings: agentBookings.length,
|
|
byAgent
|
|
};
|
|
}
|
|
|
|
private async generateCancellationsReport(dateFrom: Date, dateTo: Date) {
|
|
const cancellations = await this.prisma.bookingCancellation.findMany({
|
|
where: { createdAt: { gte: dateFrom, lte: dateTo } },
|
|
include: { booking: true }
|
|
});
|
|
|
|
const totalRefunded = cancellations.reduce((sum, c) => sum + c.refundAmount, 0);
|
|
|
|
return {
|
|
totalCancellations: cancellations.length,
|
|
totalRefundedMinor: totalRefunded,
|
|
totalRefunded: totalRefunded / 100,
|
|
currency: 'ETB'
|
|
};
|
|
}
|
|
|
|
private async generatePaymentMethodsReport(dateFrom: Date, dateTo: Date) {
|
|
const payments = await this.prisma.paymentIntent.findMany({
|
|
where: {
|
|
createdAt: { gte: dateFrom, lte: dateTo },
|
|
status: 'SUCCEEDED'
|
|
}
|
|
});
|
|
|
|
const byMethod = payments.reduce((acc, p) => {
|
|
const method = p.method;
|
|
if (!acc[method]) {
|
|
acc[method] = { count: 0, totalMinor: 0 };
|
|
}
|
|
acc[method].count += 1;
|
|
acc[method].totalMinor += p.amountMinor;
|
|
return acc;
|
|
}, {} as Record<string, any>);
|
|
|
|
return {
|
|
totalPayments: payments.length,
|
|
byMethod
|
|
};
|
|
}
|
|
|
|
async getReport(reportId: string) {
|
|
return this.prisma.operationalReport.findUnique({ where: { id: reportId } });
|
|
}
|
|
|
|
async listReports(reportType?: string) {
|
|
return this.prisma.operationalReport.findMany({
|
|
where: reportType ? { reportType } : {},
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 50
|
|
});
|
|
}
|
|
}
|