import { Body, Controller, Get, Param, ParseUUIDPipe, Post, Query, } from '@nestjs/common'; import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; import { WagonTransferCancel, WagonTransferCloseShort, WagonTransferFulfill, WagonTransferHistoryAll, WagonTransferRequest, WagonTransferView, } from '../../common/booking-guards'; import { BulkFulfillTransferRequestsDto } from './dto/bulk-fulfill-transfer-requests.dto'; import { CloseShortTransferRequestDto } from './dto/close-short-transfer-request.dto'; import { CreateTransferRequestDto } from './dto/create-transfer-request.dto'; import { FulfillTransferRequestDto } from './dto/fulfill-transfer-request.dto'; import { ListTransferRequestsQueryDto } from './dto/list-transfer-requests-query.dto'; import { WagonTransferRequestsService } from './wagon-transfer-requests.service'; /** Query-string number, or undefined when absent/garbage (service defaults it). */ const toInt = (value?: string): number | undefined => { const n = Number.parseInt(String(value ?? ''), 10); return Number.isFinite(n) && n > 0 ? n : undefined; }; /** * The wagon-transfer desk. A requester (transfer_request) files a count-only * request; OCC (transfer_fulfill) moves wagons against it in as many * instalments as the source yard allows, and closes it short * (transfer_close_short) when the yard has no more to give. Separate top-level * path so it never collides with `wagons/:id`. */ @ApiTags('wagon-transfer-requests') @Controller('wagon-transfer-requests') // No class-level key: Nest stacks class and method guards, so a class-level // `transfer_view` would AND with every action key below and lock out the OCC // staff granted only `transfer_fulfill`. Reads carry the view key themselves. export class WagonTransferRequestsController { constructor(private readonly service: WagonTransferRequestsService) {} @Post() @WagonTransferRequest() @ApiOperation({ summary: 'File a count-only wagon-transfer request' }) create( @Body() dto: CreateTransferRequestDto, @CurrentUser() user: TCurrentUser, ) { return this.service.createRequest(dto, user?.id); } @Get() @WagonTransferView() @ApiOperation({ summary: 'Transfer desk list — paginated, filterable by status (comma-separated), yards and wagon type', }) list(@Query() query: ListTransferRequestsQueryDto) { return this.service.listRequests(query); } // NOTE: static routes (`history`, `bulk-fulfill`) MUST stay above `@Get(':id')` // — Express matches in declaration order, so they would otherwise be captured // by the `:id` param route (and rejected by ParseUUIDPipe). @Post('bulk-fulfill') @WagonTransferFulfill() @ApiOperation({ summary: 'OCC: accept-and-execute a subset of pending requests (auto-picks available wagons; the rest stay PENDING)', }) bulkFulfill( @Body() dto: BulkFulfillTransferRequestsDto, @CurrentUser() user: TCurrentUser, ) { return this.service.bulkFulfill(dto.requestIds, user?.id); } // NOTE: the two `history` routes MUST stay above `@Get(':id')` — Express // matches in declaration order, so `/history` would otherwise be captured by // the `:id` param route (and rejected by ParseUUIDPipe). @Get('history') @WagonTransferView() @ApiQuery({ name: 'page', required: false }) @ApiQuery({ name: 'pageSize', required: false }) @ApiOperation({ summary: "Caller's own transfer history (requests filed/fulfilled + wagons moved)", }) myHistory( @CurrentUser() user: TCurrentUser, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { // Never fall through to the all-staff view: getHistory(undefined) means // "everyone", so a missing caller id must return empty, not leak scope. if (!user?.id) { return { requests: [], movements: [], meta: { page: 1, pageSize: 20, requestsTotal: 0, movementsTotal: 0, totalPages: 1, }, }; } return this.service.getHistory(user.id, toInt(page), toInt(pageSize)); } @Get('history/all') @WagonTransferHistoryAll() @ApiQuery({ name: 'userId', required: false }) @ApiQuery({ name: 'page', required: false }) @ApiQuery({ name: 'pageSize', required: false }) @ApiOperation({ summary: "Admin: any/all staff's transfer history (optional ?userId filter)", }) allHistory( @Query('userId') userId?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.getHistory(userId, toInt(page), toInt(pageSize)); } @Get(':id') @WagonTransferView() @ApiOperation({ summary: 'Get one transfer request' }) findOne(@Param('id', ParseUUIDPipe) id: string) { return this.service.findById(id); } @Post(':id/fulfill') @WagonTransferFulfill() @ApiOperation({ summary: 'OCC: pick wagons and execute the transfer' }) fulfill( @Param('id', ParseUUIDPipe) id: string, @Body() dto: FulfillTransferRequestDto, @CurrentUser() user: TCurrentUser, ) { return this.service.fulfillRequest(id, dto, user?.id); } @Post(':id/close-short') @WagonTransferCloseShort() @ApiOperation({ summary: 'OCC: end the request with fewer wagons than asked for — what moved stays, the requester is told the shortfall', }) closeShort( @Param('id', ParseUUIDPipe) id: string, @Body() dto: CloseShortTransferRequestDto, @CurrentUser() user: TCurrentUser, ) { return this.service.closeShort(id, dto, user?.id); } @Post(':id/cancel') @WagonTransferCancel() @ApiOperation({ summary: 'Withdraw a request that has not moved any wagon yet (use close-short once wagons have moved)', }) cancel(@Param('id', ParseUUIDPipe) id: string) { return this.service.cancelRequest(id); } }