add permissions and fix issues

This commit is contained in:
Marshal
2026-07-23 20:24:20 +00:00
parent 40f16f3cec
commit 668b5e1c9d
40 changed files with 18634 additions and 342 deletions

View File

@@ -26,8 +26,23 @@ export const BookingView = () => BookingStaff(FREIGHT_PERMS.bookings.view);
export const TrainSchedulingView = () =>
BookingStaff(FREIGHT_PERMS.trainScheduling.view);
export const TrainSchedulingManage = () =>
BookingStaff(FREIGHT_PERMS.trainScheduling.manage);
// Granular train-scheduling actions replace the retired coarse manage:
// create a schedule, update (assign/consist/loading/finalize/dispatch/arrive…),
// cancel a schedule, reschedule (+ maintenance), and manage global rules.
export const TrainSchedulingCreate = () =>
BookingStaff(FREIGHT_PERMS.trainScheduling.create);
export const TrainSchedulingUpdate = () =>
BookingStaff(FREIGHT_PERMS.trainScheduling.update);
export const TrainSchedulingCancel = () =>
BookingStaff(FREIGHT_PERMS.trainScheduling.cancel);
export const TrainSchedulingReschedule = () =>
BookingStaff(FREIGHT_PERMS.trainScheduling.reschedule);
export const TrainSchedulingRulesManage = () =>
BookingStaff(FREIGHT_PERMS.trainScheduling.rulesManage);
/**
* Fleet guards take an optional granular per-resource key (locomotives:create,

View File

@@ -13,9 +13,22 @@ export const RuleEngineView = (slug: RuleEngineResourceSlug) =>
UseGuards(JwtGuard, FreightPermissionGuard([FREIGHT_PERMS.ruleEngine.view(slug)])),
);
export const RuleEngineManage = (slug: RuleEngineResourceSlug) =>
// Granular CRUD replaces the retired coarse RuleEngineManage. Each write
// endpoint carries the specific action it performs — create on POST-new,
// update on PATCH / reorder / move-order, delete on DELETE.
export const RuleEngineCreate = (slug: RuleEngineResourceSlug) =>
applyDecorators(
UseGuards(JwtGuard, FreightPermissionGuard([FREIGHT_PERMS.ruleEngine.manage(slug)])),
UseGuards(JwtGuard, FreightPermissionGuard([FREIGHT_PERMS.ruleEngine.create(slug)])),
);
export const RuleEngineUpdate = (slug: RuleEngineResourceSlug) =>
applyDecorators(
UseGuards(JwtGuard, FreightPermissionGuard([FREIGHT_PERMS.ruleEngine.update(slug)])),
);
export const RuleEngineDelete = (slug: RuleEngineResourceSlug) =>
applyDecorators(
UseGuards(JwtGuard, FreightPermissionGuard([FREIGHT_PERMS.ruleEngine.delete(slug)])),
);
/**

View File

@@ -526,6 +526,8 @@ export class ContractsController {
contractId: view.bookingId,
reference: view.reference,
status: view.status,
// Drives the per-freight-type sign permission on the client.
freightType: contract.freightType,
templateKey: view.templateKey,
title: view.template.title,
html,
@@ -577,19 +579,21 @@ export class ContractsController {
@Post(':id/contract/sign')
@UseGuards(JwtGuard)
@ApiOperation({ summary: 'Apply digital signature (customer or staff/director/ceo)' })
signContract(
async signContract(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: SignContractDto,
@CurrentUser() user: TCurrentUser,
) {
// Each staff signing role maps to the permission that step already requires;
// customers sign their own contract with no permission key.
const signRolePermission: Record<string, string> = {
STAFF: FREIGHT_PERMS.contracts.signStaff,
DIRECTOR: FREIGHT_PERMS.contracts.approveDirector,
CEO: FREIGHT_PERMS.contracts.approveCeo,
};
if (dto.role !== 'CUSTOMER') {
// Each staff signing role maps to the permission that step already
// requires; the STAFF counter-signature is split per freight type, so a
// bulk signer cannot counter-sign a container contract (and vice versa).
const contract = await this.contractsService.findById(id);
const signRolePermission: Record<string, string> = {
STAFF: forFreightType(FREIGHT_PERMS.contracts.signStaff, contract.freightType),
DIRECTOR: FREIGHT_PERMS.contracts.approveDirector,
CEO: FREIGHT_PERMS.contracts.approveCeo,
};
assertFreightPermission(user, signRolePermission[dto.role]);
}
return this.transitionService.sign(id, dto, {

View File

@@ -2,7 +2,7 @@ import {
Body, Controller, Delete, Get, HttpCode, HttpStatus,
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate, RuleEngineView } from '../../../common/rule-engine-guards';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateApprovalRuleDto } from '../dto/create-approval-rule.dto';
import { ListApprovalRulesQueryDto } from '../dto/list-rule-engine-query.dto';
@@ -41,7 +41,7 @@ export class ApprovalRulesController {
}
@Post('reorder')
@RuleEngineManage('approval-rules')
@RuleEngineUpdate('approval-rules')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Bulk reorder approval steps within a chain' })
reorder(@Body() dto: ReorderItemsDto) {
@@ -49,7 +49,7 @@ export class ApprovalRulesController {
}
@Post(':id/move-order')
@RuleEngineManage('approval-rules')
@RuleEngineUpdate('approval-rules')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Move an approval step up or down within its chain' })
moveOrder(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveOrderDto) {
@@ -64,21 +64,21 @@ export class ApprovalRulesController {
}
@Post()
@RuleEngineManage('approval-rules')
@RuleEngineCreate('approval-rules')
@ApiOperation({ summary: 'Create an approval rule step' })
create(@Body() dto: CreateApprovalRuleDto) {
return this.service.create(dto);
}
@Patch(':id')
@RuleEngineManage('approval-rules')
@RuleEngineUpdate('approval-rules')
@ApiOperation({ summary: 'Update an approval rule' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateApprovalRuleDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('approval-rules')
@RuleEngineDelete('approval-rules')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete an approval rule' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -3,7 +3,7 @@ import {
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { RuleEngineManage } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate } from '../../../common/rule-engine-guards';
import { StaffReference } from '../../../common/booking-guards';
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
import { ListCargoTypesQueryDto } from '../dto/list-rule-engine-query.dto';
@@ -26,7 +26,7 @@ export class CargoTypesController {
}
@Post('reorder')
@RuleEngineManage('cargo-types')
@RuleEngineUpdate('cargo-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Bulk reorder cargo types by ID list' })
reorder(@Body() dto: ReorderItemsDto) {
@@ -34,7 +34,7 @@ export class CargoTypesController {
}
@Post(':id/move-order')
@RuleEngineManage('cargo-types')
@RuleEngineUpdate('cargo-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Move a cargo type up or down in display order' })
moveOrder(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveOrderDto) {
@@ -49,21 +49,21 @@ export class CargoTypesController {
}
@Post()
@RuleEngineManage('cargo-types')
@RuleEngineCreate('cargo-types')
@ApiOperation({ summary: 'Create a cargo type' })
create(@Body() dto: CreateCargoTypeDto) {
return this.service.create(dto);
}
@Patch(':id')
@RuleEngineManage('cargo-types')
@RuleEngineUpdate('cargo-types')
@ApiOperation({ summary: 'Update a cargo type' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateCargoTypeDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('cargo-types')
@RuleEngineDelete('cargo-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a cargo type' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -3,7 +3,7 @@ import {
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { RuleEngineManage } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate } from '../../../common/rule-engine-guards';
import { StaffReference } from '../../../common/booking-guards';
import { CreateContainerTypeDto } from '../dto/create-container-type.dto';
import { ListContainerTypesQueryDto } from '../dto/list-rule-engine-query.dto';
@@ -26,7 +26,7 @@ export class ContainerTypesController {
}
@Post('reorder')
@RuleEngineManage('container-types')
@RuleEngineUpdate('container-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Bulk reorder container types by ID list' })
reorder(@Body() dto: ReorderItemsDto) {
@@ -34,7 +34,7 @@ export class ContainerTypesController {
}
@Post(':id/move-order')
@RuleEngineManage('container-types')
@RuleEngineUpdate('container-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Move a container type up or down in display order' })
moveOrder(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveOrderDto) {
@@ -49,21 +49,21 @@ export class ContainerTypesController {
}
@Post()
@RuleEngineManage('container-types')
@RuleEngineCreate('container-types')
@ApiOperation({ summary: 'Create a container type' })
create(@Body() dto: CreateContainerTypeDto) {
return this.service.create(dto);
}
@Patch(':id')
@RuleEngineManage('container-types')
@RuleEngineUpdate('container-types')
@ApiOperation({ summary: 'Update a container type' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContainerTypeDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('container-types')
@RuleEngineDelete('container-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a container type' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -3,7 +3,7 @@ import {
Body, Controller, Delete, Get, HttpCode, HttpStatus,
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate, RuleEngineView } from '../../../common/rule-engine-guards';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreatePriorityConfigDto } from '../dto/create-priority-config.dto';
import { ListPriorityConfigsQueryDto } from '../dto/list-rule-engine-query.dto';
@@ -49,14 +49,14 @@ export class PriorityConfigsController {
}
@Post()
@RuleEngineManage('priority-configs')
@RuleEngineCreate('priority-configs')
@ApiOperation({ summary: 'Create a priority config' })
create(@Body() dto: CreatePriorityConfigDto) {
return this.service.create(dto);
}
@Post('reorder')
@RuleEngineManage('priority-configs')
@RuleEngineUpdate('priority-configs')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Bulk reorder priority configs by ID list' })
reorder(@Body() dto: ReorderItemsDto) {
@@ -64,7 +64,7 @@ export class PriorityConfigsController {
}
@Post(':id/move-order')
@RuleEngineManage('priority-configs')
@RuleEngineUpdate('priority-configs')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Move a priority config up or down in display order' })
moveOrder(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveOrderDto) {
@@ -72,14 +72,14 @@ export class PriorityConfigsController {
}
@Patch(':id')
@RuleEngineManage('priority-configs')
@RuleEngineUpdate('priority-configs')
@ApiOperation({ summary: 'Update a priority config' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdatePriorityConfigDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('priority-configs')
@RuleEngineDelete('priority-configs')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a priority config' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -11,7 +11,7 @@ import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'
import { CurrentUser } from '@edr/api-common';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineUpdate, RuleEngineView } from '../../../common/rule-engine-guards';
import { isSuperAdmin } from '../../../common/freight-permission.util';
import {
DecidePriorityRuleChangeDto,
@@ -32,7 +32,7 @@ export class PriorityRuleChangeRequestsController {
constructor(private readonly service: PriorityRuleChangeRequestsService) {}
@Post()
@RuleEngineManage('priority-configs')
@RuleEngineCreate('priority-configs')
@ApiOperation({ summary: 'Submit a priority-rule change for approval' })
submit(
@Body() dto: SubmitPriorityRuleChangeDto,
@@ -50,7 +50,7 @@ export class PriorityRuleChangeRequestsController {
}
@Post(':id/approve')
@RuleEngineManage('priority-configs')
@RuleEngineUpdate('priority-configs')
@ApiOperation({ summary: 'Approve and apply a pending change' })
approve(
@Param('id', ParseUUIDPipe) id: string,
@@ -63,7 +63,7 @@ export class PriorityRuleChangeRequestsController {
}
@Post(':id/reject')
@RuleEngineManage('priority-configs')
@RuleEngineUpdate('priority-configs')
@ApiOperation({ summary: 'Reject a pending change' })
reject(
@Param('id', ParseUUIDPipe) id: string,

View File

@@ -4,7 +4,7 @@ import { CurrentUser } from '@edr/api-common';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
import { isSuperAdmin } from '../../../common/freight-permission.util';
import { RuleEngineApprove, RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
import { RuleEngineApprove, RuleEngineCreate, RuleEngineView } from '../../../common/rule-engine-guards';
import { DecideRateChangeDto, SubmitRateChangeDto } from '../dto/rate-change-request.dto';
import { RateChangeStatus } from '../entities/rate-change-request.entity';
import { RateChangeRequestsService } from '../services/rate-change-requests.service';
@@ -21,7 +21,7 @@ export class RateChangeRequestsController {
constructor(private readonly service: RateChangeRequestsService) {}
@Post()
@RuleEngineManage('rates')
@RuleEngineCreate('rates')
@ApiOperation({ summary: 'Propose a change to a LIVE rate' })
submit(@Body() dto: SubmitRateChangeDto, @CurrentUser() user: TCurrentUser) {
return this.service.submit(dto, user?.id);

View File

@@ -5,7 +5,7 @@ import {
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '@edr/api-common';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate, RuleEngineView } from '../../../common/rule-engine-guards';
import { isSuperAdmin } from '../../../common/freight-permission.util';
import { CreateRateDto } from '../dto/create-rate.dto';
import { ListRatesQueryDto } from '../dto/list-rule-engine-query.dto';
@@ -44,7 +44,7 @@ export class RatesController {
}
@Post()
@RuleEngineManage('rates')
@RuleEngineCreate('rates')
@ApiOperation({ summary: 'Create a rate (DRAFT)' })
create(
@Body() dto: CreateRateDto,
@@ -54,21 +54,21 @@ export class RatesController {
}
@Patch(':id')
@RuleEngineManage('rates')
@RuleEngineUpdate('rates')
@ApiOperation({ summary: 'Update a DRAFT rate' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateRateDto) {
return this.service.update(id, dto);
}
@Post(':id/submit')
@RuleEngineManage('rates')
@RuleEngineUpdate('rates')
@ApiOperation({ summary: 'Submit rate for CEO approval' })
submit(@Param('id', ParseUUIDPipe) id: string) {
return this.service.submitForApproval(id);
}
@Post(':id/approve')
@RuleEngineManage('rates')
@RuleEngineUpdate('rates')
@ApiOperation({ summary: 'CEO approves a rate' })
approve(
@Param('id', ParseUUIDPipe) id: string,
@@ -80,7 +80,7 @@ export class RatesController {
}
@Delete(':id')
@RuleEngineManage('rates')
@RuleEngineDelete('rates')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a rate' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -2,7 +2,7 @@ import {
Body, Controller, Delete, Get, HttpCode, HttpStatus,
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { RuleEngineManage } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate } from '../../../common/rule-engine-guards';
import { StaffReference } from '../../../common/booking-guards';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateServiceTypeDto } from '../dto/create-service-type.dto';
@@ -26,7 +26,7 @@ export class ServiceTypesController {
}
@Post('reorder')
@RuleEngineManage('service-types')
@RuleEngineUpdate('service-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Bulk reorder service types by ID list' })
reorder(@Body() dto: ReorderItemsDto) {
@@ -34,7 +34,7 @@ export class ServiceTypesController {
}
@Post(':id/move-order')
@RuleEngineManage('service-types')
@RuleEngineUpdate('service-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Move a service type up or down in display order' })
moveOrder(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveOrderDto) {
@@ -49,21 +49,21 @@ export class ServiceTypesController {
}
@Post()
@RuleEngineManage('service-types')
@RuleEngineCreate('service-types')
@ApiOperation({ summary: 'Create a service type' })
create(@Body() dto: CreateServiceTypeDto) {
return this.service.create(dto);
}
@Patch(':id')
@RuleEngineManage('service-types')
@RuleEngineUpdate('service-types')
@ApiOperation({ summary: 'Update a service type' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateServiceTypeDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('service-types')
@RuleEngineDelete('service-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a service type' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -2,7 +2,7 @@ import {
Body, Controller, Delete, Get, HttpCode, HttpStatus,
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { RuleEngineManage } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate } from '../../../common/rule-engine-guards';
import { StaffReference } from '../../../common/booking-guards';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateShippingLineDto } from '../dto/create-shipping-line.dto';
@@ -31,21 +31,21 @@ export class ShippingLinesController {
}
@Post()
@RuleEngineManage('shipping-lines')
@RuleEngineCreate('shipping-lines')
@ApiOperation({ summary: 'Create a shipping line' })
create(@Body() dto: CreateShippingLineDto) {
return this.service.create(dto);
}
@Patch(':id')
@RuleEngineManage('shipping-lines')
@RuleEngineUpdate('shipping-lines')
@ApiOperation({ summary: 'Update a shipping line' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateShippingLineDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('shipping-lines')
@RuleEngineDelete('shipping-lines')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a shipping line' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -2,7 +2,7 @@ import {
Body, Controller, Delete, Get, HttpCode, HttpStatus,
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate, RuleEngineView } from '../../../common/rule-engine-guards';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateWeightLimitRuleDto } from '../dto/create-weight-limit-rule.dto';
import { ListWeightLimitRulesQueryDto } from '../dto/list-rule-engine-query.dto';
@@ -30,21 +30,21 @@ export class WeightLimitRulesController {
}
@Post()
@RuleEngineManage('weight-limit-rules')
@RuleEngineCreate('weight-limit-rules')
@ApiOperation({ summary: 'Create a weight limit rule' })
create(@Body() dto: CreateWeightLimitRuleDto) {
return this.service.create(dto);
}
@Patch(':id')
@RuleEngineManage('weight-limit-rules')
@RuleEngineUpdate('weight-limit-rules')
@ApiOperation({ summary: 'Update a weight limit rule' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWeightLimitRuleDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('weight-limit-rules')
@RuleEngineDelete('weight-limit-rules')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a weight limit rule' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -12,7 +12,7 @@ import {
Query,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { RuleEngineManage } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate } from '../../../common/rule-engine-guards';
import { StaffReference } from '../../../common/booking-guards';
import { CreateYardDistanceDto } from '../dto/create-yard-distance.dto';
import { ListYardDistancesQueryDto } from '../dto/list-rule-engine-query.dto';
@@ -40,21 +40,21 @@ export class YardDistancesController {
}
@Post()
@RuleEngineManage('yard-distances')
@RuleEngineCreate('yard-distances')
@ApiOperation({ summary: 'Create a yard distance' })
create(@Body() dto: CreateYardDistanceDto) {
return this.service.create(dto);
}
@Patch(':id')
@RuleEngineManage('yard-distances')
@RuleEngineUpdate('yard-distances')
@ApiOperation({ summary: 'Update a yard distance' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateYardDistanceDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('yard-distances')
@RuleEngineDelete('yard-distances')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a yard distance' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -2,7 +2,7 @@ import {
Body, Controller, Delete, Get, HttpCode, HttpStatus,
Param, ParseUUIDPipe, Patch, Post, Query,
} from '@nestjs/common';
import { RuleEngineManage } from '../../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate } from '../../../common/rule-engine-guards';
import { StaffReference } from '../../../common/booking-guards';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateYardDto } from '../dto/create-yard.dto';
@@ -28,7 +28,7 @@ export class YardsController {
}
@Post('reorder')
@RuleEngineManage('yards')
@RuleEngineUpdate('yards')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Bulk reorder yards by ID list' })
reorder(@Body() dto: ReorderItemsDto) {
@@ -36,7 +36,7 @@ export class YardsController {
}
@Post(':id/move-order')
@RuleEngineManage('yards')
@RuleEngineUpdate('yards')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Move a yard up or down in display order' })
moveOrder(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveOrderDto) {
@@ -51,21 +51,21 @@ export class YardsController {
}
@Post()
@RuleEngineManage('yards')
@RuleEngineCreate('yards')
@ApiOperation({ summary: 'Create a yard' })
create(@Body() dto: CreateYardDto) {
return this.service.create(dto);
}
@Patch(':id')
@RuleEngineManage('yards')
@RuleEngineUpdate('yards')
@ApiOperation({ summary: 'Update a yard' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateYardDto) {
return this.service.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('yards')
@RuleEngineDelete('yards')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a yard' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -2,7 +2,7 @@ import { Body, Controller, Param, ParseUUIDPipe, Post } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '@edr/api-common';
import { TrainSchedulingManage } from '../../common/booking-guards';
import { TrainSchedulingReschedule } from '../../common/booking-guards';
import {
type AuthUserPayload,
resolveAuthUserId,
@@ -18,7 +18,7 @@ export class SchedulingRescheduleController {
constructor(private readonly schedulingRescheduleService: SchedulingRescheduleService) {}
@Post('preview')
@TrainSchedulingManage()
@TrainSchedulingReschedule()
@ApiOperation({ summary: 'Preview reschedule / government preempt plan' })
preview(
@Param('id', ParseUUIDPipe) id: string,
@@ -28,7 +28,7 @@ export class SchedulingRescheduleController {
}
@Post('execute')
@TrainSchedulingManage()
@TrainSchedulingReschedule()
@ApiOperation({ summary: 'Execute a confirmed reschedule plan' })
execute(
@Param('id', ParseUUIDPipe) id: string,
@@ -50,7 +50,7 @@ export class SchedulingMaintenanceController {
constructor(private readonly schedulingRescheduleService: SchedulingRescheduleService) {}
@Post('maintenance')
@TrainSchedulingManage()
@TrainSchedulingReschedule()
@ApiOperation({ summary: 'Reschedule train for maintenance (new departure + rebalance)' })
maintenance(
@Param('id', ParseUUIDPipe) id: string,

View File

@@ -1,23 +1,18 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
Query,
Res,
} from "@nestjs/common";
import { CurrentUser } from "@edr/api-common";
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
import type { Response } from "express";
import type { AuthUserPayload } from "../../common/resolve-auth-user-id";
import { resolveAuthUserId } from "../../common/resolve-auth-user-id";
import {
TrainSchedulingManage,
Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query, Res,
} from "@nestjs/common";
import { CurrentUser } from "@edr/api-common";
import {
TrainSchedulingCancel,
TrainSchedulingCreate,
TrainSchedulingReschedule,
TrainSchedulingRulesManage,
TrainSchedulingUpdate,
TrainSchedulingView,
} from "../../common/booking-guards";
import { AcceptIntercityBookingsDto } from "./dto/accept-intercity-bookings.dto";
@@ -114,7 +109,7 @@ export class TrainSchedulingController {
}
@Patch("global-rules")
@TrainSchedulingManage()
@TrainSchedulingRulesManage()
@ApiOperation({ summary: "Update global train scheduling rules (singleton)" })
updateGlobalRules(@Body() dto: UpdateTrainSchedulingGlobalRulesDto) {
return this.trainSchedulingService.updateTrainSchedulingGlobalRules(dto);
@@ -181,7 +176,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/adjust-consist")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Permanently trim free wagons off / couple yard wagons onto the schedule's built train (weight & length limits incl. tolerance enforced, every change logged)",
@@ -283,21 +278,21 @@ export class TrainSchedulingController {
}
@Post("container/schedules")
@TrainSchedulingManage()
@TrainSchedulingCreate()
@ApiOperation({ summary: "Create a container train schedule" })
createContainerTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) {
return this.trainSchedulingService.createContainerTrainSchedule(dto);
}
@Post("bulk/schedules")
@TrainSchedulingManage()
@TrainSchedulingCreate()
@ApiOperation({ summary: "Create a bulk train schedule" })
createBulkTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) {
return this.trainSchedulingService.createContainerTrainSchedule(dto);
}
@Post("schedules/:id/assign-bookings")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary: "Assign bookings to a train schedule (mixed-capable)",
})
@@ -309,7 +304,7 @@ export class TrainSchedulingController {
}
@Post("container/schedules/:id/assign-bookings")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Assign container bookings to a train schedule" })
assignContainerBookings(
@Param("id", ParseUUIDPipe) id: string,
@@ -323,7 +318,7 @@ export class TrainSchedulingController {
}
@Post("bulk/schedules/:id/assign-bookings")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Assign bulk bookings to a train schedule" })
assignBulkBookings(
@Param("id", ParseUUIDPipe) id: string,
@@ -337,7 +332,7 @@ export class TrainSchedulingController {
}
@Delete("schedules/:id/bookings/:bookingId")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Unassign a booking from a train schedule" })
unassignBooking(
@Param("id", ParseUUIDPipe) id: string,
@@ -352,7 +347,7 @@ export class TrainSchedulingController {
}
@Delete("schedules/:id/wagons/:trainSetWagonId")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Remove an empty wagon slot from a train" })
removeWagonSlot(
@Param("id", ParseUUIDPipe) id: string,
@@ -365,7 +360,7 @@ export class TrainSchedulingController {
}
@Patch("schedules/:id/container-items/:itemId")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Update a container number on a wagon slot" })
updateContainerItem(
@Param("id", ParseUUIDPipe) id: string,
@@ -376,7 +371,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/wagons/:wagonId/move-load")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Move a wagon's whole load to another wagon (empty → move/repin, loaded → swap loads)",
@@ -397,7 +392,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/assign-unassigned-booking")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Assign one linked unallocated booking to wagons (preserves existing assignments)",
@@ -429,7 +424,7 @@ export class TrainSchedulingController {
}
@Patch("schedules/:id/import-loading-status")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Mark import bookings loaded/unloaded on this schedule (tracking only, does not affect dispatch)",
@@ -442,7 +437,7 @@ export class TrainSchedulingController {
}
@Patch("schedules/:id/loading-status")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Mark bookings loaded/unloaded on this schedule (any direction, pre-dispatch only)",
@@ -455,21 +450,21 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/pin-wagons")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Pin physical wagons to train set slots" })
pinWagons(@Param("id", ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) {
return this.trainSchedulingService.pinWagons(id, dto);
}
@Post("schedules/:id/finalize")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Finalize a draft train schedule" })
finalizeSchedule(@Param("id", ParseUUIDPipe) id: string) {
return this.trainSchedulingService.finalizeSchedule(id);
}
@Post("schedules/:id/dispatch")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Dispatch a scheduled train" })
dispatchSchedule(@Param("id", ParseUUIDPipe) id: string) {
return this.trainSchedulingService.dispatchSchedule(id);
@@ -496,7 +491,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/intercity/accept")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Accept intercity bookings onto this train (opens their pay window; capacity re-checked per booking)",
@@ -519,7 +514,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/bookings/:bookingId/load")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Confirm a booking's cargo loaded at its origin yard (any direction; train must be at that yard)",
@@ -532,7 +527,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/bookings/:bookingId/unload")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Confirm a booking's cargo unloaded at its destination yard — per-booking arrival, may precede the train's final arrival",
@@ -545,7 +540,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/intercity/:bookingId/load")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary: "Confirm intercity cargo loaded (train must be at the booking's origin yard)",
})
@@ -557,7 +552,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/intercity/:bookingId/unload")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Confirm intercity cargo unloaded at the booking's destination yard (completes the booking)",
@@ -577,7 +572,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/import-djibouti/documents")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Upload/check an import Djibouti-side document" })
uploadImportDjiboutiDocument(
@Param("id", ParseUUIDPipe) id: string,
@@ -587,7 +582,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/import-djibouti/gatepass-granted")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Mark import Djibouti gatepass permission granted" })
grantImportDjiboutiGatepass(
@Param("id", ParseUUIDPipe) id: string,
@@ -597,7 +592,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/import-djibouti/ready-for-loading")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Mark import train ready for loading at Djibouti" })
markImportReadyForLoading(
@Param("id", ParseUUIDPipe) id: string,
@@ -607,7 +602,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/import-djibouti/loaded-on-train")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Confirm import cargo loaded on train at Djibouti" })
confirmImportLoadedOnTrain(
@Param("id", ParseUUIDPipe) id: string,
@@ -617,7 +612,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/confirm-loading")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Confirm cargo loaded on the train (any direction; unblocks import-Djibouti dispatch)",
@@ -630,7 +625,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/import-djibouti/depart")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Depart loaded import train from Djibouti" })
departImportFromDjibouti(
@Param("id", ParseUUIDPipe) id: string,
@@ -640,7 +635,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/import-djibouti/load-list")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Generate import load list / marshalling document summary" })
generateImportLoadList(
@Param("id", ParseUUIDPipe) id: string,
@@ -680,7 +675,7 @@ export class TrainSchedulingController {
// ---- batch / booking-window staff actions ----
@Post("schedules/:id/run-batch")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Manually run the batch fill for a schedule" })
async runBatch(@Param("id", ParseUUIDPipe) id: string) {
await this.bookingBatchService.fillSchedule(id);
@@ -688,7 +683,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/run-allocation")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary: "Run wagon-level allocation for all eligible linked bookings",
})
@@ -697,7 +692,7 @@ export class TrainSchedulingController {
}
@Patch("schedules/:id/booking-window")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({ summary: "Open or close a schedule booking window" })
async setBookingWindow(
@Param("id", ParseUUIDPipe) id: string,
@@ -711,7 +706,7 @@ export class TrainSchedulingController {
}
@Patch("schedules/:id/window-rule")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Override the booking-window rule for one schedule (open/close hour, duration, doc-review, payment, lead days) — only before the window opens",
@@ -725,7 +720,7 @@ export class TrainSchedulingController {
}
@Patch("schedules/:id/schedule-date")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Reschedule a train's departure date — only before the booking window opens, and only if the new date still leaves room for the booking lead window",
@@ -739,7 +734,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/maintenance")
@TrainSchedulingManage()
@TrainSchedulingReschedule()
@ApiOperation({
summary:
"Maintenance reschedule: move the train to a new departure with every allocated booking aboard — links, wagons and window settings unchanged",
@@ -753,7 +748,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/doc-review-complete")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Staff finished document review early — run the batch/payment phase now (applies to the whole route-day group)",
@@ -764,7 +759,7 @@ export class TrainSchedulingController {
}
@Post("bookings/:bookingId/mark-paid")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary: "Staff: mark a reserved booking paid and allocate it now",
})
@@ -774,7 +769,7 @@ export class TrainSchedulingController {
}
@Post("bookings/:bookingId/expire")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary: "Staff: expire a reservation and free its capacity",
})
@@ -784,7 +779,7 @@ export class TrainSchedulingController {
}
@Post("bookings/:bookingId/move-schedule")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary: "Re-point a booking to another OPEN same-route schedule",
})
@@ -806,7 +801,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/checkpoints")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary: "Log the train passing a station (final station triggers arrival)",
})
@@ -818,7 +813,7 @@ export class TrainSchedulingController {
}
@Post("schedules/:id/arrive")
@TrainSchedulingManage()
@TrainSchedulingUpdate()
@ApiOperation({
summary:
"Mark a dispatched train arrived (move assets to destination yard, free assets)",
@@ -856,14 +851,14 @@ export class TrainSchedulingController {
}
@Post("container/schedules/:id/cancel")
@TrainSchedulingManage()
@TrainSchedulingCancel()
@ApiOperation({ summary: "Cancel container train schedule" })
cancelTrainSchedule(@Param("id", ParseUUIDPipe) id: string) {
return this.trainSchedulingService.cancelTrainSchedule(id);
}
@Post('bulk/schedules/:id/cancel')
@TrainSchedulingManage()
@TrainSchedulingCancel()
@ApiOperation({ summary: "Cancel bulk train schedule" })
cancelBulkTrainSchedule(@Param("id", ParseUUIDPipe) id: string) {
return this.trainSchedulingService.cancelTrainSchedule(id);

View File

@@ -2303,14 +2303,19 @@ export class TrainSchedulingService {
);
if (direction !== 'EXPORT') return;
// Only bookings boarding at the schedule's ORIGIN station gate dispatch —
// a mid-corridor boarder (origin B on an A→B→C→D run) is loaded when the
// train reaches its yard, so its warehouse state says nothing at departure.
const rows: Array<{ reference: string | null; status: string }> = await this.dataSource.query(
`WITH ${SCHEDULE_BOOKINGS_CTE}
SELECT DISTINCT b.reference AS "reference", inv.status AS "status"
FROM sched_bookings sb
JOIN freight.bookings b ON b.id = sb.booking_id AND b.deleted_at IS NULL
JOIN freight.train_schedules ts ON ts.id = sb.schedule_id
JOIN freight.warehouse_inventory inv
ON inv.booking_id = b.id AND inv.deleted_at IS NULL
WHERE sb.schedule_id = $1
AND b.origin_yard_id = ts.origin_station_id
AND inv.status IN ('RECEIVED', 'STORED', 'READY_FOR_LOADING')`,
[scheduleId],
);

View File

@@ -13,7 +13,7 @@ import {
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { RuleEngineManage, RuleEngineView } from '../../common/rule-engine-guards';
import { RuleEngineCreate, RuleEngineDelete, RuleEngineUpdate, RuleEngineView } from '../../common/rule-engine-guards';
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
@@ -51,21 +51,21 @@ export class WagonTypesController {
}
@Post()
@RuleEngineManage('wagon-types')
@RuleEngineCreate('wagon-types')
@ApiOperation({ summary: 'Create a wagon type' })
create(@Body() dto: CreateWagonTypeDto) {
return this.wagonTypesService.create(dto);
}
@Patch(':id')
@RuleEngineManage('wagon-types')
@RuleEngineUpdate('wagon-types')
@ApiOperation({ summary: 'Update a wagon type' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonTypeDto) {
return this.wagonTypesService.update(id, dto);
}
@Delete(':id')
@RuleEngineManage('wagon-types')
@RuleEngineDelete('wagon-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a wagon type' })
remove(@Param('id', ParseUUIDPipe) id: string) {

View File

@@ -57,7 +57,6 @@ export const BOOKING_PERMISSIONS: FreightPermissionSeed[] = [
perm('a1000001-0001-4000-8000-000000000021', 'edr_freight_app:bookings:upload_clearance_output', 'Upload customs output documents'),
perm('a1000001-0001-4000-8000-000000000022', 'edr_freight_app:bookings:finalize_clearance', 'Finalize document clearance'),
perm('a1000001-0001-4000-8000-00000000000f', 'edr_freight_app:train_scheduling:view', 'View train scheduling'),
perm('a1000001-0001-4000-8000-000000000010', 'edr_freight_app:train_scheduling:manage', 'Manage train scheduling'),
perm('a1000001-0001-4000-8000-000000000011', 'edr_freight_app:fleet:view', 'View fleet'),
perm('a1000001-0001-4000-8000-000000000012', 'edr_freight_app:fleet:manage', 'Manage fleet'),
perm('a1000001-0001-4000-8000-000000000013', 'edr_freight_app:admin', 'Freight administration'),
@@ -83,7 +82,10 @@ export const CONTRACT_PERMISSIONS: FreightPermissionSeed[] = [
perm('a3000001-0001-4000-8000-000000000006', 'edr_freight_app:contracts:approve_director', 'Approve contract as director'),
perm('a3000001-0001-4000-8000-000000000007', 'edr_freight_app:contracts:approve_ceo', 'Approve contract as CEO'),
perm('a3000001-0001-4000-8000-000000000008', 'edr_freight_app:contracts:generate_contract', 'Generate contract document'),
perm('a3000001-0001-4000-8000-000000000009', 'edr_freight_app:contracts:sign_staff', 'Staff contract signature'),
// Staff counter-signature is split per freight type too — fresh ids for the
// same reason as the intake keys above.
perm('a3000001-0001-4000-8000-000000000017', 'edr_freight_app:contracts:sign_staff:bulk', 'Staff contract signature: bulk'),
perm('a3000001-0001-4000-8000-000000000018', 'edr_freight_app:contracts:sign_staff:container', 'Staff contract signature: container'),
perm('a3000001-0001-4000-8000-00000000000a', 'edr_freight_app:contracts:clearance_review', 'Review pre-booking clearance docs'),
perm('a3000001-0001-4000-8000-00000000000b', 'edr_freight_app:contracts:finalize_clearance', 'Finalize pre-booking clearance'),
perm('a3000001-0001-4000-8000-00000000000c', 'edr_freight_app:contracts:create_booking', 'GL ET create booking under contract'),
@@ -93,24 +95,42 @@ export const CONTRACT_PERMISSIONS: FreightPermissionSeed[] = [
perm('a3000001-0001-4000-8000-000000000010', 'edr_freight_app:contracts:clearance_duty_advise', 'Advise contract duty/tax'),
];
const RULE_ENGINE_PERMISSION_IDS: Record<RuleEngineResourceSlug, { view: string; manage: string }> = {
'cargo-types': { view: 'b2000001-0001-4000-8000-000000000001', manage: 'b2000001-0001-4000-8000-000000000002' },
'container-types': { view: 'b2000001-0001-4000-8000-000000000003', manage: 'b2000001-0001-4000-8000-000000000004' },
'wagon-types': { view: 'b2000001-0001-4000-8000-000000000015', manage: 'b2000001-0001-4000-8000-000000000016' },
'service-types': { view: 'b2000001-0001-4000-8000-000000000005', manage: 'b2000001-0001-4000-8000-000000000006' },
yards: { view: 'b2000001-0001-4000-8000-000000000007', manage: 'b2000001-0001-4000-8000-000000000008' },
'shipping-lines': { view: 'b2000001-0001-4000-8000-000000000009', manage: 'b2000001-0001-4000-8000-00000000000a' },
'weight-limit-rules': { view: 'b2000001-0001-4000-8000-00000000000b', manage: 'b2000001-0001-4000-8000-00000000000c' },
'priority-configs': { view: 'b2000001-0001-4000-8000-00000000000f', manage: 'b2000001-0001-4000-8000-000000000010' },
rates: { view: 'b2000001-0001-4000-8000-000000000011', manage: 'b2000001-0001-4000-8000-000000000012' },
'approval-rules': { view: 'b2000001-0001-4000-8000-000000000013', manage: 'b2000001-0001-4000-8000-000000000014' },
'yard-distances': { view: 'b2000001-0001-4000-8000-000000000018', manage: 'b2000001-0001-4000-8000-000000000019' },
// Existing per-slug view ids are kept as-is: position-type grants reference
// them by id, so re-minting would orphan those rows.
const RULE_ENGINE_VIEW_IDS: Record<RuleEngineResourceSlug, string> = {
'cargo-types': 'b2000001-0001-4000-8000-000000000001',
'container-types': 'b2000001-0001-4000-8000-000000000003',
'wagon-types': 'b2000001-0001-4000-8000-000000000015',
'service-types': 'b2000001-0001-4000-8000-000000000005',
yards: 'b2000001-0001-4000-8000-000000000007',
'shipping-lines': 'b2000001-0001-4000-8000-000000000009',
'weight-limit-rules': 'b2000001-0001-4000-8000-00000000000b',
'priority-configs': 'b2000001-0001-4000-8000-00000000000f',
rates: 'b2000001-0001-4000-8000-000000000011',
'approval-rules': 'b2000001-0001-4000-8000-000000000013',
'yard-distances': 'b2000001-0001-4000-8000-000000000018',
};
// CRUD replaces the retired coarse `:manage`. New ids live in a fresh block
// (b2000002-…) so a stale `:manage` grant can never silently confer a CRUD
// action — the migration re-grants create/update/delete explicitly.
const RULE_ENGINE_CRUD_ACTIONS = ['create', 'update', 'delete'] as const;
type RuleEngineCrudAction = (typeof RULE_ENGINE_CRUD_ACTIONS)[number];
const ruleEngineCrudId = (
slug: RuleEngineResourceSlug,
action: RuleEngineCrudAction,
): string => {
const n =
RULE_ENGINE_RESOURCE_SLUGS.indexOf(slug) * 3 +
RULE_ENGINE_CRUD_ACTIONS.indexOf(action) +
1; // 1..33
return `b2000002-0001-4000-8000-${n.toString(16).padStart(12, '0')}`;
};
/**
* Slugs whose changes go through a separate approver. `manage` lets a staff
* member propose a change; only `approve` lets someone put it into effect.
* Only listed slugs get the permission — the rest are manage-only.
* Slugs whose changes go through a separate approver. CRUD lets a staff member
* propose a change; only `approve` lets someone put it into effect. Only listed
* slugs get the permission.
*/
const RULE_ENGINE_APPROVE_PERMISSION_IDS: Partial<Record<RuleEngineResourceSlug, string>> = {
rates: 'b2000001-0001-4000-8000-000000000017',
@@ -121,11 +141,12 @@ export type RuleEngineApprovableSlug = 'rates';
export const RULE_ENGINE_PERMISSIONS: FreightPermissionSeed[] = RULE_ENGINE_RESOURCE_SLUGS.flatMap(
(slug) => {
const resource = slugToResourceKey(slug);
const ids = RULE_ENGINE_PERMISSION_IDS[slug];
const approveId = RULE_ENGINE_APPROVE_PERMISSION_IDS[slug];
return [
perm(ids.view, `edr_freight_app:rule_engine:${resource}:view`, `View ${slug}`),
perm(ids.manage, `edr_freight_app:rule_engine:${resource}:manage`, `Manage ${slug}`),
perm(RULE_ENGINE_VIEW_IDS[slug], `edr_freight_app:rule_engine:${resource}:view`, `View ${slug}`),
perm(ruleEngineCrudId(slug, 'create'), `edr_freight_app:rule_engine:${resource}:create`, `Create ${slug}`),
perm(ruleEngineCrudId(slug, 'update'), `edr_freight_app:rule_engine:${resource}:update`, `Update ${slug}`),
perm(ruleEngineCrudId(slug, 'delete'), `edr_freight_app:rule_engine:${resource}:delete`, `Delete ${slug}`),
...(approveId
? [perm(approveId, `edr_freight_app:rule_engine:${resource}:approve`, `Approve ${slug} changes`)]
: []),
@@ -395,7 +416,10 @@ export const FREIGHT_PERMS = {
approveDirector: 'edr_freight_app:contracts:approve_director',
approveCeo: 'edr_freight_app:contracts:approve_ceo',
generateContract: 'edr_freight_app:contracts:generate_contract',
signStaff: 'edr_freight_app:contracts:sign_staff',
signStaff: {
bulk: 'edr_freight_app:contracts:sign_staff:bulk',
container: 'edr_freight_app:contracts:sign_staff:container',
},
clearanceReview: 'edr_freight_app:contracts:clearance_review',
finalizeClearance: 'edr_freight_app:contracts:finalize_clearance',
createBooking: 'edr_freight_app:contracts:create_booking',
@@ -406,7 +430,6 @@ export const FREIGHT_PERMS = {
},
trainScheduling: {
view: 'edr_freight_app:train_scheduling:view',
manage: 'edr_freight_app:train_scheduling:manage',
create: 'edr_freight_app:train_scheduling:create',
update: 'edr_freight_app:train_scheduling:update',
cancel: 'edr_freight_app:train_scheduling:cancel',
@@ -421,8 +444,12 @@ export const FREIGHT_PERMS = {
ruleEngine: {
view: (slug: RuleEngineResourceSlug) =>
`edr_freight_app:rule_engine:${slugToResourceKey(slug)}:view`,
manage: (slug: RuleEngineResourceSlug) =>
`edr_freight_app:rule_engine:${slugToResourceKey(slug)}:manage`,
create: (slug: RuleEngineResourceSlug) =>
`edr_freight_app:rule_engine:${slugToResourceKey(slug)}:create`,
update: (slug: RuleEngineResourceSlug) =>
`edr_freight_app:rule_engine:${slugToResourceKey(slug)}:update`,
delete: (slug: RuleEngineResourceSlug) =>
`edr_freight_app:rule_engine:${slugToResourceKey(slug)}:delete`,
approve: (slug: RuleEngineApprovableSlug) =>
`edr_freight_app:rule_engine:${slugToResourceKey(slug)}:approve`,
},
@@ -751,7 +778,11 @@ export const ROLE_PERMISSION_PRESETS = {
FREIGHT_PERMS.bookings.view,
FREIGHT_PERMS.bookings.operations,
FREIGHT_PERMS.trainScheduling.view,
FREIGHT_PERMS.trainScheduling.manage,
FREIGHT_PERMS.trainScheduling.create,
FREIGHT_PERMS.trainScheduling.update,
FREIGHT_PERMS.trainScheduling.cancel,
FREIGHT_PERMS.trainScheduling.reschedule,
FREIGHT_PERMS.trainScheduling.rulesManage,
FREIGHT_PERMS.fleet.view,
FREIGHT_PERMS.fleet.manage,
...FLEET_GRANULAR_KEYS,
@@ -836,7 +867,7 @@ export const ROLE_PERMISSION_PRESETS = {
...bothFreightTypes(FREIGHT_PERMS.contracts.reject),
FREIGHT_PERMS.contracts.approveLineStaff,
FREIGHT_PERMS.contracts.generateContract,
FREIGHT_PERMS.contracts.signStaff,
...bothFreightTypes(FREIGHT_PERMS.contracts.signStaff),
],
orgManager: [...BOOKING_RULE_ENGINE_PERMISSION_KEYS],
} as const;