mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
changes
This commit is contained in:
@@ -32,11 +32,14 @@ function makeService(overrides?: {
|
||||
workflowThrows?: boolean;
|
||||
/** Resolve the input doc set with no required fields → every doc counts approved. */
|
||||
docsApproved?: boolean;
|
||||
/** Yard ids the caller is scoped to; `null` (default) = unrestricted. */
|
||||
yardScope?: string[] | null;
|
||||
}) {
|
||||
const booking = overrides?.booking ?? generalImportBooking;
|
||||
const bookingsRepository = {
|
||||
findDocumentReviews: jest.fn().mockResolvedValue([]),
|
||||
update: jest.fn().mockResolvedValue(booking),
|
||||
findByStatuses: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const bookingsService = {
|
||||
findById: jest.fn().mockResolvedValue(booking),
|
||||
@@ -111,6 +114,7 @@ function makeService(overrides?: {
|
||||
.mockResolvedValue({ id: 'ta-1', name: 'Ahmed Bourhan' }),
|
||||
} as never, // transit agents
|
||||
{ findAll: jest.fn().mockResolvedValue([]) } as never, // contracts repository
|
||||
{ getScopedYardIds: jest.fn().mockResolvedValue(overrides?.yardScope ?? null) } as never, // yard scope
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -124,6 +128,30 @@ function makeService(overrides?: {
|
||||
}
|
||||
|
||||
describe('BookingClearanceService', () => {
|
||||
describe('etQueue yard scope', () => {
|
||||
const queueBookings = [
|
||||
{ ...generalImportBooking, id: 'b-mojo-out', originYardId: 'mojo', destinationYardId: 'dire' },
|
||||
{ ...generalImportBooking, id: 'b-mojo-in', originYardId: 'addis', destinationYardId: 'mojo' },
|
||||
{ ...generalImportBooking, id: 'b-elsewhere', originYardId: 'addis', destinationYardId: 'dire' },
|
||||
] as unknown as Booking[];
|
||||
|
||||
it('keeps only bookings whose origin or destination is in scope', async () => {
|
||||
const { service, bookingsRepository, workflowService } = makeService({ yardScope: ['mojo'] });
|
||||
bookingsRepository.findByStatuses.mockResolvedValue(queueBookings);
|
||||
workflowService.listMilestonesForBooking.mockResolvedValue([{ status: 'PENDING' }]);
|
||||
const rows = await service.etQueue({});
|
||||
expect(rows.map((b) => b.id)).toEqual(['b-mojo-out', 'b-mojo-in']);
|
||||
});
|
||||
|
||||
it('shows everything when the position has no yard mapping', async () => {
|
||||
const { service, bookingsRepository, workflowService } = makeService({ yardScope: null });
|
||||
bookingsRepository.findByStatuses.mockResolvedValue(queueBookings);
|
||||
workflowService.listMilestonesForBooking.mockResolvedValue([{ status: 'PENDING' }]);
|
||||
const rows = await service.etQueue({});
|
||||
expect(rows).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adviseDuty', () => {
|
||||
it('skips duty milestones when duty is not required', async () => {
|
||||
const { service, workflowService, bookingsRepository } = makeService();
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ClearanceMilestoneService } from './clearance-milestone.service';
|
||||
import { GlOperationsService } from './gl-operations.service';
|
||||
import { GlExchangeService } from './gl-exchange.service';
|
||||
import { TransitAgentsService } from '../transit-agents/transit-agents.service';
|
||||
import { YardScopeService } from '../rule-engine/services/yard-scope.service';
|
||||
import { ContractsRepository } from './contracts.repository';
|
||||
import { AdviseContractDutyDto } from './dto/phased-clearance.dto';
|
||||
import { buildWorkflowFiles, belongsOnDjClearanceQueue, belongsOnEtClearanceQueue, DJ_BOOKING_QUEUE_STATUSES, persistDeclarationUploads, persistDeliveryOrderUploads, persistDraftDeclarationUploads, persistReleaseOrderUploads, persistTransitPermitUploads, PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES } from './phased-clearance.util';
|
||||
@@ -158,6 +159,7 @@ export class BookingClearanceService {
|
||||
private readonly glExchangeService: GlExchangeService,
|
||||
private readonly transitAgentsService: TransitAgentsService,
|
||||
private readonly contractsRepository: ContractsRepository,
|
||||
private readonly yardScope: YardScopeService,
|
||||
) {}
|
||||
|
||||
private async assertPhasedCustoms(booking: Booking): Promise<void> {
|
||||
@@ -981,7 +983,7 @@ export class BookingClearanceService {
|
||||
return this.bookingsService.findById(bookingId);
|
||||
}
|
||||
|
||||
async etQueue(): Promise<Booking[]> {
|
||||
async etQueue(user?: unknown): Promise<Booking[]> {
|
||||
const candidates = await this.bookingsRepository.findByStatuses([
|
||||
...PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES,
|
||||
]);
|
||||
@@ -991,7 +993,26 @@ export class BookingClearanceService {
|
||||
const milestones = await this.workflowService.listMilestonesForBooking(b.id);
|
||||
if (belongsOnEtClearanceQueue(milestones)) filtered.push(b);
|
||||
}
|
||||
return this.attachContractSummary(filtered);
|
||||
const rows = await this.attachContractSummary(filtered);
|
||||
return this.narrowToYardScope(rows, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep only bookings whose ORIGIN or DESTINATION yard is one of the caller's
|
||||
* assigned yards (`freight.yard_positions` via the active position). Yards in
|
||||
* the middle of a route do not count. An unmapped position, super admin or
|
||||
* `yards:view_all` holder sees everything (scope resolves to `null`).
|
||||
* Runs after {@link attachContractSummary} so route-fallback yards count too.
|
||||
*/
|
||||
private async narrowToYardScope(bookings: Booking[], user: unknown): Promise<Booking[]> {
|
||||
const scope = await this.yardScope.getScopedYardIds(user as never);
|
||||
if (scope === null) return bookings;
|
||||
const inScope = (id: string | null | undefined) => !!id && scope.includes(id);
|
||||
return bookings.filter(
|
||||
(b) =>
|
||||
inScope(b.originYardId ?? b.originYard?.id) ||
|
||||
inScope(b.destinationYardId ?? b.destinationYard?.id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,8 @@ export class BookingRequestRepository extends BaseRepository<BookingRequest> {
|
||||
async findQueue(): Promise<BookingRequest[]> {
|
||||
return this.repository.find({
|
||||
order: { createdAt: 'DESC' },
|
||||
relations: { contract: { company: true } },
|
||||
// `routes` rides along so the queue can be narrowed to the caller's yards.
|
||||
relations: { contract: { company: true, routes: true } },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import type { Freight } from '@edr/types';
|
||||
|
||||
import { YardScopeService } from '../rule-engine/services/yard-scope.service';
|
||||
import { BookingRequestRepository } from './booking-request.repository';
|
||||
import { ContractsService } from './contracts.service';
|
||||
import { ContractBookingService } from './contract-booking.service';
|
||||
@@ -28,6 +29,7 @@ export class BookingRequestService {
|
||||
private readonly contractsService: ContractsService,
|
||||
private readonly contractBookingService: ContractBookingService,
|
||||
private readonly notifier: ContractNotifierService,
|
||||
private readonly yardScope: YardScopeService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -168,8 +170,25 @@ export class BookingRequestService {
|
||||
return request;
|
||||
}
|
||||
|
||||
queue(): Promise<BookingRequest[]> {
|
||||
return this.repo.findQueue();
|
||||
/**
|
||||
* GL queue narrowed to the caller's yards: a request stays when its route's
|
||||
* ORIGIN or DESTINATION yard is one the caller's active position is mapped to
|
||||
* (unmapped position / super admin → everything). A request with no
|
||||
* resolvable route (no `contractRouteId` on a multi-route contract) has no
|
||||
* yards to judge by and is kept visible.
|
||||
*/
|
||||
async queue(user?: unknown): Promise<BookingRequest[]> {
|
||||
const rows = await this.repo.findQueue();
|
||||
const scope = await this.yardScope.getScopedYardIds(user as never);
|
||||
if (scope === null) return rows;
|
||||
return rows.filter((r) => {
|
||||
const routes = r.contract?.routes ?? [];
|
||||
const route =
|
||||
routes.find((x) => x.id === r.contractRouteId) ??
|
||||
(routes.length === 1 ? routes[0] : undefined);
|
||||
if (!route) return true;
|
||||
return scope.includes(route.originYardId) || scope.includes(route.destinationYardId);
|
||||
});
|
||||
}
|
||||
|
||||
private async findPending(requestId: string): Promise<BookingRequest> {
|
||||
|
||||
@@ -123,8 +123,8 @@ export class ContractsController {
|
||||
@Get('booking-requests/queue')
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.createBooking)
|
||||
@ApiOperation({ summary: 'GL queue: shipment requests across contracts (all statuses, newest first)' })
|
||||
bookingRequestQueue() {
|
||||
return this.bookingRequestService.queue();
|
||||
bookingRequestQueue(@CurrentUser() user: AuthUserPayload) {
|
||||
return this.bookingRequestService.queue(user);
|
||||
}
|
||||
|
||||
@Get('booking-requests/:reqId')
|
||||
|
||||
Reference in New Issue
Block a user