Pular para o conteúdo principal

Orders (feature canon)

A feature de referência viva do template — quatro endpoints que demonstram o caminho completo command/query → handler → Result → HTTP. Schemas completos no spec OpenAPI.

Todos exigem autenticação + permissão (mapeada das roles via RolePermissions):

EndpointPermissãoSucesso
POST /v1/ordersOrders.Create201 { id }
GET /v1/ordersOrders.Read200 lista paginada
GET /v1/orders/{id}Orders.Read200 OrderResponse
PATCH /v1/orders/{id}/approveOrders.Approve204

Criar

POST /v1/orders
Content-Type: application/json

{ "customer": "ACME Ltda", "total": 1250.50 }

201 Created com { "id": "<guid>" }. Validação field-level (FluentValidation) responde 400 antes do handler.

Listar (paginado)

GET /v1/orders?pageNumber=1&pageSize=20&orderBy=createdAtUtc&order=Desc&filter=ACME

Parâmetros: pageNumber, pageSize, orderBy, order (direção), filter (texto), paginated (bool). Resposta:

{
"items": [
{ "id": "...", "customer": "ACME Ltda", "total": 1250.50, "status": "...", "createdAtUtc": "..." }
],
"pageNumber": 1,
"totalPages": 3,
"totalCount": 42,
"hasPreviousPage": false,
"hasNextPage": true
}

É o pattern MapPaginatedGet<TQuery, TDto> do DevPack — qualquer listagem nova do projeto ganha o mesmo contrato.

Detalhar

GET /v1/orders/{id}OrderResponse com a trilha de auditoria do AuditTimestamp:

{
"id": "...",
"customer": "ACME Ltda",
"total": 1250.50,
"status": "...",
"createdAtUtc": "2026-06-12T14:03:00Z",
"createdAtTimeZone": "America/Sao_Paulo",
"updatedAtUtc": null,
"updatedAtTimeZone": null,
"createdBy": "alexandre.lima",
"modifiedBy": null
}

Inexistente → 404 (Error.NotFound do handler).

Aprovar

PATCH /v1/orders/{id}/approve204 No Content. A transição de estado é um invariante de domínio (Order.Approve() retorna Result) — estado inválido responde o erro mapeado, não exception. O aggregate carrega RowVersion (concorrência otimista): escrita concorrente perde com DbUpdateConcurrencyException.