mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 00:45:41 +00:00
add yard distances management to rule engine
- Introduced new yard distances resource with CRUD operations. - Created migration for yard distances table with necessary constraints. - Implemented service and repository for yard distances handling. - Added controller for API endpoints to manage yard distances. - Updated rule engine configuration to include yard distances. - Enhanced rule engine resource page to support yard distance selection. - Updated contracts and train builder pages to handle new yard distance logic. - Added error handling utility for better error message extraction.
This commit is contained in:
@@ -41,6 +41,7 @@ import {
|
||||
ContractDocumentSnapshotInput,
|
||||
} from './entities/contract.entity';
|
||||
import { ContractSignerRole } from './entities/contract-signature.entity';
|
||||
import { ContractApprovalStep } from './entities/contract-approval-step.entity';
|
||||
import { SignContractDto } from './dto/sign-contract.dto';
|
||||
|
||||
/** The editable contract-document draft returned for the accept/edit dialog. */
|
||||
@@ -550,17 +551,24 @@ export class ContractTransitionService {
|
||||
|
||||
/**
|
||||
* Reject one approval step (line staff / director / CEO). The rejecting
|
||||
* approver must supply a reason. A rejection is terminal: the whole contract
|
||||
* moves to REJECTED and the customer must create a new one — there is no
|
||||
* resubmit of the same contract. The reason is recorded both on the step and
|
||||
* as a REJECTION review note so it is visible to the customer and the rest of
|
||||
* the approval chain.
|
||||
* approver must supply a reason, and picks where the rejection lands:
|
||||
*
|
||||
* - **To the customer** (`returnToStepId` omitted — the only option for the
|
||||
* first approver): terminal. The whole contract moves to REJECTED with a
|
||||
* REJECTION review note visible to the customer, who must resubmit.
|
||||
* - **To an earlier approver** (`returnToStepId` = an already-APPROVED
|
||||
* earlier step): internal send-back. That step and everything after it
|
||||
* reset to PENDING and the chain re-runs from there; the contract stays
|
||||
* PENDING_APPROVAL and the customer never sees it. E.g. the director can
|
||||
* return a contract to line staff, who fix it and approve again, after
|
||||
* which every later stage re-approves in order.
|
||||
*/
|
||||
async rejectStep(
|
||||
contractId: string,
|
||||
stepId: string,
|
||||
actorId: string,
|
||||
reason: string,
|
||||
returnToStepId?: string,
|
||||
): Promise<Contract> {
|
||||
const contract = await this.contractsService.findById(contractId);
|
||||
assertContractStatus(contract, ['PENDING_APPROVAL', 'APPROVED_PENDING_SIGNATURE']);
|
||||
@@ -568,6 +576,20 @@ export class ContractTransitionService {
|
||||
const step = await this.contractsRepository.findApprovalStepById(contractId, stepId);
|
||||
if (!step) throw new BadRequestException('Approval step not found');
|
||||
|
||||
// Only the approver whose turn it is may reject — same ordering rule as
|
||||
// approveStep. Without this, an already-actioned or future step could be
|
||||
// "rejected" and wipe chain state it never owned.
|
||||
const next = await this.contractsRepository.findNextPendingApprovalStep(contractId);
|
||||
if (!next || next.id !== step.id) {
|
||||
throw new BadRequestException(
|
||||
'Only the current pending approval step can be rejected',
|
||||
);
|
||||
}
|
||||
|
||||
if (returnToStepId) {
|
||||
return this.sendBackToStep(contract, step, actorId, reason, returnToStepId);
|
||||
}
|
||||
|
||||
await this.contractsRepository.completeApprovalStep(step.id, actorId, 'REJECTED', reason);
|
||||
|
||||
await this.contractsRepository.createReviewNote(
|
||||
@@ -590,6 +612,67 @@ export class ContractTransitionService {
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal send-back branch of rejectStep: return the contract to an earlier,
|
||||
* already-approved stage of the chain instead of rejecting it outright.
|
||||
* Deliberately NOT the terminal path: no clearance-fee expiry (the contract
|
||||
* is still alive) and no customer-facing REJECTION note — the trail is a
|
||||
* staff note plus a backoffice inbox ping.
|
||||
*/
|
||||
private async sendBackToStep(
|
||||
contract: Contract,
|
||||
rejectingStep: ContractApprovalStep,
|
||||
actorId: string,
|
||||
reason: string,
|
||||
returnToStepId: string,
|
||||
): Promise<Contract> {
|
||||
const target = await this.contractsRepository.findApprovalStepById(
|
||||
contract.id,
|
||||
returnToStepId,
|
||||
);
|
||||
if (!target) throw new BadRequestException('Return-to approval step not found');
|
||||
if (target.stepOrder >= rejectingStep.stepOrder) {
|
||||
throw new BadRequestException(
|
||||
'A rejection can only be returned to an EARLIER step in the chain — to reject to the customer, omit returnToStepId',
|
||||
);
|
||||
}
|
||||
if (target.status !== 'APPROVED') {
|
||||
throw new BadRequestException(
|
||||
`Return-to step ${target.requiredRole} has not approved yet (status ${target.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Staff-visible trail. Written before the reset so the reason survives the
|
||||
// wipe of per-step notes.
|
||||
await this.contractsRepository.createReviewNote(
|
||||
contract.id,
|
||||
`Returned to ${target.requiredRole} (step ${target.stepOrder}) by ${rejectingStep.requiredRole}: ${reason}`,
|
||||
'STAFF_NOTE',
|
||||
actorId,
|
||||
'STAFF',
|
||||
);
|
||||
|
||||
// Chain re-runs from the target stage: it and every later step (including
|
||||
// the rejecting one) go back to PENDING. Legacy approved-by columns are
|
||||
// left stale on purpose — approval steps are the source of truth and the
|
||||
// columns get re-stamped on re-approval.
|
||||
await this.contractsRepository.resetApprovalStepsFrom(
|
||||
contract.id,
|
||||
target.stepOrder,
|
||||
);
|
||||
|
||||
// A send-back can only happen mid-chain, so the contract must remain (or
|
||||
// return to) PENDING_APPROVAL — relevant when rejecting from
|
||||
// APPROVED_PENDING_SIGNATURE.
|
||||
await this.contractsRepository.update(contract.id, {
|
||||
status: 'PENDING_APPROVAL',
|
||||
} as never);
|
||||
|
||||
const updated = await this.contractsService.findById(contract.id);
|
||||
this.notifier.sentBackToStep(updated, target.requiredRole, reason);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** Approve one approval step in sequence; → APPROVED when all complete. */
|
||||
async approveStep(
|
||||
contractId: string,
|
||||
|
||||
Reference in New Issue
Block a user