mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
Refactored the whole app based on the requirements shared
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ReportsService } from './reports.service';
|
||||
import { GenerateReportDto } from './reports.dto';
|
||||
import { IamGuard, IamRoles } from '../../common/iam-adapter';
|
||||
import { UserRole } from '@prisma/client';
|
||||
|
||||
@ApiTags('Reports')
|
||||
@Controller('reports')
|
||||
@UseGuards(IamGuard)
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
export class ReportsController {
|
||||
constructor(private service: ReportsService) {}
|
||||
|
||||
@Post('generate')
|
||||
@IamRoles('ADMIN', 'SUPERVISOR')
|
||||
@ApiOperation({ summary: 'Generate operational report' })
|
||||
generateReport(@Body() dto: GenerateReportDto) {
|
||||
return this.service.generateReport(dto);
|
||||
}
|
||||
|
||||
@Get(':reportId')
|
||||
@IamRoles('ADMIN', 'SUPERVISOR')
|
||||
@ApiOperation({ summary: 'Get report by ID' })
|
||||
getReport(@Param('reportId') reportId: string) {
|
||||
return this.service.getReport(reportId);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@IamRoles('ADMIN', 'SUPERVISOR')
|
||||
@ApiOperation({ summary: 'List reports' })
|
||||
listReports(@Query('type') type?: string) {
|
||||
return this.service.listReports(type);
|
||||
}
|
||||
}
|
||||
29
apps/edr-passenger-api/src/modules/reports/reports.dto.ts
Normal file
29
apps/edr-passenger-api/src/modules/reports/reports.dto.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { IsString, IsDateString, IsOptional, IsEnum } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export enum ReportType {
|
||||
REVENUE = 'REVENUE',
|
||||
OCCUPANCY = 'OCCUPANCY',
|
||||
AGENT_SALES = 'AGENT_SALES',
|
||||
CANCELLATIONS = 'CANCELLATIONS',
|
||||
PAYMENT_METHODS = 'PAYMENT_METHODS'
|
||||
}
|
||||
|
||||
export enum ExportFormat {
|
||||
JSON = 'JSON',
|
||||
CSV = 'CSV',
|
||||
PDF = 'PDF'
|
||||
}
|
||||
|
||||
export class GenerateReportDto {
|
||||
@ApiProperty({ enum: ReportType }) @IsEnum(ReportType) reportType: ReportType;
|
||||
@ApiProperty({ example: '2026-01-01' }) @IsDateString() dateFrom: string;
|
||||
@ApiProperty({ example: '2026-01-31' }) @IsDateString() dateTo: string;
|
||||
@ApiPropertyOptional() @IsOptional() @IsString() routeId?: string;
|
||||
@ApiPropertyOptional() @IsOptional() @IsString() agentId?: string;
|
||||
}
|
||||
|
||||
export class ExportReportDto {
|
||||
@ApiProperty() @IsString() reportId: string;
|
||||
@ApiProperty({ enum: ExportFormat }) @IsEnum(ExportFormat) format: ExportFormat;
|
||||
}
|
||||
12
apps/edr-passenger-api/src/modules/reports/reports.module.ts
Normal file
12
apps/edr-passenger-api/src/modules/reports/reports.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { ReportsController } from './reports.controller';
|
||||
import { ReportsService } from './reports.service';
|
||||
|
||||
@Module({
|
||||
imports: [HttpModule],
|
||||
controllers: [ReportsController],
|
||||
providers: [ReportsService],
|
||||
exports: [ReportsService]
|
||||
})
|
||||
export class ReportsModule {}
|
||||
185
apps/edr-passenger-api/src/modules/reports/reports.service.ts
Normal file
185
apps/edr-passenger-api/src/modules/reports/reports.service.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
import { GenerateReportDto, ReportType } from './reports.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ReportsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async generateReport(dto: GenerateReportDto) {
|
||||
const dateFrom = new Date(dto.dateFrom);
|
||||
const dateTo = new Date(dto.dateTo);
|
||||
|
||||
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) {
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: {
|
||||
createdAt: { gte: dateFrom, lte: dateTo },
|
||||
status: { in: ['CONFIRMED', 'COMPLETED'] }
|
||||
},
|
||||
include: { paymentIntent: true }
|
||||
});
|
||||
|
||||
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>);
|
||||
|
||||
return {
|
||||
totalBookings: bookings.length,
|
||||
totalRevenueMinor: totalRevenue,
|
||||
totalRevenue: totalRevenue / 100,
|
||||
currency: 'ETB',
|
||||
byPaymentMethod
|
||||
};
|
||||
}
|
||||
|
||||
private async generateOccupancyReport(dateFrom: Date, dateTo: Date) {
|
||||
const trips = await this.prisma.trip.findMany({
|
||||
where: { departureAt: { gte: dateFrom, lte: dateTo } },
|
||||
include: {
|
||||
coaches: { include: { seats: true } },
|
||||
bookings: { where: { status: { in: ['CONFIRMED', 'COMPLETED'] } }, include: { seats: true } }
|
||||
}
|
||||
});
|
||||
|
||||
const tripData = trips.map(trip => {
|
||||
const totalSeats = trip.coaches.reduce((sum, c) => sum + c.seats.length, 0);
|
||||
const bookedSeats = trip.bookings.reduce((sum, b) => sum + b.seats.length, 0);
|
||||
const occupancyRate = totalSeats > 0 ? (bookedSeats / totalSeats) * 100 : 0;
|
||||
|
||||
return {
|
||||
tripId: trip.id,
|
||||
departureAt: trip.departureAt,
|
||||
totalSeats,
|
||||
bookedSeats,
|
||||
occupancyRate: +occupancyRate.toFixed(2)
|
||||
};
|
||||
});
|
||||
|
||||
const avgOccupancy = tripData.length > 0
|
||||
? tripData.reduce((sum, t) => sum + t.occupancyRate, 0) / tripData.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
totalTrips: trips.length,
|
||||
averageOccupancyRate: +avgOccupancy.toFixed(2),
|
||||
trips: 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: { include: { user: true } },
|
||||
booking: true
|
||||
}
|
||||
});
|
||||
|
||||
const byAgent = agentBookings.reduce((acc, ab) => {
|
||||
const agentName = ab.agent.user.fullName;
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user