mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Approval now snapshots the rate estimate and generates a last-mile contract instead of invoicing immediately. The customer picks a delivery date on the confirm form, then reviews and signs the contract in the portal (saved signature or drawn); the signed PDF is stored as LM_<CustomerName>.pdf and only then is the advance invoice issued. Backoffice shows signature status and the contract download.
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
import { Body, Controller, Get, Param, ParseUUIDPipe, Post, Query, Res } from '@nestjs/common';
|
||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||
import type { Response } from 'express';
|
||
|
||
import { CurrentUser } from '@edr/api-common';
|
||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||
import { LastMileRequestStatus } from '@edr/types';
|
||
|
||
import { BookingStaff } from '../../common/booking-guards';
|
||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||
import { ApproveLastMileRequestDto } from './dto/approve-last-mile-request.dto';
|
||
import { RejectLastMileRequestDto } from './dto/reject-last-mile-request.dto';
|
||
import { SignLastMileContractDto } from './dto/sign-last-mile-contract.dto';
|
||
import { SubmitLastMileRequestDto } from './dto/submit-last-mile-request.dto';
|
||
import { LastMileContractService } from './last-mile-contract.service';
|
||
import { LastMileRequestsService } from './last-mile-requests.service';
|
||
|
||
@ApiTags('last-mile-requests')
|
||
@ApiBearerAuth()
|
||
@Controller('last-mile-requests')
|
||
export class LastMileRequestsController {
|
||
constructor(
|
||
private readonly requestsService: LastMileRequestsService,
|
||
private readonly contractService: LastMileContractService,
|
||
) {}
|
||
|
||
@Get()
|
||
@BookingStaff(FREIGHT_PERMS.lastMile.requestView)
|
||
@ApiOperation({ summary: 'List last-mile confirmation requests' })
|
||
findAll(
|
||
@Query('status') status?: string,
|
||
@Query('bookingId') bookingId?: string,
|
||
@Query('page') page?: string,
|
||
@Query('pageSize') pageSize?: string,
|
||
) {
|
||
return this.requestsService.findAll({
|
||
status: status as LastMileRequestStatus | undefined,
|
||
bookingId,
|
||
page: page ? parseInt(page, 10) : undefined,
|
||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||
});
|
||
}
|
||
|
||
@Get('free-truck-count')
|
||
@BookingStaff(FREIGHT_PERMS.lastMile.requestView)
|
||
@ApiOperation({ summary: 'Free (ACTIVE + unassigned) trucks — informational context for approval' })
|
||
freeTruckCount() {
|
||
return this.requestsService.freeTruckCount().then((count) => ({ count }));
|
||
}
|
||
|
||
@Get(':id/price-estimate')
|
||
@BookingStaff(FREIGHT_PERMS.lastMile.requestView)
|
||
@ApiOperation({
|
||
summary:
|
||
'Rule-based last-mile price estimate (estimated km × live last-mile rates) — informational context for approval',
|
||
})
|
||
priceEstimate(@Param('id', ParseUUIDPipe) id: string) {
|
||
return this.requestsService.priceEstimate(id);
|
||
}
|
||
|
||
// Customer-facing like :id/submit — the service ownership-checks against the
|
||
// resolved company; staff may also open it (read-only view).
|
||
@Get(':id/contract/view')
|
||
@ApiOperation({ summary: 'LM contract view model + rendered HTML + saved signature' })
|
||
contractView(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: TCurrentUser) {
|
||
return this.contractService.getContractView(id, user?.id ?? null);
|
||
}
|
||
|
||
@Get(':id/contract/document')
|
||
@ApiOperation({ summary: 'Download the LM contract PDF (LM_<CustomerName>.pdf)' })
|
||
async contractDocument(
|
||
@Param('id', ParseUUIDPipe) id: string,
|
||
@Res() res: Response,
|
||
): Promise<void> {
|
||
const { stream, record } = await this.contractService.streamContract(id);
|
||
res.setHeader('Content-Type', record.mimeType ?? 'application/pdf');
|
||
res.setHeader('Content-Disposition', `attachment; filename="${record.name}"`);
|
||
stream.pipe(res);
|
||
}
|
||
|
||
@Post(':id/contract/sign')
|
||
@ApiOperation({ summary: 'Customer agrees and signs the LM contract — then the advance invoice is issued' })
|
||
signContract(
|
||
@Param('id', ParseUUIDPipe) id: string,
|
||
@Body() dto: SignLastMileContractDto,
|
||
@CurrentUser() user: TCurrentUser,
|
||
) {
|
||
return this.contractService.sign(id, dto, user?.id ?? null);
|
||
}
|
||
|
||
@Get(':id')
|
||
@BookingStaff(FREIGHT_PERMS.lastMile.requestView)
|
||
@ApiOperation({ summary: 'Get a last-mile confirmation request by ID' })
|
||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||
return this.requestsService.findById(id);
|
||
}
|
||
|
||
// No @BookingStaff — the customer (portal) fills this, not backoffice staff.
|
||
// TODO: integrate @edr/auth — @CurrentUser is a stub until then; the service
|
||
// still cross-checks the request's booking against the resolved company.
|
||
@Post(':id/submit')
|
||
@ApiOperation({ summary: "Customer confirms which containers go via EDR last-mile" })
|
||
submit(
|
||
@Param('id', ParseUUIDPipe) id: string,
|
||
@Body() dto: SubmitLastMileRequestDto,
|
||
@CurrentUser() user: TCurrentUser,
|
||
) {
|
||
return this.requestsService.submit(id, user?.id ?? null, dto.containerNumbers, dto.deliveryDate);
|
||
}
|
||
|
||
@Post(':id/approve')
|
||
@BookingStaff(FREIGHT_PERMS.lastMile.requestApprove)
|
||
@ApiOperation({ summary: 'Truck & Machinery chief approves the request — LM contract becomes signable; the advance invoice follows the customer signature' })
|
||
approve(
|
||
@Param('id', ParseUUIDPipe) id: string,
|
||
@Body() dto: ApproveLastMileRequestDto,
|
||
@CurrentUser() user: TCurrentUser,
|
||
) {
|
||
return this.requestsService.approve(id, user?.id ?? null, dto.advanceAmount);
|
||
}
|
||
|
||
@Post(':id/reject')
|
||
@BookingStaff(FREIGHT_PERMS.lastMile.requestApprove)
|
||
@ApiOperation({ summary: 'Truck & Machinery chief rejects the request with a reason' })
|
||
reject(
|
||
@Param('id', ParseUUIDPipe) id: string,
|
||
@Body() dto: RejectLastMileRequestDto,
|
||
@CurrentUser() user: TCurrentUser,
|
||
) {
|
||
return this.requestsService.reject(id, user?.id ?? null, dto.reason);
|
||
}
|
||
}
|