This commit is contained in:
Marshal
2026-08-20 18:06:13 +00:00
parent 934ed43ccb
commit b72cbab535
8 changed files with 495 additions and 277 deletions

View File

@@ -34,7 +34,7 @@ export class CreateServiceTypeDto {
@ApiPropertyOptional({
default: false,
description: 'Customs cleared on the Ethiopian side only — requires includesCustoms. Prices off the Ethiopian customs rate.',
description: 'Customs cleared on the Ethiopian side only (alternative to full includesCustoms; implies it). Prices off the Ethiopian customs rate.',
})
@IsOptional()
@IsBoolean()

View File

@@ -28,9 +28,11 @@ export class ServiceType extends BaseEntity {
includesCustoms!: boolean;
/**
* EDR clears customs on the Ethiopian side only. Requires includesCustoms
* the clearance flow (GL review, duty) is identical; only the fee differs:
* pricing looks up ETHIOPIAN_CUSTOMS_CLEARANCE instead of CUSTOMS_CLEARANCE.
* EDR clears customs on the Ethiopian side only. The admin picks full customs
* OR Ethiopian-only, never both; the API stores includesCustoms = true for
* either so every clearance read (GL review, duty, docs) stays unchanged —
* only the fee differs: pricing looks up ETHIOPIAN_CUSTOMS_CLEARANCE instead
* of CUSTOMS_CLEARANCE.
*/
@Column({ name: 'includes_ethiopian_customs_only', type: 'boolean', default: false })
includesEthiopianCustomsOnly!: boolean;

View File

@@ -49,7 +49,10 @@ export class ServiceTypesService {
const existing = await this.repository.findByCode(code);
if (existing) throw new ConflictException(`Service type with name "${dto.serviceName}" conflicts with existing code "${code}"`);
this.assertCustomsFlags(dto.includesCustoms ?? false, dto.includesEthiopianCustomsOnly ?? false);
const customs = this.resolveCustomsFlags(
dto.includesCustoms ?? false,
dto.includesEthiopianCustomsOnly ?? false,
);
const displayOrder = await this.displayOrder.resolveCreateOrder(ServiceType, 'displayOrder', {
explicitOrder: dto.displayOrder,
insertAfterId: dto.insertAfterId,
@@ -62,8 +65,7 @@ export class ServiceTypesService {
canBeBookedAlone: dto.canBeBookedAlone ?? true,
includesFirstMile: dto.includesFirstMile ?? false,
includesLastMile: dto.includesLastMile ?? false,
includesCustoms: dto.includesCustoms ?? false,
includesEthiopianCustomsOnly: dto.includesEthiopianCustomsOnly ?? false,
...customs,
isActive: dto.isActive ?? true,
displayOrder,
});
@@ -72,23 +74,41 @@ export class ServiceTypesService {
/** Update an existing service type. */
async update(id: string, dto: UpdateServiceTypeDto): Promise<ServiceType> {
const existing = await this.findById(id);
this.assertCustomsFlags(
dto.includesCustoms ?? existing.includesCustoms,
dto.includesEthiopianCustomsOnly ?? existing.includesEthiopianCustomsOnly,
);
const { ...patch } = dto;
const ethiopian = dto.includesEthiopianCustomsOnly ?? existing.includesEthiopianCustomsOnly;
// The form sends both flags whenever either is touched; a payload with only
// one is a plain edit (name, order…) that keeps the stored pair.
const customs =
dto.includesCustoms !== undefined || dto.includesEthiopianCustomsOnly !== undefined
? this.resolveCustomsFlags(
dto.includesCustoms ?? (existing.includesCustoms && !existing.includesEthiopianCustomsOnly),
ethiopian,
)
: {};
const patch = { ...dto, ...customs };
const updated = await this.repository.update(id, patch);
if (!updated) throw new NotFoundException(`Service type ${id} not found`);
return updated;
}
/** "Ethiopian customs only" narrows a customs service — it cannot stand alone. */
private assertCustomsFlags(includesCustoms: boolean, ethiopianOnly: boolean): void {
if (ethiopianOnly && !includesCustoms) {
/**
* Full customs and Ethiopian-only customs are alternatives: the admin picks
* one. Ethiopian-only is still a customs service, so it is stored with
* includesCustoms = true — every clearance read keeps working unchanged and
* only pricing looks at the Ethiopian flag.
*/
private resolveCustomsFlags(
includesCustoms: boolean,
ethiopianOnly: boolean,
): Pick<ServiceType, 'includesCustoms' | 'includesEthiopianCustomsOnly'> {
if (includesCustoms && ethiopianOnly) {
throw new BadRequestException(
'"Ethiopian customs only" requires "Includes customs" to be enabled.',
'Pick either "Includes customs" or "Ethiopian customs only", not both.',
);
}
return {
includesCustoms: includesCustoms || ethiopianOnly,
includesEthiopianCustomsOnly: ethiopianOnly,
};
}
/** Soft-delete a service type. */