feat(c4): lançamento de pedido — catálogo, alçada por linha, POST /orders
- Prisma: Product + RepDiscountLimit + productCategory em OrderItem + migration - Seed: 28 produtos (5 categorias) + alçadas user-001 (default 10%, bebidas 8%, perecíveis 5%) - @sar/api-interface: ProductSummarySchema, ProductDetailSchema, ProductSyncRequestSchema, CreateOrderSchema - API: CatalogModule (GET /catalog, GET /catalog/:id, POST /catalog/sync) - API: POST /orders — valida alçada por linha/produto (OQ-2), idempotency-key (FR-4.3), desnorm cliente - Web: NewOrderPage (3 steps: catálogo → desconto/obs → confirmação) - Web: botão Novo Pedido na ClientDetailPage (desabilitado se financialStatus=blocked) - Web: rota /pedidos/novo com search param clientId Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
135
apps/api/src/app/catalog/catalog.service.ts
Normal file
135
apps/api/src/app/catalog/catalog.service.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type {
|
||||
ProductDetail,
|
||||
ProductListQuery,
|
||||
ProductListResponse,
|
||||
ProductSummary,
|
||||
ProductSyncRequest,
|
||||
ProductSyncResponse,
|
||||
} from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
|
||||
function decimalToString(v: Prisma.Decimal | null | undefined): string | null {
|
||||
return v ? v.toString() : null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CatalogService {
|
||||
constructor(private readonly cls: ClsService<WorkspaceClsStore>) {}
|
||||
|
||||
async list(query: ProductListQuery): Promise<ProductListResponse> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
const { q, category, page, limit } = query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: Prisma.ProductWhereInput = {
|
||||
deletedAt: null,
|
||||
active: true,
|
||||
...(category ? { category } : {}),
|
||||
...(q
|
||||
? {
|
||||
OR: [
|
||||
{ name: { contains: q, mode: 'insensitive' } },
|
||||
{ code: { contains: q, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.product.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
category: true,
|
||||
unitPrice: true,
|
||||
stock: true,
|
||||
active: true,
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: [{ category: 'asc' }, { name: 'asc' }],
|
||||
}),
|
||||
prisma.product.count({ where }),
|
||||
]);
|
||||
|
||||
const data: ProductSummary[] = rows.map((p) => ({
|
||||
id: p.id,
|
||||
code: p.code,
|
||||
name: p.name,
|
||||
category: p.category,
|
||||
unitPrice: decimalToString(p.unitPrice) ?? '0',
|
||||
stock: decimalToString(p.stock),
|
||||
active: p.active,
|
||||
}));
|
||||
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<ProductDetail | null> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
const p = await prisma.product.findFirst({ where: { id, deletedAt: null, active: true } });
|
||||
if (!p) return null;
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
code: p.code,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
category: p.category,
|
||||
unitPrice: decimalToString(p.unitPrice) ?? '0',
|
||||
stock: decimalToString(p.stock),
|
||||
active: p.active,
|
||||
erpCode: p.erpCode,
|
||||
syncedAt: p.syncedAt?.toISOString() ?? null,
|
||||
createdAt: p.createdAt.toISOString(),
|
||||
updatedAt: p.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async sync(req: ProductSyncRequest): Promise<ProductSyncResponse> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
const syncedAt = new Date();
|
||||
let upserted = 0;
|
||||
|
||||
for (const item of req.items) {
|
||||
await prisma.product.upsert({
|
||||
where: { code: item.code },
|
||||
create: {
|
||||
code: item.code,
|
||||
name: item.name,
|
||||
description: item.description ?? null,
|
||||
category: item.category ?? 'geral',
|
||||
unitPrice: item.unitPrice,
|
||||
stock: item.stock ?? null,
|
||||
active: item.active ?? true,
|
||||
erpCode: item.erpCode ?? null,
|
||||
syncedAt,
|
||||
},
|
||||
update: {
|
||||
name: item.name,
|
||||
description: item.description ?? null,
|
||||
category: item.category ?? 'geral',
|
||||
unitPrice: item.unitPrice,
|
||||
stock: item.stock ?? null,
|
||||
active: item.active ?? true,
|
||||
erpCode: item.erpCode ?? null,
|
||||
syncedAt,
|
||||
},
|
||||
});
|
||||
upserted++;
|
||||
}
|
||||
|
||||
return { upserted, syncedAt: syncedAt.toISOString() };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user