26 jul 2025·8 min de lectura

Modelo de datos para app de facturación: clientes, facturas y totales fiables

Modelo de datos para app de facturación: modela clientes, facturas, partidas y estado de pagos para que los totales sean correctos, auditables y fáciles de mantener.

Modelo de datos para app de facturación: clientes, facturas y totales fiables

Por qué los totales de facturación fallan en apps reales

Los totales de las facturas suelen verse bien el primer día. Se rompen al décimo día, cuando la gente real empieza a editar facturas, aplicar descuentos, registrar pagos parciales y pedir reembolsos.

La mayoría de los problemas vienen de no tener una fuente de la verdad clara para los totales. Una pantalla generada por IA podría guardar total = 120.00 en la factura mientras que las partidas suman 119.99 después del redondeo. Más tarde, alguien edita una cantidad y el total almacenado no se actualiza. Ahora el PDF, la base de datos y el registro de pago no coinciden.

“Totales correctos” es más que matemáticas básicas. Significa que tu app puede reproducir los mismos números cada vez usando las mismas reglas, incluso meses después. También significa que los totales coinciden con lo que el usuario vio cuando envió la factura, incluidos impuestos, descuentos y ajustes manuales.

Otra causa es la pérdida de historial. Si sobrescribes una factura en el mismo registro, no puedes responder preguntas básicas más tarde: ¿qué cambió?, ¿quién lo cambió?, ¿el cambio ocurrió antes o después de un pago o reembolso?, ¿qué versión fue la que realmente se envió?

Un escenario pequeño muestra lo rápido que esto se rompe: envías una factura por 10 horas de trabajo. El cliente paga la mitad. Luego corriges las horas a 9.5. Si tu modelo de datos no separa versiones de factura de los pagos, tu app puede mostrar al cliente como sobrepagado, con pago incompleto o totalmente pagado según la pantalla que abra.

Guardar un único campo “total” parece rápido al principio, pero crea dolor cuando añades ediciones, pagos parciales, créditos y reembolsos.

Entidades principales que necesitas (y qué significa cada una)

Un modelo de datos fiable para facturación separa a quién facturas, qué facturas y qué pasó después de enviar la factura. Si se mezclan, los totales y los estados se desvían.

Contactos: el cliente no siempre es quien paga

Un Customer es la cuenta con la que haces negocios (una empresa o persona). Mantén los contactos separados para poder cambiar dónde se envían las facturas sin modificar el registro del cliente.

Una configuración común es un registro de cliente de larga duración (nombre, moneda por defecto, ID fiscal, notas) más contactos de facturación y envío separados. Así, “Enviar facturas a contabilidad, pero enviar al almacén” no se convierte en direcciones sobrescritas.

Facturas y partidas: lo que enviaste vs lo que estás editando

Una Invoice es el documento que piensas enviar, con un cliente, fecha de emisión, fecha de vencimiento, moneda y número de factura. Trata borrador vs enviado como fases de la misma factura. Si permites cambios después de enviar, añade una Invoice Version (o revisión) para conservar un historial limpio.

Para los cargos, trata las Line Items como la fuente de la verdad para los importes. Una partida puede referenciar un Product/Service del catálogo o ser un ítem personalizado en texto libre. No fuerces todo en una tabla de productos, y no almacenes totales extra en las partidas salvo que tengas una razón clara.

Eventos de dinero: pagos no son reembolsos

Un Payment registra dinero recibido y cómo se aplicó a una o varias facturas. Un Refund es dinero devuelto, normalmente vinculado a un pago. (Un Payout es dinero que envías y a menudo no está relacionado con facturas.)

Mantén los estados de ciclo de vida mínimos y estables (draft, sent, void). Trata “paid/overdue/partially paid” como derivados de los pagos y las fechas, no como un campo editado manualmente.

Paso a paso: diseña el esquema antes de construir pantallas

Si construyes la interfaz primero, tus tablas tienden a copiar lo que la primera pantalla necesitó. Así es como un modelo de datos de facturación acaba con huecos: estado poco claro, historial faltante o facturas que se pueden editar hasta volverse incoherentes.

Empieza por el ciclo de vida de la factura porque determina tus reglas y tus datos. Un ciclo simple funciona para muchas apps: draft (no final), sent (el cliente la ve), paid (liquidada), void (cancelada y no cobrable). Decide qué cambios están permitidos en cada paso antes de nombrar una sola columna.

Un orden práctico:

  • Define estados y transiciones (draft -> sent -> paid, y void desde draft o sent).
  • Usa IDs internos inmutables (como UUIDs) y mantén los números de factura separados para humanos.
  • Decide qué queda bloqueado después de enviar (partidas, tasas de impuesto, dirección de facturación).
  • Lista los campos mínimos requeridos por tabla.
  • Toma la decisión sobre moneda temprano (moneda única vs multi-moneda real).

Para IDs, usa una clave interna inmutable por cada registro. Luego añade un invoice_number que sea único, legible y que nunca cambie una vez enviada. Los usuarios citarán el número de factura, no tu clave primaria.

Sé explícito sobre la editabilidad. Puede que permitas corregir una errata en el nombre del cliente después de enviar, pero no cambiar cantidades o precios. Si quieres permitir cambios monetarios, módelos como una nueva versión, una nota de crédito o un ajuste, no como una reescritura silenciosa.

Los campos mínimos viables pueden ser pequeños:

  • Customer: id, name, email, billing_address
  • Invoice: id, customer_id, invoice_number, status, issued_at, currency
  • Line item: id, invoice_id, description, quantity, unit_price
  • Payment (si lo rastreas): id, invoice_id, amount, received_at

Las apps de moneda única pueden almacenar dinero como unidades menores enteras (por ejemplo, céntimos). Las apps multi-moneda necesitan moneda en cada campo monetario y reglas claras de tipo de cambio.

Cómo mantener los totales correctos y consistentes

La mayoría de los bugs de “total incorrecto” vienen de propiedad poco clara. Si tu UI edita una partida, el total de la factura debe cambiar en un lugar predecible, cada vez. Decide temprano qué se calcula, qué se almacena y cuándo los valores se vuelven definitivos.

Empieza en la partida. Almacena las entradas crudas (unit_price y quantity) y almacena el line_total que tu app calculó en ese momento. Usa unidades menores enteras (como céntimos) para cada campo monetario, incluidos descuentos y montos de impuestos. Esto evita sorpresas de punto flotante como 19.999999 que aparezcan como 20.00.

Una regla útil: calcula a partir de los campos crudos, guarda lo que muestras y sé consistente sobre cuándo recomputas.

Elige una fuente de la verdad clara

Tienes tres opciones prácticas. Mezclarlas sin una regla crea deriva:

  • Calcular totales al vuelo a partir de las partidas cada vez que cargas una factura.
  • Almacenar totales de factura y actualizarlos en cada edición.
  • Hacer ambas cosas: calcular, guardar y luego validar.

Si haces ambas, trata los valores calculados como el validador. Cuando no concuerden, márcalo en vez de elegir silenciosamente uno.

Manejar ediciones sin reescribir la historia

Decide cuándo una factura es editable. Un enfoque común: los borradores son editables, las facturas enviadas están bloqueadas y los cambios se realizan mediante una nueva versión, una nota de crédito o una línea de ajuste.

Ejemplo: enviaste una factura con una tasa de impuesto del 7.5%. Un mes después la tasa cambia. Si recomputas usando la tasa de hoy, el total de la factura antigua cambia y el cliente ve una cantidad distinta a la enviada.

Para evitarlo, toma una instantánea de las entradas que importan cuando la factura se vuelve final: tasa de impuesto usada, reglas de descuento, modo de redondeo y moneda. La recalculación debe usar la instantánea, no las configuraciones actuales de la cuenta.

Modelar el estado de pagos sin encasillarte

Fix the Prototype Without Rewriting
Most fixes complete in 48-72 hours with expert verification, not guesswork.

Muchos errores de facturación empiezan cuando un campo intenta hacer demasiadas cosas. “Status” a menudo se usa para todo: draft vs sent, late vs paid, refunded vs disputed. Eso funciona para una demo y luego se rompe cuando admites pagos parciales.

Mantén dos ideas separadas:

  • Invoice lifecycle status: qué es el documento (draft, sent, void, written off)
  • Payment status: qué dinero ha ocurrido (unpaid, partial, paid, overpaid, refunded, disputed)

Pueden discrepar y eso es normal. Una factura puede estar “sent” pero “unpaid”. Puede estar “void” pero aún tener un pago que debe reembolsarse.

Modela los movimientos de dinero, no solo una etiqueta

En vez de guardar “paid: true”, registra cada movimiento de dinero en tablas como payments y refunds (o una sola tabla payment_events). También registra los intentos de pago aparte de los pagos exitosos. Las tarjetas fallan, se reintentan y luego tienen éxito. Si solo guardas los éxitos, pierdes la historia y el soporte se complica.

Una regla simple: una fila por evento real, nunca reescribir la historia. Si un pago se revierte, añade un evento de reembolso o contracargo vinculado al pago original.

Define “paid” como un cálculo

Decide qué significa “paid” y hazlo consistente en la UI, emails e informes. Una definición común:

  • net_paid = sum(successful payments) - sum(refunds and chargebacks)
  • amount_due = invoice_total - net_paid
  • El estado de pago se basa en amount_due (0 significa pagado, negativo significa sobrepagado)

Ejemplo: una factura total es $100. El cliente paga $60 y luego $50. El neto pagado es $110, así que la factura está “overpaid” por $10. Si después reembolsas $10, añade un evento de reembolso y vuelve a “paid” sin editar los pagos antiguos.

Impuestos, descuentos y redondeo que no sorprendan a los usuarios

La mayoría de los bugs de “mis totales no coinciden” ocurren porque impuestos, descuentos y redondeo se añaden tarde. Si tu modelo de datos hace explícitas esas reglas, tu UI puede ser simple y los usuarios pueden verificar cada número.

Dónde almacenar el impuesto

El impuesto puede vivir a nivel de partida, de factura o ambos. Lo que importa es tener una fuente de cálculo clara.

El impuesto por línea es lo más fácil de explicar: cada ítem tiene un indicador de gravabilidad, una instantánea de la tasa de impuesto y un monto de impuesto computado. Esto funciona bien cuando distintos ítems tienen tasas diferentes, o algunos están exentos.

El impuesto a nivel de factura está bien cuando todo comparte una tasa, pero se complica con tasas mixtas. Si usas impuesto a nivel de factura, almacena la tasa usada y la base imponible a la que la aplicaste para que las ediciones posteriores no cambien la historia.

Descuentos y reglas de redondeo

Los descuentos deben modelarse como porcentaje o cantidad fija. Decide a qué aplican (por línea, subtotal o total tras impuestos) y codifícalo.

Un patrón práctico es tratar los ajustes como partidas de primera clase: envío o manejo como una línea positiva, descuentos y créditos como líneas negativas y cupones como entradas explícitas.

El redondeo también necesita una regla clara. Si redondeas por línea, el total puede diferir en un céntimo de redondear una vez en el subtotal. Elige una política y guarda los resultados redondeados que muestras.

Para que los totales inspiren confianza, muestra la misma matemática que usas para calcular: subtotal, cada ajuste, desglose de impuestos y total general.

Ediciones, versiones e historial de auditoría

Las facturas parecen simples hasta que alguien pregunta: “¿Qué enviamos realmente?” Si tu app permite editar facturas antiguas libremente, los totales y los impuestos pueden cambiar después del hecho y la confianza desaparece.

Cuando una factura se envía

Trata “sent” como una línea en la arena. Conserva una instantánea inmutable de lo que el cliente vio, incluso si tu producto cambia precios, tasas de impuesto o redondeo más tarde.

Puedes hacerlo con filas versionadas o con una carga congelada (snapshot). Como mínimo, toma una instantánea de la información mostrada al cliente al enviar, las partidas tal como se enviaron (incluida la tasa de impuesto), los totales tal como se enviaron, los términos y la fecha de vencimiento, y metadatos de envío como sent_at y el correo del destinatario.

Luego decide qué puede seguir cambiando sin afectar el dinero. Notas y etiquetas internas suelen ser seguras para seguir editables. Campos monetarios como unit_price, quantity, descuentos, impuestos y moneda deben bloquearse una vez enviados (o cambiar solo vía nueva versión).

Manejo de correcciones

Los negocios reales cometen errores. No “edites la historia” para ocultarlos. Usa acciones de corrección explícitas: una nota de crédito por la diferencia, anular con motivo o cancelar y reemitir con un nuevo número de factura (manteniendo el antiguo para auditoría).

Evita borrados duros. Las facturas borradas rompen la numeración, los informes y la conciliación de pagos. Anular mantiene el registro, deja la cantidad adeudada en cero y preserva la pista.

Para equipos no técnicos, guarda un registro de cambios pequeño: quién cambió qué, cuándo y por qué.

Ejemplo: un cliente, dos facturas, pago parcial, reembolso

Make One Source of Truth
We’ll map your invoice lifecycle and show exactly what should be stored vs computed.

Aquí hay un escenario concreto que puedes usar para comprobar el sentido de tu modelo de datos de facturación.

Customer: Green Field Studio (un contacto de facturación, una moneda).

Factura 1: tarifa de instalación única

La factura INV-1001 tiene una partida: tarifa de instalación por $500.00. Supón impuesto de ventas del 8%.

Subtotal: $500.00 | Impuesto: $40.00 | Total: $540.00

Se registra un pago único de $540.00. La factura pasa de Sent a Paid, y el saldo abierto queda en $0.00.

Factura 2: plan mensual con partidas, descuento, impuesto

La factura INV-1002 tiene tres partidas: plan mensual ($200.00), asientos extra (3 x $20 = $60.00) y un complemento de soporte prioritario ($50.00). El subtotal es $310.00.

Aplica un descuento del 10% sobre el subtotal: -$31.00, por lo que el subtotal descontado es $279.00. Impuesto al 8% es $22.32.

Total a pagar: $279.00 + $22.32 = $301.32

Ahora los eventos que deberías ver tras cada paso:

StepWhat happensPayments totalRefunds totalNet paidOpen balanceStatus
1Invoice sent$0.00$0.00$0.00$301.32Sent
2Partial payment of $150.00$150.00$0.00$150.00$151.32Partially paid
3Final payment of $151.32$301.32$0.00$301.32$0.00Paid
4Refund $50.00 tied to the support line item$301.32$50.00$251.32$0.00Paid (partially refunded)

Fíjate en lo que cambia (y lo que no cambia) en el Paso 4: el total de la factura se mantiene $301.32, y el saldo abierto sigue siendo $0.00 porque ya cobraste el importe completo. El reembolso es un movimiento de dinero separado, por lo que ahora el cliente tiene $50.00 a devolver (a menudo rastreado como crédito a favor del cliente).

Errores comunes que cometen los prototipos generados por IA

Las herramientas de IA pueden darte una demo funcional rápido, pero la facturación está llena de reglas pequeñas que fallan de forma silenciosa. Si tu modelo de datos está aunque sea ligeramente equivocado, terminas con totales que cambian, pagos que no coinciden e informes en los que no puedes confiar.

Matemáticas de dinero que derivan con el tiempo

Un error común en prototipos es usar números de punto flotante para dinero (como 19.99) y sumarlos en distintos lugares. Esto crea pequeñas diferencias de redondeo que aparecen como un desajuste de 1 céntimo entre el total de la factura, el total de pagos y el PDF.

Usa unidades menores enteras (céntimos) para valores almacenados y redondea una vez en el borde (vista, PDF, exportación). Si debes almacenar decimales, usa un tipo decimal de precisión fija y sé consistente.

Totales que dejan de coincidir tras ediciones

Muchos prototipos guardan subtotal, tax_total y total en la factura pero nunca los recalculan cuando cambian las partidas. O los recalculan en la creación, pero no en ediciones, importaciones o actualizaciones por API.

Un patrón más seguro es: calcula totales a partir de partidas y ajustes, persiste los totales calculados y registra un momento claro de “último calculado”. Si algo cambia, dispara la recalculación en un solo lugar, no en cada pantalla.

Los errores detrás de la mayoría de los “¿por qué este total está mal?” son previsibles: repartir reglas de cálculo entre UI y backend, permitir ediciones monetarias después de que se registraron pagos sin un historial claro, actualizar totales sin actualizar el saldo debido, mezclar impuestos por línea y por factura de forma inconsistente y redondear líneas de forma distinta al total final.

Campos de estado que te encasillan

Los prototipos a menudo mezclan “sent” y “paid” en un mismo estado, lo que se rompe en cuanto tienes pagos parciales, pagos fallidos o reembolsos. Mantén el estado de entrega (draft/sent/void) separado del estado de pago (unpaid/partial/paid/overpaid).

Además, nunca borres pagos para “deshacerlos”. Registra una reversión o un reembolso para que el historial se mantenga fiel.

Lista de verificación rápida antes de lanzar

Untangle Payments and Refunds
If refunds or partial payments break balances, we’ll rebuild the money events cleanly.

Antes de lanzar, haz una pasada enfocada en la corrección del dinero, no en el pulido de la UI.

  • Almacena dinero como unidades menores enteras (por ejemplo, céntimos), no como floats. Mantén la moneda en la factura y en cada registro de pago para no mezclar USD y EUR.
  • Haz las facturas inmutables una vez enviadas. Si debes permitir ediciones, crea una nueva versión (o una nota de crédito) y guarda campos de snapshot (nombre del cliente, dirección, IDs fiscales, descripciones de partidas, precios) exactamente como estaban al enviar.
  • Modela pagos y reembolsos como registros separados. Un reembolso no es un pago negativo oculto en la misma tabla salvo que seas explícito sobre reportes y comportamiento de “paid”.
  • Verifica los totales con una única fuente de la verdad. Los totales de factura almacenados deben coincidir: sum(line totals) + impuestos/tasas - descuentos, con redondeo aplicado en un solo lugar.
  • Define “paid” por escrito e inviértelo en código. Por ejemplo: una factura está pagada cuando (pagos - reembolsos) es mayor o igual que el importe adeudado, y no está anulada. Decide cómo manejas el sobrepago.

Después de eso, ejecuta un informe de reconciliación simple: para un rango de fechas, lista el importe adeudado de cada factura, pagos totales, reembolsos totales y saldo restante. Si no puedes generar esto limpiamente desde tu modelo, falta algo importante.

Siguientes pasos: validar, refactorizar y llevarlo a producción

Una vez que tus tablas y estados se ven bien en papel, prueba tus totales con casos que imiten comportamiento real:

  • Editar una partida después de enviar una factura (cambio de cantidad, cambio de precio, eliminar un ítem)
  • Pago parcial, luego otro pago parcial, luego un reembolso
  • Aplicar un descuento, luego cambiar la tasa de impuesto, luego quitar el descuento
  • Anular una factura tras un intento de pago
  • Cambiar datos del cliente y confirmar que las facturas históricas no reescriben el pasado

Añade una comprobación de conciliación que puedas ejecutar en cualquier momento: compara el total de la factura (lo que facturaste) frente a los pagos netos (suma de pagos menos reembolsos). Si no coinciden con el saldo esperado, ya sabes dónde mirar.

Si heredaste una app de facturación creada por IA que “funciona en su mayoría” pero se desmorona con totales, cambios de estado o reembolsos, los equipos a menudo recurren a FixMyMess (fixmymess.ai) para un diagnóstico y reparación rápidos del código. Una auditoría gratuita puede ser suficiente para mostrar dónde se calculan los totales, dónde ocurren los cambios de estado y qué campos están derivando para que puedas dejarlo seguro antes de producción.

Preguntas Frecuentes

What should be the “source of truth” for an invoice total?

Usa las partidas (más ajustes explícitos como descuentos, envío y créditos) como fuente de la verdad y calcula los totales a partir de ellas en un único lugar. Si también guardas totales de factura para mejorar el rendimiento, trata esos totales almacenados como una caché y valídalos contra una recomputación para que la deriva se detecte pronto.

How do I stop 1-cent rounding errors from showing up?

Almacena el dinero como unidades menores enteras (por ejemplo, céntimos) y aplica redondeo con una regla consistente. Las operaciones en punto flotante eventualmente producirán desajustes de 1 céntimo entre la UI, los PDFs, las exportaciones y los registros de pago.

Should users be able to edit an invoice after it’s sent?

Por defecto, haz que las facturas sean editables solo en borrador y bloquea los campos monetarios una vez enviadas. Si una corrección cambia dinero, crea una nueva versión, una nota de crédito o una entrada de ajuste en vez de reescribir silenciosamente la factura antigua.

Why model payments and refunds as separate records?

Sepáralos porque representan eventos diferentes del mundo real. Un pago registra dinero recibido; un reembolso registra dinero devuelto, normalmente vinculado a un pago específico, y ambos deben permanecer en el historial para soporte y conciliación.

What invoice statuses should I store vs calculate?

Usa un estado pequeño para el ciclo de vida del documento, como draft, sent y void. Deriva etiquetas relacionadas con pagos como “unpaid”, “partial”, “paid” u “overpaid” a partir del total de la factura, las fechas y el neto de pagos menos reembolsos.

How do I keep historical totals stable when tax rates or pricing rules change?

Haz una instantánea de las entradas que afectan los totales en el momento de enviar: la tasa de impuesto usada, las reglas de descuento, el modo de redondeo y los datos que se muestran al cliente. Las recalculaciones deben usar esa instantánea para que las facturas antiguas no cambien cuando cambien las configuraciones de la cuenta.

Should tax be calculated per line item or at the invoice level?

Elige un enfoque y mantente consistente. El impuesto por línea suele ser más sencillo de explicar y soporta tasas mixtas; el impuesto a nivel de factura funciona si todo comparte la misma tasa, pero debes almacenar la tasa usada y la base imponible aplicada.

How do I define “paid” when there are partial payments and refunds?

Defínelo como cálculo: amount_due = invoice_total - (sum(payments) - sum(refunds/chargebacks)). Esto gestiona pagos parciales, sobrepagos y reembolsos sin necesitar un campo frágil tipo “paid: true”.

Should I delete invoices or void them?

Evita borrados permanentes porque rompen la numeración, el historial de auditoría y la conciliación. Prefiere anular con un motivo para que el registro permanezca, la cantidad adeudada se vuelva no cobrable y puedas explicar lo ocurrido más tarde.

My AI-generated invoicing app “mostly works” but breaks around totals—what should I do?

Si tienes un prototipo creado por IA donde los totales derivan, los estados son inconsistentes o los reembolsos rompen los saldos, FixMyMess puede diagnosticar y reparar rápidamente la base de código. Ofrecemos una auditoría gratuita para señalar dónde divergen los cálculos y los cambios de estado, y luego lo dejamos listo para producción con correcciones verificadas por humanos.