Files
edr-platform/apps/edr-passenger-api/src/modules/reports/reports.dto.ts

195 lines
7.9 KiB
TypeScript

import { IsString, IsDateString, IsOptional, IsEnum, IsInt, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { PaymentMethodType } from '@prisma/client';
import { SeatBlockReasonCategory } from '../seats/seats.dto';
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;
}
// ── Blocked Seat Revenue Loss ────────────────────────────────────────────────
export enum BlockedSeatsLossSortBy {
/** Largest estimated loss first (default). */
LOSS_DESC = 'lossMinor',
/** Smallest estimated loss first. */
LOSS_ASC = 'lossMinorAsc',
/** Most blocked seats first. */
BLOCKED_SEATS = 'blockedSeatCount',
/** Soonest departure first. */
DEPARTURE = 'departureAt',
}
export class BlockedSeatsRevenueLossQueryDto {
@ApiPropertyOptional({
example: '2026-07-01',
description: 'Start of the window, inclusive, matched on TrainSchedule.departureAt. Defaults to the earliest scheduled departure on record.',
})
@IsOptional() @IsDateString() dateFrom?: string;
@ApiPropertyOptional({
example: '2026-07-31',
description: 'End of the window, inclusive, matched on TrainSchedule.departureAt. Defaults to the latest scheduled departure on record.',
})
@IsOptional() @IsDateString() dateTo?: string;
@ApiPropertyOptional({ description: 'Restrict to a single TrainSchedule.' })
@IsOptional() @IsString() scheduleId?: string;
@ApiPropertyOptional({ description: 'Restrict to schedules running this route.' })
@IsOptional() @IsString() routeId?: string;
@ApiPropertyOptional({ description: 'Restrict to schedules operated by this train.' })
@IsOptional() @IsString() trainId?: string;
@ApiPropertyOptional({
description:
'Restrict to blocks on seats in this coach. Load factor still reflects the whole train, so the percentage stays comparable.',
})
@IsOptional() @IsString() coachId?: string;
@ApiPropertyOptional({
enum: SeatBlockReasonCategory,
description: 'Restrict to blocks in this reporting bucket. Legacy uncategorized blocks are excluded when set.',
})
@IsOptional() @IsEnum(SeatBlockReasonCategory) reasonCategory?: SeatBlockReasonCategory;
@ApiPropertyOptional({
description: "Blocker filter — matches the IAM user id exactly, or the recorded name case-insensitively.",
})
@IsOptional() @IsString() blockedBy?: string;
@ApiPropertyOptional({
example: 'Ethiopian',
default: 'Ethiopian',
description:
'Nationality the counterfactual fares are priced at. Drives both the seat-class tariff variant (LOCAL vs INTERNATIONAL) and the billing currency. Defaults to Ethiopian — the local tariff in ETB.',
})
@IsOptional() @IsString() nationality?: string;
@ApiPropertyOptional({ default: 1, minimum: 1, description: 'Page of schedules, 1-based.' })
@IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number;
@ApiPropertyOptional({ default: 25, minimum: 1, maximum: 200, description: 'Schedules per page.' })
@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(200) pageSize?: number;
@ApiPropertyOptional({
enum: BlockedSeatsLossSortBy,
default: BlockedSeatsLossSortBy.LOSS_DESC,
description: 'Schedule ordering. Defaults to largest estimated loss first.',
})
@IsOptional() @IsEnum(BlockedSeatsLossSortBy) sortBy?: BlockedSeatsLossSortBy;
}
// ── Finance Summary ──────────────────────────────────────────────────────────
export enum FinanceGranularity {
DAILY = 'daily',
WEEKLY = 'weekly',
MONTHLY = 'monthly',
}
/**
* Domestic vs cross-border traffic, derived from the endpoints' `Station.countryCode`:
* a trip is INTERCITY only when both endpoints sit in Ethiopia, and INTERNATIONAL as soon
* as either endpoint is outside it — which on this line means Djibouti (Alisabieh, Holhol,
* Nagad). A trip wholly inside Djibouti counts as INTERNATIONAL too.
*/
export enum FinanceTripType {
INTERCITY = 'intercity',
INTERNATIONAL = 'international',
}
/**
* Travel-package revenue vs ordinary ticket sales, derived from `Booking.packageId`.
* NOT the `Booking.bookingType` column, which holds ONE_WAY / ROUND_TRIP — every package
* booking happens to be ROUND_TRIP, but that is a different question from this one.
*/
export enum FinanceBookingType {
REGULAR = 'regular',
PACKAGE = 'package',
}
/**
* Which revenue stream a row came from. `ticket` is the booking fare — all this report used to
* count. The rest are fees collected after the fare: `excess_baggage` from ExcessBaggageCharge,
* `outstanding` from a PAID SupplementaryCharge with reason UNDERPAYMENT, and `other` from every
* remaining supplementary reason (UPGRADE, RESCHEDULE, and anything added later — `reason` is a
* free-text column, so this bucket is deliberately open-ended).
*/
export enum FinanceRevenueType {
TICKET = 'ticket',
EXCESS_BAGGAGE = 'excess_baggage',
OUTSTANDING = 'outstanding',
OTHER = 'other',
}
export class FinanceSummaryQueryDto {
@ApiProperty({ example: '2026-07-01', description: 'Start of the window, inclusive, matched on PaymentIntent.paidAt.' })
@IsDateString() dateFrom: string;
@ApiProperty({ example: '2026-07-31', description: 'End of the window, inclusive, matched on PaymentIntent.paidAt.' })
@IsDateString() dateTo: string;
@ApiPropertyOptional({ enum: FinanceGranularity, default: FinanceGranularity.DAILY })
@IsOptional() @IsEnum(FinanceGranularity) granularity?: FinanceGranularity;
@ApiPropertyOptional({ description: 'Restrict to bookings departing from this station.' })
@IsOptional() @IsString() originStationId?: string;
@ApiPropertyOptional({ description: 'Restrict to bookings arriving at this station.' })
@IsOptional() @IsString() destinationStationId?: string;
@ApiPropertyOptional({ enum: PaymentMethodType, description: 'Restrict to payments made with this method.' })
@IsOptional() @IsEnum(PaymentMethodType) method?: PaymentMethodType;
@ApiPropertyOptional({
enum: FinanceTripType,
description:
'Restrict to trips wholly inside Ethiopia (intercity) or trips touching a station outside ' +
'Ethiopia (international — in practice Djibouti). Omit for all trips.',
})
@IsOptional() @IsEnum(FinanceTripType) tripType?: FinanceTripType;
@ApiPropertyOptional({
enum: FinanceBookingType,
description:
'Restrict to ordinary ticket sales (regular) or travel-package bookings (package). ' +
'Omit for all bookings. Unrelated to the ONE_WAY/ROUND_TRIP booking type.',
})
@IsOptional() @IsEnum(FinanceBookingType) bookingType?: FinanceBookingType;
@ApiPropertyOptional({
enum: FinanceRevenueType,
description:
'Restrict to one revenue stream: the booking fare (ticket), excess-baggage fees ' +
'(excess_baggage), recovered underpayments (outstanding), or every other supplementary ' +
'charge such as upgrades and reschedules (other). Omit for all revenue.',
})
@IsOptional() @IsEnum(FinanceRevenueType) revenueType?: FinanceRevenueType;
}