Diário
Engenharia3 min de leitura

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.

AEA equipa de engenharia do Bookatu

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.

REBUILT ON THE SERVER, EVERY BOOKINGBase priceStaff priceCategory saleBest couponCharged
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);}
1Behind a flagoff by default2Merge & deploystill off3Audit passes0 money bugs4Flip the flagthe go-liveLive
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.

BROWSERShows a price so the clientknows what they will pay.A display only. Never trusted.requestSERVER · SOURCE OF TRUTH1 · Load the real service2 · Resolve staff + sale price3 · Apply the best coupon4 · 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.

New pricing featureOvercharges?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.

feature flagscódigo seguro com dinheiroarquitetura do motor de preçosvalidação no servidorfronteira de confiança
Pronto para pôr isto em prática?

O Bookatu dá-lhe um site de marcações com a sua marca, sinais, subscrições, vales-presente e lembretes, com 0% de comissão nas suas marcações.

Começar grátis