import { BadRequestException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { ReceiptImportService } from './receipt-import.service'; import { SaveReceiptDto } from './dto/save-receipt.dto'; // ────────────────────────────────────────────────────────────────────────────── // Hjälpfunktioner // ────────────────────────────────────────────────────────────────────────────── function makeItem(overrides: Partial = {}): SaveReceiptDto['items'][0] { return { rawName: 'ARLA MJOLK 1L', quantity: 2, unit: 'st', destination: 'inventory', productId: 100, ...overrides, } as SaveReceiptDto['items'][0]; } // ────────────────────────────────────────────────────────────────────────────── // Spec // ────────────────────────────────────────────────────────────────────────────── describe('ReceiptImportService.saveReceipt', () => { let txMock: { product: { findUnique: jest.Mock; create: jest.Mock; update: jest.Mock }; inventoryItem: { create: jest.Mock; update: jest.Mock }; pantryItem: { create: jest.Mock }; receiptAlias: { upsert: jest.Mock }; unitMapping: { upsert: jest.Mock }; }; let prismaMock: { pantryItem: { findMany: jest.Mock }; inventoryItem: { findMany: jest.Mock }; $transaction: jest.Mock; }; let service: ReceiptImportService; beforeEach(() => { txMock = { product: { findUnique: jest.fn(), create: jest.fn(), update: jest.fn(), }, inventoryItem: { create: jest.fn().mockResolvedValue({ id: 1 }), update: jest.fn().mockResolvedValue({ id: 1 }), }, pantryItem: { create: jest.fn().mockResolvedValue({ id: 1 }), }, receiptAlias: { upsert: jest.fn().mockResolvedValue({ id: 1 }), }, unitMapping: { upsert: jest.fn().mockResolvedValue({ id: 1 }), }, }; prismaMock = { pantryItem: { findMany: jest.fn().mockResolvedValue([]) }, inventoryItem: { findMany: jest.fn().mockResolvedValue([]) }, $transaction: jest.fn().mockImplementation(async (cb: (tx: typeof txMock) => Promise) => cb(txMock)), }; service = new ReceiptImportService( prismaMock as any, {} as any, // aiService – används ej i saveReceipt {} as any, // categoriesService – används ej i saveReceipt { commitReceiptMatches: jest.fn().mockResolvedValue({ boughtCount: 0, ambiguousCount: 0, unmatchedCount: 0 }) } as any, ); }); // ── 1. Skapar ny inventariepost ───────────────────────────────────────────── it('skapar ny inventariepost när produkten finns och inte finns i inventariet', async () => { txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true }); const dto: SaveReceiptDto = { items: [makeItem()] }; const result = await service.saveReceipt(1, dto); expect(txMock.inventoryItem.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ productId: 100, userId: 1 }), }), ); expect(result.created).toBe(1); expect(result.merged).toBe(0); expect(result.errors).toHaveLength(0); }); // ── 2. Slår samman mängd när produkten redan finns i inventariet ──────────── it('slår samman mängd när produkten redan finns i inventariet', async () => { prismaMock.inventoryItem.findMany.mockResolvedValue([ { id: 50, productId: 100, quantity: new Prisma.Decimal(3), unit: 'st' }, ]); txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true }); const dto: SaveReceiptDto = { items: [makeItem({ quantity: 2 })] }; const result = await service.saveReceipt(1, dto); expect(txMock.inventoryItem.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 50 }, data: { quantity: { increment: new Prisma.Decimal(2) } }, }), ); expect(result.merged).toBe(1); expect(result.created).toBe(0); }); // ── 3. Lägger till i skafferi ─────────────────────────────────────────────── it('lägger till i skafferi när destination är pantry och produkten inte finns där', async () => { txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true }); const dto: SaveReceiptDto = { items: [makeItem({ destination: 'pantry' })] }; const result = await service.saveReceipt(1, dto); expect(txMock.pantryItem.create).toHaveBeenCalledWith( expect.objectContaining({ data: { userId: 1, productId: 100 } }), ); expect(result.pantryAdded).toBe(1); expect(result.pantrySkipped).toBe(0); }); // ── 4. Hoppar över skafferidublett ────────────────────────────────────────── it('hoppar över produkt som redan finns i skafferiet', async () => { prismaMock.pantryItem.findMany.mockResolvedValue([{ productId: 100 }]); txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true }); const dto: SaveReceiptDto = { items: [makeItem({ destination: 'pantry' })] }; const result = await service.saveReceipt(1, dto); expect(txMock.pantryItem.create).not.toHaveBeenCalled(); expect(result.pantrySkipped).toBe(1); expect(result.pantryAdded).toBe(0); }); // ── 5. Lär in alias ───────────────────────────────────────────────────────── it('lär in alias när learnAlias är true', async () => { txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true }); const dto: SaveReceiptDto = { items: [makeItem({ learnAlias: true, learnAliasGlobally: false })], }; const result = await service.saveReceipt(1, dto); expect(txMock.receiptAlias.upsert).toHaveBeenCalledWith( expect.objectContaining({ create: expect.objectContaining({ receiptName: 'arla mjolk 1l', productId: 100, isGlobal: false, ownerId: 1, }), }), ); expect(result.aliasesLearned).toBe(1); }); // ── 6. Skapar ny privat produkt via createProductName ─────────────────────── it('skapar ny produkt när createProductName anges och produkten inte finns', async () => { txMock.product.findUnique.mockResolvedValue(null); // ingen befintlig txMock.product.create.mockResolvedValue({ id: 200 }); const dto: SaveReceiptDto = { items: [ makeItem({ productId: undefined, createProductName: 'Hemmagjord Senap' }), ], }; const result = await service.saveReceipt(1, dto); expect(txMock.product.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ name: 'Hemmagjord Senap', isPrivate: true, ownerId: 1, }), }), ); expect(result.created).toBe(1); expect(result.errors).toHaveLength(0); }); // ── 7. Produkt hittades inte – registrerar fel utan att kasta ─────────────── it('registrerar fel för okänt productId utan att kasta exception', async () => { txMock.product.findUnique.mockResolvedValue(null); const dto: SaveReceiptDto = { items: [makeItem({ productId: 999 })] }; const result = await service.saveReceipt(1, dto); expect(result.errors).toHaveLength(1); expect(result.errors![0].index).toBe(0); expect(result.created).toBe(0); }); // ── 8. Kastar BadRequestException om transaktionen misslyckas ─────────────── it('kastar BadRequestException om $transaction kastar', async () => { prismaMock.$transaction.mockRejectedValue(new Error('DB krasch')); const dto: SaveReceiptDto = { items: [makeItem()] }; await expect(service.saveReceipt(1, dto)).rejects.toThrow(BadRequestException); }); });