fix: reject a negative quantity on IN/OUT stock movements
addMovement() negates quantity internally for OUT (delta = -quantity)
and applies it directly for IN. Nothing stopped a negative quantity
from being submitted, so e.g. { type: 'OUT', quantity: -5 } would
compute delta = -(-5) = +5 - silently ADDING stock while being
recorded in the audit trail as a removal. The inverse held for IN.
ADJUSTMENT is the one type that must stay allowed to be negative,
since that's the only way to correct a stock count downward, so this
is a service-level guard (BadRequestException) rather than a DTO
validator - class-validator's @ValidateIf skips every validator on a
property when its condition is false, including @IsNumber, so it
can't express "always a number, but only sometimes positive" cleanly;
verified this empirically before choosing the service-level approach.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
8bf9aa467d
commit
93d0f2b208
3 files changed
+53
-3
No files matched your search
@@ -7,7 +7,11 @@ export class CreateStockMovementDto {
|
||||
@IsEnum(StockMovementType)
|
||||
type!: StockMovementType;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Für IN/OUT immer positiv angeben - die Richtung ergibt sich aus "type". ' +
|
||||
'Nur ADJUSTMENT erlaubt einen negativen Wert, um den Bestand direkt zu korrigieren.',
|
||||
})
|
||||
@IsNumber()
|
||||
quantity!: number;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { StorageService } from './storage.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
@@ -100,4 +100,36 @@ describe('StorageService', () => {
|
||||
|
||||
expect(tx.stockItem.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('addMovement rejects a negative quantity for OUT instead of silently adding stock', async () => {
|
||||
prisma.stockItem.findUnique.mockResolvedValue(stockItem);
|
||||
|
||||
await expect(
|
||||
service.addMovement('item-1', { type: 'OUT', quantity: -3 }, 'user-1'),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(tx.stockItem.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('addMovement rejects a negative quantity for IN', async () => {
|
||||
prisma.stockItem.findUnique.mockResolvedValue(stockItem);
|
||||
|
||||
await expect(
|
||||
service.addMovement('item-1', { type: 'IN', quantity: -3 }, 'user-1'),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('addMovement allows a negative quantity for ADJUSTMENT', async () => {
|
||||
prisma.stockItem.findUnique.mockResolvedValue(stockItem);
|
||||
|
||||
await service.addMovement(
|
||||
'item-1',
|
||||
{ type: 'ADJUSTMENT', quantity: -3 },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
const [[updateArg]] = tx.stockItem.update.mock.calls as [
|
||||
[{ data: { quantity: { increment: number } } }],
|
||||
];
|
||||
expect(updateArg.data.quantity.increment).toBe(-3);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||
@@ -123,6 +127,16 @@ export class StorageService {
|
||||
actingUserId: string,
|
||||
) {
|
||||
await this.findStockItemById(id);
|
||||
// IN/OUT direction comes entirely from `type`, which then negates
|
||||
// quantity for OUT below - a negative quantity submitted for OUT would
|
||||
// therefore silently ADD stock while being logged as a removal,
|
||||
// inverting the audit trail. Only ADJUSTMENT may be negative, since
|
||||
// that is the only way to correct a stock count downward.
|
||||
if (dto.type !== 'ADJUSTMENT' && dto.quantity <= 0) {
|
||||
throw new BadRequestException(
|
||||
'quantity must be positive for IN/OUT movements - the direction is determined by "type"',
|
||||
);
|
||||
}
|
||||
const delta =
|
||||
dto.type === 'IN' || dto.type === 'ADJUSTMENT'
|
||||
? dto.quantity
|
||||
|
||||
Reference in new issue
Block a user