mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +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:
@@ -20,6 +20,9 @@ import { BookingsRepository } from '../bookings/bookings.repository';
|
||||
import { BookingPricingService } from '../bookings/booking-pricing.service';
|
||||
import { BookingTransitionService } from '../bookings/booking-transition.service';
|
||||
import { BookingLifecycleNotifierService } from '../bookings/booking-lifecycle-notifier.service';
|
||||
import { TransitAgentCountry } from '../transit-agents/entities/transit-agent.entity';
|
||||
import { TransitAgentsRepository } from '../transit-agents/transit-agents.repository';
|
||||
import { TransitAssignmentsService } from '../transit-assignments/transit-assignments.service';
|
||||
import { ConsolidationService } from '../bookings/consolidation.service';
|
||||
import { ConsolidationApprovalService } from '../bookings/consolidation-approval.service';
|
||||
import { PriceLineItemDto } from '../bookings/dto/generate-price-response.dto';
|
||||
@@ -136,6 +139,8 @@ export class ContractBookingService {
|
||||
private readonly bookingTransitionService: BookingTransitionService,
|
||||
@Inject(forwardRef(() => ConsolidationApprovalService))
|
||||
private readonly consolidationApprovalService: ConsolidationApprovalService,
|
||||
private readonly transitAgentsRepository: TransitAgentsRepository,
|
||||
private readonly transitAssignmentsService: TransitAssignmentsService,
|
||||
) {}
|
||||
|
||||
async createUnderContract(
|
||||
@@ -857,34 +862,66 @@ export class ContractBookingService {
|
||||
if (!dto.scheduledDate) {
|
||||
throw new BadRequestException('A binding shipment day is required');
|
||||
}
|
||||
// Without-customs import/export: the customer's own clearing agent (name,
|
||||
// email, phone) is captured per booking at completion. A resubmit may omit
|
||||
// the fields and keep what the booking already stored. Customs contracts
|
||||
// (GL clears) and intercity (no border) never collect an agent.
|
||||
// Without-customs import/export: the customer names who clears customs for
|
||||
// this booking, one of two ways. Either a registered Ethiopian transit
|
||||
// agent (a freight forwarder on the platform) — the booking is assigned to
|
||||
// it and the forwarder is told — or their own clearing agent typed in
|
||||
// (name, email, phone). A resubmit may omit the typed fields and keep what
|
||||
// the booking already stored. Customs contracts (GL clears) and intercity
|
||||
// (no border) never collect an agent.
|
||||
let assignedTransitAgent: { id: string; name: string } | null = null;
|
||||
if (
|
||||
!contract.customsClearingEnabled &&
|
||||
contract.tradeDirection !== 'DOMESTIC'
|
||||
) {
|
||||
const agentName =
|
||||
dto.customsClearingAgent?.trim() || booking.customsClearingAgent || null;
|
||||
const agentEmail =
|
||||
dto.customsClearingAgentEmail?.trim() ||
|
||||
booking.customsClearingAgentEmail ||
|
||||
null;
|
||||
const agentPhone =
|
||||
dto.customsClearingAgentPhone?.trim() ||
|
||||
booking.customsClearingAgentPhone ||
|
||||
null;
|
||||
if (!agentName || !agentEmail || !agentPhone) {
|
||||
throw new BadRequestException(
|
||||
'Customs clearing agent name, email and phone are required to complete this booking.',
|
||||
);
|
||||
if (dto.transitAgentId) {
|
||||
const agent = await this.transitAgentsRepository.findById(dto.transitAgentId);
|
||||
if (
|
||||
!agent ||
|
||||
!agent.isActive ||
|
||||
agent.country !== TransitAgentCountry.Ethiopia
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'The selected transit agent is not an active Ethiopian transit agent — pick another one or enter your clearing agent details.',
|
||||
);
|
||||
}
|
||||
// The forwarder company's own contact goes on the booking, so the
|
||||
// customer sees who to reach; an Ethiopian agent row carries none.
|
||||
const [forwarder]: Array<{ email: string | null; phone: string | null }> =
|
||||
await this.dataSource.query(
|
||||
`SELECT email, phone FROM freight.companies
|
||||
WHERE transit_agent_id = $1 AND deleted_at IS NULL
|
||||
LIMIT 1`,
|
||||
[agent.id],
|
||||
);
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
customsClearingAgent: agent.name,
|
||||
customsClearingAgentEmail: forwarder?.email ?? null,
|
||||
customsClearingAgentPhone: forwarder?.phone ?? null,
|
||||
} as never);
|
||||
assignedTransitAgent = { id: agent.id, name: agent.name };
|
||||
} else {
|
||||
const agentName =
|
||||
dto.customsClearingAgent?.trim() || booking.customsClearingAgent || null;
|
||||
const agentEmail =
|
||||
dto.customsClearingAgentEmail?.trim() ||
|
||||
booking.customsClearingAgentEmail ||
|
||||
null;
|
||||
const agentPhone =
|
||||
dto.customsClearingAgentPhone?.trim() ||
|
||||
booking.customsClearingAgentPhone ||
|
||||
null;
|
||||
if (!agentName || !agentEmail || !agentPhone) {
|
||||
throw new BadRequestException(
|
||||
'Customs clearing agent name, email and phone are required to complete this booking — or pick a registered transit agent.',
|
||||
);
|
||||
}
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
customsClearingAgent: agentName,
|
||||
customsClearingAgentEmail: agentEmail,
|
||||
customsClearingAgentPhone: agentPhone,
|
||||
} as never);
|
||||
}
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
customsClearingAgent: agentName,
|
||||
customsClearingAgentEmail: agentEmail,
|
||||
customsClearingAgentPhone: agentPhone,
|
||||
} as never);
|
||||
}
|
||||
// No expiry gate here on purpose: this booking was already initiated
|
||||
// before the contract lapsed (createUnderContract/initiateUnderContract
|
||||
@@ -1092,6 +1129,18 @@ export class ContractBookingService {
|
||||
dto.scheduledDate,
|
||||
dto.trainScheduleId ?? null,
|
||||
);
|
||||
|
||||
// The forwarder's work list and its notice come AFTER the booking is
|
||||
// committed: a customer must never be told a forwarder has the job when
|
||||
// the completion itself was refused a line above.
|
||||
if (assignedTransitAgent) {
|
||||
await this.transitAssignmentsService.ensureAssignment(
|
||||
booking.id,
|
||||
assignedTransitAgent.id,
|
||||
(actorPermissions as { id?: string } | undefined)?.id,
|
||||
);
|
||||
void this.bookingNotifier.transitAgentAssigned(completed, assignedTransitAgent);
|
||||
}
|
||||
return { booking: completed, warnings };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user