Diagnóstico & Corrección · Incidente de campo

Arrastre de un medio de pago del día anterior — POS 1, Sucursal Caroya

Un pago de tarjeta débito ya cobrado reaparecía como saldo en el primer ticket del día siguiente.
Sistema: pcPOS (Borland C++ 3.1 / Phar Lap 286 / Btrieve 6.15) Versión: 20251007FF Reclamo: 03/09/2026 Estado: Corregido y verificado
Al iniciar la caja, el primer cobro traía además del importe real un débito BANCOR de $170.081 que pertenecía a un ticket del día anterior, dejando el ticket con saldo negativo de −$162.372. No es un error de cálculo ni de datos corruptos: es una recuperación de pago tras corte de energía que revivía un pago ya cobrado en un ticket que ya estaba cerrado. La corrección impide resucitar el pago de un ticket finalizado, y se validó reproduciendo el bug y comprobando que desaparece.

1. El síntoma

Reportado por Mesa de Ayuda (Grupo Dinosaurio) sobre POS 1 de Caroya:

ConceptoEsperadoLo que mostraba la caja
Compra del cliente (efectivo)$7.709$7.709
Medio de pago extra (no ingresado)BANCOR MC DÉBITO $170.081
Saldo del ticket$7.709 a cobrar−$162.372

El log del día del reclamo no tenía ese pago; el mismo importe aparecía en el log del día anterior (28-08). Y el ticket que finalmente se anuló mostraba NroRef: 132103el mismo número del ticket del 27-08.

2. La evidencia en los logs

Día 27-08 — pago legítimo y corte

10:47:34  MDEP ADD:BANCOR MC DEBIT MIngr:170081.00 SaldT:0.00   ; pago aplicado, saldo 0
10:47:38  NroRef:132103 Items:56 TOTAL:170081.00 SU PAGO:170081.00 ; ticket impreso
10:47:39  QR-JSON ... nroCmp 126101                        ; comprobante CAEA emitido → FINALIZADO
   ...    (1 hora sin un solo registro — corte de energía)
11:55:53  Inicia POS=20251007FF                            ; reinicio
11:56:00  22000=SAFETY_MARKPAGO=1  22001=SAFETY_MARKNROTICKET=132103
11:56:01  Rec DDMP/TMdeP T:132103 Reg:1 170081.00          ; REVIVE el pago ya cobrado

El ticket 132103 se había completado (impreso + comprobante fiscal CAEA) a las 10:47:39. Segundos después se cortó la energía. Al reiniciar, la rutina de recuperación reinyectó el pago.

Día 28-08 — el arrastre al cliente siguiente

09:34:57  SaYaCanc TOT:7709.00 SAL:-162372.00   ; el ticket nuevo ya nace con el fantasma
09:36:32  MDEP DEL:BANCOR MC DEBIT TotDel:170081.00 SaldTi:7709.00 ; la cajera intenta borrarlo
16:27:24  TICKET ANULADO  NroRef:132103          ; mismo ticket del día 27

El contador de ticket nunca avanzó de 132103, porque el corte ocurrió antes de que el software lo incrementara.

3. Causa raíz

El cobro tiene una sección crítica protegida contra corte de energía (pos_vnta.cpp):

SetEndVueltoOK(2, CurrentTicket);          // marca "pago pendiente ticket N"
   ... imprime ticket, emite CAEA ...
if (GCmos.LastTicket==CurrentTicket)
    MCoun->assign(str_LastTicket, ++GCmos.LastTicket);  // avanza el contador
ResetEndVueltoOK();                        // limpia la marca
El corte cayó en la ventana exacta El ticket ya estaba impreso y con comprobante CAEA, pero la energía se cortó antes de avanzar el contador y limpiar la marca. Quedó: marca activa + dE_DDMP con el pago + ETres apuntando al ticket 132103.

Al reiniciar, ReStartEndVueltoOK()ActualizarDDMPyTMdeP() hace lo que debe para un ticket no terminado: reinyecta el pago en la tabla de medios de pago del ticket (dE_TMdeP). El problema es que no distingue "se cortó antes de finalizar" de "se cortó después de finalizar" — y este ticket ya tenía su comprobante. El guard original sólo comparaba lTicket == CurrTick, sin mirar si el ticket ya estaba cerrado.

4. La corrección

Un chequeo mínimo en ActualizarDDMPyTMdeP(): si el ticket ya tiene comprobante emitido (iNroComprob ≠ 0 en dE_TResu), el pago ya fue cobrado en ese ticket cerrado y no se reinyecta. Reutiliza el camino seguro que el código ya tenía para HaySafeMarkSaveTMdeP.

int iNoActualizarTMdeP = HaySafeMarkSaveTMdeP();
if(_TicketYaFinalizadoConComprobante(CurrTick)){        // ← NUEVO
    iNoActualizarTMdeP = 1;   // pago ya cobrado en ticket cerrado: NO reinyectar
    Log(LOGERROR, LOG_AJUSTADDMPTMDEP, GCmos.NroZ,
        "DDMP: T:%ld ya finalizado (comprob emitido), NO se reinyecta MdeP", CurrTick);
    }

Helper: abre dE_TResu, ubica el ticket, devuelve 1 si iNroComprob ≠ 0.

Por qué es seguro El comprobante CAEA se emite en el cierre, después del cobro. Entonces: corte después de finalizar → iNroComprob ≠ 0 → el guard frena (caso del bug). Corte durante el pago, antes del comprobante → iNroComprob = 0 → la recuperación legítima sigue funcionando igual.

5. Simulación — reproducción y verificación

Se sembró en un banco de prueba el estado exacto del post-corte (bT_TResu apuntando al ticket, dE_TResu finalizado con comprobante, y dE_DDMP con el pago fantasma) y se arrancó el POS con cada ejecutable. Único cambio: el binario. Se leyó de disco la tabla de medios de pago (dE_TMdeP) después de cada arranque.

EscenarioLog de recuperacióndE_TMdeP tras arrancarResultado
Semilla (estado post-corte) vacío
ORIG (sin corrección) Rec DDMP/TMdeP T:317075 Reg:1 170081.00 uId=10 fTotal=170081.00 Arrastra
FIXED (con corrección) DDMP: T:317075 ya finalizado (comprob emitido) TOTAL = 0 Limpio
Verificado de punta a punta Con el mismo estado post-corte, el ejecutable original reinyecta el pago de $170.081 en el ticket (fantasma persistido en disco); el corregido lo detecta finalizado y deja la tabla vacía. El reclamo de Caroya queda reproducido y resuelto.

6. Alcance y pendientes