import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { ShoppingListItemResponse } from './dto/shopping-list-item.response'; @Injectable() export class ShoppingListService { constructor(private readonly prisma: PrismaService) {} async listOpen(userId: number): Promise { const rows = await this.prisma.shoppingListItem.findMany({ where: { userId, status: 'open' }, orderBy: [{ createdAt: 'desc' }], }); return rows.map((row) => this.toResponse(row)); } async updateCheckedStatus( userId: number, itemId: number, checked: boolean, ): Promise { const existing = await this.prisma.shoppingListItem.findUnique({ where: { id: itemId } }); if (!existing) { throw new NotFoundException('Inköpsrad hittades inte.'); } if (existing.userId !== userId) { throw new ForbiddenException('Du saknar åtkomst till denna inköpsrad.'); } const updated = await this.prisma.shoppingListItem.update({ where: { id: itemId }, data: { status: checked ? 'checked' : 'open', checkedAt: checked ? new Date() : null, }, }); return this.toResponse(updated); } async upsertFromFlyerSelections( sessionId: number, userId: number, itemIds?: number[], ): Promise<{ created: number; updated: number; processedSelectionIds: number[] }> { const selections = await this.prisma.flyerSelection.findMany({ where: { sessionId, userId, status: 'planned', ...(itemIds && itemIds.length > 0 ? { itemId: { in: itemIds } } : {}), }, include: { item: { select: { id: true, rawName: true, categoryId: true, matchedProductId: true, priceUnit: true, }, }, }, }); if (selections.length === 0) { return { created: 0, updated: 0, processedSelectionIds: [] }; } let created = 0; let updated = 0; await this.prisma.$transaction(async (tx) => { for (const selection of selections) { const quantity = selection.plannedQuantity ?? new Prisma.Decimal(1); const unit = selection.plannedUnit ?? selection.item.priceUnit ?? 'st'; const normalizedUnit = unit.trim() || 'st'; const productId = selection.item.matchedProductId ?? null; let existing: { id: number } | null = null; if (productId != null) { existing = await tx.shoppingListItem.findFirst({ where: { userId, status: 'open', productId, unit: normalizedUnit, }, select: { id: true }, }); } if (existing) { await tx.shoppingListItem.update({ where: { id: existing.id }, data: { quantity: { increment: quantity, }, name: selection.item.rawName, categoryId: selection.item.categoryId ?? undefined, source: 'flyer', }, }); updated += 1; continue; } await tx.shoppingListItem.create({ data: { userId, name: selection.item.rawName, productId, categoryId: selection.item.categoryId, quantity, unit: normalizedUnit, source: 'flyer', status: 'open', }, }); created += 1; } }); return { created, updated, processedSelectionIds: selections.map((selection) => selection.id), }; } private toResponse(row: { id: number; userId: number; name: string; productId: number | null; categoryId: number | null; quantity: Prisma.Decimal | null; unit: string | null; source: string; status: string; checkedAt: Date | null; createdAt: Date; updatedAt: Date; }): ShoppingListItemResponse { return { id: row.id, userId: row.userId, name: row.name, productId: row.productId, categoryId: row.categoryId, quantity: row.quantity == null ? null : Number(row.quantity), unit: row.unit, source: row.source, status: row.status, checkedAt: row.checkedAt?.toISOString() ?? null, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } }