feat(flyer): add flyer session and selection system
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 2m49s
Test Suite / flutter-quality (push) Successful in 2m0s

- Add FlyerSession, FlyerItem, and FlyerSelection models to Prisma schema
- Implement session persistence with weekly key generation in FlyerImportService
- Add FlyerSelectionModule to AppModule
- Extend FlyerImportResponse with sessionId and flyerItemId fields
- Create new flyer-selection module directory structure
- Add migration for flyer session and selection tables

BREAKING CHANGE: Flyer import now persists data to FlyerSession and FlyerItem tables
This commit is contained in:
Nils-Johan Gynther
2026-05-18 19:02:32 +02:00
parent a31aff7c35
commit 24a96c3da1
11 changed files with 619 additions and 9 deletions
@@ -4,6 +4,7 @@ import {
Logger,
ServiceUnavailableException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { normalizeName } from '../common/utils/normalize-name';
import {
@@ -79,6 +80,7 @@ export class FlyerImportService {
const items: FlyerImportItem[] = parsed.items.map((item) => {
const match = this.matchItem(item, products, aliasToProduct, productById);
return {
flyerItemId: null,
rawName: item.rawName,
normalizedName: item.normalizedName,
category: item.category,
@@ -97,14 +99,74 @@ export class FlyerImportService {
};
});
const persistedItems = await this.persistSessionWithItems(userId, parsed.retailer, items);
return {
sessionId: persistedItems.sessionId,
retailer: parsed.retailer,
parserVersion: parsed.parserVersion,
items,
items: persistedItems.items,
warnings: parsed.warnings,
};
}
private async persistSessionWithItems(
userId: number,
retailer: 'willys',
items: FlyerImportItem[],
): Promise<{ sessionId: number; items: FlyerImportItem[] }> {
const weekKey = this.toWeekKey(new Date());
const session = await this.prisma.flyerSession.create({
data: {
userId,
retailer,
weekKey,
status: 'draft',
},
select: { id: true },
});
const savedItems: FlyerImportItem[] = [];
for (const item of items) {
const created = await this.prisma.flyerItem.create({
data: {
sessionId: session.id,
rawName: item.rawName,
normalizedName: item.normalizedName,
categoryHint: item.category,
price: item.price != null ? new Prisma.Decimal(item.price) : null,
priceUnit: item.priceUnit,
comparisonPrice:
item.comparisonPrice != null ? new Prisma.Decimal(item.comparisonPrice) : null,
comparisonUnit: item.comparisonUnit,
offerText: item.offerText,
parseConfidence: item.parseConfidence,
parseReasons: item.parseReasons,
matchedProductId: item.matchedProductId,
matchedProductName: item.matchedProductName,
matchedVia: item.matchedVia,
matchConfidence: item.matchConfidence,
matchReasons: item.matchReasons,
},
select: { id: true },
});
savedItems.push({ ...item, flyerItemId: created.id });
}
return { sessionId: session.id, items: savedItems };
}
private toWeekKey(date: Date): string {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
}
private matchItem(
item: FlyerParseItem,
products: ProductLite[],