---
title: "Como lançamos funcionalidades que mexem em dinheiro sem mudar o preço de ninguém por engano"
description: "Um erro de preços é daqueles que não se podem retirar depois de um cartão ser cobrado. Eis a disciplina que usamos para acrescentar descontos, preços por profissional e promoções ao fluxo de reservas sem que um único preço se mexa antes de nós querermos."
url: "https://bookatu.com/pt-pt/blog/shipping-money-features-safely"
language: "pt"
section: "Blog"
category: "Engenharia"
author: "A equipa de engenharia do Bookatu"
published: "2026-07-01"
updated: "2026-07-01"
tags: ["feature flags", "código seguro com dinheiro", "arquitetura do motor de preços", "validação no servidor", "fronteira de confiança"]
---

# Como lançamos funcionalidades que mexem em dinheiro sem mudar o preço de ninguém por engano

Um erro de preços é daqueles que não se podem retirar depois de um cartão ser cobrado. Eis a disciplina que usamos para acrescentar descontos, preços por profissional e promoções ao fluxo de reservas sem que um único preço se mexa antes de nós querermos.

> **A versão curta**
>
> - Tudo o que possa mexer num preço é lançado atrás de um interruptor que está desligado por omissão. O código está em produção mas não faz nada até o ligarmos.
> - O preço final é calculado outra vez no servidor no momento do pagamento. Um número que veio do navegador nunca é de confiança.
> - As contas do desconto vivem numa pequena função pura, com testes próprios, para que se possa raciocinar sobre ela isoladamente.
> - Antes de uma funcionalidade de preços entrar em produção, um conjunto de verificações adversariais tenta fazê-la cobrar a mais, cobrar a menos ou descer abaixo de zero.

A maioria dos erros custa-lhe um pedido de desculpa. Um erro de preços custa-lhe um reembolso, uma devolução no cartão e um cliente que deixou de confiar no número que aparece no ecrã. O risco é desequilibrado, por isso o processo à volta de tudo o que toca em dinheiro é deliberadamente mais lento do que o resto do nosso trabalho. É esta a forma que ele tem, usando uma funcionalidade recente como exemplo: uma promoção por categoria, em que um salão pode pôr todos os tratamentos de rosto em promoção de uma só vez.

*Diagram: The price pipeline: base price, then staff price, then category sale, then the best coupon, then the amount charged*

Diagram labels: REBUILT ON THE SERVER, EVERY BOOKING · Base price · Staff price · Category sale · Best coupon · Charged

*O preço de cada reserva é construído sempre pela mesma ordem fixa, e a soma inteira é refeita no servidor.*

## Desligado por omissão, em produção, sem fazer nada

A primeira linha da funcionalidade é um interruptor. Lê uma variável de ambiente e fica desligado por omissão. Isso significa que podemos integrar e publicar o código muito antes de ele fazer o que quer que seja, e todos os preços ficam exatamente iguais ao dia anterior. Não há um lançamento em grande com que ficar nervoso, porque o lançamento é um passo separado e aborrecido que damos quando já estamos confiantes.

```ts
export const FEATURE_LIVE =
  process.env.FEATURE_LIVE === "true";

// Read at the one place it matters. When the flag is off, the branch
// is skipped entirely: no extra query, no price change, nothing.
if (FEATURE_LIVE) {
  const pct = await activeCategorySalePercent(orgId, "service", service.category);
  if (pct > 0) fullPriceCents = applyCategorySaleCents(fullPriceCents, pct);
}
```

*Diagram: The flag lifecycle: written behind a flag that is off by default, merged and deployed while still off, then after the audit passes the flag is flipped and the feature goes live*

Diagram labels: 1 · Behind a flag · off by default · 2 · Merge & deploy · still off · 3 · Audit passes · 0 money bugs · 4 · Flip the flag · the go-live · Live

*O lançamento é um passo separado e aborrecido. O código sai muito antes de fazer alguma coisa.*

## Uma função, um trabalho

A aritmética do desconto vive numa única função pura. Recebe cêntimos e uma percentagem e devolve cêntimos. Não tem base de dados, não tem pedido e não tem relógio, o que significa que é trivial testar todos os extremos isoladamente: uma promoção a zero, uma promoção normal, um limite no topo, um valor negativo que nunca deveria acontecer. Quando as contas cabem numa pequena função que se consegue ter toda na cabeça, deixa-se de andar a adivinhar.

```ts
export function applyCategorySaleCents(
  baseCents: number,
  salePercent: number | null | undefined,
): number {
  const base = cents(baseCents);
  const pct = Math.max(0, Math.min(100, Math.round(salePercent ?? 0)));
  if (pct === 0) return base;
  return cents((base * (100 - pct)) / 100);
}
```

## Recalcular no servidor, sempre

O navegador mostra um preço para o cliente saber quanto vai pagar, mas esse número é apenas visual. Quando chega uma reserva, o servidor carrega o serviço verdadeiro, resolve o preço do profissional, aplica qualquer promoção de categoria ativa e só depois calcula o desconto, tudo a partir de dados que ele próprio foi buscar. Se alguém tiver editado o pedido pelo caminho, não muda nada, porque o preço cobrado ao cartão nunca foi o preço que o navegador enviou. Este é o hábito mais importante em código que lida com dinheiro: a fronteira de confiança é o servidor, e é o servidor que faz a conta.

*Diagram: The browser only displays a price; the server independently loads the service, resolves the staff and sale price, applies the best coupon, and charges that amount*

Diagram labels: BROWSER · Shows a price so the client · knows what they will pay. · A display only. Never trusted. · request · SERVER · SOURCE OF TRUTH · 1 · Load the real service · 2 · Resolve staff + sale price · 3 · Apply the best coupon · 4 · Charge exactly that

*O número que o navegador mostra é apenas visual. O servidor vai buscar os seus próprios dados e refaz a conta.*

## Tente parti-lo antes que um cliente o faça

Testes que passam dizem-lhe que os casos em que pensou funcionam. Não lhe dizem nada sobre o caso que lhe escapou. Por isso, antes de ligarmos um interruptor de preços, fazemos uma passagem adversarial cuja única função é encontrar forma de deixar o dinheiro errado. Verificações separadas atacam cada uma por um ângulo diferente: uma procura qualquer caminho que cobre a mais ao cliente, outra procura um caminho que faça o negócio perder dinheiro, outra confirma que a funcionalidade está mesmo inerte com o interruptor desligado e outra vai atrás dos arredondamentos e dos desvios nos limites. Uma promoção sobreposta a um cupão sobre um sinal é exatamente onde se esconde um cêntimo fracionado, por isso é aí que olhamos com mais atenção.

*Diagram: One new pricing feature is checked by four adversarial skeptics — does it overcharge, lose money, leak when the flag is off, or drift on rounding — and only a clean sweep is a go*

Diagram labels: New pricing feature · Overcharges? · Loses money? · Leaks when off? · Rounding drift? · Clean sweep → GO

*Quatro céticos, cada um uma forma diferente de estar errado. Só uma ronda limpa dá luz verde.*

Nada disto é inteligente. É um interruptor, uma função pequena, um servidor que faz as suas próprias contas e o hábito de tentar partir o nosso próprio trabalho antes de o lançar. Tudo junto, é a diferença entre acrescentar uma funcionalidade que mexe em dinheiro e perder o sono por causa dela.
