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:
@@ -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';
|
||||
|
||||
@@ -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>;
|
||||
|
||||
71
libs/shared/api-interface/src/lib/product.contract.ts
Normal file
71
libs/shared/api-interface/src/lib/product.contract.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user