mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 11:18:17 +00:00
feat(companies): add Transit Agent service linked to transit-agent roster; forwarder/agent onboarding step, assigned-bookings tab, booking assignment + notify, drop agent validity window
This commit is contained in:
@@ -2,6 +2,7 @@ import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { Booking } from "../bookings/entities/booking.entity";
|
||||
import { ExternalProfile } from "../companies/entities/external-profile.entity";
|
||||
import { ClearanceMilestone } from "../contracts/entities/clearance-milestone.entity";
|
||||
import { FilesModule } from "../files/files.module";
|
||||
import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity";
|
||||
@@ -19,11 +20,14 @@ import { TransitAssignmentsService } from "./transit-assignments.service";
|
||||
// Milestones and train schedules are read for the agent's dashboard
|
||||
// timings (declaration stamps, departure/arrival fallbacks) — entities
|
||||
// only, for the same reason as Booking.
|
||||
// ExternalProfile: `/my` resolves a freight forwarder's portal user to the
|
||||
// transit agent its company registered as — entity only, same reason.
|
||||
TypeOrmModule.forFeature([
|
||||
TransitAssignment,
|
||||
Booking,
|
||||
ClearanceMilestone,
|
||||
TrainSchedule,
|
||||
ExternalProfile,
|
||||
]),
|
||||
FilesModule,
|
||||
TransitAgentsModule,
|
||||
|
||||
@@ -93,6 +93,7 @@ describe("TransitAssignmentsService", () => {
|
||||
files as never,
|
||||
milestones as never,
|
||||
trainSchedules as never,
|
||||
{ findOne: jest.fn().mockResolvedValue(null) } as never, // externalProfiles
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
} from "@edr/types";
|
||||
|
||||
import { Booking } from "../bookings/entities/booking.entity";
|
||||
import { ExternalProfile } from "../companies/entities/external-profile.entity";
|
||||
import {
|
||||
ProfileStatus,
|
||||
ProfileType,
|
||||
} from "../companies/entities/company-profile.entity";
|
||||
import { ClearanceMilestone } from "../contracts/entities/clearance-milestone.entity";
|
||||
import { FilesService } from "../files/files.service";
|
||||
import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity";
|
||||
@@ -170,6 +175,11 @@ export class TransitAssignmentsService {
|
||||
private readonly milestonesRepository: Repository<ClearanceMilestone>,
|
||||
@InjectRepository(TrainSchedule)
|
||||
private readonly trainSchedulesRepository: Repository<TrainSchedule>,
|
||||
// The ExternalProfile ENTITY (not CompaniesModule) for the same reason as
|
||||
// Booking above: `/my` only has to walk portal user → company → the
|
||||
// transit agent that company registered itself as.
|
||||
@InjectRepository(ExternalProfile)
|
||||
private readonly externalProfilesRepository: Repository<ExternalProfile>,
|
||||
) {}
|
||||
|
||||
private static minutesBetween(
|
||||
@@ -252,9 +262,52 @@ export class TransitAssignmentsService {
|
||||
// client-supplied id: an agent must not be able to read or edit another
|
||||
// agent's assignments by guessing one.
|
||||
|
||||
/** The transit agent this portal user signs in as. */
|
||||
private async requireAgentForUser(userId: string) {
|
||||
const agent = await this.transitAgentsRepository.findByUserId(userId);
|
||||
/**
|
||||
* The transit agent this portal user acts as.
|
||||
*
|
||||
* Two kinds of account reach `/my`: a Djibouti transit officer, who signs in
|
||||
* AS the agent (`transit_agents.user_id`), and a customer company that
|
||||
* registered itself as an Ethiopian transit agent (`companies.
|
||||
* transit_agent_id`) — under the transit agent role, the forwarder role, or
|
||||
* both. It may look at its assigned bookings from the moment the role is
|
||||
* requested — that is how it learns work is waiting — but may only act on
|
||||
* them (`forWrite`) once a roster role has been approved.
|
||||
*/
|
||||
private async requireAgentForUser(
|
||||
userId: string,
|
||||
opts: { forWrite?: boolean } = {},
|
||||
) {
|
||||
const own = await this.transitAgentsRepository.findByUserId(userId);
|
||||
if (own) return own;
|
||||
|
||||
const profile = await this.externalProfilesRepository.findOne({
|
||||
where: { userId },
|
||||
relations: { company: { companyProfiles: true } },
|
||||
});
|
||||
const company = profile?.company;
|
||||
if (!company?.transitAgentId) {
|
||||
throw new ForbiddenException("This account is not a transit agent");
|
||||
}
|
||||
// Either roster role will do — a plain transit agent or a forwarder.
|
||||
const agentRoles = (company.companyProfiles ?? []).filter(
|
||||
(p) =>
|
||||
p.type === ProfileType.transitAgent ||
|
||||
p.type === ProfileType.freightForwarder,
|
||||
);
|
||||
if (agentRoles.length === 0) {
|
||||
throw new ForbiddenException("This account is not a transit agent");
|
||||
}
|
||||
if (
|
||||
opts.forWrite &&
|
||||
!agentRoles.some((p) => p.status === ProfileStatus.Active)
|
||||
) {
|
||||
throw new ForbiddenException(
|
||||
"Your transit agent role is not approved yet — you can view assigned bookings but not act on them until it is.",
|
||||
);
|
||||
}
|
||||
const agent = await this.transitAgentsRepository.findById(
|
||||
company.transitAgentId,
|
||||
);
|
||||
if (!agent) {
|
||||
throw new ForbiddenException("This account is not a transit agent");
|
||||
}
|
||||
@@ -640,8 +693,12 @@ export class TransitAssignmentsService {
|
||||
return { ...this.toView(assignment), files: await this.listFiles(id) };
|
||||
}
|
||||
|
||||
/** Assert the assignment is this user's before any write reaches it. */
|
||||
/**
|
||||
* Assert the assignment is this user's before any write reaches it — and
|
||||
* that the user may write at all (an unapproved forwarder may only look).
|
||||
*/
|
||||
private async assertMine(userId: string, id: string): Promise<void> {
|
||||
await this.requireAgentForUser(userId, { forWrite: true });
|
||||
await this.findMineById(userId, id);
|
||||
}
|
||||
|
||||
@@ -677,6 +734,7 @@ export class TransitAssignmentsService {
|
||||
id: string,
|
||||
input: { finish: boolean; note?: string },
|
||||
): Promise<TransitAssignmentView> {
|
||||
await this.requireAgentForUser(userId, { forWrite: true });
|
||||
const current = await this.findMineById(userId, id);
|
||||
if (current.status === TransitAssignmentStatus.Finished) {
|
||||
throw new ForbiddenException("This assignment is already finished.");
|
||||
|
||||
Reference in New Issue
Block a user