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):
| Endpoint | Permissão | Sucesso |
|---|---|---|
POST /v1/orders | Orders.Create | 201 { id } |
GET /v1/orders | Orders.Read | 200 lista paginada |
GET /v1/orders/{id} | Orders.Read | 200 OrderResponse |
PATCH /v1/orders/{id}/approve | Orders.Approve | 204 |
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}/approve → 204 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.