mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
Luggage processing,, schedule times and tariff related updates
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AgentsService } from './agents.service';
|
||||
import { CreateAgentDto, CreateAgentBookingDto, OpenShiftDto, CloseShiftDto } from './agents.dto';
|
||||
@@ -34,6 +34,12 @@ export class AgentsController {
|
||||
updateAgent(@Param('id') id: string, @Body() dto: Partial<CreateAgentDto> & { active?: boolean }) {
|
||||
return this.service.updateAgent(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete agent profile' })
|
||||
deleteAgent(@Param('id') id: string) {
|
||||
return this.service.deleteAgent(id);
|
||||
}
|
||||
@Post('bookings')
|
||||
@ApiOperation({ summary: 'Create agent booking with cash payment' })
|
||||
createBooking(@Body() dto: CreateAgentBookingDto) {
|
||||
|
||||
@@ -210,4 +210,11 @@ export class AgentsService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { ExcessBaggageService } from './excess-baggage.service';
|
||||
@@ -26,7 +26,8 @@ export class ExcessBaggageAgentController {
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Log excess baggage charge and optionally collect cash' })
|
||||
logCharge(@Body() dto: LogExcessBaggageDto) {
|
||||
logCharge(@Request() req: any, @Body() dto: LogExcessBaggageDto) {
|
||||
dto.agentId = req.user?.id ?? req.user?.sub ?? dto.agentId;
|
||||
return this.service.logCharge(dto);
|
||||
}
|
||||
|
||||
@@ -50,24 +51,6 @@ export class ExcessBaggageAgentController {
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a single charge by ID (agent polling)' })
|
||||
getCharge(@Param('id') id: string) {
|
||||
return this.service.getCharge(id);
|
||||
}
|
||||
|
||||
@Post(':id/resend')
|
||||
@ApiOperation({ summary: 'Resend payment link (extends expiry by 30 min)' })
|
||||
resendLink(@Param('id') id: string) {
|
||||
return this.service.resendLink(id);
|
||||
}
|
||||
|
||||
@Patch(':id/waive')
|
||||
@ApiOperation({ summary: 'Waive a charge (supervisor only)' })
|
||||
waiveCharge(@Param('id') id: string, @Body() dto: WaiveChargeDto) {
|
||||
return this.service.waiveCharge(id, dto);
|
||||
}
|
||||
|
||||
@Get('allowances')
|
||||
@ApiOperation({ summary: 'List all baggage allowance rules' })
|
||||
getAllowances() {
|
||||
@@ -92,6 +75,24 @@ export class ExcessBaggageAgentController {
|
||||
return this.service.deleteAllowance(id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a single charge by ID (agent polling)' })
|
||||
getCharge(@Param('id') id: string) {
|
||||
return this.service.getCharge(id);
|
||||
}
|
||||
|
||||
@Post(':id/resend')
|
||||
@ApiOperation({ summary: 'Resend payment link (extends expiry by 30 min)' })
|
||||
resendLink(@Param('id') id: string) {
|
||||
return this.service.resendLink(id);
|
||||
}
|
||||
|
||||
@Patch(':id/waive')
|
||||
@ApiOperation({ summary: 'Waive a charge (supervisor only)' })
|
||||
waiveCharge(@Param('id') id: string, @Body() dto: WaiveChargeDto) {
|
||||
return this.service.waiveCharge(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete excess baggage charge (admin only)' })
|
||||
deleteCharge(@Param('id') id: string) {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class LogExcessBaggageDto {
|
||||
@ApiProperty({ example: 'booking-uuid' }) @IsString() bookingId: string;
|
||||
@ApiProperty({ example: 'agent-uuid' }) @IsString() agentId: string;
|
||||
@ApiPropertyOptional({ example: 'agent-uuid', description: 'Injected from IAM token; optional override' })
|
||||
@IsOptional() @IsString() agentId?: string;
|
||||
@ApiProperty({ example: 7, description: 'Excess weight in kg above the free allowance' })
|
||||
@IsInt() @IsPositive() excessWeightKg: number;
|
||||
@ApiPropertyOptional({ description: 'Collect cash now instead of sending a payment link' })
|
||||
|
||||
@@ -75,7 +75,7 @@ export class ExcessBaggageService {
|
||||
const charge = await this.prisma.excessBaggageCharge.create({
|
||||
data: {
|
||||
bookingId: dto.bookingId,
|
||||
agentId: dto.agentId,
|
||||
agentId: dto.agentId ?? '',
|
||||
excessWeightKg: dto.excessWeightKg,
|
||||
feePerKgMinor,
|
||||
totalMinor,
|
||||
|
||||
@@ -154,43 +154,54 @@ export class SearchService {
|
||||
) {
|
||||
const [y, m, d] = dateStr.split('-').map(Number);
|
||||
const requestedDate = new Date(y, m - 1, d, 0, 0, 0, 0);
|
||||
|
||||
const now = new Date();
|
||||
const daysBefore = Math.min(7, Math.floor(requestedDate.getTime() / 86_400_000));
|
||||
const daysAfter = 14 - daysBefore;
|
||||
|
||||
const windowStart = new Date(requestedDate);
|
||||
windowStart.setDate(windowStart.getDate() - daysBefore);
|
||||
if (windowStart < now) windowStart.setTime(now.getTime());
|
||||
|
||||
const windowEnd = new Date(requestedDate);
|
||||
windowEnd.setDate(windowEnd.getDate() + daysAfter + 1);
|
||||
|
||||
const totalPassengers = adultCount + (childCount ?? 0);
|
||||
|
||||
const requestedNextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0);
|
||||
const now = new Date();
|
||||
const totalPassengers = adultCount + (childCount ?? 0);
|
||||
const NEEDED = 3;
|
||||
|
||||
const schedules = await this.prisma.trainSchedule.findMany({
|
||||
where: {
|
||||
status: 'SCHEDULED',
|
||||
isPackageOnly: false,
|
||||
OR: [
|
||||
{ departureAt: { gte: windowStart, lt: requestedDate } },
|
||||
{ departureAt: { gte: requestedNextDay < now ? now : requestedNextDay, lt: windowEnd } },
|
||||
],
|
||||
stopTimes: { some: { stationId: originStationId } },
|
||||
coachAssignments: { some: {} },
|
||||
},
|
||||
include: SCHEDULE_INCLUDE,
|
||||
orderBy: { departureAt: 'asc' },
|
||||
});
|
||||
const baseWhere = {
|
||||
status: 'SCHEDULED',
|
||||
isPackageOnly: false,
|
||||
stopTimes: { some: { stationId: originStationId } },
|
||||
coachAssignments: { some: {} },
|
||||
} as const;
|
||||
|
||||
const results = await Promise.all(
|
||||
schedules.map(schedule =>
|
||||
this.buildScheduleResult(schedule as any, originStationId, destinationStationId, totalPassengers, nationality)
|
||||
)
|
||||
);
|
||||
return results.filter((r): r is NonNullable<typeof r> => !!r && r.hasAvailability);
|
||||
// Fetch candidates before and after in parallel; take more than needed to
|
||||
// account for routes that don't serve the destination or have no availability.
|
||||
const FETCH_LIMIT = NEEDED * 5;
|
||||
|
||||
const [beforeCandidates, afterCandidates] = await Promise.all([
|
||||
this.prisma.trainSchedule.findMany({
|
||||
where: { ...baseWhere, departureAt: { gte: now < requestedDate ? now : new Date(0), lt: requestedDate } },
|
||||
include: SCHEDULE_INCLUDE,
|
||||
orderBy: { departureAt: 'desc' },
|
||||
take: FETCH_LIMIT,
|
||||
}),
|
||||
this.prisma.trainSchedule.findMany({
|
||||
where: { ...baseWhere, departureAt: { gte: requestedNextDay > now ? requestedNextDay : now } },
|
||||
include: SCHEDULE_INCLUDE,
|
||||
orderBy: { departureAt: 'asc' },
|
||||
take: FETCH_LIMIT,
|
||||
}),
|
||||
]);
|
||||
|
||||
const pickN = async (candidates: typeof beforeCandidates, limit: number) => {
|
||||
const out: NonNullable<Awaited<ReturnType<typeof this.buildScheduleResult>>>[] = [];
|
||||
for (const schedule of candidates) {
|
||||
if (out.length >= limit) break;
|
||||
const r = await this.buildScheduleResult(schedule as any, originStationId, destinationStationId, totalPassengers, nationality);
|
||||
if (r?.hasAvailability) out.push(r);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const [before, after] = await Promise.all([
|
||||
pickN(beforeCandidates, NEEDED),
|
||||
pickN(afterCandidates, NEEDED),
|
||||
]);
|
||||
|
||||
// before was fetched desc (closest first); reverse so result is chronological
|
||||
return [...before.reverse(), ...after];
|
||||
}
|
||||
|
||||
private async searchSchedules(
|
||||
@@ -415,7 +426,7 @@ export class SearchService {
|
||||
}
|
||||
}
|
||||
|
||||
const coachTypes = this.buildCoachTypeDetails(schedule, faresByClass);
|
||||
const coachTypes = this.buildCoachTypeDetails(schedule, faresByClass, nationality);
|
||||
const legDepartureAt = originStop.plannedDepartureAt ?? schedule.departureAt;
|
||||
const legArrivalAt = destStop.plannedArrivalAt ?? schedule.arrivalAt;
|
||||
|
||||
@@ -638,6 +649,10 @@ export class SearchService {
|
||||
): Promise<Array<{ seatClassName: string; baseFareMinor: number; displayCurrency: Currency; displayAmountMinor: number }>> {
|
||||
const displayCurrency = resolveCurrencyFromNationality(nationality);
|
||||
|
||||
const nationalityUpper = (nationality ?? '').toUpperCase();
|
||||
const nationalityType = (nationalityUpper === 'ETHIOPIAN' || nationalityUpper === 'DJIBOUTIAN')
|
||||
? 'LOCAL' : 'INTERNATIONAL';
|
||||
|
||||
// Collect seat class IDs from the schedule include for the ID set,
|
||||
// but fetch fresh records from DB so updated baseFareMinor is always current
|
||||
const seatClassIdSet = new Set<string>();
|
||||
@@ -647,7 +662,14 @@ export class SearchService {
|
||||
}
|
||||
}
|
||||
const freshSeatClasses = await this.prisma.seatClass.findMany({
|
||||
where: { id: { in: Array.from(seatClassIdSet) }, isActive: true },
|
||||
where: {
|
||||
id: { in: Array.from(seatClassIdSet) },
|
||||
isActive: true,
|
||||
OR: [
|
||||
{ nationalityType: null },
|
||||
{ nationalityType: nationalityType },
|
||||
],
|
||||
},
|
||||
});
|
||||
const seatClassMap = new Map(freshSeatClasses.map(sc => [sc.id, sc]));
|
||||
const seatClasses = freshSeatClasses.sort((a, b) => a.baseFareMinor - b.baseFareMinor);
|
||||
@@ -725,6 +747,7 @@ export class SearchService {
|
||||
private buildCoachTypeDetails(
|
||||
schedule: ScheduleWithIncludes,
|
||||
faresByClass: Array<{ seatClassName: string; baseFareMinor: number; displayCurrency: Currency; displayAmountMinor: number }>,
|
||||
nationality?: string,
|
||||
): Array<{
|
||||
coachTypeId: string;
|
||||
coachTypeName: string;
|
||||
@@ -749,8 +772,16 @@ export class SearchService {
|
||||
});
|
||||
}
|
||||
|
||||
const nationalityUpper = (nationality ?? '').toUpperCase();
|
||||
const resolvedNationalityType = (nationalityUpper === 'ETHIOPIAN' || nationalityUpper === 'DJIBOUTIAN')
|
||||
? 'LOCAL' : 'INTERNATIONAL';
|
||||
|
||||
const entry = coachTypeMap.get(coachType.id)!;
|
||||
coachType.seatClasses?.forEach((sc: any) => entry.classNames.add(sc.name));
|
||||
coachType.seatClasses?.forEach((sc: any) => {
|
||||
// Exclude classes that belong to the wrong nationality type
|
||||
if (sc.nationalityType && sc.nationalityType !== resolvedNationalityType) return;
|
||||
if (faresByClass.some(f => f.seatClassName === sc.name)) entry.classNames.add(sc.name);
|
||||
});
|
||||
}
|
||||
|
||||
const result = [];
|
||||
@@ -769,6 +800,7 @@ export class SearchService {
|
||||
.filter((c): c is { name: string; baseFareMinor: number; displayCurrency: Currency; displayAmountMinor: number } => c !== null)
|
||||
.sort((a, b) => a.baseFareMinor - b.baseFareMinor);
|
||||
|
||||
if (classes.length === 0) continue;
|
||||
result.push({
|
||||
coachTypeId: coachType.id,
|
||||
coachTypeName: coachType.name,
|
||||
|
||||
Reference in New Issue
Block a user