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:
Nils-Johan Gynther
2026-04-19 10:34:21 +02:00
parent 0286ab0991
commit 054a19ed7c
30 changed files with 917 additions and 77 deletions
@@ -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);
}
}