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
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user