mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 18:55:42 +00:00
add dispute functionality for contract duty and implement collection dates
This commit is contained in:
@@ -26,6 +26,8 @@ import { Contract, CONTRACT_STATUSES, CONTRACT_CUSTOMER_EDITABLE_STATUSES } from
|
||||
import { ContractRoute } from './entities/contract-route.entity';
|
||||
import { ContractCargoScope } from './entities/contract-cargo-scope.entity';
|
||||
import { isEffectivelyExpired } from './utils/contract-expiry.util';
|
||||
import { diffContractFields } from './contract-document-diff.util';
|
||||
import { ContractDocumentHistoryService } from './contract-document-history.service';
|
||||
import { FileRecord } from '../files/entities/file.entity';
|
||||
|
||||
/** Paginated contract list: flat `total` (backoffice) + `meta` block (portal). */
|
||||
@@ -42,6 +44,35 @@ export interface PaginatedContracts {
|
||||
};
|
||||
}
|
||||
|
||||
/** Route list as a readable lane string, e.g. "Nagad → Mojo, Mojo → Adama". */
|
||||
function describeRoutes(routes?: ContractRoute[]): string | null {
|
||||
if (!routes?.length) return null;
|
||||
return [...routes]
|
||||
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
|
||||
.map(
|
||||
(r) =>
|
||||
`${r.originYard?.label ?? r.originYardId} → ${r.destinationYard?.label ?? r.destinationYardId}`,
|
||||
)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
/** Cargo scope as a readable string, e.g. "20ft ×2, 40ft ×1" or "Wheat ×500". */
|
||||
function describeCargoScope(scope?: ContractCargoScope[]): string | null {
|
||||
if (!scope?.length) return null;
|
||||
return scope
|
||||
.map((row) => {
|
||||
const label =
|
||||
row.containerSize ??
|
||||
row.cargoType?.cargoTypeName ??
|
||||
row.cargoFreeText ??
|
||||
row.cargoTypeId ??
|
||||
'cargo';
|
||||
return row.quantityCap != null ? `${label} ×${row.quantityCap}` : String(label);
|
||||
})
|
||||
.sort()
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
const NEEDS_ACTION_STATUSES = [
|
||||
'SUBMITTED',
|
||||
'PENDING_APPROVAL',
|
||||
@@ -57,6 +88,7 @@ export class ContractsService {
|
||||
private readonly companiesService: CompaniesService,
|
||||
private readonly filesService: FilesService,
|
||||
private readonly minioService: MinioService,
|
||||
private readonly documentHistory: ContractDocumentHistoryService,
|
||||
) {}
|
||||
|
||||
/** Generate a unique contract reference number (CTR-YYYY-NNNNN). */
|
||||
@@ -332,7 +364,11 @@ export class ContractsService {
|
||||
tradeDirection: dto.tradeDirection,
|
||||
freightType: dto.freightType,
|
||||
serviceTypeId: dto.serviceTypeId,
|
||||
paymentCurrency: dto.paymentCurrency,
|
||||
// A contract is always QUOTED in USD — the billing currency is chosen per
|
||||
// booking (or on the shipment request when GL books for the customer), so
|
||||
// any client-supplied currency here is ignored. Contracts created before
|
||||
// this rule keep whatever they stored; update() never rewrites it.
|
||||
paymentCurrency: 'USD',
|
||||
customsClearingEnabled: includesCustoms,
|
||||
customsClearingAgent: includesCustoms ? null : (dto.customsClearingAgent ?? null),
|
||||
equipmentReturn: dto.equipmentReturn ?? null,
|
||||
@@ -490,6 +526,7 @@ export class ContractsService {
|
||||
id: string,
|
||||
dto: UpdateContractDto,
|
||||
files: Express.Multer.File[],
|
||||
actorId?: string,
|
||||
): Promise<{ contract: Contract; warnings: string[] }> {
|
||||
const existing = await this.findById(id);
|
||||
if (!CONTRACT_CUSTOMER_EDITABLE_STATUSES.includes(existing.status as never)) {
|
||||
@@ -516,7 +553,9 @@ export class ContractsService {
|
||||
tradeDirection: dto.tradeDirection ?? existing.tradeDirection,
|
||||
freightType,
|
||||
serviceTypeId: dto.serviceTypeId ?? existing.serviceTypeId,
|
||||
paymentCurrency: dto.paymentCurrency ?? existing.paymentCurrency,
|
||||
// Never rewritten: grandfathered contracts keep the currency (and frozen
|
||||
// snapshots) they were signed with.
|
||||
paymentCurrency: existing.paymentCurrency,
|
||||
isHazardous: dto.isHazardous ?? existing.isHazardous,
|
||||
isReefer: dto.isReefer ?? existing.isReefer,
|
||||
// Same rule as create: clearing the flag clears the declaration with it.
|
||||
@@ -576,7 +615,52 @@ export class ContractsService {
|
||||
existing.companyProfileId ?? null,
|
||||
);
|
||||
|
||||
return { contract: await this.findById(id), warnings };
|
||||
const updated = await this.findById(id);
|
||||
// Audit what this edit actually changed. Runs after the writes so the
|
||||
// "after" side is read back from the contract rather than from the DTO.
|
||||
await this.recordFieldRevision(existing, updated, actorId);
|
||||
|
||||
return { contract: updated, warnings };
|
||||
}
|
||||
|
||||
/** Fields worth auditing on a customer edit, read off a loaded contract. */
|
||||
private auditableFields(contract: Contract): Record<string, unknown> {
|
||||
return {
|
||||
contractKind: contract.contractKind,
|
||||
tradeDirection: contract.tradeDirection,
|
||||
freightType: contract.freightType,
|
||||
serviceType: contract.serviceType?.serviceName ?? contract.serviceTypeId,
|
||||
paymentCurrency: contract.paymentCurrency,
|
||||
contractType: contract.contractType,
|
||||
isHazardous: contract.isHazardous,
|
||||
hazardClass: contract.hazardClass,
|
||||
unNumber: contract.unNumber,
|
||||
isReefer: contract.isReefer,
|
||||
equipmentReturn: contract.equipmentReturn,
|
||||
customsClearingAgent: contract.customsClearingAgent,
|
||||
firstMilePickupAddress: contract.firstMilePickupAddress,
|
||||
lastMileDeliveryAddress: contract.lastMileDeliveryAddress,
|
||||
routes: describeRoutes(contract.routes),
|
||||
cargoScope: describeCargoScope(contract.cargoScope),
|
||||
};
|
||||
}
|
||||
|
||||
/** Append a revision describing a customer's edit to the contract itself. */
|
||||
private async recordFieldRevision(
|
||||
before: Contract,
|
||||
after: Contract,
|
||||
actorId?: string,
|
||||
): Promise<void> {
|
||||
const changes = diffContractFields(
|
||||
this.auditableFields(before),
|
||||
this.auditableFields(after),
|
||||
);
|
||||
await this.documentHistory.recordChanges({
|
||||
contractId: after.id,
|
||||
changes,
|
||||
actorId: actorId ?? null,
|
||||
actorRole: 'Customer',
|
||||
});
|
||||
}
|
||||
|
||||
/** Parse comma-separated or repeated status query values. */
|
||||
|
||||
Reference in New Issue
Block a user