67a7590525
- 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
127 lines
3.8 KiB
TypeScript
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);
|
|
});
|
|
});
|