mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 07:51:02 +00:00
contrat nad booking modification
This commit is contained in:
@@ -31,6 +31,15 @@ export class ApprovalRulesController {
|
||||
return this.service.findChain(flag === 'true');
|
||||
}
|
||||
|
||||
@Get('position-types')
|
||||
@RuleEngineView('approval-rules')
|
||||
@ApiOperation({
|
||||
summary: 'IAM position types to choose from when building an approval chain',
|
||||
})
|
||||
listPositionTypes() {
|
||||
return this.service.listPositionTypes();
|
||||
}
|
||||
|
||||
@Post('reorder')
|
||||
@RuleEngineManage('approval-rules')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
|
||||
|
||||
const ROLES = ['LINE_STAFF', 'DIRECTOR', 'CEO'] as const;
|
||||
|
||||
export class CreateApprovalRuleDto {
|
||||
@ApiProperty({ description: 'True = Director+CEO chain; False = LineStaff+Director chain' })
|
||||
@IsBoolean()
|
||||
@@ -19,9 +17,12 @@ export class CreateApprovalRuleDto {
|
||||
@IsUUID('4')
|
||||
insertAfterId?: string;
|
||||
|
||||
@ApiProperty({ enum: ROLES, description: 'Role required to action this step' })
|
||||
@ApiProperty({
|
||||
description:
|
||||
'IAM position-type key required to action this step (see GET /approval-rules/position-types)',
|
||||
})
|
||||
@IsString()
|
||||
@MaxLength(30)
|
||||
@MaxLength(64)
|
||||
requiredRole!: string;
|
||||
|
||||
@ApiProperty({ description: 'Label shown in UI, e.g. "Review & Approve"', maxLength: 50 })
|
||||
@@ -29,9 +30,11 @@ export class CreateApprovalRuleDto {
|
||||
@MaxLength(50)
|
||||
actionLabel!: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: ROLES, description: 'Role explicitly blocked from actioning this step' })
|
||||
@ApiPropertyOptional({
|
||||
description: 'IAM position-type key explicitly blocked from actioning this step',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(30)
|
||||
@MaxLength(64)
|
||||
blocksRole?: string;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ export class ApprovalRule extends BaseEntity {
|
||||
@Column({ name: 'step_order', type: 'smallint' })
|
||||
stepOrder!: number;
|
||||
|
||||
@Column({ name: 'required_role', type: 'varchar', length: 30 })
|
||||
@Column({ name: 'required_role', type: 'varchar', length: 64 })
|
||||
requiredRole!: string;
|
||||
|
||||
@Column({ name: 'action_label', type: 'varchar', length: 50 })
|
||||
actionLabel!: string;
|
||||
|
||||
@Column({ name: 'blocks_role', type: 'varchar', length: 30, nullable: true })
|
||||
@Column({ name: 'blocks_role', type: 'varchar', length: 64, nullable: true })
|
||||
blocksRole?: string | null;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ import { RuleEngineService } from './rule-engine.service';
|
||||
|
||||
import { NotificationInboxModule } from '../notification-inbox/notification-inbox.module';
|
||||
|
||||
import { BookingApprovalStep } from '../bookings/entities/booking-approval-step.entity';
|
||||
import { BookingCargoModifier } from '../bookings/entities/booking-cargo-modifier.entity';
|
||||
import { BookingContainer } from '../bookings/entities/booking-container.entity';
|
||||
import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.entity';
|
||||
@@ -87,7 +86,6 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
ApprovalRule,
|
||||
BookingContainer,
|
||||
BookingCargoModifier,
|
||||
BookingApprovalStep,
|
||||
BookingRateSnapshot,
|
||||
]),
|
||||
// Team notifications for the priority-rule approval workflow.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Inject, Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { BookingApprovalStep } from '../bookings/entities/booking-approval-step.entity';
|
||||
import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.entity';
|
||||
import { Rate, RateTrigger } from './entities/rate.entity';
|
||||
import {
|
||||
@@ -23,15 +22,10 @@ import {
|
||||
IRatesRepository,
|
||||
RATES_REPOSITORY,
|
||||
} from './interfaces/rates.repository.interface';
|
||||
import {
|
||||
IApprovalRulesRepository,
|
||||
APPROVAL_RULES_REPOSITORY,
|
||||
} from './interfaces/approval-rules.repository.interface';
|
||||
import {
|
||||
IShippingLinesRepository,
|
||||
SHIPPING_LINES_REPOSITORY,
|
||||
} from './interfaces/shipping-lines.repository.interface';
|
||||
import { DEFAULT_APPROVAL_RULE_ROWS } from './approval-rules.defaults';
|
||||
import { GOVERNMENT_PRIORITY_BONUS } from './government-priority.constants';
|
||||
|
||||
export interface BookingContainerEvalInput {
|
||||
@@ -124,8 +118,6 @@ export class RuleEngineService {
|
||||
private readonly priorityConfigsRepo: IPriorityConfigsRepository,
|
||||
@Inject(RATES_REPOSITORY)
|
||||
private readonly ratesRepo: IRatesRepository,
|
||||
@Inject(APPROVAL_RULES_REPOSITORY)
|
||||
private readonly approvalRulesRepo: IApprovalRulesRepository,
|
||||
@Inject(SHIPPING_LINES_REPOSITORY)
|
||||
private readonly shippingLinesRepo: IShippingLinesRepository,
|
||||
private readonly dataSource: DataSource,
|
||||
@@ -390,79 +382,6 @@ export class RuleEngineService {
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure ITMLS default approval chains exist (container + bulk). Idempotent.
|
||||
*/
|
||||
async ensureDefaultApprovalRules(): Promise<void> {
|
||||
for (const flag of [false, true] as const) {
|
||||
const existing = await this.approvalRulesRepo.findChainForCargo(flag);
|
||||
if (existing.length > 0) continue;
|
||||
|
||||
const rows = DEFAULT_APPROVAL_RULE_ROWS.filter(
|
||||
(r) => r.requiresDirectorApproval === flag,
|
||||
);
|
||||
for (const row of rows) {
|
||||
await this.approvalRulesRepo.create({
|
||||
requiresDirectorApproval: row.requiresDirectorApproval,
|
||||
stepOrder: row.stepOrder,
|
||||
requiredRole: row.requiredRole,
|
||||
actionLabel: row.actionLabel,
|
||||
blocksRole: row.blocksRole,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate booking_approval_step rows from approval_rules by freight type.
|
||||
*/
|
||||
async instantiateApprovalSteps(
|
||||
bookingId: string,
|
||||
options: {
|
||||
freightType: 'CONTAINER' | 'BULK';
|
||||
cargoTypeId?: string | null;
|
||||
},
|
||||
): Promise<BookingApprovalStep[]> {
|
||||
await this.ensureDefaultApprovalRules();
|
||||
|
||||
let requiresDirectorApproval = false;
|
||||
|
||||
if (options.cargoTypeId) {
|
||||
const cargoType = await this.cargoTypesRepo.findById(options.cargoTypeId);
|
||||
if (!cargoType) {
|
||||
throw new BadRequestException(`Cargo type ${options.cargoTypeId} not found`);
|
||||
}
|
||||
requiresDirectorApproval = cargoType.requiresDirectorApproval;
|
||||
}
|
||||
|
||||
const chain = await this.approvalRulesRepo.findChainForCargo(
|
||||
requiresDirectorApproval,
|
||||
);
|
||||
|
||||
if (chain.length === 0) {
|
||||
throw new BadRequestException(
|
||||
`Approval chain could not be loaded for requiresDirectorApproval=${requiresDirectorApproval}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const stepRepo = this.dataSource.getRepository(BookingApprovalStep);
|
||||
const steps: BookingApprovalStep[] = [];
|
||||
|
||||
for (const rule of chain) {
|
||||
const step = stepRepo.create({
|
||||
bookingId,
|
||||
approvalRuleId: rule.id,
|
||||
stepOrder: rule.stepOrder,
|
||||
requiredRole: rule.requiredRole,
|
||||
blocksRole: rule.blocksRole ?? null,
|
||||
status: 'PENDING',
|
||||
});
|
||||
steps.push(await stepRepo.save(step));
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot only the rates used in a booking's final price.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PaginatedResponse } from '@edr/types';
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { CreateApprovalRuleDto } from '../dto/create-approval-rule.dto';
|
||||
import { ListApprovalRulesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
import { ReorderItemsDto } from '../dto/reorder-items.dto';
|
||||
@@ -17,6 +18,7 @@ export class ApprovalRulesService {
|
||||
@Inject(APPROVAL_RULES_REPOSITORY)
|
||||
private readonly repository: IApprovalRulesRepository,
|
||||
private readonly displayOrder: DisplayOrderService,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
/** List approval rules — standard paginated envelope with server-side search. */
|
||||
@@ -29,6 +31,21 @@ export class ApprovalRulesService {
|
||||
return this.repository.findChainForCargo(requiresDirectorApproval);
|
||||
}
|
||||
|
||||
/**
|
||||
* IAM position types, for the approval-step role picker. A chain step names
|
||||
* the position type that must approve it, so this is the vocabulary an admin
|
||||
* builds chains from. Read straight from the shared `iam` schema — the same
|
||||
* pattern the freight API already uses for `iam.users`.
|
||||
*/
|
||||
async listPositionTypes(): Promise<Array<{ label: string; value: string }>> {
|
||||
const rows = await this.dataSource.query<
|
||||
Array<{ key: string; label: string }>
|
||||
>(`SELECT key, COALESCE(name->>'en', key) AS label
|
||||
FROM iam.position_types
|
||||
ORDER BY 2`);
|
||||
return rows.map((row) => ({ label: row.label, value: row.key }));
|
||||
}
|
||||
|
||||
/** Get an approval rule by ID. */
|
||||
async findById(id: string): Promise<ApprovalRule> {
|
||||
const entity = await this.repository.findById(id);
|
||||
|
||||
Reference in New Issue
Block a user