Este es un recorrido de producto, no el caso de ningún comercio real: describe cómo se mueve —y dónde se desconecta— una venta dentro de VendeWare, desde que se cobra en el punto de venta hasta el comprobante fiscal que debería ampararla. La pregunta que resuelve es concreta: si el sistema ya sabe resolver este mismo problema en otro módulo, ¿en qué paso exacto deja de aplicarlo aquí (según lo visto en el CFDI global de VendeWare se arma a mano, sin comparar nunca contra una venta real)?
Esquema del recorrido · ilustración, no es una pantalla del sistema
- 1. El cliente compra y no pide facturaEl cajero cobra la venta en el punto de venta; queda registrada en
sales/pos, sin ningún vínculo hacia facturación. - 2. Llega el cierre del período (día, semana o mes)Alguien tiene que armar el CFDI global que ampare todas las ventas al público en general de ese período.
- 3. Se escribe cada renglón a manoEl campo que identifica cada venta (
sourceRef) es texto libre de hasta 120 caracteres — no una lista real del punto de venta. - 4. El sistema arma el CFDI con lo que le dieron, sin comparar
create()nunca consultaposnisalespara saber qué se vendió de verdad en el período. - 5. Nada impide un ticket duplicado o un ticket faltante
insertLine()inserta cada renglón sin índice único sobresource_ref. - 6. La venta olvidada queda sin ningún comprobanteIndefinidamente, sin que ninguna parte del sistema lo note o lo señale.
1. La venta se cobra, sin ningún enganche hacia facturación
El punto de venta y el módulo de ventas de VendeWare cobran una venta y la registran con todos sus detalles —productos, importes, forma de pago—. En ningún punto de ese código, en ningún archivo de apps/services/pos/src ni apps/services/sales/src, existe una llamada al módulo de facturación: una búsqueda exhaustiva de "invoice", "cfdi" y "factur" en ambos módulos no encuentra ninguna lógica real, sólo un comentario que confirma que es así por diseño —sales y facturacion se desacoplaron a propósito, tras un incidente en producción, para que un problema de facturación nunca bloquee una venta.
2. Al cierre, alguien tiene que reconstruir esa lista a mano
Cuando llega el momento de emitir el CFDI global del período —diario, semanal o mensual, según lo que exige la regla 2.7.1.21 de la RMF 2026—, quien lo arma no tiene, dentro del propio VendeWare, una lista de "esto es lo que se vendió y todavía no tiene comprobante". Tiene que reconstruirla por su cuenta, típicamente desde el reporte de ventas o el corte de caja, y transcribir cada renglón a mano.
3-4. El campo que debería ser una referencia real es sólo texto, y nadie lo verifica
Cada renglón que se captura pasa por globalInvoiceLineInputSchema, cuyo campo sourceRef es z.string().min(1).max(120) —cualquier cadena de hasta 120 caracteres, no una referencia validada contra ningún ticket real—. El método create() del servicio de facturas globales arma el comprobante exactamente con esos renglones, sin ninguna consulta de vuelta al punto de venta o al módulo de ventas para confirmar que, en efecto, esa venta existió y no se ha facturado ya por otra vía.
5. Ni el duplicado ni el faltante generan ninguna señal
insertLine() guarda cada renglón con un INSERT simple, sin restricción de unicidad sobre source_ref. Si el mismo ticket se captura dos veces —por error, o porque dos personas armaron el mismo CFDI global— el sistema lo acepta sin aviso: el ingreso reportado queda inflado. Si un ticket se omite, el sistema tampoco lo nota: no hay ninguna comparación contra el total real del punto de venta que pudiera señalar la diferencia.
6. La venta olvidada no vuelve a aparecer en ningún lado
Una vez cerrado el período, esa venta cobrada y sin comprobante no queda marcada como pendiente en ninguna pantalla, reporte o alerta. Es indistinguible, dentro del sistema, de una venta que sí se facturó correctamente —hasta que alguien, por fuera del software, la encuentre por otro medio.
El detalle que hace este caso distinto: VendeWare ya sabe resolverlo, en otro módulo
A unas carpetas de distancia del servicio de facturas globales vive commissions.repository.ts, el módulo que calcula comisiones de vendedores. Su función insertAccrual() resuelve exactamente el mismo problema —no contar dos veces el mismo sourceRef— con un INSERT ... ON CONFLICT DO NOTHING contra un índice único parcial, un endurecimiento documentado en el propio código como respuesta a una auditoría que encontró una condición de carrera real. El patrón para evitar el doble conteo de un sourceRef ya existe en la base de código de VendeWare; simplemente nunca se aplicó al renglón que determina si una venta terminó, o no, con comprobante fiscal.
| Parada | ¿Se compara contra una venta real? |
|---|---|
| 1. Venta cobrada en el punto de venta | No aplica — aquí nace el dato real |
| 2. Reporte de ventas / corte de caja | Sí, existe — pero es manual, fuera del flujo de facturación |
3. Renglón del CFDI global (sourceRef) | No — texto libre de hasta 120 caracteres |
4. create() del servicio de facturas globales | No — arma el comprobante con lo que reciba, sin consultar ventas |
5. insertLine() al guardar cada renglón | No — sin índice único sobre source_ref |
Comparar: insertAccrual() de comisiones | Sí — ON CONFLICT con índice único parcial sobre el mismo tipo de campo |
Puedes estimar la exposición de este hallazgo, con tus propios números, en la calculadora de exposición por ventas sin comprobante fiscal.
Preguntas frecuentes
¿VendeWare rompe la conexión entre ventas y facturación por descuido?
No: es una decisión deliberada de resiliencia, documentada en el propio código, para que un problema en facturación nunca bloquee el cobro de una venta. El hallazgo no es esa decisión —es que, además de desacoplar el bloqueo, tampoco quedó ninguna forma de reconciliar después lo que se vendió contra lo que se facturó.
¿Esto afecta sólo al CFDI global, o también a las facturas individuales?
El hallazgo verificado es específicamente sobre el CFDI global. Una factura individual normalmente la pide el cliente en el momento, con sus propios datos, un flujo distinto al de agrupar ventas al cierre del período.
¿VendeWare corrige esto automáticamente?
No, hoy no. Es un hallazgo verificado en el código actual, no una función ya resuelta; el propio código ya tiene, en otro módulo, el patrón que haría falta para resolverlo.
Ve el recorrido completo en tu propia demo
Te mostramos en vivo el punto de venta, el corte de caja y el CFDI global que se arma con esas ventas.
Agenda una demo gratis