MAJOR UPPDATE: "First Ai"
feat: add AI categorization for products and enhance user management - Integrated AI service for category suggestions in receipt import and product management. - Added premium subscription feature for users with corresponding API endpoints. - Implemented admin interface for managing pending product suggestions. - Enhanced user management to include premium status and corresponding UI updates. - Updated database schema to support new fields for premium status and product status.
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
||||
import * as pdfParse from 'pdf-parse';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { ParsedReceiptItem } from './dto/parsed-receipt-item.dto';
|
||||
import { AiService } from '../ai/ai.service';
|
||||
import { CategoriesService } from '../categories/categories.service';
|
||||
|
||||
const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions';
|
||||
|
||||
@@ -36,9 +38,13 @@ ${text}`;
|
||||
export class ReceiptImportService {
|
||||
private readonly logger = new Logger(ReceiptImportService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly aiService: AiService,
|
||||
private readonly categoriesService: CategoriesService,
|
||||
) {}
|
||||
|
||||
async parseReceipt(file: Express.Multer.File): Promise<ParsedReceiptItem[]> {
|
||||
async parseReceipt(file: Express.Multer.File, isPremium = false): Promise<ParsedReceiptItem[]> {
|
||||
const apiKey = process.env.MISTRAL_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new ServiceUnavailableException(
|
||||
@@ -51,7 +57,12 @@ export class ReceiptImportService {
|
||||
? await this.parseReceiptFromPdf(file.buffer, apiKey)
|
||||
: await this.parseReceiptFromImage(file.buffer, file.mimetype, apiKey);
|
||||
|
||||
return this.matchProducts(rawItems);
|
||||
const matched = await this.matchProducts(rawItems);
|
||||
|
||||
if (isPremium) {
|
||||
return this.enrichWithAiCategories(matched);
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private async parseReceiptFromImage(
|
||||
@@ -221,4 +232,29 @@ export class ReceiptImportService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async enrichWithAiCategories(items: ParsedReceiptItem[]): Promise<ParsedReceiptItem[]> {
|
||||
const unmatched = items.filter((i) => !i.matchedProductId && !i.suggestedProductId && i.rawName);
|
||||
if (unmatched.length === 0) return items;
|
||||
|
||||
let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
|
||||
try {
|
||||
categories = await this.categoriesService.findFlattened();
|
||||
} catch {
|
||||
return items; // Om kategoritjänsten är otillgänglig, returnera utan AI-förslag
|
||||
}
|
||||
|
||||
const enriched = new Map<string, ParsedReceiptItem>();
|
||||
for (const item of unmatched) {
|
||||
try {
|
||||
const suggestion = await this.aiService.suggestCategory(item.rawName, categories);
|
||||
enriched.set(item.rawName, { ...item, categorySuggestion: suggestion });
|
||||
} catch {
|
||||
// Om AI-anrop misslyckas för enskild vara — hoppa över utan att kasta
|
||||
enriched.set(item.rawName, item);
|
||||
}
|
||||
}
|
||||
|
||||
return items.map((item) => enriched.get(item.rawName) ?? item);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user