import { Injectable, NotFoundException, BadRequestException, } from '@nestjs/common'; import type { CreateProductRequest, ProductDto, UpdateProductRequest, } from '@mento/shared'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { InventoryService } from '../inventory/inventory.service'; type ProductRow = { id: string; kind: string; name: string; sku: string | null; description: string | null; category: string | null; unit: string | null; stockQty: number | null; brand: string | null; model: string | null; country: string | null; tags: string[]; unitPrice: Prisma.Decimal | null; currency: string; isActive: boolean; createdAt: Date; updatedAt: Date; }; @Injectable() export class ProductsService { constructor( private readonly prisma: PrismaService, private readonly inventory: InventoryService, ) {} async list( organizationId: string, opts?: { activeOnly?: boolean; q?: string; category?: string; kind?: string; }, ): Promise { const rows = await this.prisma.product.findMany({ where: { organizationId, ...(opts?.activeOnly ? { isActive: true } : {}), ...(opts?.category?.trim() ? { category: opts.category.trim() } : {}), ...(opts?.kind ? { kind: opts.kind } : {}), ...(opts?.q?.trim() ? { OR: [ { name: { contains: opts.q.trim(), mode: 'insensitive' } }, { sku: { contains: opts.q.trim(), mode: 'insensitive' } }, { description: { contains: opts.q.trim(), mode: 'insensitive', }, }, { category: { contains: opts.q.trim(), mode: 'insensitive', }, }, ], } : {}), }, orderBy: [{ isActive: 'desc' }, { name: 'asc' }], }); return rows.map((row) => this.toDto(row)); } async get(organizationId: string, id: string): Promise { const row = await this.prisma.product.findFirst({ where: { id, organizationId }, }); if (!row) throw new NotFoundException('محصول پیدا نشد'); return this.toDto(row); } async create( organizationId: string, dto: CreateProductRequest, ): Promise { const name = dto.name.trim(); if (!name) throw new BadRequestException('نام محصول الزامی است'); const row = await this.prisma.product.create({ data: { organizationId, kind: dto.kind === 'service' ? 'service' : 'product', name, sku: emptyToNull(dto.sku), description: emptyToNull(dto.description), category: emptyToNull(dto.category), unit: emptyToNull(dto.unit) ?? 'عدد', stockQty: normalizeStock(dto.stockQty), brand: emptyToNull(dto.brand), model: emptyToNull(dto.model), country: emptyToNull(dto.country), tags: normalizeTags(dto.tags), unitPrice: dto.unitPrice == null || Number.isNaN(Number(dto.unitPrice)) ? null : new Prisma.Decimal(dto.unitPrice), currency: (dto.currency ?? 'IRR').trim() || 'IRR', isActive: dto.isActive ?? true, }, }); await this.inventory.onProductCreated( organizationId, row.id, row.stockQty, row.kind, ); return this.toDto(row); } async update( organizationId: string, id: string, dto: UpdateProductRequest, ): Promise { await this.ensureExists(organizationId, id); const data: Prisma.ProductUpdateInput = {}; if (dto.kind !== undefined) { data.kind = dto.kind === 'service' ? 'service' : 'product'; } if (dto.name !== undefined) { const name = dto.name.trim(); if (!name) throw new BadRequestException('نام محصول الزامی است'); data.name = name; } if ('sku' in dto) data.sku = emptyToNull(dto.sku); if ('description' in dto) data.description = emptyToNull(dto.description); if ('category' in dto) data.category = emptyToNull(dto.category); if ('unit' in dto) data.unit = emptyToNull(dto.unit) ?? 'عدد'; if ('stockQty' in dto) data.stockQty = normalizeStock(dto.stockQty); if ('brand' in dto) data.brand = emptyToNull(dto.brand); if ('model' in dto) data.model = emptyToNull(dto.model); if ('country' in dto) data.country = emptyToNull(dto.country); if (dto.tags !== undefined) data.tags = normalizeTags(dto.tags); if ('unitPrice' in dto) { data.unitPrice = dto.unitPrice == null || Number.isNaN(Number(dto.unitPrice)) ? null : new Prisma.Decimal(dto.unitPrice); } if (dto.currency !== undefined) { data.currency = dto.currency.trim() || 'IRR'; } if (dto.isActive !== undefined) data.isActive = dto.isActive; const row = await this.prisma.product.update({ where: { id }, data }); if ('stockQty' in dto) { await this.inventory.onProductStockEdited( organizationId, row.id, row.stockQty, row.kind, ); } return this.toDto(row); } async remove(organizationId: string, id: string): Promise { await this.ensureExists(organizationId, id); await this.prisma.product.delete({ where: { id } }); } async ensureInOrg(organizationId: string, productId: string): Promise { await this.ensureExists(organizationId, productId); } private async ensureExists(organizationId: string, id: string) { const found = await this.prisma.product.findFirst({ where: { id, organizationId }, select: { id: true }, }); if (!found) throw new NotFoundException('محصول پیدا نشد'); } private toDto(row: ProductRow): ProductDto { return { id: row.id, kind: row.kind === 'service' ? 'service' : 'product', name: row.name, sku: row.sku, description: row.description, category: row.category, unit: row.unit, stockQty: row.stockQty, brand: row.brand, model: row.model, country: row.country, tags: row.tags ?? [], unitPrice: row.unitPrice == null ? null : Number(row.unitPrice), currency: row.currency, isActive: row.isActive, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } } function emptyToNull(value?: string | null): string | null { if (value == null) return null; const trimmed = value.trim(); return trimmed.length ? trimmed : null; } function normalizeStock(value?: number | null): number | null { if (value == null) return null; const n = Number(value); if (!Number.isFinite(n) || n < 0) return null; return Math.floor(n); } function normalizeTags(tags?: string[] | null): string[] { if (!tags?.length) return []; const out: string[] = []; for (const t of tags) { const s = String(t ?? '').trim(); if (s && !out.includes(s)) out.push(s.slice(0, 40)); } return out.slice(0, 20); }