import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; import { AccountingHooksService } from '../accounting-sync/accounting-hooks.service'; import { CatalogCacheService } from './catalog-cache.service'; @Injectable() export class CatalogService { constructor( private readonly prisma: PrismaService, private readonly accounting: AccountingHooksService, private readonly catalogCache: CatalogCacheService, ) {} /** Served from CatalogCacheService; see there for what invalidates it. */ listProducts() { return this.catalogCache.get(() => this.buildProductList()); } private async buildProductList() { const products = await this.prisma.product.findMany({ // D27 — a product with no image is never shown in the app. Products now // also arrive from the accounting software, which carries no pictures; // they appear once an admin photographs them. where: { isActive: true, images: { some: {} } }, include: { // Only orderable variants reach the storefront. An inactive one used to // be listed and then rejected by getVariantOrThrow at checkout, so the // customer could pick a chip and get a 404 on the order. variants: { where: { isActive: true }, // Each variant carries its own gallery picture, so the app can render // the chip without a second request per variant. include: { image: true }, }, // Explicit order: the UI treats images[0] as the product's main photo, // which is only meaningful if the rows come back sorted. images: { orderBy: { sortOrder: 'asc' } }, category: true, brand: true, collections: { include: { collection: true } }, }, }); const stockBlocked = await this.accounting.stockBlocker(); const out = []; for (const p of products) { const { accountingStockWeight, accountingStockCount, accountingStockAt, ...product } = p; // D27 — out of stock in the accounting software: every variant reads as // qty 0, which the apps already render as «ناموجود» and refuse to add to // the cart unless it is a pre-order variant. The stock figures themselves // stay server-side; this endpoint is public. const blocked = stockBlocked({ accountingStockWeight, accountingStockCount, accountingStockAt }); out.push(blocked ? { ...product, variants: product.variants.map((v) => (v.isPreOrder ? v : { ...v, qty: 0 })) } : product); } return out; } async getVariantOrThrow(variantId: string) { const variant = await this.prisma.productVariant.findUnique({ where: { id: variantId }, include: { product: { select: { id: true, accountingStockWeight: true, accountingStockCount: true, accountingStockAt: true, _count: { select: { images: true } }, }, }, }, }); if (!variant || !variant.isActive) { throw new NotFoundException(`Product variant ${variantId} not found or inactive`); } // The same two rules the listing applies, so a stale client cannot order // around them. if (variant.product._count.images === 0) { throw new BadRequestException('این محصول هنوز تصویر ندارد و قابل سفارش نیست'); } // Live from the accounting software at order time; on no answer the last // stored figure decides (liveStock). if (!variant.isPreOrder && (await this.accounting.stockBlocked(await this.accounting.liveStock(variant.product)))) { throw new BadRequestException('این محصول در انبار موجود نیست'); } const { product: _product, ...plain } = variant; return plain; } }