ALMACOR - Fix del doble descuento en promociones por medio de pago
POS_MAIN.EXE sobre las fuentes de produccion 20260928P - 6 de noviembre de 2026
1. Resumen ejecutivo
En la sucursal 9 (Salsipuedes), POS 3, Z 12, el ticket 110 imprimio la linea PROMOMDEP[EFT/DEB]-124800.00 cuando el cliente esperaba un descuento de 92.100. La diferencia proviene de que el cajero cargo medios de pago en efectivo, los borro y los volvio a cargar: el acumulador del descuento conservo el descuento de los medios borrados y lo sumo de nuevo.
La causa es un contador de indices que se desfasa respecto de los registros de control cuando un medio de pago no alcanza el minimo de la promocion. El fix ubica el registro a revertir por promocion (el ultimo registro de esa promo) en lugar de usar el contador global. Solo cambian pos_prom.cpp y pcpos.h; no hay cambios de base ni de perfiles.
El error se reprodujo en una VM con el EXE de produccion (descuento de 13.299,98 en lugar de 9.699,98) y quedo corregido con el EXE nuevo (9.699,98); ver seccion 6.
corregidovalidado en VM
2. Evidencia del cliente
Nota del cliente: "Deberia ser $DTO 92.100 y fue $124.800". Log del POS: Ticket_N°110_00031104.26L. El ticket impreso tiene la linea PROMOMDEP[EFT/DEB]-124800.00 (promo 2250000, promocion por medio de pago tipo 2, 10 % en efectivo/debito).
Linea de tiempo (ticket 110, subtotal 921.000,00)
| Hora (04/11/2026) | Evento | Ingresado | Descuento |
|---|---|---|---|
| 21:21:07 | MDEP ADD efectivo | 300.000 | -30.000 |
| 21:21:10 | MDEP ADD efectivo | 30.000 | -3.000 |
| 21:21:12 | MDEP ADD efectivo | 3.000 | 0 (sin descuento) |
| 21:21:38 | 3 x MDEP DEL (3.000, 27.000 y 270.000 netos) | - | 0 / -3.000 / -30.000 |
| 21:22:14 | MDEP ADD efectivo | 300.000 | -30.000 |
| 21:22:16 | MDEP ADD efectivo | 30.000 | -3.000 |
| 21:22:18 | MDEP ADD efectivo | 3.000 | 0 |
| 21:23:12 | MDEP ADD transferencia | 529.200 | -58.800 |
| 21:23:14 | Impresion PROMOMDEP[EFT/DEB] | - | -124.800 |
Importes
| Concepto | Importe |
|---|---|
| Descuento vigente al final (30.000 + 3.000 + 58.800) | 91.800 |
| Descuento esperado segun el cliente | 92.100 |
| Descuento impreso | 124.800 |
| Exceso sobre lo vigente (124.800 - 91.800) | 33.000 |
El exceso de 33.000 coincide exactamente con los descuentos de los medios borrados a las 21:21:38 (30.000 + 3.000): se contaron dos veces. La diferencia de 300 entre los 91.800 vigentes y los 92.100 indicados por el cliente no se explica con este log y no forma parte del fix.
3. Causa raiz
Funciones PromocionesMdeP y TICKET::PromocionesMdeP_DeleteMdeP (pos_prom.cpp, compilada con #define DELETESPECIALMODEPROMO).
globalarrayPromMdepControl(estructuraarrayPromMdepControlenpcpos.h) guarda un registro por cada medio de pago que disparo una promo por medio de pago: (indexin, idpromo, idmdep, descuento, importe).PromocionesMdePllama agetNextIndexIn()por cada medio de pago que evalua, es decir, solo cuando el saldo pendiente todavia permite evaluar la promo (en el log,MDEP ADDconMRefSinDmayor que 0).Add(getCurrentIndexIn(), ...)se ejecuta solo cuando la promo dispara.- En la funcion original
PromocionesMdeP_DeleteMdeP, lo unico que decrementa el contador (indexinglobal--) esDel(), yDel()solo corre siuTuvoAlgunaPromoesta activo, lo que ocurre unicamente despues de queGetLast(getCurrentIndexIn(), ...)encontro un registro. - Un medio de pago por debajo del minimo (3.000 contra 15.000) pero evaluado (en el cliente,
MRefSinD:624000.00) avanza el contador sin dejar registro. Al borrarlo,GetLastno encuentra nada, se ejecutaif(!VALIDAR(dRecDesc)) continue;yDel()nunca corre: el contador queda trabado. - Los borrados siguientes (30.000 y 300.000 en el cliente) miran el mismo indice vacio, no encuentran nada, no revierten nada y el contador sigue sin moverse. Al recargar los medios de pago, los descuentos se suman otra vez sobre el acumulador que nunca se limpio: 33.000 de mas en el cliente.
Escenario reproducido en VM (ticket de 99.999,75: 25 items de 3.999,99)
| Paso | Contador | Registros | acupro.MontoDesc |
|---|---|---|---|
| Alta efectivo 20.000 (-2.000) | 1 | idx1 | -2.000 |
| Alta efectivo 16.000 (-1.600) | 2 | idx1, idx2 | -3.600 |
| Alta efectivo 3.000 (evaluada, no alcanza el minimo) | 3 | idx1, idx2 | -3.600 |
Borrar 3.000: GetLast(3) no encuentra, Del() no corre | 3 | idx1, idx2 | -3.600 |
Borrar 16.000: GetLast(3) no encuentra, sin reversion | 3 | idx1, idx2 | -3.600 (deberia ser -2.000) |
| Borrar 20.000: igual | 3 | idx1, idx2 | -3.600 (deberia ser 0) |
| Recargar 20.000 y 16.000, cerrar con 61.000 (-6.099,98) | 6 | ... | -13.299,98 (correcto: -9.699,98) |
Por que el sintoma es intermitente. Si el medio de pago pequeno se agrega cuando el saldo pendiente ya es menor que 15.000 (MRefSinD:0.00), PromocionesMdeP retorna antes de getNextIndexIn() y el error no aparece. Depende del orden y de los importes de los medios de pago.
4. Fix
pcpos.h - metodo nuevo GetLastIndexIn
Devuelve el indexin mas alto registrado para la promo (0 si no hay). Los medios de pago se borran siempre de atras para adelante, por lo que el ultimo registro de la promo es el del medio que se borra. Igual que el resto de la estructura, depende de profile.iPagoOnlineEMV.
unsigned POS_EXPORT GetLastIndexIn(unsigned long idpromo)
{
if( (!profile.iPagoOnlineEMV))
return 0;
unsigned uMax=0;
for(int i=iidx;i>=0;i--){
if(pmdep[i].idpromo!=idpromo)
continue;
if(pmdep[i].indexin>uMax)
uMax=pmdep[i].indexin;
}
return uMax;
}
pos_prom.cpp - PromocionesMdeP_DeleteMdeP
Se guardan los valores entrantes antes del lazo por promocion:
double dRecDescIng=dRecDesc; //20261106 double dImporteIng=dImporte; //20261106 unsigned uIdxDel=0; //20261106 indice del registro de control a borrar
Codigo original (reemplazado):
double _dRecDesc=0L;
double _dImporte=0L;
globalarrayPromMdepControl.GetLast(globalarrayPromMdepControl.getCurrentIndexIn(), Id, iIdMdeP, _dRecDesc, _dImporte);
dImporte=_dImporte;
dRecDesc=_dRecDesc;
if(!VALIDAR(dRecDesc))
continue;
Codigo nuevo:
if(!VALIDAR(dRecDescIng))
continue;
dImporte=dImporteIng;
dRecDesc=dRecDescIng;
{
unsigned uIdx=globalarrayPromMdepControl.GetLastIndexIn(Id);
if(uIdx){
double _dRecDesc=0L;
double _dImporte=0L;
globalarrayPromMdepControl.GetLast(uIdx, Id, iIdMdeP, _dRecDesc, _dImporte);
if(VALIDAR(_dRecDesc)){
dImporte=_dImporte;
dRecDesc=_dRecDesc;
uIdxDel=uIdx;
}
}
}
Log(LOGOK, LOG_PROMOMDEP, GCmos.NroZ, "[DelMdeP] promo %ld idx %u rec %.2lf imp %.2lf", Id, uIdxDel, dRecDesc, dImporte);
El borrado del registro de control usa el indice encontrado, con el contador como respaldo:
globalarrayPromMdepControl.Del(uIdxDel ? uIdxDel : globalarrayPromMdepControl.getCurrentIndexIn(),iIdMdeP);
Comportamiento resultante:
- El registro se busca por promo:
GetLastIndexIn(Id)devuelve elindexinmas alto registrado para esa promo y, como los medios de pago se borran del ultimo al primero, es el del medio que se esta borrando. La reversion ya no depende del contador que se desfasa, yDel()elimina exactamente ese registro. - Un medio de pago sin descuento propio (
dRecDescIngigual a 0, por ejemplo el efectivo de 3.000) se omite sin tocar los registros. - Si el medio tenia descuento pero no hay registro (por ejemplo con
PAGOONLINE_EMV=0), se revierte con el descuento propio que llega como parametro yDel()usa el contador como respaldo. - Nuevo log de diagnostico (nivel
LOG_PROMOMDEP):[DelMdeP] promo <id> idx <indice> rec <descuento> imp <importe>.
5. Compilacion y paquete
Compilado con make -f postcpnu y implib pos_main.lib pos_main.exe bajo Phar Lap RUN286: 0 errores, 20 modulos recompilados (pcpos.h cambio). EXE anterior: 3.041.012 bytes, MD5 4358638d... (respaldos POS_MAIN.EXE.bak_20261106 y POS_MAIN.LIB.bak_20261106). Respaldos de fuentes: pos_prom.cpp.bak_promodel y pcpos.h.bak_promodel.
| Archivo | Que es | Bytes | MD5 |
|---|---|---|---|
POS_MAIN.EXE | Binario a instalar. Reemplaza al EXE de produccion 20260928P (3.041.012 bytes, MD5 4358638d...). | 3.041.647 | e1aae4c0a4e5015d13130e72da153c0a |
src_modificado/pos_prom.cpp | Fuente con el fix en PromocionesMdeP_DeleteMdeP. | 155.200 | 37038897a365555caf021e132a56dcba |
src_modificado/pcpos.h | Fuente con el metodo nuevo GetLastIndexIn. | 90.618 | bff39602903fa66397a4e7900464e273 |
pos_prom.diff | Diferencias contra pos_prom.cpp.bak_promodel. | 2.140 | 7ccdc01ffbd41d609878cbf3af33bad0 |
pcpos_h.diff | Diferencias contra pcpos.h.bak_promodel. | 748 | b6db1b620536959d7e73d843c9616641 |
LG261104_VM_DOBLEDTO.LOG | Log completo de la VM con las corridas A y B (seccion 6). | 1.460.585 | 433c6e2e28a7154d7b85529c478c68f0 |
6. Validacion en VM
VM VirtualBox Almacor_CAE_v260928P_test (MS-DOS 6.22, Btrieve 6.15, pcPOS rama ALMACOR) con los dM_Promo.dat y dM_ProAr.dat del cliente instalados. Promo 2250000 EFT/DEB: global, -10 %, minimo 15.000, valida solo los miercoles, con efectivo (1,1) entre los medios de pago. Fecha de la VM: miercoles 04/11/2026.
Misma secuencia de caja en las dos corridas: 25 x EAN 7790895000430 (3.999,99) = 99.999,75; efectivo 20.000, 16.000 y 3.000; borrar los tres (tecla - del teclado numerico); recargar los tres; cerrar con efectivo 61.000.
| Corrida A - EXE de produccion 20260928P | Corrida B - EXE corregido | |
|---|---|---|
| Tamano / MD5 | 3.041.012 bytes4358638d4e46e4ee72173c8f709aaf36 | 3.041.647 bytese1aae4c0a4e5015d13130e72da153c0a |
| Ticket / hora (reloj de la VM) | 130, 16:11 a 16:13 | 131, 16:18 a 16:20 |
Lineas [DelMdeP] | no existen en este EXE | dos: idx 2 (-1.600) e idx 1 (-2.000); ninguna para el medio de 3.000, que no tenia descuento |
PROMOMDEP[EFT/DEB] impreso | -13.299,98 | -9.699,98 |
| Esperado (2.000 + 1.600 + 6.099,98) | -9.699,98 | -9.699,98 |
| Diferencia | 3.600 de mas (los dos descuentos borrados), el patron del cliente | 0 |
Corrida A (EXE de produccion), lineas del log
MDEP ADD:EFECTIVO MRefSinD:67599.75 MRefConD:57599.77 MIngr:3000.00 MNetRes:3000.00 RecD:0.00 SaldT:60999.75 MDEP DEL:EFECTIVO TotDel:3000.00 RecDes:0.00 SaldTi:63999.75 MDEP DEL:EFECTIVO TotDel:14400.00 RecDes:-1600.00 SaldTi:79999.75 MDEP DEL:EFECTIVO TotDel:18000.00 RecDes:-2000.00 SaldTi:99999.75 (recarga de 20.000, 16.000 y 3.000) MDEP ADD:EFECTIVO MRefSinD:60999.75 MRefConD:54899.77 MIngr:61000.00 MNetRes:61000.00 RecD:-6099.98 SaldT:-6100.23 PROMOMDEP[ EFT/DEB]-13299.98 (TOTAL)
Corrida B (EXE corregido), lineas del log
MDEP ADD:EFECTIVO MRefSinD:67599.75 MRefConD:57599.77 MIngr:3000.00 MNetRes:3000.00 RecD:0.00 SaldT:60999.75 MDEP DEL:EFECTIVO TotDel:3000.00 RecDes:0.00 SaldTi:63999.75 [DelMdeP] promo 2250000 idx 2 rec -1600.00 imp 16000.00 MDEP DEL:EFECTIVO TotDel:14400.00 RecDes:-1600.00 SaldTi:79999.75 [DelMdeP] promo 2250000 idx 1 rec -2000.00 imp 20000.00 MDEP DEL:EFECTIVO TotDel:18000.00 RecDes:-2000.00 SaldTi:99999.75 (recarga de 20.000, 16.000 y 3.000) MDEP ADD:EFECTIVO MRefSinD:60999.75 MRefConD:54899.77 MIngr:61000.00 MNetRes:61000.00 RecD:-6099.98 SaldT:-6100.23 PROMOMDEP[ EFT/DEB]-9699.98 (TOTAL)
Prueba de la condicion de saldo. Un primer intento con un ticket de 39.999,90 imprimio el -3.600 correcto con el EXE anterior, porque el medio de 3.000 se agrego con el saldo pendiente ya por debajo del minimo (MRefSinD:0.00) y no avanzo el contador. Esto confirma la condicion descrita en la seccion 3.
El log completo esta en el paquete como LG261104_VM_DOBLEDTO.LOG.
7. Despliegue y pendientes
Despliegue
- Cerrar la POS (modo tecnico).
- Respaldar
C:\PCPOS\POS_MAIN.EXE(por ejemplo comoPOS_MAIN.EXE.bak). - Copiar el
POS_MAIN.EXEdel paquete aC:\PCPOSde cada POS y reiniciar.
No hay cambios de base de datos ni de perfiles. Perfiles involucrados en el cliente: 472 PAGOONLINE_EMV=1 y 256 MODOCANCELAMDEPPROMO=2. Rollback: restaurar el EXE respaldado.
Con una promo por medio de pago activa, el log de la POS mostrara la linea [DelMdeP] en cada borrado de medio de pago; sirve para el diagnostico en campo.
observacion pendiente Posible segunda fuga
En pos_mis2.cpp (aprox. lineas 3800-3935), PromocionesMdeP podria aplicar la promo antes de algunos return 1 que rechazan el medio de pago. No esta confirmado y no se cubre con este fix.