mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 09:30:59 +00:00
changes
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
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 {
|
||||
DecidePriorityRuleChangeDto,
|
||||
SubmitPriorityRuleChangeDto,
|
||||
} from '../dto/priority-rule-change-request.dto';
|
||||
import { PriorityRuleChangeStatus } from '../entities/priority-rule-change-request.entity';
|
||||
import { PriorityRuleChangeRequestsService } from '../services/priority-rule-change-requests.service';
|
||||
|
||||
/**
|
||||
* Approval workflow for priority-rule changes. Anyone with the manage
|
||||
* permission SUBMITS a change; an approver (same permission — the team decides
|
||||
* who reviews) approves or rejects it. The team is notified at each step.
|
||||
*/
|
||||
@ApiTags('priority-rule-change-requests')
|
||||
@Controller('priority-rule-change-requests')
|
||||
@ApiBearerAuth()
|
||||
export class PriorityRuleChangeRequestsController {
|
||||
constructor(private readonly service: PriorityRuleChangeRequestsService) {}
|
||||
|
||||
@Post()
|
||||
@RuleEngineManage('priority-configs')
|
||||
@ApiOperation({ summary: 'Submit a priority-rule change for approval' })
|
||||
submit(
|
||||
@Body() dto: SubmitPriorityRuleChangeDto,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
return this.service.submit(dto, user?.id);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RuleEngineView('priority-configs')
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['PENDING', 'APPROVED', 'REJECTED'] })
|
||||
@ApiOperation({ summary: 'List priority-rule change requests' })
|
||||
list(@Query('status') status?: PriorityRuleChangeStatus) {
|
||||
return this.service.list(status);
|
||||
}
|
||||
|
||||
@Post(':id/approve')
|
||||
@RuleEngineManage('priority-configs')
|
||||
@ApiOperation({ summary: 'Approve and apply a pending change' })
|
||||
approve(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: DecidePriorityRuleChangeDto,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
return this.service.approve(id, user?.id, dto.decisionNote);
|
||||
}
|
||||
|
||||
@Post(':id/reject')
|
||||
@RuleEngineManage('priority-configs')
|
||||
@ApiOperation({ summary: 'Reject a pending change' })
|
||||
reject(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: DecidePriorityRuleChangeDto,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
return this.service.reject(id, user?.id, dto.decisionNote);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsIn,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { CreatePriorityConfigDto } from './create-priority-config.dto';
|
||||
import { UpdatePriorityConfigDto } from './update-priority-config.dto';
|
||||
|
||||
/**
|
||||
* File a priority-rule change for approval. CREATE carries a full `create`
|
||||
* payload; UPDATE carries the target id + an `update` patch; DELETE carries
|
||||
* only the target id.
|
||||
*/
|
||||
export class SubmitPriorityRuleChangeDto {
|
||||
@ApiProperty({ enum: ['CREATE', 'UPDATE', 'DELETE'] })
|
||||
@IsIn(['CREATE', 'UPDATE', 'DELETE'])
|
||||
action!: 'CREATE' | 'UPDATE' | 'DELETE';
|
||||
|
||||
@ApiPropertyOptional({ description: 'Target rule id (UPDATE / DELETE)' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
priorityConfigId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Proposed new rule (CREATE)' })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => CreatePriorityConfigDto)
|
||||
create?: CreatePriorityConfigDto;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Proposed field changes (UPDATE)' })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => UpdatePriorityConfigDto)
|
||||
update?: UpdatePriorityConfigDto;
|
||||
}
|
||||
|
||||
export class DecidePriorityRuleChangeDto {
|
||||
@ApiPropertyOptional({ description: 'Optional note shown to the requester' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(1000)
|
||||
decisionNote?: string;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
|
||||
import { PriorityConfig } from './priority-config.entity';
|
||||
|
||||
export type PriorityRuleChangeAction = 'CREATE' | 'UPDATE' | 'DELETE';
|
||||
export type PriorityRuleChangeStatus = 'PENDING' | 'APPROVED' | 'REJECTED';
|
||||
|
||||
/**
|
||||
* One proposed change to a priority rule, awaiting approval. Every
|
||||
* create/update/delete of a priority config is filed here first; an approver
|
||||
* applies (which runs the real mutation, including range-collision checks) or
|
||||
* rejects it. `payload` holds the proposed field values (null for DELETE);
|
||||
* `priorityConfigId` the target rule (null for CREATE).
|
||||
*/
|
||||
@Entity({ schema: 'freight', name: 'priority_rule_change_requests' })
|
||||
@Index(['status'])
|
||||
export class PriorityRuleChangeRequest extends BaseEntity {
|
||||
@Column({ name: 'action', type: 'varchar', length: 10 })
|
||||
action!: PriorityRuleChangeAction;
|
||||
|
||||
@Column({ name: 'priority_config_id', type: 'uuid', nullable: true })
|
||||
priorityConfigId?: string | null;
|
||||
|
||||
@ManyToOne(() => PriorityConfig, { nullable: true })
|
||||
@JoinColumn({ name: 'priority_config_id' })
|
||||
priorityConfig?: PriorityConfig | null;
|
||||
|
||||
@Column({ name: 'payload', type: 'jsonb', nullable: true })
|
||||
payload?: Record<string, unknown> | null;
|
||||
|
||||
@Column({ name: 'status', type: 'varchar', length: 10, default: 'PENDING' })
|
||||
status!: PriorityRuleChangeStatus;
|
||||
|
||||
@Column({ name: 'requested_by_user_id', type: 'uuid', nullable: true })
|
||||
requestedByUserId?: string | null;
|
||||
|
||||
@Column({ name: 'decided_by_user_id', type: 'uuid', nullable: true })
|
||||
decidedByUserId?: string | null;
|
||||
|
||||
@Column({ name: 'decided_at', type: 'timestamptz', nullable: true })
|
||||
decidedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'decision_note', type: 'text', nullable: true })
|
||||
decisionNote?: string | null;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { ApprovalRulesController } from './controllers/approval-rules.controller
|
||||
import { CargoTypesController } from './controllers/cargo-types.controller';
|
||||
import { ContainerTypesController } from './controllers/container-types.controller';
|
||||
import { PriorityConfigsController } from './controllers/priority-configs.controller';
|
||||
import { PriorityRuleChangeRequestsController } from './controllers/priority-rule-change-requests.controller';
|
||||
import { RatesController } from './controllers/rates.controller';
|
||||
import { ServiceTypesController } from './controllers/service-types.controller';
|
||||
import { ShippingLinesController } from './controllers/shipping-lines.controller';
|
||||
@@ -15,6 +16,7 @@ import { ApprovalRule } from './entities/approval-rule.entity';
|
||||
import { CargoType } from './entities/cargo-type.entity';
|
||||
import { ContainerType } from './entities/container-type.entity';
|
||||
import { PriorityConfig } from './entities/priority-config.entity';
|
||||
import { PriorityRuleChangeRequest } from './entities/priority-rule-change-request.entity';
|
||||
import { Rate } from './entities/rate.entity';
|
||||
import { ServiceType } from './entities/service-type.entity';
|
||||
import { ShippingLine } from './entities/shipping-line.entity';
|
||||
@@ -46,6 +48,7 @@ import { DisplayOrderService } from './services/display-order.service';
|
||||
import { CargoTypesService } from './services/cargo-types.service';
|
||||
import { ContainerTypesService } from './services/container-types.service';
|
||||
import { PriorityConfigsService } from './services/priority-configs.service';
|
||||
import { PriorityRuleChangeRequestsService } from './services/priority-rule-change-requests.service';
|
||||
import { RatesService } from './services/rates.service';
|
||||
import { ServiceTypesService } from './services/service-types.service';
|
||||
import { ShippingLinesService } from './services/shipping-lines.service';
|
||||
@@ -54,6 +57,8 @@ import { YardsService } from './services/yards.service';
|
||||
|
||||
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';
|
||||
@@ -66,6 +71,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
CargoType,
|
||||
ContainerType,
|
||||
PriorityConfig,
|
||||
PriorityRuleChangeRequest,
|
||||
ServiceType,
|
||||
WeightLimitRule,
|
||||
Yard,
|
||||
@@ -77,11 +83,14 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
BookingApprovalStep,
|
||||
BookingRateSnapshot,
|
||||
]),
|
||||
// Team notifications for the priority-rule approval workflow.
|
||||
NotificationInboxModule,
|
||||
],
|
||||
controllers: [
|
||||
CargoTypesController,
|
||||
ContainerTypesController,
|
||||
PriorityConfigsController,
|
||||
PriorityRuleChangeRequestsController,
|
||||
ServiceTypesController,
|
||||
WeightLimitRulesController,
|
||||
YardsController,
|
||||
@@ -111,6 +120,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot.
|
||||
CargoTypesService,
|
||||
ContainerTypesService,
|
||||
PriorityConfigsService,
|
||||
PriorityRuleChangeRequestsService,
|
||||
ServiceTypesService,
|
||||
WeightLimitRulesService,
|
||||
YardsService,
|
||||
|
||||
@@ -31,6 +31,12 @@ export class PriorityConfigsService {
|
||||
|
||||
async create(dto: CreatePriorityConfigDto): Promise<PriorityConfig> {
|
||||
this.validateCurrencyField(dto.type, dto.currency);
|
||||
await this.assertNoRangeCollision({
|
||||
type: dto.type,
|
||||
currency: dto.currency ?? null,
|
||||
minWagonCount: dto.minWagonCount,
|
||||
maxWagonCount: dto.maxWagonCount,
|
||||
});
|
||||
|
||||
const displayOrder = await this.displayOrder.resolveCreateOrder(PriorityConfig, 'displayOrder', {});
|
||||
|
||||
@@ -52,6 +58,13 @@ export class PriorityConfigsService {
|
||||
const type = dto.type ?? existing.type;
|
||||
const currency = dto.currency !== undefined ? dto.currency : existing.currency;
|
||||
this.validateCurrencyField(type, currency);
|
||||
await this.assertNoRangeCollision({
|
||||
type,
|
||||
currency: currency ?? null,
|
||||
minWagonCount: dto.minWagonCount ?? existing.minWagonCount,
|
||||
maxWagonCount: dto.maxWagonCount ?? existing.maxWagonCount,
|
||||
excludeId: id,
|
||||
});
|
||||
|
||||
const { ...patch } = dto;
|
||||
const updated = await this.repository.update(id, patch);
|
||||
@@ -59,6 +72,43 @@ export class PriorityConfigsService {
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* No two rules of the same type (and, for CURRENCY rules, the same currency)
|
||||
* may cover overlapping wagon-count ranges — a booking must match at most one
|
||||
* rule per type. Rejects an exact duplicate (1–5 vs 1–5) and any partial
|
||||
* overlap (1–5 vs 4–7). Ranges are inclusive on both ends.
|
||||
*/
|
||||
async assertNoRangeCollision(input: {
|
||||
type: 'WAGON' | 'CURRENCY' | 'CUSTOMS';
|
||||
currency?: string | null;
|
||||
minWagonCount: number;
|
||||
maxWagonCount: number;
|
||||
excludeId?: string;
|
||||
}): Promise<void> {
|
||||
if (input.minWagonCount > input.maxWagonCount) {
|
||||
throw new BadRequestException(
|
||||
'Min wagon count cannot be greater than max wagon count',
|
||||
);
|
||||
}
|
||||
const siblings = await this.repository.findAll({
|
||||
where: { type: input.type },
|
||||
});
|
||||
const clash = siblings.find(
|
||||
(s) =>
|
||||
s.id !== input.excludeId &&
|
||||
(input.type !== 'CURRENCY' || (s.currency ?? null) === (input.currency ?? null)) &&
|
||||
input.minWagonCount <= s.maxWagonCount &&
|
||||
input.maxWagonCount >= s.minWagonCount,
|
||||
);
|
||||
if (clash) {
|
||||
throw new BadRequestException(
|
||||
`Wagon range ${input.minWagonCount}–${input.maxWagonCount} overlaps existing rule ` +
|
||||
`"${clash.label}" (${clash.minWagonCount}–${clash.maxWagonCount}). ` +
|
||||
'Adjust the range so rules do not collide.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findById(id);
|
||||
await this.repository.softDelete(id);
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import {
|
||||
NotificationAudience,
|
||||
NotificationType,
|
||||
} from '@edr/types';
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { NotificationInboxService } from '../../notification-inbox/notification-inbox.service';
|
||||
import { CreatePriorityConfigDto } from '../dto/create-priority-config.dto';
|
||||
import { SubmitPriorityRuleChangeDto } from '../dto/priority-rule-change-request.dto';
|
||||
import { UpdatePriorityConfigDto } from '../dto/update-priority-config.dto';
|
||||
import {
|
||||
PriorityRuleChangeRequest,
|
||||
PriorityRuleChangeStatus,
|
||||
} from '../entities/priority-rule-change-request.entity';
|
||||
import { PriorityConfigsService } from './priority-configs.service';
|
||||
|
||||
/** Backoffice rule-engine page — where both queue and rules live. */
|
||||
const RULES_LINK = '/dashboard/rules/priority-configs';
|
||||
|
||||
/**
|
||||
* Approval workflow for priority-rule changes. Nobody mutates priority configs
|
||||
* directly any more: a change is SUBMITTED here (validated up front so the
|
||||
* requester gets immediate feedback on range collisions), the team is
|
||||
* notified, and an approver later applies or rejects it. Applying re-runs the
|
||||
* full validation — the winning state is whatever is true at approval time.
|
||||
*/
|
||||
@Injectable()
|
||||
export class PriorityRuleChangeRequestsService {
|
||||
private readonly logger = new Logger(PriorityRuleChangeRequestsService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(PriorityRuleChangeRequest)
|
||||
private readonly repo: Repository<PriorityRuleChangeRequest>,
|
||||
private readonly configs: PriorityConfigsService,
|
||||
private readonly inbox: NotificationInboxService,
|
||||
) {}
|
||||
|
||||
async submit(
|
||||
dto: SubmitPriorityRuleChangeDto,
|
||||
userId?: string | null,
|
||||
): Promise<PriorityRuleChangeRequest> {
|
||||
const payload = await this.validateSubmission(dto);
|
||||
|
||||
const request = await this.repo.save(
|
||||
this.repo.create({
|
||||
action: dto.action,
|
||||
priorityConfigId: dto.priorityConfigId ?? null,
|
||||
payload,
|
||||
status: 'PENDING',
|
||||
requestedByUserId: userId ?? null,
|
||||
}),
|
||||
);
|
||||
|
||||
this.notifyTeam(
|
||||
'Priority rule change submitted',
|
||||
`A ${dto.action.toLowerCase()} of a priority rule was submitted and awaits approval.`,
|
||||
request,
|
||||
);
|
||||
return request;
|
||||
}
|
||||
|
||||
async list(status?: PriorityRuleChangeStatus): Promise<PriorityRuleChangeRequest[]> {
|
||||
return this.repo.find({
|
||||
where: status ? { status } : {},
|
||||
relations: { priorityConfig: true },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async approve(
|
||||
id: string,
|
||||
userId?: string | null,
|
||||
decisionNote?: string,
|
||||
): Promise<PriorityRuleChangeRequest> {
|
||||
const request = await this.findPending(id);
|
||||
|
||||
// Apply the change through the normal service so currency + range-collision
|
||||
// validation runs against the CURRENT rules; a stale request that now
|
||||
// collides fails here and stays PENDING for the approver to see the error.
|
||||
if (request.action === 'CREATE') {
|
||||
await this.configs.create(request.payload as unknown as CreatePriorityConfigDto);
|
||||
} else if (request.action === 'UPDATE') {
|
||||
await this.configs.update(
|
||||
this.requireTarget(request),
|
||||
request.payload as unknown as UpdatePriorityConfigDto,
|
||||
);
|
||||
} else {
|
||||
await this.configs.remove(this.requireTarget(request));
|
||||
}
|
||||
|
||||
request.status = 'APPROVED';
|
||||
request.decidedByUserId = userId ?? null;
|
||||
request.decidedAt = new Date();
|
||||
request.decisionNote = decisionNote ?? null;
|
||||
const saved = await this.repo.save(request);
|
||||
|
||||
this.notifyTeam(
|
||||
'Priority rule change approved',
|
||||
`The ${request.action.toLowerCase()} priority-rule change was approved and applied.` +
|
||||
(decisionNote ? ` Note: ${decisionNote}` : ''),
|
||||
saved,
|
||||
);
|
||||
return saved;
|
||||
}
|
||||
|
||||
async reject(
|
||||
id: string,
|
||||
userId?: string | null,
|
||||
decisionNote?: string,
|
||||
): Promise<PriorityRuleChangeRequest> {
|
||||
const request = await this.findPending(id);
|
||||
request.status = 'REJECTED';
|
||||
request.decidedByUserId = userId ?? null;
|
||||
request.decidedAt = new Date();
|
||||
request.decisionNote = decisionNote ?? null;
|
||||
const saved = await this.repo.save(request);
|
||||
|
||||
this.notifyTeam(
|
||||
'Priority rule change rejected',
|
||||
`The ${request.action.toLowerCase()} priority-rule change was rejected.` +
|
||||
(decisionNote ? ` Note: ${decisionNote}` : ''),
|
||||
saved,
|
||||
);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a submission the way applying it would, so bad requests are
|
||||
* refused at the door — most importantly the wagon-range collision rule.
|
||||
* Returns the payload to persist.
|
||||
*/
|
||||
private async validateSubmission(
|
||||
dto: SubmitPriorityRuleChangeDto,
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
if (dto.action === 'CREATE') {
|
||||
if (!dto.create) {
|
||||
throw new BadRequestException('CREATE requires the proposed rule in `create`');
|
||||
}
|
||||
await this.configs.assertNoRangeCollision({
|
||||
type: dto.create.type,
|
||||
currency: dto.create.currency ?? null,
|
||||
minWagonCount: dto.create.minWagonCount,
|
||||
maxWagonCount: dto.create.maxWagonCount,
|
||||
});
|
||||
return { ...dto.create };
|
||||
}
|
||||
|
||||
if (!dto.priorityConfigId) {
|
||||
throw new BadRequestException(`${dto.action} requires priorityConfigId`);
|
||||
}
|
||||
const existing = await this.configs.findById(dto.priorityConfigId);
|
||||
|
||||
if (dto.action === 'DELETE') return null;
|
||||
|
||||
if (!dto.update || Object.keys(dto.update).length === 0) {
|
||||
throw new BadRequestException('UPDATE requires the field changes in `update`');
|
||||
}
|
||||
await this.configs.assertNoRangeCollision({
|
||||
type: dto.update.type ?? existing.type,
|
||||
currency:
|
||||
dto.update.currency !== undefined ? dto.update.currency : existing.currency,
|
||||
minWagonCount: dto.update.minWagonCount ?? existing.minWagonCount,
|
||||
maxWagonCount: dto.update.maxWagonCount ?? existing.maxWagonCount,
|
||||
excludeId: existing.id,
|
||||
});
|
||||
return { ...dto.update };
|
||||
}
|
||||
|
||||
private async findPending(id: string): Promise<PriorityRuleChangeRequest> {
|
||||
const request = await this.repo.findOne({
|
||||
where: { id },
|
||||
relations: { priorityConfig: true },
|
||||
});
|
||||
if (!request) throw new NotFoundException(`Change request ${id} not found`);
|
||||
if (request.status !== 'PENDING') {
|
||||
throw new ConflictException(
|
||||
`Change request is already ${request.status.toLowerCase()}`,
|
||||
);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private requireTarget(request: PriorityRuleChangeRequest): string {
|
||||
if (!request.priorityConfigId) {
|
||||
throw new BadRequestException(
|
||||
`${request.action} change request has no target rule`,
|
||||
);
|
||||
}
|
||||
return request.priorityConfigId;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-app notification to the whole backoffice team (submission AND decision
|
||||
* both notify the team; the requester is staff, so they are included).
|
||||
* Fire-and-forget — a notification failure never blocks the workflow.
|
||||
*/
|
||||
private notifyTeam(
|
||||
title: string,
|
||||
body: string,
|
||||
request: PriorityRuleChangeRequest,
|
||||
): void {
|
||||
void this.inbox
|
||||
.notify({
|
||||
recipients: { allBackoffice: true },
|
||||
audience: NotificationAudience.BACKOFFICE,
|
||||
type: NotificationType.REQUEST_SUBMITTED,
|
||||
title,
|
||||
body,
|
||||
link: RULES_LINK,
|
||||
data: { priorityRuleChangeRequestId: request.id, action: request.action },
|
||||
})
|
||||
.catch((err) =>
|
||||
this.logger.warn(
|
||||
`Priority-rule notification failed: ${(err as Error).message}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user