dE_PromoReconstrucción completa del análisis realizado sobre tickets con promociones combinadas, desde el síntoma inicial hasta la identificación de la carrera entre la carga de db_tdeta/dE_TResu y la llegada posterior de dE_Promo.
Caso principal: sucursal 43 · POS 32 · Z 20260750 · ticket 475987 · promoción 279730000 · combo CMB-FERNET+COC (37054). Documento preparado como evidencia técnica, handoff y material de auditoría interna. Fecha de documentación: 11/08/2026.
POS 32 · Z 20260750
Artículo virtual 37054
llegó después del detalle
iUpStatus=8 + iProcesadoPromo=0 + línea combo iPromoPrior=7, bAcumulado=1.
El ticket fue marcado completo y consumido por STO_ComputoVentaNuevo antes de que existiera su dE_Promo. El SP no reintenta promos después de convertir el ticket a bit 8.
bAcumulado, ni el cálculo monetario de la promoción. La falla fue temporal: dE_Promo llegó demasiado tarde respecto del ciclo de stock.
Se compararon tres tickets con la misma promoción 279730000. Dos históricos funcionaban correctamente y el nuevo no generaba correctamente las transformaciones agrupadas.
| Ticket | Fecha | Estructura | Combo 37054 | bAcumulado | iProcesadoPromo | Resultado |
|---|---|---|---|---|---|---|
| 471566 | 27/06/2026 | Fernet + Coca + Coca + combo | -5.129 | 1 | 1 | Procesado |
| 471577 | 27/06/2026 | Fernet + Coca + combo | -5.129 | 1 | 1 | Procesado |
| 475987 | 08/08/2026 | Fernet + Coca + Coca + Hielo + combo | -3.499 | 1 | 0 | No procesado como combo |
La Coca pasó de $5.629 a $3.999, pero la promoción mantenía conceptualmente el mismo precio final:
Junio: Fernet 18.399 + Coca 5.629 = 24.028 Descuento combo = -5.129 Total promo = 18.899 Agosto: Fernet 18.399 + Coca 3.999 = 22.398 Descuento combo = -3.499 Total promo = 18.899
Por lo tanto, el cambio -5129 → -3499 no era anomalía: era coherente con el nuevo precio de Coca.
| iOrden | cBarCode | Descripción | Precio | iPromocion | iPromoPrior | bAcumulado | iProcesadoPromo | iUpStatus |
|---|---|---|---|---|---|---|---|---|
| 86430411 | 7790290101602 | FERNET BRANCA | 18399 | 279730000 | 1 | 0 | 0 | 8 |
| 86430412 | 7790895005794 | GASEOSA COCA | 3999 | 279730000 | 2 | 0 | 0 | 8 |
| 86430413 | 7790895005794 | GASEOSA COCA | 3999 | 279730000 | 2 | 0 | 0 | 8 |
| 86430414 | 763571660973 | HIELO GELOMAX KG | 2698.92 | 0 | 0 | 0 | 0 | 8 |
| 86430415 | 37054 | CMB-FERNET+COC | -3499 | 279730000 | 7 | 1 | 0 | 8 |
bAcumulado=0 en la línea combo. Al releer el dataset original se confirmó que siempre estuvo en 1. La anomalía real era iProcesadoPromo=0. Esta rectificación se conserva porque fue parte del proceso de diagnóstico.
| Hipótesis | Estado | Evidencia |
|---|---|---|
El POS nuevo dejó de setear bAcumulado=1 | Descartada | El dataset SQL original y un segundo importador mostraron bAcumulado=1 en 37054. |
| La versión POS 20260728F cambió la lógica del combo | Descartada | La rutina de generación conserva explícitamente la asignación de bAcumulado=1. |
El importador Btrieve→SQL perdió el campo bAcumulado | Descartada | Un segundo sistema leyendo Btrieve directo a SQL obtuvo acumulado=1, y el primer SQL ya lo tenía en 1. |
dE_Promo contenía datos monetarios incorrectos | Descartada | El precio de promo final seguía siendo $18.899 y los registros negativos/positivos eran estructuralmente coherentes. |
El ticket estaba incompleto según dE_TResu | Descartada | db_tdeta COUNT=5 y iNroItemsEnDetalle=5. |
Algún stored procedure ponía bAcumulado=0 | Descartada | En el script de DB no aparece ningún UPDATE de ese campo en el flujo analizado. |
dE_Promo llegó después de que el ticket fue consumido | Confirmada | Timestamps + logs de Nuevo Computo de Ventas prueban la carrera. |
Fuente auditada: dump st.sql. Procedimientos principales:
STO_ComputoVentaNuevoMarca como “en proceso” sólo tickets que considera completos, usando la igualdad entre cantidad de detalle y cantidad declarada por dE_TResu:
HAVING COUNT(*) = MAX(ISNULL(dE_TResu.iNroItemsEnDetalle, 0))
Luego ejecuta:
EXEC spu_PreComputoPromociones EXEC STO_Procesar_ComputoPromociones_TransformacionAgrupada @Z=@Z, @u=@u, @Origen=@Origen
Y al final convierte indiscriminadamente los registros con bit 16 en bit 8:
UPDATE dB_TDeta SET iUPStatus = ((iUPStatus ^ 16) | 8) WHERE (iUPStatus & 16) = 16;
spu_PreComputoPromocionesConstruye #PromoArticulos cruzando dE_Promo negativa contra las líneas físicas de db_tdeta.
FROM dE_Promo WHERE iId < 0 AND iAction = 7 AND (bTStatus & 1) != 1 AND iMetodo != 99
Los artículos participantes salen de db_tdeta con:
bAcumulado != 1 AND iPromocion != 0 AND iProcesadoPromo = 0 AND (iUPStatus & 16) = 16
La línea virtual del combo se reconoce con:
iPromoPrior = 7 AND bAcumulado = 1 AND iPromocion != 0 AND iProcesadoPromo = 0 AND (iUPStatus & 16) = 16
Si el cruce funciona, genera ComputoPromociones con operaciones Alta X TA, Baja X TA y Baja X Vta, y termina marcando:
db_tdeta.iProcesadoPromo = 1 dE_Promo.bTStatus |= 1
STO_Procesar_ComputoPromociones_TransformacionAgrupadaConsume ComputoPromociones, crea la transformación agrupada, actualiza stock y puede reinsertar ventas en db_tdeta con iUpStatus=16. Esto es correcto en el pipeline normal, pero vuelve peligroso reutilizar el procedimiento completo para reparar tickets históricos ya vendidos.
El encabezado del SP conserva la nota:
Se demora la lectura de la db_tdeta a 8 min. porque el precomputos esta demorado 6 minutos (para no leer info sin procesar)
Sin embargo, en la versión auditada no aparece una condición temporal efectiva de 8 minutos dentro del criterio actual de completitud. El caso real demuestra que, aun existiendo un scheduler alrededor de ese orden de magnitud, una demora de 48 minutos en dE_Promo supera ampliamente esa protección.
dE_TResu.dStaDateTime: inicio del ticket.dE_TResu.dEndDateTime: fin de ticket.dE_TResu.dUPFechaHora. Declara iNroItemsEnDetalle=5.db_tdeta: Fernet, Coca, Coca, Hielo, combo 37054.Nuevo Computo de Ventas iId:1627482. El ticket ya era completo 5/5.iUpStatus=8.dE_Promo para promo 279730000.El motor SQL tomaba como condición de completitud únicamente:
db_tdeta completo según dE_TResu
pero no verificaba:
si el ticket declara combo
entonces dE_Promo correspondiente debe existir y estar completa
Cuando dE_Promo tardó aproximadamente 48 minutos, ocurrió:
db_tdeta quedó completo 5/5.STO_ComputoVentaNuevo habilitó el ticket con bit 16.spu_PreComputoPromociones no encontró dE_Promo, por lo tanto no creó #PromoArticulos para ese ticket.Alta X TA/Baja X TA/Baja X Vta de combo.iProcesadoPromo=1 ni dE_Promo.bTStatus|=1.dE_Promo, ya no existía reintento automático sobre el ticket.db_tdeta: iPromoPrior = 7 bAcumulado = 1 iPromocion != 0 iProcesadoPromo = 0 (iUpStatus & 8) = 8 dE_Promo: existen registros para la misma promo bTStatus sigue sin bit 1
La decisión final fue corregir el origen/configuración de transformación (dm_tranf) para restablecer la llegada correcta de dE_Promo, evitando inicialmente modificar stored procedures productivos por el riesgo de bloquear tickets cuando una promo venga incompleta o corrupta.
Consulta recomendada para encontrar tickets con la misma firma:
;WITH ComboPendiente AS
(
SELECT
d.iNroPOS,
d.iNroZ,
d.iNroTicket,
d.iPromocion,
d.cBarCode AS cBarCodeCombo,
MAX(d.cDescripcion) AS DescripcionCombo,
MAX(d.fechainsercion) AS FechaComboTDeta,
MAX(d.iUpStatus) AS iUpStatus,
MAX(d.iProcesadoPromo) AS iProcesadoPromo
FROM dB_TDeta d WITH (NOLOCK)
WHERE d.iPromoPrior = 7
AND d.bAcumulado = 1
AND d.iPromocion <> 0
AND d.iProcesadoPromo = 0
AND (d.iUpStatus & 8) = 8
GROUP BY
d.iNroPOS, d.iNroZ, d.iNroTicket,
d.iPromocion, d.cBarCode
),
PromoEstado AS
(
SELECT
dp.iNroPOS,
dp.iNroZ,
dp.iNroTicket,
ABS(dp.iId) AS iPromocion,
MIN(dp.fechainsercion) AS PrimeraDEPromo,
MAX(dp.fechainsercion) AS UltimaDEPromo,
SUM(CASE
WHEN dp.iId < 0
AND dp.iAction = 7
AND dp.iMetodo <> 99
THEN 1 ELSE 0 END) AS RegNegativos,
SUM(CASE
WHEN dp.iId > 0
AND dp.iAction = 7
THEN 1 ELSE 0 END) AS RegPositivos,
MAX(CASE
WHEN (dp.bTStatus & 1) = 1
THEN 1 ELSE 0 END) AS AlgunaFinalizada
FROM dE_Promo dp WITH (NOLOCK)
WHERE dp.iAction = 7
GROUP BY dp.iNroPOS, dp.iNroZ, dp.iNroTicket, ABS(dp.iId)
)
SELECT
c.iNroPOS, c.iNroZ, c.iNroTicket, c.iPromocion,
c.cBarCodeCombo, c.DescripcionCombo,
c.FechaComboTDeta,
p.PrimeraDEPromo, p.UltimaDEPromo,
DATEDIFF(SECOND,c.FechaComboTDeta,p.PrimeraDEPromo)
AS SegundosDEPromoDespuesTDeta,
p.RegNegativos, p.RegPositivos, p.AlgunaFinalizada,
CASE
WHEN p.PrimeraDEPromo > c.FechaComboTDeta
THEN 'RACE: dE_Promo llego despues de db_tdeta'
ELSE 'REVISAR: otro motivo'
END AS DiagnosticoProbable
FROM ComboPendiente c
JOIN PromoEstado p
ON p.iNroPOS = c.iNroPOS
AND p.iNroZ = c.iNroZ
AND p.iNroTicket = c.iNroTicket
AND p.iPromocion = c.iPromocion
WHERE p.RegNegativos > 0
AND p.RegPositivos > 0
AND p.AlgunaFinalizada = 0
ORDER BY c.iNroZ,c.iNroPOS,c.iNroTicket,c.iPromocion;
SELECT * FROM ComputoPromociones WHERE iNroPOS = @POS AND iNroZ = @Z AND iNroTicket = @Ticket;
ComputoPromociones, el caso puede ser un procesamiento parcial. No debe entrar en una reparación automática “limpia”.
No ejecutar indiscriminadamente:
UPDATE db_tdeta SET iUpStatus = 0 ...
ni:
UPDATE db_tdeta SET iUpStatus = 16 ... EXEC spu_PreComputoPromociones EXEC STO_Procesar_ComputoPromociones_TransformacionAgrupada
El motivo es que la venta original ya impactó stock y oAccionesDetalle. Reintroducir el ticket al pipeline normal puede duplicar movimientos.
Para un caso limpio —venta ya procesada, combo no procesado, dE_Promo ahora completa, cero filas previas en ComputoPromociones— la reconciliación lógica puede limitarse a:
BEGIN TRAN; -- Mantener iUpStatus = 8. -- NO reabrir venta. UPDATE d SET iProcesadoPromo = 1 FROM db_tdeta d WHERE d.iNroPOS = @POS AND d.iNroZ = @Z AND d.iNroTicket = @Ticket AND d.iPromocion = @Promo AND d.iProcesadoPromo = 0; UPDATE dp SET bTStatus = bTStatus | 1 FROM dE_Promo dp WHERE dp.iNroPOS = @POS AND dp.iNroZ = @Z AND dp.iNroTicket = @Ticket AND ABS(dp.iId) = @Promo AND dp.iAction = 7; -- Primera ejecución: ROLLBACK; -- Luego de validar: -- COMMIT;
En la venta fallida, los productos físicos ya se descargaron correctamente en cantidad neta:
Fernet -1 Coca -2 Hielo -1
El código virtual 37054 sí quedó con una venta directa -1, mientras que el flujo correcto de TA habría hecho:
TA: 37054 +1 Fernet -1 Coca -1 Venta: 37054 -1 Coca -1 NETO: 37054 0 Fernet -1 Coca -2
Por eso, para repair histórico, el desvío principal a estudiar en stock es el artículo virtual de combo. Cualquier ajuste debe validarse contra oStock, oAccionesDetalle, mAcciones e inventarios posteriores.
Aunque se decidió corregir primero el origen y no desplegar cambios de SP en producción, se diseñó un safeguard para revisión.
En los casos observados, para una promo P aparecen:
iId=-P por los artículos participantes;iId=+P como resumen de la promo.El positivo puede utilizarse como completion marker, pero debe confirmarse como contrato estable del importador antes de depender de él productivamente.
Después de spu_PreComputoPromociones, si todavía existe:
iPromoPrior = 7 AND bAcumulado = 1 AND iPromocion != 0 AND iProcesadoPromo = 0 AND (iUpStatus & 16) = 16
el ticket debería salir del lote de venta en esa corrida, en vez de pasar silenciosamente a bit 8.
Para evitar que un dE_Promo corrupto bloquee un ticket indefinidamente, el diseño propuesto contempla timeout configurable y logging. Esto es un safeguard de resiliencia, no un sustituto de arreglar la llegada de datos en origen.
Se revisaron dos paquetes fuente:
20250127CF: versión asociada a tickets históricos correctos.20260728F: versión asociada al incidente.La investigación buscó específicamente dónde se genera la línea de descuento/combos y cómo se asigna bAcumulado.
La rutina identificada para la línea de combo conserva conceptualmente:
ETdet->assign(str_bAcumulado, 1); ETdet->assign(str_iPromocion, Id); ETdet->assign(str_iPromoPrior, iAction); ETdet->Append();
Esto reforzó la conclusión de que el origen POS no era responsable de transformar el 1 en 0; posteriormente, al releer el dataset, quedó demostrado que el 1 nunca se había perdido.
COUNT(db_tdeta) no representaba necesariamente la cantidad original.iUpStatus), promo procesada (iProcesadoPromo) y promo finalizada en dE_Promo (bTStatus).iUpStatus y bTStatus; comparar valores sin respetar bitmaps llevaba a interpretaciones incorrectas.db_tdeta, lo que hace peligroso “reprocesar” históricos como si fueran tickets nuevos.bAcumulado=1 correctamente.dE_TResu.dE_Promo llegó aproximadamente 48 minutos tarde.STO_ComputoVentaNuevo consumió el ticket antes de que la promo estuviera disponible.iUpStatus=8 / iProcesadoPromo=0 / bTStatus=0 es consistente con esa carrera.dm_tranf.iPromocion.iPromoPrior=7, bAcumulado=1.iProcesadoPromo.iUpStatus.COUNT(db_tdeta) con dE_TResu.iNroItemsEnDetalle.db_tdeta, dE_TResu y dE_Promo.Nuevo Computo de Ventas entre esos timestamps.ComputoPromociones.dE_Promo posterior: clasificar como race probable.iUpStatus sin analizar movimientos de stock ya aplicados.st.sql — dump SQL analizado.20250127CF.ZIP — fuente POS asociado a comportamiento histórico correcto.20260728F.ZIP — fuente POS asociado al período del incidente.STO_ComputoVentaNuevo_guardrails_review.sql — propuesta de SP con safeguards.diagnostico_y_flags_tickets_combo_afectados.sql — detección de históricos afectados.combos_guardrails_y_recuperacion_completo.sql — consolidado técnico para revisión.MAX(bit)Al comparar db_tdeta contra dE_TResu se intentó inicialmente:
MAX(r.bStatus)
SQL Server devolvió:
Msg 8117, Level 16, State 1 Operand data type bit is invalid for max operator.
La corrección fue convertir explícitamente el bit:
MAX(CAST(r.bStatus AS int)) AS TResuStatus
Los resultados:
471566 → CantidadTDeta=10 / CantidadTResu=8 471577 → CantidadTDeta=10 / CantidadTResu=9 475987 → CantidadTDeta=5 / CantidadTResu=5
no significaban necesariamente que los tickets viejos hubieran llegado incompletos. El pipeline de promociones puede insertar registros derivados nuevamente en db_tdeta. Por eso, para establecer completitud histórica, la comparación debe considerar si el ticket ya fue procesado y qué filas son originales vs. derivadas.
Inicialmente el extracto reducido mostraba cuatro filas relevantes para la promo. La consulta completa reveló la quinta:
86430414 | 763571660973 | HIELO GELOMAX KG 2698.92
Esto cerró la igualdad exacta:
db_tdeta = 5 filas dE_TResu.iNroItemsEnDetalle = 5
El diagnóstico se cerró al combinar:
db_tdeta.fechainsercion;dE_TResu.dUPFechaHora;dE_Promo.fechainsercion;oLogs.dInsertTime de Nuevo Computo de Ventas.Ese cruce permitió demostrar que hubo ejecuciones del cómputo de ventas entre la llegada del detalle y la llegada de dE_Promo, transformando una hipótesis de carrera en una causa soportada por evidencia.