Files
tehran_tala/apps/api/src/modules/catalog/catalog.service.ts
T

90 lines
3.8 KiB
TypeScript

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;
}
}