chore(import): improve error handling and add flyer integration
- Replace BadRequestException with UnauthorizedException for authentication failures in flyer-import and flyer-selection controllers - Add bulk selection endpoint in FlyerSelectionController for creating multiple selections in one request - Update FlyerSelectionModule to include new FlyerSelectionMatcherService and FlyerSelectionSyncController - Extend FlyerSelectionService with createMany method for bulk operations - Add new DTOs for bulk selection and receipt matching functionality - Update ReceiptImportService to accept FlyerSelectionService dependency and track successful rows - Extend SaveReceiptResponse with flyerAutoSync field for receipt-to-flyer matching results - Add new API paths for flyer import and selection endpoints - Update Flutter UI to include Flyer import tab and adjust tab controller length - Add new domain models and repository methods for flyer import functionality - Update test files to include new FlyerSelectionService dependency - Modify .kilo plan documentation to reflect current system architecture
This commit is contained in:
@@ -12,11 +12,12 @@ import { SaveReceiptResponse } from './dto/save-receipt.response';
|
||||
import { AiService, CategorySuggestion } from '../ai/ai.service';
|
||||
import { CategoriesService } from '../categories/categories.service';
|
||||
import { normalizeName } from '../common/utils/normalize-name';
|
||||
import {
|
||||
isIgnoredReceiptAliasName,
|
||||
normalizeReceiptAliasName,
|
||||
validateReceiptAliasName,
|
||||
} from '../common/utils/receipt-alias';
|
||||
import {
|
||||
isIgnoredReceiptAliasName,
|
||||
normalizeReceiptAliasName,
|
||||
validateReceiptAliasName,
|
||||
} from '../common/utils/receipt-alias';
|
||||
import { FlyerSelectionService } from '../flyer-selection/flyer-selection.service';
|
||||
|
||||
const IMPORTER_SERVICE_URL =
|
||||
process.env.IMPORTER_SERVICE_URL || 'http://importer-api:3001';
|
||||
@@ -125,11 +126,12 @@ type MatchDebug = {
|
||||
export class ReceiptImportService {
|
||||
private readonly logger = new Logger(ReceiptImportService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly aiService: AiService,
|
||||
private readonly categoriesService: CategoriesService,
|
||||
) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly aiService: AiService,
|
||||
private readonly categoriesService: CategoriesService,
|
||||
private readonly flyerSelectionService: FlyerSelectionService,
|
||||
) {}
|
||||
|
||||
async parseReceipt(file: Express.Multer.File, _isPremium = false, userId?: number): Promise<ParsedReceiptItem[]> {
|
||||
// Steg 1: Delegera AI-parsning till microservice-importer
|
||||
@@ -297,7 +299,7 @@ export class ReceiptImportService {
|
||||
});
|
||||
}
|
||||
|
||||
async saveReceipt(userId: number, dto: SaveReceiptDto): Promise<SaveReceiptResponse> {
|
||||
async saveReceipt(userId: number, dto: SaveReceiptDto): Promise<SaveReceiptResponse> {
|
||||
const response: SaveReceiptResponse = {
|
||||
created: 0,
|
||||
merged: 0,
|
||||
@@ -308,7 +310,13 @@ export class ReceiptImportService {
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const prismaAny = this.prisma as any;
|
||||
const prismaAny = this.prisma as any;
|
||||
const successfulRows: Array<{
|
||||
rawName: string;
|
||||
productId?: number;
|
||||
quantity?: number;
|
||||
unit?: string;
|
||||
}> = [];
|
||||
|
||||
// Preload existierande pantry-poster för denna användare
|
||||
const userPantry = await this.prisma.pantryItem.findMany({
|
||||
@@ -386,7 +394,7 @@ export class ReceiptImportService {
|
||||
}
|
||||
|
||||
// === Steg 2: Hantera pantry eller inventory ===
|
||||
if (item.destination === 'pantry') {
|
||||
if (item.destination === 'pantry') {
|
||||
if (pantryProductIds.has(productId)) {
|
||||
response.pantrySkipped++;
|
||||
} else {
|
||||
@@ -395,8 +403,8 @@ export class ReceiptImportService {
|
||||
});
|
||||
response.pantryAdded++;
|
||||
pantryProductIds.add(productId);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
// inventory
|
||||
const quantity = item.quantity ?? 0;
|
||||
const unit = (item.unit ?? '').trim() || 'st';
|
||||
@@ -461,8 +469,15 @@ export class ReceiptImportService {
|
||||
});
|
||||
response.unitMappingsLearned++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
successfulRows.push({
|
||||
rawName: item.rawName,
|
||||
productId,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
});
|
||||
|
||||
// === Steg 4: Lär in alias om requested ===
|
||||
if (item.learnAlias) {
|
||||
@@ -510,10 +525,45 @@ export class ReceiptImportService {
|
||||
throw new BadRequestException(
|
||||
`Transaktionfel vid sparande av kvittovaror: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
if (successfulRows.length > 0) {
|
||||
const syncPayload = {
|
||||
items: successfulRows,
|
||||
receiptImportBatchId: `receipt-save-${Date.now()}-${userId}`,
|
||||
boughtSource: 'receipt_auto' as const,
|
||||
};
|
||||
try {
|
||||
const sync = await this.flyerSelectionService.commitReceiptMatches(userId, syncPayload);
|
||||
response.flyerAutoSync = {
|
||||
bought: sync.boughtCount,
|
||||
ambiguous: sync.ambiguousCount,
|
||||
unmatched: sync.unmatchedCount,
|
||||
};
|
||||
} catch (err) {
|
||||
this.logger.warn(`Flyer auto-sync failed after receipt save (attempt 1): ${String(err)}`);
|
||||
try {
|
||||
const sync = await this.flyerSelectionService.commitReceiptMatches(userId, syncPayload);
|
||||
response.flyerAutoSync = {
|
||||
bought: sync.boughtCount,
|
||||
ambiguous: sync.ambiguousCount,
|
||||
unmatched: sync.unmatchedCount,
|
||||
};
|
||||
} catch (retryErr) {
|
||||
const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
||||
this.logger.warn(`Flyer auto-sync failed after receipt save (attempt 2): ${message}`);
|
||||
response.flyerAutoSync = {
|
||||
bought: 0,
|
||||
ambiguous: 0,
|
||||
unmatched: 0,
|
||||
error: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private async parseReceiptViaImporter(file: Express.Multer.File): Promise<ParsedReceiptItem[]> {
|
||||
const form = new FormData();
|
||||
|
||||
Reference in New Issue
Block a user