per-user trade-direction access scope

This commit is contained in:
Marshal
2026-08-02 22:29:58 +00:00
parent c055abe8c1
commit f4fd469643
47 changed files with 1451 additions and 107 deletions

View File

@@ -1437,6 +1437,7 @@ export class BookingBatchService implements OnModuleInit {
*/
async getBatchBoard(
query: BatchBoardQueryDto = {},
allowedDirections?: string[],
): Promise<BatchBoardListResponse> {
// Board cards are heavy (per-schedule booking summaries), so the default
// page is smaller than the toolkit-wide 20.
@@ -1444,6 +1445,11 @@ export class BookingBatchService implements OnModuleInit {
defaultPageSize: 12,
});
// The board is IMPORT-only — a user scoped away from IMPORT sees nothing.
if (allowedDirections && !allowedDirections.includes("IMPORT")) {
return { items: [], meta: buildPaginationMeta(0, page, pageSize) };
}
// Status filter: any subset of the lifecycle. Omitted = all statuses, so
// arrived / cancelled / dispatched schedules stay visible as history.
const allowedStatuses = new Set<string>(BATCH_BOARD_STATUSES);

View File

@@ -1,6 +1,7 @@
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
import type { Response } from "express";
import type { AuthUserPayload } from "../../common/resolve-auth-user-id";
import { UserTradeAccessService } from "../user-trade-access/user-trade-access.service";
import { resolveAuthUserId } from "../../common/resolve-auth-user-id";
import {
@@ -66,6 +67,7 @@ export class TrainSchedulingController {
private readonly intercityService: IntercityService,
private readonly bookingJourneyService: BookingJourneyService,
private readonly billingService: BillingService,
private readonly userTradeAccessService: UserTradeAccessService,
) { }
@Get("my-booking-windows")
@@ -130,8 +132,14 @@ export class TrainSchedulingController {
summary:
"Batch monitoring board: paginated import schedules (all statuses) with bookings grouped by state",
})
getBatchBoard(@Query() query: BatchBoardQueryDto) {
return this.bookingBatchService.getBatchBoard(query);
async getBatchBoard(
@Query() query: BatchBoardQueryDto,
@CurrentUser() user: AuthUserPayload,
) {
// Batch board is IMPORT-only — a user without IMPORT access sees nothing.
const allowed =
await this.userTradeAccessService.resolveAllowedDirections(user);
return this.bookingBatchService.getBatchBoard(query, allowed ?? undefined);
}
@Get("batch-board/:scheduleId")
@@ -868,15 +876,31 @@ export class TrainSchedulingController {
@Get("container/schedules")
@TrainSchedulingView()
@ApiOperation({ summary: "List container train schedules (paginated)" })
getContainerTrainSchedules(@Query() query: ListTrainSchedulesQueryDto) {
return this.trainSchedulingService.getContainerTrainSchedules(query);
async getContainerTrainSchedules(
@Query() query: ListTrainSchedulesQueryDto,
@CurrentUser() user: AuthUserPayload,
) {
const allowed =
await this.userTradeAccessService.resolveAllowedDirections(user);
return this.trainSchedulingService.getContainerTrainSchedules(
query,
allowed ?? undefined,
);
}
@Get("bulk/schedules")
@TrainSchedulingView()
@ApiOperation({ summary: "List bulk train schedules (paginated)" })
getBulkTrainSchedules(@Query() query: ListTrainSchedulesQueryDto) {
return this.trainSchedulingService.getContainerTrainSchedules(query);
async getBulkTrainSchedules(
@Query() query: ListTrainSchedulesQueryDto,
@CurrentUser() user: AuthUserPayload,
) {
const allowed =
await this.userTradeAccessService.resolveAllowedDirections(user);
return this.trainSchedulingService.getContainerTrainSchedules(
query,
allowed ?? undefined,
);
}
@Get("container/schedules/:id")

View File

@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Session } from '@tria-plc/iamapi-common/entities/iam/user/session.entity';
import { BillingModule } from '../billing/billing.module';
import { UserTradeAccessModule } from '../user-trade-access/user-trade-access.module';
import { BookingsModule } from '../bookings/bookings.module';
import { Container } from '../container-management/entities/container.entity';
import { LocomotivesModule } from '../locomotives/locomotives.module';
@@ -63,6 +64,7 @@ import { ContractsModule } from '../contracts/contracts.module';
]),
forwardRef(() => BookingsModule),
BillingModule,
UserTradeAccessModule,
NotificationsModule,
NotificationInboxModule,
LocomotivesModule,

View File

@@ -3899,13 +3899,25 @@ export class TrainSchedulingService {
return Object.assign(detail, { warehouseAutomation });
}
async getContainerTrainSchedules(query: ListTrainSchedulesQueryDto = {}) {
async getContainerTrainSchedules(
query: ListTrainSchedulesQueryDto = {},
allowedDirections?: string[],
) {
const { page, pageSize, skip, take } = normalizePagination(query);
// Per-user trade-direction scope: schedules carry a `direction` column.
if (allowedDirections && allowedDirections.length === 0) {
return {
items: [],
meta: buildPaginationMeta(0, page, pageSize),
};
}
// Exact-match filters (enum/id semantics). Freight type is derived from
// the bookings aboard — no column to match — so it rides on `id` as an
// EXISTS fragment instead.
const base: FindOptionsWhere<TrainSchedule> = {};
if (allowedDirections) base.direction = In(allowedDirections) as never;
if (query.status) base.status = query.status;
if (query.originStationId) base.originStationId = query.originStationId;
if (query.destinationStationId) base.destinationStationId = query.destinationStationId;