Files
recipe-app/backend/src/receipt-import/receipt-import.parse-flow.spec.ts
T
Nils-Johan Gynther 67a7590525
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 12m45s
Test Suite / flutter-quality (push) Failing after 7m24s
feat(ai): add AI trace tracking and admin panel
- Add AiTrace model to Prisma schema with relations to User
- Implement AiTraceService with CRUD operations for AI traces
- Add new admin panel for AI traces with filtering and detail views
- Integrate trace persistence in receipt import flow
- Add API endpoints for listing and retrieving AI traces
- Update Flutter admin UI with new AI tab and navigation
- Add new domain models for AI traces and details
- Add migration for AiTrace table creation

BREAKING CHANGE: None
2026-05-21 17:33:21 +02:00

127 lines
3.8 KiB
TypeScript

import { ReceiptImportService } from './receipt-import.service';
import { FlatCategory } from '../categories/categories.service';
function cat(id: number, name: string, path: string): FlatCategory {
return { id, name, path };
}
describe('ReceiptImportService parseReceipt flow', () => {
const categories: FlatCategory[] = [
cat(30, 'Mejeri, ost & ägg', 'Mejeri, ost & ägg'),
cat(53, 'Choklad', 'Glass, godis & snacks > Choklad'),
cat(51, 'Godis', 'Glass, godis & snacks > Godis'),
];
const prismaMock = {
aiTrace: { create: jest.fn() },
receiptAlias: { findMany: jest.fn() },
product: { findMany: jest.fn() },
unitMapping: { findMany: jest.fn() },
user: { findUnique: jest.fn() },
};
const aiServiceMock = {
suggestCategory: jest.fn(),
};
const categoriesServiceMock = {
findFlattened: jest.fn(),
};
const service = new ReceiptImportService(
prismaMock as any,
aiServiceMock as any,
categoriesServiceMock as any,
{ commitReceiptMatches: jest.fn() } as any,
);
beforeEach(() => {
jest.clearAllMocks();
categoriesServiceMock.findFlattened.mockResolvedValue(categories);
prismaMock.unitMapping.findMany.mockResolvedValue([]);
prismaMock.user.findUnique.mockResolvedValue({ aiEngineEnabled: true });
});
it('kör prioriteringskedjan i parseReceipt: user alias -> global alias -> wordmatch -> AI', async () => {
prismaMock.receiptAlias.findMany.mockResolvedValue([
{
receiptName: 'mixad vara',
product: {
id: 901,
name: 'User Produkt',
canonicalName: 'User Produkt',
categoryRef: { id: 30, name: 'Mejeri, ost & ägg' },
},
},
{
receiptName: 'global choklad',
product: {
id: 900,
name: 'Global Produkt',
canonicalName: 'Global Produkt',
categoryRef: { id: 53, name: 'Choklad' },
},
},
]);
prismaMock.product.findMany.mockResolvedValue([
{
id: 777,
name: 'Specialprodukt',
canonicalName: 'Specialprodukt',
categoryRef: { id: 30, name: 'Mejeri, ost & ägg' },
},
]);
aiServiceMock.suggestCategory.mockResolvedValue({
categoryId: 51,
categoryName: 'Godis',
path: 'Glass, godis & snacks > Godis',
confidence: 'low',
});
jest
.spyOn(service as any, 'parseReceiptViaImporter')
.mockResolvedValue({
items: [
{ rawName: 'MIXAD VARA', quantity: 1, unit: 'st' },
{ rawName: 'GLOBAL CHOKLAD', quantity: 1, unit: 'st' },
{ rawName: 'SPECIALPRODUKT 1st', quantity: 1, unit: 'st' },
{ rawName: 'helt okänd vara', quantity: 1, unit: 'st' },
],
trace: {
prompt: 'test prompt',
rawOutput: '{"items":[]}',
normalizedOutput: { items: [] },
},
});
const file = {
buffer: Buffer.from('dummy'),
mimetype: 'image/jpeg',
originalname: 'receipt.jpg',
} as any;
const result = await service.parseReceipt(file, false, 10);
expect(result).toHaveLength(4);
expect(result[0].matchedVia).toBe('alias');
expect(result[0].matchedProductId).toBe(901);
expect(result[1].matchedVia).toBe('alias');
expect(result[1].matchedProductId).toBe(900);
expect(result[2].matchedVia).toBe('wordmatch');
expect(result[2].suggestedProductId).toBe(777);
expect(result[2].categorySuggestion?.categoryId).toBe(30);
expect(result[3].matchedVia).toBe('none');
expect(result[3].categorySuggestion?.categoryId).toBe(51);
expect(aiServiceMock.suggestCategory).toHaveBeenCalledTimes(1);
expect(aiServiceMock.suggestCategory).toHaveBeenCalledWith('helt okänd vara', categories);
});
});