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.
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.
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.
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);}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.
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.
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.
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.