fix: portal cannot load last-mile confirmation request

LAST_MILE_REQUESTS URL constants were missing the /api prefix every other
endpoint in URLS.ts carries — all five calls (get, submit, contract
view/document/sign) 404'd against the deployed API, so the departure
notification's confirm link never loaded for the customer.

Also widen GET /last-mile-requests/:id from @BookingStaff to @MixedAudience
with the same ownership check submit()/sign() already use — the confirm
page calls this as its first request, before the customer has done
anything else, so it can't be staff-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-08-10 12:00:08 +00:00
parent a08b097039
commit bf8eadbc85
3 changed files with 27 additions and 9 deletions

View File

@@ -91,11 +91,15 @@ export class LastMileRequestsController {
return this.contractService.sign(id, dto, user?.id ?? null); return this.contractService.sign(id, dto, user?.id ?? null);
} }
// Customer-facing like :id/contract/view — the portal's confirm page opens
// this straight from the departure notification link before the customer
// has done anything else, so it can't be staff-only. Service ownership-
// checks against the resolved company; staff may also open it.
@Get(':id') @Get(':id')
@BookingStaff(FREIGHT_PERMS.lastMile.requestView) @MixedAudience(FREIGHT_PERMS.lastMile.requestView)
@ApiOperation({ summary: 'Get a last-mile confirmation request by ID' }) @ApiOperation({ summary: 'Get a last-mile confirmation request by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) { findOne(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: TCurrentUser) {
return this.requestsService.findById(id); return this.requestsService.findById(id, user?.id ?? null);
} }
// No @BookingStaff — the customer (portal) fills this, not backoffice staff. // No @BookingStaff — the customer (portal) fills this, not backoffice staff.

View File

@@ -199,11 +199,25 @@ export class LastMileRequestsService {
}; };
} }
async findById(id: string): Promise<LastMileRequest> { /**
* `userId` is set only when a portal customer calls this directly (the
* confirm-page deep link from the departure notification, before they've
* submitted or signed anything) — staff and every internal caller pass
* nothing and skip the check, same convention as submit()/sign().
*/
async findById(id: string, userId?: string | null): Promise<LastMileRequest> {
const record = await this.requestsRepository.findById(id, { const record = await this.requestsRepository.findById(id, {
relations: { booking: { company: true } }, relations: { booking: { company: true } },
}); });
if (!record) throw new NotFoundException(`Last-mile request ${id} not found`); if (!record) throw new NotFoundException(`Last-mile request ${id} not found`);
if (userId) {
const companyId = await this.bookingsService.resolveCustomerCompanyId(userId);
if (companyId && record.booking?.companyId && companyId !== record.booking.companyId) {
throw new BadRequestException('This request does not belong to your company');
}
}
return record; return record;
} }

View File

@@ -224,10 +224,10 @@ export const URL_CONSTANTS = {
}, },
LAST_MILE_REQUESTS: { LAST_MILE_REQUESTS: {
BY_ID: (id: string) => `/last-mile-requests/${id}`, BY_ID: (id: string) => `/api/last-mile-requests/${id}`,
SUBMIT: (id: string) => `/last-mile-requests/${id}/submit`, SUBMIT: (id: string) => `/api/last-mile-requests/${id}/submit`,
CONTRACT_VIEW: (id: string) => `/last-mile-requests/${id}/contract/view`, CONTRACT_VIEW: (id: string) => `/api/last-mile-requests/${id}/contract/view`,
CONTRACT_DOCUMENT: (id: string) => `/last-mile-requests/${id}/contract/document`, CONTRACT_DOCUMENT: (id: string) => `/api/last-mile-requests/${id}/contract/document`,
CONTRACT_SIGN: (id: string) => `/last-mile-requests/${id}/contract/sign`, CONTRACT_SIGN: (id: string) => `/api/last-mile-requests/${id}/contract/sign`,
}, },
}; };