feat: add unit tests for ReceiptImportService.saveReceipt method
This commit is contained in:
@@ -0,0 +1,209 @@
|
|||||||
|
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]> = {}): 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<void>) => cb(txMock)),
|
||||||
|
};
|
||||||
|
|
||||||
|
service = new ReceiptImportService(
|
||||||
|
prismaMock as any,
|
||||||
|
{} as any, // aiService – används ej i saveReceipt
|
||||||
|
{} as any, // categoriesService – används ej i saveReceipt
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user