"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProductsService = void 0; const common_1 = require("@nestjs/common"); const prisma_service_1 = require("../prisma/prisma.service"); const normalize_name_1 = require("../common/utils/normalize-name"); const ai_service_1 = require("../ai/ai.service"); const categories_service_1 = require("../categories/categories.service"); let ProductsService = class ProductsService { constructor(prisma, aiService, categoriesService) { this.prisma = prisma; this.aiService = aiService; this.categoriesService = categoriesService; } async findAll(filters) { return this.prisma.product.findMany({ where: { isActive: true, isPrivate: false, ...(filters?.tag ? { tags: { some: { tag: { name: filters.tag } } } } : {}), }, include: { tags: { include: { tag: true } }, nutrition: true, categoryRef: { include: { parent: { include: { parent: true } } } }, }, orderBy: { name: 'asc' }, }); } async findByOwner(userId) { return this.prisma.product.findMany({ where: { ownerId: userId, isActive: true }, select: { id: true, name: true, canonicalName: true, categoryId: true }, orderBy: { name: 'asc' }, }); } async createPrivate(data, userId) { const name = data.name.trim(); const normalizedName = `private:${userId}:${(0, normalize_name_1.normalizeName)(name)}`; const existing = await this.prisma.product.findUnique({ where: { normalizedName }, }); if (existing && existing.isActive) return existing; if (existing) { return this.prisma.product.update({ where: { id: existing.id }, data: { isActive: true, deletedAt: null, name, canonicalName: name }, }); } return this.prisma.product.create({ data: { name, normalizedName, canonicalName: name, isActive: true, isPrivate: true, ownerId: userId, ...(data.categoryId != null ? { categoryId: data.categoryId } : {}), }, }); } async findDuplicateCandidates() { const products = await this.prisma.product.findMany({ where: { isActive: true, }, orderBy: { name: 'asc', }, }); const grouped = new Map(); for (const product of products) { const key = product.normalizedName; if (!grouped.has(key)) { grouped.set(key, []); } grouped.get(key).push(product); } return Array.from(grouped.entries()) .filter(([, items]) => items.length > 1) .map(([normalizedName, items]) => ({ normalizedName, count: items.length, products: items, })); } async findOne(id) { const product = await this.prisma.product.findUnique({ where: { id }, }); if (!product) { throw new common_1.NotFoundException(`Product with id ${id} not found`); } return product; } async create(data, ownerId) { const name = data.name.trim(); const normalizedName = (0, normalize_name_1.normalizeName)(name); const existing = await this.prisma.product.findUnique({ where: { normalizedName }, }); if (existing) { if (!existing.isActive) { return this.prisma.product.update({ where: { id: existing.id }, data: { isActive: true, deletedAt: null, name, canonicalName: name, }, }); } return existing; } if (!ownerId) { throw new Error('ownerId är obligatorisk för att skapa en produkt'); } return this.prisma.product.create({ data: { ownerId, name, normalizedName, canonicalName: name, isActive: true, deletedAt: null, ...(data.categoryId != null ? { categoryId: data.categoryId } : {}), }, }); } async update(id, data) { await this.findOne(id); const updateData = {}; if (typeof data.name === 'string') { const name = data.name.trim(); const normalizedName = (0, normalize_name_1.normalizeName)(name); const existing = await this.prisma.product.findUnique({ where: { normalizedName }, }); if (existing && existing.id !== id) { throw new Error('Det finns redan en annan produkt med detta namn. Välj ett unikt namn.'); } updateData.name = name; updateData.normalizedName = normalizedName; } if (typeof data.canonicalName === 'string') { updateData.canonicalName = data.canonicalName.trim() || undefined; } if (typeof data.category === 'string') { updateData.category = data.category.trim() || null; } if ('categoryId' in data) { updateData.categoryId = data.categoryId ?? null; } return this.prisma.product.update({ where: { id }, data: updateData, include: { tags: { include: { tag: true } }, nutrition: true }, }); } async updateCanonicalName(id, canonicalName) { await this.findOne(id); const cleaned = canonicalName.trim(); return this.prisma.product.update({ where: { id }, data: { canonicalName: cleaned, }, }); } async findDeleted() { return this.prisma.product.findMany({ where: { isActive: false }, orderBy: { deletedAt: 'desc' }, }); } async remove(id) { await this.findOne(id); return this.prisma.product.update({ where: { id }, data: { isActive: false, deletedAt: new Date(), }, }); } async permanentDelete(id) { await this.findOne(id); await this.prisma.productTag.deleteMany({ where: { productId: id } }); await this.prisma.userProduct.deleteMany({ where: { productId: id } }); return this.prisma.product.delete({ where: { id } }); } async restore(id) { const product = await this.findOne(id); if (product.isActive) { return product; } return this.prisma.product.update({ where: { id }, data: { isActive: true, deletedAt: null, }, }); } async findProductByIdOrThrow(id, label) { const product = await this.prisma.product.findUnique({ where: { id } }); if (!product) { throw new common_1.NotFoundException(`${label} product with id ${id} not found`); } return product; } async merge(sourceProductId, targetProductId) { if (sourceProductId === targetProductId) { throw new Error('sourceProductId och targetProductId kan inte vara samma'); } const source = await this.findProductByIdOrThrow(sourceProductId, 'Source'); const target = await this.findProductByIdOrThrow(targetProductId, 'Target'); return this.prisma.$transaction(async (tx) => { const movedInventoryCount = await tx.inventoryItem.updateMany({ where: { productId: sourceProductId }, data: { productId: targetProductId }, }); const softDeletedSource = await tx.product.update({ where: { id: sourceProductId }, data: { isActive: false, deletedAt: new Date(), }, }); return { message: 'Products merged successfully', sourceProductId, targetProductId, movedInventoryCount: movedInventoryCount.count, softDeletedSource, }; }); } async previewMerge(sourceProductId, targetProductId) { if (sourceProductId === targetProductId) { throw new Error('sourceProductId och targetProductId kan inte vara samma'); } const [source, target, sourceInventoryCount, targetInventoryCount] = await Promise.all([ this.findProductByIdOrThrow(sourceProductId, 'Source'), this.findProductByIdOrThrow(targetProductId, 'Target'), this.prisma.inventoryItem.count({ where: { productId: sourceProductId }, }), this.prisma.inventoryItem.count({ where: { productId: targetProductId }, }), ]); return { source: { ...source, inventoryCount: sourceInventoryCount, }, target: { ...target, inventoryCount: targetInventoryCount, }, outcome: { inventoryItemsToMove: sourceInventoryCount, sourceWillBeSoftDeleted: true, targetWillRemainActive: true, }, }; } async backfillCanonicalNames() { const products = await this.prisma.product.findMany({ where: { canonicalName: null, }, }); const results = await this.prisma.$transaction(products.map((product) => this.prisma.product.update({ where: { id: product.id }, data: { canonicalName: product.name, }, }))); return { message: 'Canonical names backfilled successfully', updatedCount: results.length, products: results, }; } async setTags(productId, tagNames) { await this.findOne(productId); const tags = await this.prisma.$transaction(tagNames.map((name) => this.prisma.tag.upsert({ where: { name }, create: { name }, update: {}, }))); await this.prisma.productTag.deleteMany({ where: { productId } }); if (tags.length > 0) { await this.prisma.productTag.createMany({ data: tags.map((tag) => ({ productId, tagId: tag.id })), skipDuplicates: true, }); } return this.prisma.product.findUnique({ where: { id: productId }, include: { tags: { include: { tag: true } }, nutrition: true }, }); } async upsertNutrition(productId, data) { await this.findOne(productId); return this.prisma.nutrition.upsert({ where: { productId }, create: { productId, ...data }, update: { ...data }, }); } async findAllTags() { return this.prisma.tag.findMany({ orderBy: { name: 'asc' } }); } async resetAll() { await this.prisma.$transaction([ this.prisma.receiptAlias.deleteMany(), this.prisma.inventoryConsumption.deleteMany(), this.prisma.inventoryItem.deleteMany(), this.prisma.pantryItem.deleteMany(), this.prisma.nutrition.deleteMany(), this.prisma.productTag.deleteMany(), this.prisma.tag.deleteMany(), this.prisma.userProduct.deleteMany(), this.prisma.recipeIngredient.deleteMany(), this.prisma.product.deleteMany(), ]); return { ok: true }; } async bulkUpdate(ids, data) { const updateData = {}; if ('categoryId' in data) { updateData.categoryId = data.categoryId; } if (Object.keys(updateData).length === 0) return { updated: 0 }; await this.prisma.product.updateMany({ where: { id: { in: ids } }, data: updateData }); return { updated: ids.length }; } async findUncategorized() { return this.prisma.product.findMany({ where: { isActive: true, categoryId: null, status: 'active' }, select: { id: true, name: true, canonicalName: true }, orderBy: { name: 'asc' }, }); } async aiCategorizeBulk(productIds) { const categories = await this.categoriesService.findFlattened(); let products; if (productIds && productIds.length > 0) { const found = await Promise.all(productIds.map((id) => this.findOne(id))); products = found.map((p) => ({ id: p.id, name: p.canonicalName ?? p.name })); } else { products = await this.findUncategorized(); } const results = []; 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; } async findPending() { return this.prisma.product.findMany({ where: { status: 'pending' }, include: { categoryRef: { include: { parent: true } }, owner: { select: { id: true, username: true } }, }, orderBy: { createdAt: 'desc' }, }); } async createPending(data, userId) { const name = data.name.trim(); const normalizedName = (0, normalize_name_1.normalizeName)(name); const existing = await this.prisma.product.findUnique({ where: { normalizedName }, }); if (existing) { if (existing.isActive && existing.status === 'active') { return existing; } if (existing.status === 'pending') { return existing; } } return this.prisma.product.create({ data: { name, normalizedName, canonicalName: name, isActive: false, status: 'pending', ownerId: userId, }, }); } setStatus(id, status) { return this.prisma.product.update({ where: { id }, data: { status } }); } }; exports.ProductsService = ProductsService; exports.ProductsService = ProductsService = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [prisma_service_1.PrismaService, ai_service_1.AiService, categories_service_1.CategoriesService]) ], ProductsService); //# sourceMappingURL=products.service.js.map