diff --git a/ApiCore-Backend/src/storage/dto/create-stock-movement.dto.ts b/ApiCore-Backend/src/storage/dto/create-stock-movement.dto.ts index 3974133..ef53adc 100644 --- a/ApiCore-Backend/src/storage/dto/create-stock-movement.dto.ts +++ b/ApiCore-Backend/src/storage/dto/create-stock-movement.dto.ts @@ -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; diff --git a/ApiCore-Backend/src/storage/storage.service.spec.ts b/ApiCore-Backend/src/storage/storage.service.spec.ts index 496def0..d0946d2 100644 --- a/ApiCore-Backend/src/storage/storage.service.spec.ts +++ b/ApiCore-Backend/src/storage/storage.service.spec.ts @@ -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); + }); }); diff --git a/ApiCore-Backend/src/storage/storage.service.ts b/ApiCore-Backend/src/storage/storage.service.ts index cf48a2d..839fef5 100644 --- a/ApiCore-Backend/src/storage/storage.service.ts +++ b/ApiCore-Backend/src/storage/storage.service.ts @@ -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