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
+70 -1
View File
@@ -2,6 +2,7 @@ import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
Param,
@@ -10,6 +11,7 @@ import {
Post,
Put,
Query,
Request,
} from '@nestjs/common';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
@@ -20,10 +22,29 @@ import { SetTagsDto } from './dto/set-tags.dto';
import { UpsertNutritionDto } from './dto/upsert-nutrition.dto';
import { BulkUpdateProductsDto } from './dto/bulk-update-products.dto';
import { Roles } from '../auth/decorators/roles.decorator';
import { AiService } from '../ai/ai.service';
import { CategoriesService } from '../categories/categories.service';
import { IsArray, IsIn, IsInt, IsOptional } from 'class-validator';
class AiCategorizeBulkDto {
@IsOptional()
@IsArray()
@IsInt({ each: true })
productIds?: number[];
}
class SetProductStatusDto {
@IsIn(['active', 'rejected'])
status: string;
}
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
constructor(
private readonly productsService: ProductsService,
private readonly aiService: AiService,
private readonly categoriesService: CategoriesService,
) {}
@Get()
findAll(
@@ -57,11 +78,50 @@ export class ProductsController {
return this.productsService.backfillCanonicalNames();
}
@Roles('admin')
@Get('pending')
findPending() {
return this.productsService.findPending();
}
@Roles('admin')
@Post('ai-categorize-bulk')
@HttpCode(200)
async aiCategorizeBulk(@Body() body: AiCategorizeBulkDto) {
const categories = await this.categoriesService.findFlattened();
let products: { id: number; name: string }[];
if (body.productIds && body.productIds.length > 0) {
const found = await Promise.all(body.productIds.map((id) => this.productsService.findOne(id)));
products = found.map((p) => ({ id: p.id, name: p.canonicalName ?? p.name }));
} else {
products = await this.productsService.findUncategorized();
}
const results: { productId: number; productName: string; suggestion: object }[] = [];
for (const product of products) {
const suggestion = await this.aiService.suggestCategory(product.name, categories);
results.push({ productId: product.id, productName: product.name, suggestion });
}
return results;
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.productsService.findOne(id);
}
@Get(':id/suggest-category')
async suggestCategory(
@Param('id', ParseIntPipe) id: number,
@Request() req: { user: { role: string; isPremium: boolean } },
) {
if (req.user.role !== 'admin' && !req.user.isPremium) {
throw new ForbiddenException('Denna funktion kräver premiumkonto');
}
const product = await this.productsService.findOne(id);
const categories = await this.categoriesService.findFlattened();
return this.aiService.suggestCategory(product.canonicalName ?? product.name, categories);
}
@Post()
create(@Body() body: CreateProductDto) {
return this.productsService.create(body);
@@ -111,6 +171,15 @@ export class ProductsController {
return this.productsService.remove(id);
}
@Roles('admin')
@Patch(':id/status')
setStatus(
@Param('id', ParseIntPipe) id: number,
@Body() body: SetProductStatusDto,
) {
return this.productsService.setStatus(id, body.status);
}
@Roles('admin')
@Post(':id/restore')
restore(@Param('id', ParseIntPipe) id: number) {