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:
@@ -1,3 +1,5 @@
|
||||
import type { CategorySuggestion } from '../../ai/ai.service';
|
||||
|
||||
export interface ParsedReceiptItem {
|
||||
rawName: string;
|
||||
quantity: number;
|
||||
@@ -9,4 +11,6 @@ export interface ParsedReceiptItem {
|
||||
// ordbaserad match: förslag, kräver bekräftelse
|
||||
suggestedProductId?: number;
|
||||
suggestedProductName?: string;
|
||||
// AI-kategorisuggestion för ej matchade varor (premium)
|
||||
categorySuggestion?: CategorySuggestion;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Request,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
@@ -9,6 +11,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { ReceiptImportService } from './receipt-import.service';
|
||||
import { ParsedReceiptItem } from './dto/parsed-receipt-item.dto';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
|
||||
const ALLOWED_MIMES = [
|
||||
'image/jpeg',
|
||||
@@ -24,6 +27,7 @@ export class ReceiptImportController {
|
||||
constructor(private readonly receiptImportService: ReceiptImportService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
@@ -32,6 +36,7 @@ export class ReceiptImportController {
|
||||
)
|
||||
async parseReceipt(
|
||||
@UploadedFile() file?: Express.Multer.File,
|
||||
@Request() req?: any,
|
||||
): Promise<ParsedReceiptItem[]> {
|
||||
if (!file?.buffer) {
|
||||
throw new BadRequestException('Ingen fil skickades med.');
|
||||
@@ -41,6 +46,7 @@ export class ReceiptImportController {
|
||||
'Otillåten filtyp. Använd JPEG, PNG, WebP eller PDF.',
|
||||
);
|
||||
}
|
||||
return this.receiptImportService.parseReceipt(file);
|
||||
const isPremium = req?.user?.isPremium === true || req?.user?.role === 'admin';
|
||||
return this.receiptImportService.parseReceipt(file, isPremium);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
import { ReceiptImportController } from './receipt-import.controller';
|
||||
import { ReceiptImportService } from './receipt-import.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { AiModule } from '../ai/ai.module';
|
||||
import { CategoriesModule } from '../categories/categories.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, AiModule, CategoriesModule],
|
||||
controllers: [ReceiptImportController],
|
||||
providers: [ReceiptImportService],
|
||||
})
|
||||
|
||||
@@ -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