feat: implement user-scoped receipt aliases with global fallback; enhance alias management in admin panel
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-04 19:43:13 +02:00
parent d73ea5ef7c
commit 64b06435cf
15 changed files with 751 additions and 36 deletions
@@ -56,6 +56,12 @@ describe('ReceiptImportService test matrix', () => {
categoriesServiceMock as any,
);
beforeEach(() => {
jest.clearAllMocks();
prismaMock.receiptAlias.findMany.mockResolvedValue([]);
prismaMock.product.findMany.mockResolvedValue([]);
});
describe('ignore patterns', () => {
it.each([
'Willys Plus:Bröd',
@@ -96,4 +102,125 @@ describe('ReceiptImportService test matrix', () => {
expect(suggestion?.path).toBe(expectedPath);
});
});
describe('alias fallback och prioritet', () => {
it('prioriterar user-alias före global alias för samma receiptName', async () => {
prismaMock.receiptAlias.findMany.mockResolvedValue([
{
receiptName: 'mjolk 1l',
productId: 501,
product: {
id: 501,
name: 'Mjolk user',
canonicalName: 'Mjolk user',
categoryId: 30,
categoryRef: { id: 30, name: 'Mejeri' },
},
},
{
receiptName: 'mjolk 1l',
productId: 999,
product: {
id: 999,
name: 'Mjolk global',
canonicalName: 'Mjolk global',
categoryId: 30,
categoryRef: { id: 30, name: 'Mejeri' },
},
},
]);
prismaMock.product.findMany.mockResolvedValue([]);
const result = await (service as any).matchProducts(
[{ rawName: 'MJOLK 1L' }],
77,
);
expect(prismaMock.receiptAlias.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
OR: [
{ ownerId: 77, isGlobal: false },
{ isGlobal: true },
],
},
}),
);
expect(result[0].matchedProductId).toBe(501);
expect(result[0].matchedProductName).toBe('Mjolk user');
});
it('använder global alias när user-alias saknas', async () => {
prismaMock.receiptAlias.findMany.mockResolvedValue([
{
receiptName: 'snickers',
productId: 222,
product: {
id: 222,
name: 'Snickers',
canonicalName: 'Snickers',
categoryId: 53,
categoryRef: { id: 53, name: 'Choklad' },
},
},
]);
prismaMock.product.findMany.mockResolvedValue([]);
const result = await (service as any).matchProducts(
[{ rawName: 'SNICKERS' }],
88,
);
expect(result[0].matchedProductId).toBe(222);
expect(result[0].matchedProductName).toBe('Snickers');
});
it('flöde: manuell korrigering lär alias och nästa import matchar direkt', async () => {
const aliases: any[] = [];
prismaMock.receiptAlias.findMany.mockImplementation(async () => aliases);
prismaMock.product.findMany.mockResolvedValue([
{
id: 700,
name: 'Arla Mjolk 1l',
canonicalName: 'Mjolk',
categoryId: 30,
categoryRef: { id: 30, name: 'Mejeri' },
},
]);
const first = await (service as any).matchProducts(
[{ rawName: 'ARLA MJOLK 1L' }],
42,
);
expect(first[0].matchedProductId).toBeUndefined();
expect(first[0].suggestedProductId).toBe(700);
// Simulerar att användaren manuellt korrigerar och alias lärs in.
aliases.push({
receiptName: 'arla mjolk 1l',
productId: 700,
product: {
id: 700,
name: 'Arla Mjolk 1l',
canonicalName: 'Mjolk',
categoryId: 30,
categoryRef: { id: 30, name: 'Mejeri' },
},
});
const second = await (service as any).matchProducts(
[{ rawName: 'ARLA MJOLK 1L' }],
42,
);
expect(second[0].matchedProductId).toBe(700);
expect(second[0].matchedProductName).toBe('Mjolk');
expect(second[0].suggestedProductId).toBeUndefined();
});
});
});
@@ -208,11 +208,20 @@ export class ReceiptImportService {
// Hämta alias och produkter parallellt — filtrera på userId om angivet
const productFilter = userId ? { isActive: true, ownerId: userId } : { isActive: true };
const aliasFilter = userId
? { product: { ownerId: userId } }
: {};
? {
OR: [
{ ownerId: userId, isGlobal: false },
{ isGlobal: true },
],
}
: { isGlobal: true };
const [aliases, products] = await Promise.all([
this.prisma.receiptAlias.findMany({
where: aliasFilter,
orderBy: [
{ isGlobal: 'asc' },
{ id: 'asc' },
],
select: { receiptName: true, productId: true, product: { select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true } } } } },
}),
this.prisma.product.findMany({