Modelar recargas de operador sin columnas nulas ni un JSON imposible de consultar
Un catálogo de saldo, datos y bonos combinados revienta el clásico par importe-divisa. La propuesta pasa por separar precio de valor entregado y sacar los allowances a su propia tabla.

Un catálogo de recargas se rompe en cuanto alguien intenta representar cada producto como un importe más una divisa. El artículo que firma el equipo de MobileRings en Dev.to plantea lo contrario: separar el precio de compra del valor que recibe el destinatario, tipar los productos de forma explícita y sacar los allowances a su propia tabla. No hay nada revolucionario en el enfoque, pero el orden en que lo expone evita los dos extremos de siempre: la tabla con treinta columnas anulables y el bloque JSON que nadie consigue consultar.
El precio no es el producto
Dos productos pueden costar lo mismo y entregar cosas distintas. Uno cuesta 10 EUR y el destinatario recibe 10 EUR de saldo. Otro cuesta 10 EUR y entrega 8 GB válidos durante 14 días. Si el esquema asume que precio de compra y valor entregado son la misma cosa, el segundo caso no cabe en ninguna parte. La propuesta guarda ambos pares por separado: purchase_amount y purchase_currency por un lado, recipient_value y recipient_currency por otro, y deja los segundos en nulo cuando el producto es un bono.
A partir de ahí, cada producto lleva un product_type explícito: airtime, data_bundle, voice_bundle, sms_bundle o combo_bundle. El nombre queda como dato de presentación y el tipo como dato de aplicación. Deducir el comportamiento leyendo un texto que contiene "GB" funciona hasta que aparece un operador que escribe los minutos en el nombre o un paquete que regala gigas sin ser un bono de datos. Con el tipo declarado, filtrar, validar, testear y pintar la interfaz dejan de ser adivinanzas.
Allowances y vigencia fuera de la tabla principal
Un bono de 10 GB, 200 minutos y 100 SMS con 30 días de validez no se modela con columnas. La alternativa es una tabla product_allowances con product_id, allowance_type, amount y unit: tres filas para ese combo en lugar de tres columnas nuevas cada vez que un operador inventa un paquete. La vigencia sigue el mismo criterio. Se guarda como validity_value y validity_unit, o como un objeto anidado, y no escondida en la frase promocional de la descripción. Así se puede ordenar por duración, comparar bonos y avisar de los que caducan en tres días. Cuando un producto no tiene vigencia con sentido, mejor declararlo que rellenar el campo con cualquier cosa.
Identidad interna frente a identidad del proveedor
Los identificadores que devuelve el proveedor (ABC-UK-10, prod_847261, sku_2291) no deberían ser la clave principal. Quien integra renombra SKUs, migra APIs, reutiliza convenciones y devuelve duplicados entre entornos. La clave interna conviene que sea un UUID propio y el identificador externo, un campo más. El operador merece el mismo tratamiento: cuelga de país y producto, y necesita su propio mapeo externo para que dos proveedores que cubren al mismo operador no dupliquen el catálogo entero.
La tentación de meter todo en una columna JSON es real, y permite sacar la funcionalidad en una tarde. El problema llega con la primera consulta decente: todos los bonos de datos activos de al menos 5 GB, válidos 14 días o más, para un operador concreto. Con campos estructurados eso es una consulta; con JSON es código de aplicación recorriendo registros. Este tipo de decisiones de esquema sobreviven a la integración que las motivó, y cambiarlas después cuesta bastante más que pensarlas ahora.

