Skip to content

feat(orders): cart and quote totals, computed server-side - #18

Merged
ArtRiv merged 1 commit into
mainfrom
feature/cart-totals
Aug 27, 2026
Merged

ArtRiv merged 1 commit into
mainfrom
feature/cart-totals

Conversation

@ArtRiv

@ArtRiv ArtRiv commented Aug 26, 2026

Copy link
Copy Markdown
Owner

O que muda

GET /cart devolvia { items } e mais nada, e POST /shipping/quote
devolvia só a lista de opções. Qualquer vitrine tinha que somar
priceCents × quantity no navegador pra montar carrinho, revisão de
checkout ou badge — aritmética de dinheiro no cliente, que este projeto
trata como buraco de backend e não como exercício de front-end.

Tudo aqui é aditivo: nenhum campo existente muda de significado,
nenhum consumidor quebra.

Por quê

Toda loja que implantar este core tem um carrinho com um total dentro. E
orderTotalCents na cotação é o número que o botão de "Finalizar pedido"
exibe antes de existir pedido — o mesmo que o POST /orders vai
cobrar, o que torna qualquer divergência entre os dois um bug com teste
em cima.

Superfície nova

CartResponse (GET /cart, e as quatro rotas de escrita, que já
devolviam o carrinho inteiro):

{
  "items": [ /* inalterado */ ],
  "itemsSubtotalCents": 12480, // soma de priceCents × quantity, preço VIVO
  "itemCount": 3               // soma das quantidades — peças, não linhas
}

ShippingQuoteResponse (POST /shipping/quote):

{
  "options": [
    {
      "code": "padrao-brasil",
      "label": "Entrega padrão",
      "priceCents": 2990,
      "estimatedDays": 10,
      "carrier": null,
      "orderTotalCents": 52730 // itemsSubtotalCents + priceCents
    }
  ],
  "itemsSubtotalCents": 49740  // presente mesmo com options vazia
}

Carrinho vazio devolve 0 nos dois totais — nunca null, nunca ausente.
GET /cart não ganhou total do pedido: sem CEP não há frete, e um
"total" sem frete é exatamente o número que um checkout não pode exibir.

Critérios de aceitação

Todos verdes (docs/specs/cart-totals.md):

  • 2 × R$ 49,90 + 1 × R$ 25,00 ⇒ itemsSubtotalCents: 12480, itemCount: 3
  • Carrinho vazio (ou inexistente) ⇒ 0 nos dois, presentes
  • Preço alterado depois da linha entrar ⇒ subtotal segue o preço novo
  • DELETE /cart ⇒ items: [] e os dois totais em 0
  • POST/PATCH/DELETE de item ⇒ totais já refletindo a mudança
  • Subtotal da cotação == subtotal de GET /cart
  • orderTotalCents == itemsSubtotalCents + priceCents em toda opção
  • Opção com priceCents: 0 ⇒ orderTotalCents == itemsSubtotalCents
  • orderTotalCents da opção escolhida == totalCents do pedido criado
  • Carrinho vazio na cotação continua 409
  • openapi.json regenerado descreve os três campos

Nota de implementação

itemsSubtotalCents saiu de shipping-quote.service.ts para
src/orders/money.ts (junto com itemCount, novo). CartService passou
a precisar da soma e ShippingQuoteService já depende de CartService:
deixar a função onde estava fecharia um ciclo de import em runtime, porque
o design:paramtypes do construtor é referência de valor, não só de tipo.
Continua havendo uma definição de subtotal, usada por carrinho, cotação
e checkout.

Verificação

pnpm lint:check ✅
pnpm typecheck ✅
pnpm test ✅ 462 unitários (eram 457)
pnpm build ✅
pnpm test:e2e ✅ 179 testes, 8 arquivos

O e2e rodou contra o Supabase de desenvolvimento (commerce-core-dev),
confirmado antes de rodar — a suíte dá TRUNCATE no banco pra onde
DATABASE_URL aponta.

O caso de frete grátis é coberto no unitário e não no e2e: a tabela de
frete é lida do ambiente no boot, então uma opção gratuita exigiria subir
um segundo app com outra SHIPPING_TABLE.

Sem migration — os três números saem de dados que já estão em memória no
momento da resposta.

GET /cart returned `{ items }` and nothing else, so every storefront had
to sum priceCents × quantity itself. Money arithmetic in a client is a
backend gap, not a frontend puzzle — and every store that deploys this
core has a cart with a total in it.

GET /cart gains `itemsSubtotalCents` (live catalogue prices, never a
snapshot — the cart still freezes nothing) and `itemCount` (pieces, not
lines). An empty cart answers 0 for both, never null.

POST /shipping/quote gains `itemsSubtotalCents` and, on each option,
`orderTotalCents` — the exact amount POST /orders will charge if that
option is chosen. That is what lets a checkout button render "Finalizar
pedido — R$ 522,30" before an order exists, and an e2e pins it against
the totalCents of the order actually created, because any drift between
those two numbers is a bug worth catching.

itemsSubtotalCents moved out of shipping-quote.service.ts into
src/orders/money.ts: CartService now needs it and ShippingQuoteService
already depends on CartService, so leaving it there would close a runtime
import cycle through design:paramtypes.

Both changes are additive — no existing field changes meaning.

See docs/specs/cart-totals.md.
@ArtRiv
ArtRiv merged commit f755a2b into main Aug 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant