---
title: "Cómo publicamos funciones que tocan dinero sin cambiarle el precio a nadie sin querer"
description: "Un fallo en los precios es de los que no se pueden deshacer una vez que has cobrado una tarjeta. Esta es la disciplina que usamos para añadir descuentos, precios por profesional y ofertas al flujo de reservas sin que se mueva un solo precio hasta que queremos."
url: "https://bookatu.com/es-es/blog/shipping-money-features-safely"
language: "es"
section: "Blog"
category: "Ingeniería"
author: "El equipo de ingeniería de Bookatu"
published: "2026-07-01"
updated: "2026-07-01"
tags: ["feature flags", "código seguro con el dinero", "diseño del motor de precios", "validación en el servidor", "frontera de confianza"]
---

# Cómo publicamos funciones que tocan dinero sin cambiarle el precio a nadie sin querer

Un fallo en los precios es de los que no se pueden deshacer una vez que has cobrado una tarjeta. Esta es la disciplina que usamos para añadir descuentos, precios por profesional y ofertas al flujo de reservas sin que se mueva un solo precio hasta que queremos.

> **En resumen**
>
> - Todo lo que pueda mover un precio se publica detrás de un interruptor que está apagado por defecto. El código está en producción pero no hace nada hasta que lo encendemos.
> - El precio final se vuelve a calcular en el servidor al pagar. Nunca se confía en un número que llegó desde el navegador.
> - Las cuentas del descuento viven en una única función pura pequeña con sus propias pruebas, así se puede razonar sobre ella por separado.
> - Antes de que una función de precios se active, una tanda de comprobaciones hostiles intenta hacer que cobre de más, cobre de menos o baje de cero.

Casi todos los fallos te cuestan una disculpa. Un fallo de precios te cuesta una devolución, un contracargo y un cliente que ya no se fía del número que ve en pantalla. El riesgo es asimétrico, así que el proceso alrededor de cualquier cosa que toque dinero es deliberadamente más lento que el resto de nuestro trabajo. Esta es su forma, con una función reciente como ejemplo: una oferta por categoría, donde un salón puede poner todos sus faciales en oferta de una 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

*El precio de cada reserva se construye siempre en el mismo orden fijo, y la suma entera se rehace en el servidor.*

## Apagado por defecto, en producción, sin hacer nada

La primera línea de la función es un interruptor. Lee una variable de entorno y por defecto está apagado. Eso significa que podemos fusionar y desplegar el código mucho antes de que haga nada, y cada precio sigue siendo idéntico al del día anterior, byte a byte. No hay un gran lanzamiento por el que ponerse nervioso, porque el lanzamiento es un paso aparte y aburrido que damos cuando ya estamos seguros.

```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

*El lanzamiento es un paso aparte y aburrido. El código se publica mucho antes de hacer nada.*

## Una función, un trabajo

La aritmética del descuento vive en una única función pura. Recibe céntimos y un porcentaje, y devuelve céntimos. No tiene base de datos, ni petición, ni reloj, lo que hace que sea trivial probar cada uno de sus límites por separado: una oferta a cero, una oferta normal, un tope arriba, un negativo que nunca debería pasar. Cuando las cuentas son una función pequeña que te cabe en la cabeza, dejas de hacer suposiciones sobre ella.

```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 en el servidor, siempre

El navegador muestra un precio para que el cliente sepa lo que va a pagar, pero ese número no es más que una pantalla. Cuando entra una reserva, el servidor carga el servicio real, resuelve el precio del profesional, aplica cualquier oferta de categoría activa y luego calcula el descuento, todo con datos que ha traído él mismo. Si alguien manipuló la petición por el camino, no cambia nada, porque el precio que se cobra a la tarjeta nunca fue el precio que mandó el navegador. Esta es la costumbre más importante en el código que toca dinero: la frontera de confianza es el servidor, y el servidor hace la suma.

*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

*El número que muestra el navegador es solo una pantalla. El servidor trae sus propios datos y rehace la suma.*

## Intenta romperlo antes de que lo haga un cliente

Las pruebas que pasan te dicen que funcionan los casos en los que pensaste. No te dicen nada del caso que se te escapó. Así que antes de encender un interruptor de precios hacemos una pasada hostil cuyo único trabajo es encontrar la manera de que el dinero salga mal. Cada comprobación ataca desde un ángulo distinto: una busca cualquier camino que cobre de más al cliente, otra busca un camino que le haga perder dinero al negocio, otra comprueba que la función esté genuinamente inerte con el interruptor apagado, y otra va a por el redondeo y las desviaciones en los límites. Una oferta encima de un cupón encima de una señal es justo donde se esconde una fracción de céntimo, así que ahí es donde miramos con más lupa.

*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

*Cuatro escépticos, cada uno una forma distinta de equivocarse. Solo un pleno limpio es un GO.*

Nada de esto es ingenioso. Es un interruptor, una función pequeña, un servidor que hace sus propias cuentas y la costumbre de intentar romper tu propio trabajo antes de publicarlo. Todo junto, es la diferencia entre añadir una función que toca dinero y perder el sueño por ella.
