diff --git a/apps/edr-freight-api/src/modules/wagons/dto/create-transfer-request.dto.ts b/apps/edr-freight-api/src/modules/wagons/dto/create-transfer-request.dto.ts
index e747b69f2..85c879f70 100644
--- a/apps/edr-freight-api/src/modules/wagons/dto/create-transfer-request.dto.ts
+++ b/apps/edr-freight-api/src/modules/wagons/dto/create-transfer-request.dto.ts
@@ -14,7 +14,8 @@ import {
* A count-only wagon-transfer request. The requester picks source yard, wagon
* type, destination yard and HOW MANY — never the specific wagons; OCC hand-picks
* those at fulfilment. The quantity may not exceed the AVAILABLE wagons of that
- * type currently in the source yard, and a reason is mandatory.
+ * type currently in the source yard (enforced in the service, which is the only
+ * layer that can count them), and a reason is mandatory.
*/
export class CreateTransferRequestDto {
@IsUUID()
diff --git a/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.service.spec.ts b/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.service.spec.ts
index 205d86450..8f7457b37 100644
--- a/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.service.spec.ts
+++ b/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.service.spec.ts
@@ -199,7 +199,7 @@ describe('WagonTransferRequestsService — partial fulfilment', () => {
});
describe('createRequest', () => {
- it('accepts a count larger than what the yard holds today', async () => {
+ it('accepts a count up to what the yard holds today', async () => {
wagonRepo.count.mockResolvedValue(20);
await service.createRequest(
@@ -207,14 +207,50 @@ describe('WagonTransferRequestsService — partial fulfilment', () => {
fromYardId: 'yard-a',
toYardId: 'yard-b',
wagonTypeId: 'type-1',
- quantity: 50,
+ quantity: 20,
reason: 'Grain campaign',
},
'user-1',
);
expect(requestRepo.save).toHaveBeenCalled();
- expect(stored.quantity).toBe(50);
+ expect(stored.quantity).toBe(20);
+ });
+
+ it('refuses a count larger than what the yard holds today', async () => {
+ wagonRepo.count.mockResolvedValue(20);
+
+ await expect(
+ service.createRequest(
+ {
+ fromYardId: 'yard-a',
+ toYardId: 'yard-b',
+ wagonTypeId: 'type-1',
+ quantity: 50,
+ reason: 'Grain campaign',
+ },
+ 'user-1',
+ ),
+ ).rejects.toThrow(/only 20 wagon\(s\).*available/i);
+ expect(requestRepo.save).not.toHaveBeenCalled();
+ });
+
+ it('refuses when the yard has nothing of that type available', async () => {
+ wagonRepo.count.mockResolvedValue(0);
+
+ await expect(
+ service.createRequest(
+ {
+ fromYardId: 'yard-a',
+ toYardId: 'yard-b',
+ wagonTypeId: 'type-1',
+ quantity: 1,
+ reason: 'Grain campaign',
+ },
+ 'user-1',
+ ),
+ ).rejects.toThrow(/no available wagons/i);
+ expect(requestRepo.save).not.toHaveBeenCalled();
});
it('still refuses a same-yard move', async () => {
diff --git a/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.service.ts b/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.service.ts
index 8d4159726..6b63460ad 100644
--- a/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.service.ts
+++ b/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.service.ts
@@ -75,10 +75,11 @@ export class WagonTransferRequestsService {
) {}
/**
- * Record a PENDING request. Count-only — no wagons are picked here, and the
- * count is NOT capped by what the source yard holds today: OCC fulfils in
- * instalments, so asking for 50 while only 20 sit there is a normal, useful
- * request. A reason is mandatory and is shown on the OCC queue.
+ * Record a PENDING request. Count-only — no wagons are picked here, but the
+ * count IS capped by what the source yard can hand over right now: a request
+ * may not exceed the AVAILABLE, uncoupled wagons of that type in the source
+ * yard (the same number the yard desk shows). A reason is mandatory and is
+ * shown on the OCC queue.
*/
async createRequest(
dto: CreateTransferRequestDto,
@@ -89,6 +90,20 @@ export class WagonTransferRequestsService {
'Source and destination yard must be different',
);
}
+ const available = await this.countAvailable(
+ dto.fromYardId,
+ dto.wagonTypeId,
+ );
+ if (available === 0) {
+ throw new BadRequestException(
+ 'No available wagons of this type in the source yard',
+ );
+ }
+ if (dto.quantity > available) {
+ throw new BadRequestException(
+ `Only ${available} wagon(s) of this type are available in the source yard — cannot request ${dto.quantity}`,
+ );
+ }
const request = this.requestRepo.create({
fromYardId: dto.fromYardId,
toYardId: dto.toYardId,
diff --git a/apps/edr-freight-web/backoffice/src/components/wagons/WagonYardWorkspaceModal.tsx b/apps/edr-freight-web/backoffice/src/components/wagons/WagonYardWorkspaceModal.tsx
index 4a92695ca..cb9fdf86b 100644
--- a/apps/edr-freight-web/backoffice/src/components/wagons/WagonYardWorkspaceModal.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/wagons/WagonYardWorkspaceModal.tsx
@@ -44,9 +44,8 @@ const clampInt = (v: number | string, max: number): number => {
/**
* NumberInput + Slider + All/Half presets, kept in sync. `max` bounds the field
- * for actions that move real wagons; omit it for a transfer REQUEST, which may
- * legitimately ask for more than the yard holds today (OCC fulfils it in
- * instalments) — the slider then just tracks the current value.
+ * to the wagons on hand; omitting it leaves the field unbounded and the slider
+ * simply tracks the current value.
*/
const QuantityField = ({
value,
@@ -434,9 +433,14 @@ const WagonYardWorkspaceModal = ({ opened, onClose }: WagonYardWorkspaceModalPro
{availableCount} available
- {/* No max: the request may exceed what the yard holds
- today — OCC fulfils it in instalments. */}
-
+ {/* Capped at the wagons actually available in this yard
+ right now (uncoupled + Available) — a request may not
+ ask for more than the yard can hand over. */}
+