mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-05 17:43:39 +00:00
195 lines
7.9 KiB
TypeScript
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;
|
|
}
|