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:
2026-05-27 23:45:11 +00:00
parent c36451dd33
commit 6769a0d82a
16 changed files with 1372 additions and 17 deletions

View File

@@ -2,3 +2,4 @@ export * from './lib/ping.contract';
export * from './lib/auth.contract';
export * from './lib/client.contract';
export * from './lib/order.contract';
export * from './lib/product.contract';

View File

@@ -91,3 +91,24 @@ export const OrderListResponseSchema = z.object({
limit: z.number().int().positive(),
});
export type OrderListResponse = z.infer<typeof OrderListResponseSchema>;
// ─── Create Order (POST /orders) ──────────────────────────────────────────────
export const CreateOrderItemSchema = z.object({
productCode: z.string().min(1),
productName: z.string().min(1),
productCategory: z.string().default('geral'),
quantity: z.number().positive(),
unitPrice: z.number().positive(),
discountPct: z.number().min(0).max(100).default(0),
});
export type CreateOrderItem = z.infer<typeof CreateOrderItemSchema>;
export const CreateOrderSchema = z.object({
clientId: z.string().uuid(),
discountPct: z.number().min(0).max(100).default(0), // desconto global do pedido
notes: z.string().optional(),
idempotencyKey: z.string().optional(),
items: z.array(CreateOrderItemSchema).min(1),
});
export type CreateOrder = z.infer<typeof CreateOrderSchema>;

View File

@@ -0,0 +1,71 @@
import { z } from 'zod';
// Contratos canônicos de C4 — Catálogo de Produtos.
// Consumidos pela API (output tipado) e pela Web (TanStack Query + parse).
// ─── Product Summary (lista) ──────────────────────────────────────────────────
export const ProductSummarySchema = z.object({
id: z.string().uuid(),
code: z.string(),
name: z.string(),
category: z.string(),
unitPrice: z.string(), // Decimal serializado
stock: z.string().nullable(),
active: z.boolean(),
});
export type ProductSummary = z.infer<typeof ProductSummarySchema>;
// ─── Product Detail ───────────────────────────────────────────────────────────
export const ProductDetailSchema = ProductSummarySchema.extend({
description: z.string().nullable(),
erpCode: z.string().nullable(),
syncedAt: z.iso.datetime().nullable(),
createdAt: z.iso.datetime(),
updatedAt: z.iso.datetime(),
});
export type ProductDetail = z.infer<typeof ProductDetailSchema>;
// ─── List query + response ────────────────────────────────────────────────────
export const ProductListQuerySchema = z.object({
q: z.string().optional(), // busca nome/código
category: z.string().optional(), // filtra por categoria
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(200).default(50),
});
export type ProductListQuery = z.infer<typeof ProductListQuerySchema>;
export const ProductListResponseSchema = z.object({
data: z.array(ProductSummarySchema),
total: z.number().int().nonnegative(),
page: z.number().int().positive(),
limit: z.number().int().positive(),
});
export type ProductListResponse = z.infer<typeof ProductListResponseSchema>;
// ─── Sync (importação da view do ERP) ────────────────────────────────────────
export const ProductSyncItemSchema = z.object({
code: z.string().min(1),
name: z.string().min(1),
description: z.string().optional(),
category: z.string().default('geral'),
unitPrice: z.number().positive(),
stock: z.number().nonnegative().optional(),
active: z.boolean().default(true),
erpCode: z.string().optional(),
});
export type ProductSyncItem = z.infer<typeof ProductSyncItemSchema>;
export const ProductSyncRequestSchema = z.object({
items: z.array(ProductSyncItemSchema).min(1).max(5000),
});
export type ProductSyncRequest = z.infer<typeof ProductSyncRequestSchema>;
export const ProductSyncResponseSchema = z.object({
upserted: z.number().int().nonnegative(),
syncedAt: z.iso.datetime(),
});
export type ProductSyncResponse = z.infer<typeof ProductSyncResponseSchema>;