mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 23:40:56 +00:00
Nest runs class and method guards together, so a class-level view key ANDs with every action key below it. Staff granted only an action were denied before their key was ever checked: OCC could not fulfil wagon transfers, dispatchers could not create a yard, and track staff could not assign first/last-mile vehicles. Reads now carry the view key themselves, and the warehouses baseline lists every key its routes use.
177 lines
5.9 KiB
TypeScript
177 lines
5.9 KiB
TypeScript
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);
|
|
}
|
|
}
|