feat(contracts): per-cargo bulk contract templates

This commit is contained in:
Marshal
2026-08-07 23:50:27 +00:00
parent d2eb47d14b
commit cd90ccb0f5
18 changed files with 802 additions and 107 deletions

View File

@@ -135,6 +135,35 @@ export class CargoTypesService {
return map;
}
/**
* A cargo type and its parent group may not BOTH offer a contract template —
* the template would be ambiguous for bookings of the child. To enable the
* child, the parent must be turned off first (and vice versa).
*/
private async assertContractTemplateExclusive(input: {
id?: string;
parentGroupId?: string | null;
}): Promise<void> {
if (input.parentGroupId) {
const parent = await this.repository.findById(input.parentGroupId);
if (parent?.hasContractTemplate) {
throw new BadRequestException(
`Parent group "${parent.cargoTypeName}" already has a contract template — turn it off there first`,
);
}
}
if (input.id) {
const children = await this.repository.findAll({
where: { parentGroupId: input.id, hasContractTemplate: true },
});
if (children.length) {
throw new BadRequestException(
`Child cargo type(s) ${children.map((c) => `"${c.cargoTypeName}"`).join(', ')} already have their own contract template — turn those off first`,
);
}
}
}
/** Create a new cargo type. */
async create(dto: CreateCargoTypeDto): Promise<CargoType> {
const code = generateCode(dto.cargoTypeName);
@@ -144,6 +173,9 @@ export class CargoTypesService {
const parent = await this.repository.findById(dto.parentGroupId);
if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
}
if (dto.hasContractTemplate) {
await this.assertContractTemplateExclusive({ parentGroupId: dto.parentGroupId });
}
const displayOrder = await this.displayOrder.resolveCreateOrder(CargoType, 'displayOrder', {
explicitOrder: dto.displayOrder,
@@ -160,6 +192,7 @@ export class CargoTypesService {
code,
cargoTypeName: dto.cargoTypeName,
parentGroupId: dto.parentGroupId ?? null,
hasContractTemplate: dto.hasContractTemplate ?? false,
requiresDirectorApproval: dto.requiresDirectorApproval ?? false,
isActive: dto.isActive ?? true,
unitOfMeasure: dto.unitOfMeasure ?? null,
@@ -183,6 +216,18 @@ export class CargoTypesService {
const parent = await this.repository.findById(dto.parentGroupId);
if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
}
// Re-check the parent/child template exclusivity whenever the flag or the
// parent moves and the row ends up flagged.
const willHaveTemplate = dto.hasContractTemplate ?? existing.hasContractTemplate;
if (
willHaveTemplate &&
(dto.hasContractTemplate !== undefined || dto.parentGroupId !== undefined)
) {
await this.assertContractTemplateExclusive({
id,
parentGroupId: dto.parentGroupId ?? existing.parentGroupId,
});
}
const {
wagonTypeIds,
itemsPerWagonMap,