fix schule issue and contianer type issue

This commit is contained in:
Marshal
2026-07-17 09:16:35 +00:00
parent 5a1e0dba4d
commit 7b7e7c3f62
39 changed files with 508 additions and 187 deletions

View File

@@ -48,7 +48,6 @@ export class ContainerTypesService {
code,
label: dto.label,
sizeFt: dto.sizeFt,
wagonsPerUnit: dto.wagonsPerUnit,
isReefer: dto.isReefer ?? false,
isOpenTop: dto.isOpenTop ?? false,
isActive: dto.isActive ?? true,

View File

@@ -5,9 +5,8 @@ import { PriorityConfigsService } from './priority-configs.service';
/**
* Contiguous-range rules for priority configs: per type (per currency for
* CURRENCY), ranges run 1..cap with no gaps and no overlaps; the next range
* must start at the lowest uncovered wagon count. Caps: WAGON 50,
* CURRENCY 35, CUSTOMS 15.
* CURRENCY), ranges run from 1 with no gaps and no overlaps; the next range
* must start at the lowest uncovered wagon count. There is no upper ceiling.
*/
describe('PriorityConfigsService range validation', () => {
const rule = (
@@ -118,41 +117,47 @@ describe('PriorityConfigsService range validation', () => {
).rejects.toThrow(/overlaps existing rule/);
});
it('enforces the per-type ceilings (WAGON 50, CURRENCY 35, CUSTOMS 15)', async () => {
it('imposes no upper ceiling on any type', async () => {
await expect(
attempt(serviceWith([]), { minWagonCount: 1, maxWagonCount: 51 }),
).rejects.toThrow(/may not exceed 50/);
attempt(serviceWith([]), { minWagonCount: 1, maxWagonCount: 5000 }),
).resolves.toBeUndefined();
await expect(
attempt(serviceWith([]), {
type: 'CURRENCY',
currency: 'USD',
minWagonCount: 1,
maxWagonCount: 36,
maxWagonCount: 5000,
}),
).rejects.toThrow(/may not exceed 35/);
).resolves.toBeUndefined();
await expect(
attempt(serviceWith([]), {
type: 'CUSTOMS',
minWagonCount: 1,
maxWagonCount: 16,
maxWagonCount: 5000,
}),
).rejects.toThrow(/may not exceed 15/);
).resolves.toBeUndefined();
});
it('rejects any new rule once the chain covers the full range', async () => {
it('keeps extending the chain past the old caps', async () => {
await expect(
attempt(serviceWith([rule('WAGON', 1, 50)]), {
minWagonCount: 51,
maxWagonCount: 51,
maxWagonCount: 120,
}),
).rejects.toThrow(/may not exceed 50/);
).resolves.toBeUndefined();
await expect(
attempt(serviceWith([rule('CUSTOMS', 1, 15)]), {
type: 'CUSTOMS',
minWagonCount: 1,
maxWagonCount: 1,
minWagonCount: 16,
maxWagonCount: 99,
}),
).rejects.toThrow(/already cover the full 115 range/);
).resolves.toBeUndefined();
});
it('still rejects a min greater than the max', async () => {
await expect(
attempt(serviceWith([]), { minWagonCount: 9, maxWagonCount: 4 }),
).rejects.toThrow(BadRequestException);
});
it('tracks CURRENCY chains per currency — USD and ETB are independent', async () => {
@@ -214,16 +219,13 @@ describe('PriorityConfigsService range validation', () => {
it('reports the next-range prefill for the form', async () => {
const svc = serviceWith([rule('WAGON', 1, 5), rule('WAGON', 11, 20)]);
await expect(svc.nextRange('WAGON')).resolves.toEqual({
nextMin: 6,
maxCap: 50,
});
await expect(svc.nextRange('WAGON')).resolves.toEqual({ nextMin: 6 });
// Past the old CUSTOMS cap of 15 the chain simply continues.
await expect(
serviceWith([rule('CUSTOMS', 1, 15)]).nextRange('CUSTOMS'),
).resolves.toEqual({ nextMin: null, maxCap: 15 });
).resolves.toEqual({ nextMin: 16 });
await expect(serviceWith([]).nextRange('CURRENCY', 'USD')).resolves.toEqual({
nextMin: 1,
maxCap: 35,
});
});
});

View File

@@ -10,28 +10,19 @@ import {
} from '../interfaces/priority-configs.repository.interface';
import { DisplayOrderService } from './display-order.service';
/** Hard ceiling of each type's wagon-count chain (1..cap, contiguous). */
export const RANGE_CAPS: Record<'WAGON' | 'CURRENCY' | 'CUSTOMS', number> = {
WAGON: 50,
CURRENCY: 35,
CUSTOMS: 15,
};
/**
* Lowest wagon count ≥ 1 not covered by any of `rules` — where the next range
* must start. Null when the chain is already complete up to the type's cap.
* must start. The chain is unbounded above, so there is always a next start.
*/
function nextRangeStart(
rules: Pick<PriorityConfig, 'type' | 'minWagonCount' | 'maxWagonCount'>[],
): number | null {
const cap = rules.length ? RANGE_CAPS[rules[0].type] : null;
): number {
const sorted = [...rules].sort((a, b) => a.minWagonCount - b.minWagonCount);
let next = 1;
for (const r of sorted) {
if (r.minWagonCount > next) break; // gap before this rule — fill it
next = Math.max(next, r.maxWagonCount + 1);
}
if (cap != null && next > cap) return null;
return next;
}
@@ -102,8 +93,8 @@ export class PriorityConfigsService {
* - ranges never overlap — a booking matches at most one rule per type;
* - ranges are contiguous from 1: a new range must START at the lowest
* wagon count not yet covered (after 15 the next is 6…; deleting a
* middle rule opens a gap and the next create must fill it first);
* - each type has a hard ceiling: WAGON 50, CURRENCY 35, CUSTOMS 15.
* middle rule opens a gap and the next create must fill it first).
* There is no upper ceiling — max wagon count is unbounded.
* Ranges are inclusive on both ends.
*/
async assertNoRangeCollision(input: {
@@ -118,14 +109,6 @@ export class PriorityConfigsService {
'Min wagon count cannot be greater than max wagon count',
);
}
const cap = RANGE_CAPS[input.type];
if (input.maxWagonCount > cap) {
throw new BadRequestException(
`${input.type} ranges may not exceed ${cap}` +
`${input.minWagonCount}${input.maxWagonCount} goes past the ceiling.`,
);
}
const siblings = (
await this.repository.findAll({ where: { type: input.type } })
).filter(
@@ -142,12 +125,6 @@ export class PriorityConfigsService {
const currentStart = input.excludeId
? (await this.repository.findById(input.excludeId))?.minWagonCount ?? null
: null;
if (expectedStart == null && currentStart == null) {
throw new BadRequestException(
`${input.type} rules already cover the full 1${cap} range — ` +
'delete or shrink an existing rule first.',
);
}
if (
input.minWagonCount !== expectedStart &&
input.minWagonCount !== currentStart
@@ -174,21 +151,21 @@ export class PriorityConfigsService {
}
/**
* Where the next range for a type/currency must start, and the type's
* ceiling — feeds the create form so the min field is auto-filled and
* locked. `nextMin` is null when the chain already covers 1..cap.
* Where the next range for a type/currency must start — feeds the create
* form so the min field is auto-filled and locked. Always a number: the
* chain has no ceiling, so another range always fits.
*/
async nextRange(
type: 'WAGON' | 'CURRENCY' | 'CUSTOMS',
currency?: string | null,
): Promise<{ nextMin: number | null; maxCap: number }> {
): Promise<{ nextMin: number }> {
const siblings = (
await this.repository.findAll({ where: { type } })
).filter(
(s) =>
type !== 'CURRENCY' || (s.currency ?? null) === (currency ?? null),
);
return { nextMin: nextRangeStart(siblings), maxCap: RANGE_CAPS[type] };
return { nextMin: nextRangeStart(siblings) };
}
async remove(id: string): Promise<void> {