Cambios en el POS: descuento por pago QR

Configuracion y logica del POS MS-DOS (Borland C++ 3.1, Btrieve) para imputar el beneficio que informa la billetera QR
Rama BRANCH_DINO · Arbol: C:\Work\AI\DINO_20260902 · Marcadores en fuente: //20260903 QRDTO · //20260912 QRDTO percepcion · //20260913 QRFILA

1. Que hace el POS cuando la billetera devuelve un descuento

Al cobrar con PAGO QR, la respuesta de la operacion puede traer un beneficio ya aplicado por la billetera. El POS lo recibe despues de que la operacion fue autorizada — de ahi "post-pago" — y debe reflejarlo en el ticket, en los libros fiscales y en el cierre Z sin pedirle nada al cajero.

1.1 Los tres campos que el POS lee

CampoSignificado para el POSEjemplo (ticket M1)
importeImporte bruto que el POS pidio cobrar2750.00
importe_recdescBeneficio; negativo cuando es descuento, positivo seria recargo-275.00
importe_finalLo efectivamente cobrado. Es lo que se persiste como importe del medio de pago2475.00
D = -importe_recdesc                      // beneficio, siempre >= 0
r = D / Ticket.TotalCobrar                // ratio global (TotalCobrar incluye percepcion)
condicion de aceptacion: importe_final == importe + importe_recdesc  (tolerancia 0.005)

1.2 La secuencia dentro del POS

PagoQR_ImputarDescuento() (pos_mppo.cpp:3622) corre dentro del lazo de autorizacion de _ProcessKeyTOTAL(), justo antes del Update() de la fila del medio de pago. En ese punto hace, en este orden:

  1. valida las cinco guardas de entrada (seccion 5.1);
  2. agrega al detalle del ticket una fila por bucket de IVA (TICKET::PagoQR_AppendFilasDescuento, pos_tic2.cpp:1153), con bAcumulado=1 y monto negativo;
  3. reescala la percepcion (RecomputePercepcionxPromoMdeP(1, ...));
  4. prorratea dataRedondeos (relevante solo con impresora fiscal);
  5. baja los totales del ticket con ViewSaldo(-D);
  6. ajusta Ticket.total[n] (importe -= D, recdesc += importe_recdesc);
  7. escribe la fila de dE_MdeP/dE_DMdeP con importe_final, importe_recdesc y el id de autorizacion.

1.3 La decision de diseno que define todo lo demas

El beneficio se persiste igual que una promocion por medio de pago clasica: una fila real de ETdetdB_TDeta/dE_TDeta por bucket de IVA. A partir de ahi TICKET::ComputarBinario netea dE_TResu solo, porque arma totalTRESU exclusivamente barriendo ETdet; y el ticket impreso, el CAE, el Informe Impositivo y la Nota de Credito ven el descuento sin ningun parche puntual, porque todos leen ese mismo detalle.

No se reuso el motor de promociones. ImputaPromocionesXCodigoDescuento esta acoplada a EProm/MProm/acupro, a ControlUsoUnaSolaVezMdeP y al monto que teclea el cajero (dMontoIngresado), no a un hecho post-autorizacion. Lo que se reuso es el patron de datos, no la funcion.

Dos funciones intermedias quedaron retiradas y no deben volver: PagoQR_NetearTRESU() (pos_tic2.cpp, reescribia el struct totalTRESU en memoria) y PagoQR_ProrratearIVAS() (pos_inve.cpp, prorrateaba en tiempo de impresion). Esta ultima aplicaria el descuento dos veces: ImprimirIVAS arranca de tick->ComputarBinario(1,...), que ya devuelve el neto. En el arbol solo quedan comentarios que documentan el retiro.

1.4 Numeros de referencia (ticket M1)

items (bAcumulado=0)         2750.00     neto 2145.21 + IVA21 450.50 + imp.int. 154.29
fila del beneficio           -275.00     neto -214.52 + IVA21  -45.06 + imp.int. -15.42
                             --------
dE_TResu.fPrecioTotal         2475.00    fNetoFiscal1 1930.69 / fTax0Fiscal 405.44 / fImpInterno1 138.87
dE_DDMP  PAGO QR              2475.00    dRecDesc -275.00
ticket impreso                SUBTOTAL 2750.00 / Rec-Desc -275.00 / TOTAL 2475.00
QR AFIP                       "importe": 247500

2. Flujo interno del POS

01 / Respuesta PAGO QR 02 / Imputacion en COBRO 03 / Detalle y totales 04 / Persistencia 05 / Consumidores Respuesta PAGO QR importe / recdesc / final 3 campos PagoQR_Imputar Descuento, pos_mppo.cpp guardas + ratio r RecomputePercepcion xPromoMdeP, pos_tic2.cpp orden F13 PagoQR_AppendFilas Descuento, pos_tic2.cpp 1 fila por IVA dE_MdeP / dE_DMdeP fila PAGO QR importe y recdesc ViewSaldo(-D) Ticket.TotalCobrar solo memoria ComputarBinario barrido de ETdet dE_TResu buckets netos fRecDescMPP = 0 dE_DDMP + .BIN Dump2DDMP ZLOG\<Z>.bin dB_TDeta espejo Informe Impositivo pos_info.cpp:4043 Cierre Z GetGrandesTotales Impresion + CAE ImprimirIVAS Nota de Credito pos_ncre.cpp sin probar recdesc descuento informado D del lado venta bAcumulado=1, monto < 0 r x neto sin percepcion despues de las filas importe_final / recdesc vData = trxid totales del ticket no toca ETdet filas de ETdet buckets netos detalle de items bruto + fila negativa cobrado QR fuera de fRecDescMPP vT_TDeta fPrecioTotal dImporte neto e IVA II por diferencia detalle del ticket filas bAcumulado=1 Legend primary data policy / PII async batch data store
Camino completo dentro del POS: la respuesta del PAGO QR llega a PagoQR_ImputarDescuento, que agrega las filas de detalle por bucket de IVA y baja los totales en memoria por caminos disjuntos; ComputarBinario netea dE_TResu desde el detalle, y de ahi salen el cierre Z, la impresion, el CAE, el Informe Impositivo y la Nota de Credito.
Por que no hay doble descuento. ViewSaldo(-D) mueve Ticket.TotRecDesc/TotalNeto/TotalCobrar — totales en memoria: saldo, el TOTAL impreso y los impuestos internos por diferencia de ImprimirIVAS. ComputarBinario arma totalTRESU solo desde ETdet y no mira ningun campo de Ticket. Los dos caminos son disjuntos y ahora coinciden porque salen de la misma fuente. El unico punto donde se encuentran es ImprimirIVAS, que compone netos e IVA desde ComputarBinario (ya neto) y saca los impuestos internos por diferencia contra Ticket.TotalCobrar (ya neto): la identidad neto + IVA + impInt == TotalCobrar cierra por construccion.

3. Tablas afectadas y consumidores

3.1 dB_TDeta / dE_TDeta (detalle del ticket) — el cambio central

Las filas de item (bAcumulado=0) no se tocan: siguen con el precio bruto de catalogo. Lo nuevo son las filas del beneficio, que escribe TICKET::PagoQR_AppendFilasDescuento() copiando campo por campo el patron de ImputaPromocionesXCodigoDescuento (pos_tic2.cpp:869-919).

CampoValorOrigen
cEanEAN del beneficio QRperfil 564 (con fallback, seccion 4)
ArticuloPLU del beneficio QRperfil 565
iDeptodepartamento de descuentosperfil 566
iPromocionnumero que identifica la fila en los reportes que filtran por esa columnaperfil 567
bAcumulado1 (marca de descuento)fijo
fPrecio / PrecioFiscalmonto negativo del bucketcalculo (seccion 5)
fNetoFiscalPrecioFiscal - (tax + imp.internos)calculo
fTax0/Tax0Fiscal o fTax2/Tax2FiscalIVA negativo segun bucketcalculo
fImpInternoimpuestos internos negativos; recibe el residuo de centavoscalculo
iTaxttNORMAL21=0 / ttEXENTO=1 / ttNORMAL105=2pos_defi.h:783
Descripcionnombre del medio de pago informado, truncado a 25 caracteres; fallback DTO BILLETERA QRtrx.mediodepago
Cantidad / Unidades / iBonos1 / 1 / 0fijo
iPromoPrior0. El campo guarda el Metodo_Accion de la promo de origen (MA_MONTO..MA_LISTADESCUENTO); no hay accion del motor de promos detras de este beneficio, y 0 no lo usa ninguna promo real, asi que la fila queda identificablefijo, deliberado
iNroTicket, cFecha, AssignPackIddel ticket en cursocontexto

Ejemplo M1: un item 21%, beneficio 10% → una sola fila

cEan          Articulo  iDepto  iPromocion  bAcu  iTax  PrecioFiscal  fNetoFiscal  Tax0Fiscal  fImpinterno
7799999999991 QRTEST01     777      990077     1    0       -275.00      -214.52      -45.06       -15.42

Ejemplo M2: 21% + 10.5% + exento → tres filas

cEan          Articulo  iDepto  iPromocion  bAcu  iTax  PrecioFiscal  fNetoFiscal  Tax0/2Fiscal  fImpinterno
7799999999991 QRTEST01     777      990077     1    0        -274.99      -214.52       -45.06        -15.41
7799999999991 QRTEST01     777      990077     1    2         -49.91       -45.16        -4.75          0.00
7799999999991 QRTEST01     777      990077     1    1        -149.00      -149.00         0.00          0.00
                                                       suma  -473.90  ==  -D

Una fila por bucket con monto distinto de cero. Un bucket sin items no genera fila. Los valores de cEan/Articulo/iDepto/iPromocion del ejemplo son los de prueba; en produccion salen de los perfiles cargados (seccion 4).

3.2 dE_TResu (resumen del ticket)

Se llena integramente en TICKET::ComputarBinario(0, ...). Con -DNOCOMPUTAREDONDEOS activo (postcpnu.mak:137), totalTRESUPromo — donde caen las filas bAcumulado==1 — se suma a totalTRESU antes de los assign, asi que las filas del beneficio bajan los campos por si solas.

CampoSin beneficioCon beneficio (M1)Comentario
fPrecioTotal / fPrecioTot1 / fPrecioFiscal12750.002475.00total persistido = cobrado
fNetoFiscal1 / fPrecioNeto2145.211930.69neto 21%
fTax0 / fTax0Fiscal450.50405.44IVA recalculado sobre el neto nuevo
fImpInterno / fImpInterno1154.29138.87absorbe el residuo de centavos
fRecDescMPP0.000.00siempre 0 para el QR, ver 3.3
fTax1 / fTax3 / fMontoPercepcion*64.3657.92percepcion IIBB reescalada por r; no forma parte de fPrecioFiscal1..3, se suma aparte a fPrecioTotal

Las tasas salen de Fact.TaxPorc[0] (21%) y Fact.TaxPorc[2] (10.5%), con fallback IVA/neto de la propia fila si Fact no viene cargada.

3.3 dE_DDMP (medios de pago del ticket) y su espejo .BIN

CampoValorEjemplo M1
uId / uIdIdmedio de pago PAGO QR22 / 10
cDescripcionnombre del medio informadoDINI SIM
dImporteimporte_final: lo efectivamente cobrado2475.00
dRecDescimporte_recdesc: el beneficio, negativo-275.00
cCuentaid de transaccion
vDataid de autorizacion — es la marca que lee la guarda de borrado y la que habilita una anulacion futuraSIM-38
cPlanid de intencionSIM-38
El espejo .BIN no es equivalente y no hay que confiar en el. Dump2DDMP (pos_misc.cpp:4743) tiene un bloque if(EsPagoQR(uId,uIdId)) que pisa cBarCode con el id de intencion y copia vData; _SaveRecordBinDDMP no tiene ninguno de los dos. Medido: Btrieve cBarCode=SIM-4 / cPlan=0 / vData=SIM-4 contra .BIN cBarCode=(vacio) / cPlan=SIM-4 / vData=(vacio). Es preexistente, pero para identificar la autorizacion el dato confiable es la fila Btrieve.

Cambio de QRFILA en Dump2DDMP (pos_misc.cpp:4717)

if(!EsPagoQR(uId, uIdId))	//20260913 QRFILA
	dTotalRecDescMPP += dRecDesc;

Con las filas de detalle, fPrecioTotal ya viene neto; acumular ademas el dRecDesc del QR en fRecDescMPP haria que ReadTRESU_Totals (camino de recuperacion ReStartEndVueltoOK, pos_vnta.cpp) lo restara dos veces. El monto no se pierde: sigue en dE_DDMP.dRecDesc, que es de donde ya lo lee AnalyzeRecDesc (pos_info.cpp:3873) para el informe de recargos y descuentos.

Cambio de contrato a avisar: una promo por medio de pago sobre un medio que no sea QR sigue entrando en fRecDescMPP, y como esa promo tambien netea fPrecioTotal por sus propias filas, ReadTRESU_Totals la restaria dos veces en el camino de recuperacion. Es un hueco preexistente, anterior a este trabajo, pero ahora esta identificado.

3.4 dE_MdeP / dE_DMdeP y dE_TMdeP

3.5 ZLOG\<Z>.bin

Binario de items de la Z, escrito al cerrar la Z. El equivalente vivo de una Z abierta es BASES\DB_TDETA.BIN, identico en layout (mismo DDF, mismo escritor DdfBin). Es la fuente del Informe Impositivo, que lo copia a vT_TDeta y lo recorre separando por iTax y por bAcumulado: como las filas del beneficio viven en el detalle, ese informe ya las ve.

3.6 dM_Profi

Cuatro perfiles nuevos (564-567, seccion 4). No se agrego ni se modifico ningun campo DDF en todo el trabajo.

3.7 Matriz de consistencia de los consumidores

ConsumidorQue leeResultado con beneficio QREstado
Cierre ZGetGrandesTotales (pos_info.cpp:1179), xHora_Tot (:985), GetPerformanceCajeroVentas (:2877), ComputeValues (pos_mis2.cpp:717) dE_TResu.fPrecioTotal / fPrecioFiscal1..3. Ninguno lee fRecDescMPP desde disco 2475.00: el cobrado real, contado una sola vez consistente
Recuperacion de ticketReadTRESU_Totals (pos_vnta.cpp:4668) fPrecioTotal + fRecDescMPP sobre el ETres en memoria del ticket en curso 2475.00 + 0.00 = 2475.00. Esa es la razon tecnica de persistir fRecDescMPP = 0 consistente
CAEpos_cae.cpp BuildXML globalTaxesTickets, llenado al final de ImprimirIVAS (exportado en pos_main.def:161). Rama alternativa: ComputarTaxes() desde ETdet, con autocorreccion contra Ticket.TotalNeto QR AFIP "importe": 247500 consistente
Ticket impresoImprimirIVAS (pos_inve.cpp) + PrintRecDescMdeP (pos_vnta.cpp) ComputarBinario(1,...) (ya neto) e impuestos internos por diferencia contra Ticket.TotalCobrar (ya neto) SUBTOTAL 2750.00 / Rec/Desc <medio> -275.00 / TOTAL 2475.00. No hay doble impresion: las filas nuevas no salen como linea de detalle, porque el detalle se imprime al escanear cada item y estas se agregan recien en el cobro consistente
Informe ImpositivoInformeImpositivo::LeerDatos (pos_info.cpp:4043) binario de items de la Z (vT_TDeta), no dE_TResu 2750.00 - 275.00 = 2475.00 con IVA 405.44, verificado sobre las nueve corridas con beneficio consistente
Nota de Creditopos_ncre.cpp el detalle de items del ticket original; su dFactorConversionPorPromos sale de las filas bAcumulado==1 (pos_ncre.cpp:1056-1104) Con la fila presente, la NC deberia ajustar sola sin tocar pos_ncre.cpp. No se pudo ejercitar en el entorno de prueba no verificado
Envio al servidor lee dB_TDeta (y el resumen dE_TResu) Ambas fuentes quedan netas y coherentes entre si no ejercitado
Informe de recargos y descuentosAnalyzeRecDesc (pos_info.cpp:3873) dE_MdeP.dRecDesc -275.00: el beneficio sigue visible por medio de pago consistente

La identidad que ata los tres libros:

SUM(dB_TDeta.PrecioFiscal, filas no-percepcion)
  == dE_TResu.fPrecioTotal - percepciones
  == SUM(dE_DDMP.dImporte) - percepciones

4. Configuracion: perfiles

4.1 Perfiles nuevos (564-567)

Identifican la fila de detalle del beneficio. 563 era el ultimo id definido en pos_prof.h y 553 el ultimo presente en la tabla dM_Profi de la copia de prueba, asi que 564-567 estaban libres de los dos lados. Declarados en pos_prof.h:672-675; miembros de GLOBALPROFILE en pcpos.h:571-574; leidos en pos_main.cpp:4692-4699; resueltos en un solo lugar por PagoQR_FilaIds() (pos_tic2.cpp:1095).

idSimboloTipoUsoFallback si falta o viene vacio
564pro_QR_PROMO_EANtextocEan de la fila del beneficioprofile.cIdEAN_StockDescuentos; si tambien esta vacio, 9999999999999
565pro_QR_PROMO_PLUtextoArticulo de esa filaprofile.cIdArtPromoGlobal; si tambien esta vacio, 0
566pro_QR_PROMO_DEPTOintiDepto de esa filaprofile.iIdDepto_StockDescuentos; si es 0, 999
567pro_QR_PROMO_PROMONRlongiPromocion: identifica la fila en los reportes que filtran por esa columna0
Carga en produccion. Hay que definir los valores reales (el EAN, PLU y departamento que use la casa para descuentos, y un numero de promocion propio) y cargarlos por el editor de perfiles. Si se escriben con SETPROF -s el campo cName queda vacio y el volcado de perfiles del arranque (pos_main.cpp:4982, formato "%d=%.20s=%.60s") los muestra como 564=?=<valor>. El volcado recorre la tabla entera, asi que los nuevos aparecen solos sin tocar nada.

4.2 Perfiles existentes que influyen

idSimbolo realPor que importa para este cambio
493pro_PAGOQR_HABILITADOHabilita el medio de pago PAGO QR. Sin esto no hay nada que imputar
516pro_PAGOQR_IDMDEPuId del medio PAGO QR (22 en la copia de prueba). Es lo que consulta EsPagoQR(), del que dependen la guarda de borrado y la exclusion de fRecDescMPP
500pro_PAGOQR_IDTRX_A_ANULARLo escribe PagoQR_ImputarDescuento y lo consume la Reversa QR. Se limpia en cada COBRO() y en cada StartTicket (pos_main.cpp:1969/:2350), asi que solo referencia la ultima operacion
271pro_USAPERCEPCIONESGatea UtilizaPercepcion() via VALIDAR() (pos_defi.h:1026). Si esta en cero, todo el bloque de percepcion del calculo es un no-op
272pro_MONTOMINIMOPERCEPCIONESdMinimoPercepciones: monto minimo para que aplique la percepcion
273pro_PORCENTAJEPERCEPCIONESdPorcentajePercepciones: la tasa. Es el perfil que determina si el camino de percepcion (el mas delicado del calculo) se ejecuta o no
276pro_DEPTO_PERCEPCIONESiDepto de la fila sintetica PERCEPCION_IIBB que el POS ya materializa en dB_TDeta (codigo preexistente). Esa fila la excluye ComputarBinario de los buckets de IVA, y el calculo del beneficio tambien la saltea
197iUsa_dM_ProArHabilita las promociones por medio de pago (LoadMdePGloPromo(), pos_prld.cpp:1101-1141). Relevante porque una promo por MdeP y el beneficio QR pueden convivir en el mismo ticket, y de ahi sale el ratio efectivo de la seccion 5.2
190pro_SOLO_NCREDITOHabilita las Notas de Credito. Hay que confirmarlo antes de validar el comportamiento de la NC sobre un ticket con beneficio
Nomenclatura. Los nombres PERCEPCION_USA, PERCEPCION_MONTOMIN, PERCEPCION_TASA y PERCEPCION_DEPTO que circulan en notas previas no existen: los simbolos reales son los de la tabla (pos_prof.h:298-304). Los ids si son correctos.

5. Reglas de calculo y guardas

5.1 Guardas de entrada (PagoQR_ImputarDescuento, pos_mppo.cpp:3622)

#CondicionAccionLog
1!VALIDAR(importe_recdesc) (D == 0)no-op, flujo normalninguno
2D < 0 (recdesc positivo = recargo)rechazo; el ticket cierra sin descuento[QRDTO] RECHAZADO ...
3|importe_final - (importe + importe_recdesc)| >= 0.005rechazo[QRDTO] RECHAZADO ...
4D > Ticket.TotalCobrar (estricto)rechazo[QRDTO] RECHAZADO ...
5id de autorizacion ya imputado (comparado contra cQRDto_TrxId)no-op idempotente[QRDTO] ya imputado trx=...

Fuera de esta funcion hay una guarda mas: ActiveDMdeP() (pos_mppo.cpp:3240) impide cargar el PAGO QR dos veces en el mismo ticket — "Error, solo permitido ingreso PAGOQR una vez!".

5.2 Ratio global y ratio efectivo

D        = -importe_recdesc                          ROUND_DOUBLE
r        = D / Ticket.TotalCobrar_antes              // ratio global, con percepcion incluida
dDVenta  = ROUND((Ticket.TotalCobrar_antes - Ticket.Percepcion) * r)
r_eff    = dDVenta / SUM(PrecioFiscal de las filas de item)

5.3 Reparto por bucket de IVA

Se barre ETdet con los mismos buckets que ComputarBinario (indice 0 = 21%, 1 = 10.5%, 2 = EXENTO), tomando solo filas de item (bAcumulado==0 y iTax que no sea percepcion), y acumulando por bucket precio, IVA, impuestos internos y neto. Despues, por bucket:

K   = ROUND(N * r_eff)                   // recorte del neto
N'  = N - K                              // neto descontado
II' = ROUND(II * (1 - r_eff))            // impuestos internos escalados
T'  = ROUND(N' * (1 + tasa)) - N'        // IVA RECALCULADO, nunca prorrateado
fila_j = (N' + T' + II') - (N + T + II)  // el monto de la fila es el DELTA, negativo
El IVA se recalcula, no se prorratea. Es una decision explicita: las filas que pasan por este camino cumplen |IVA - ROUND(neto * tasa)| = 0.0000 exacto. Las filas brutas historicas tienen 1 centavo de desacuerdo (450.50 persistido contra 450.49 calculado), que es preexistente y es la razon por la que el control de IVA necesita una tolerancia de 0.01.
Cuidado con las tres numeraciones de "bucket" que conviven en el codigo, porque es el error de lectura mas facil de cometer:
• indice de bucket de totalTRESU y del calculo de la fila: 0=21%, 1=10.5%, 2=EXENTO;
• campo iTax persistido (enum TaxTypes, pos_defi.h:783): 0=ttNORMAL21, 1=ttEXENTO, 2=ttNORMAL105;
ImputaPromocionesXCodigoDescuento itera invertido: 0=exento, 1=10.5%, 2=21% (comentado en pos_tic2.cpp:897).

5.4 Residuo de centavos

residuo = -dDVenta - SUM(fila_j)

Va entero a los impuestos internos del bucket de neto mas grande, entre los buckets que tienen items. Es el mismo destino que le da ImprimirIVAS, que saca los impuestos internos por diferencia contra Ticket.TotalCobrar: por eso la fila persistida queda identica a la impresa y a la del CAE. Medido: M1 persiste II 138.87 (residuo +0.01) y M2, con un articulo 10.5% y uno exento mas, persiste II 138.88. En un ticket solo exento o solo 10.5% el residuo cae en los impuestos internos de ese unico bucket con neto, que es lo que hace tambien la impresion.

5.5 Percepcion

if(POSConPercepcionesHabilitadas() && VALIDAR(Ticket.Percepcion)){
    double dNetoSinPercepcion = dTotalAntes - Ticket.Percepcion;
    RecomputePercepcionxPromoMdeP(1, -1.0L * dRatio * dNetoSinPercepcion, dRecDescPorPercep);
    }

Se alimenta con -r * NetoSinPercepcion para que el ratio interno de RecomputePercepcionxPromoMdeP reproduzca r exacto. Reescala Ticket.Percepcion y Ticket.NetoTicketReal; no toca ninguna fila de ETdet, ni siquiera la fila sintetica de percepcion. Deja ademas el historial en percepcionPromoArray, que es lo que permite revertirlo con RecomputePercepcionxPromoMdeP(2, ...). Medido: sin QR, neto 2145.21 → fTax1 64.36; con beneficio del 10%, neto 1930.69 → fTax1 57.92 (cociente 0.8999).

5.6 Orden de las llamadas

La llamada a PagoQR_AppendFilasDescuento tiene que ir ANTES del bloque de percepcion. Esa funcion lee Ticket.Percepcion para calcular dDVenta, y RecomputePercepcionxPromoMdeP reescribe esa variable en el lugar. Al reves, el monto de las filas sale calculado sobre la percepcion ya reducida: medido, (2814.36 - 57.92) * 0.100001 = 275.65 en vez de 275.00, con lo que el ticket persistia 2532.27 contra 2532.92 efectivamente cobrados. En el codigo hay un comentario //OJO ORDEN que explica la dependencia. Sin percepciones el orden es indiferente, por eso ningun otro escenario lo detecto.

5.7 Caso limite: beneficio del 100% (r == 1)

La guarda 4 rechaza solo D > TotalCobrar (estricto), asi que D == G pasa. La formula por bucket con r_eff == 1 da K = N, N' = 0, II' = 0 y T' recalculado sobre neto cero, sin ningun caso especial. Medido: filas sumando -4739.00, todos los buckets en 0.00, fPrecioTotal 0.00, ticket impreso TOTAL 0.00. El POS no rechaza un total de 0.00. Los rangos que acotan este ratio usan limite superior cerrado (0 < r <= 1.0 + 1e-9, con clamp a 1.0 exacto): un intervalo abierto dejaria el caso del 100% sin netear (ver seccion 9).

6. Bloqueo de borrado de un PAGO QR autorizado

6.1 La politica

Una fila de PAGO QR ya autorizada no se puede borrar desde la pantalla de medios de pago — la billetera ya cobro. El camino correcto es la Reversa QR. Una fila de PAGO QR cargada pero no autorizada si se puede borrar, y en ese caso se limpia todo lo imputado.

01 / Fila de medio de pago 02 / Salidas por error y borrado 03 / Desenlaces 01 Cargada sin autorizar 02 Autorizada PAGOQR_Op aprueba 03 Imputada PagoQR_ImputarDescuento 04 Cierre del ticket UpdateTMdeP Salida por error ret 4 / 5 / 999 / QUIT Intento de borrado kbDELETE_MEDIO Reversa QR requiere red Fila borrada imputacion revertida Ticket cerrado cobrado = G - D Borrado bloqueado return 1 cierre fallido hay trxid kbANULACION_PAGOQR no limpia vData sin trxid: se permite Legend active state waiting terminal success failure / exit
Estados de la fila de medio de pago PAGO QR y las ramas de incidencia.

6.2 La exposicion: donde queda el ticket abierto despues de autorizar

En el camino feliz el ticket cierra en la misma tecla y no hay ventana. Las salidas por error posteriores a la imputacion son la exposicion:

Caminofile:lineQue pasa
_ProcessKeyTOTAL "Si dio error, chau"pos_mppo.cpp:3950-3955 (return 4)CloseUpdateTMdeP, ModoVuelto=0, vuelve al browse de medios de pago
Falla de UpdateTMdeP / limites / ret==999pos_mppo.cpp:3985-3994 (return 5)PError, reversa de cuenta corriente; si ret==999 marca iErrorFisSeCancTickDelAll=999
Error fiscal 999 → handler de kbTOTALpos_mppo.cpp:4178DeleteVueltos() + while(!DeleteLastMDEP(iTeniaPromo));
kbQUITpos_mppo.cpp:4198idem
kbDELETE_MEDIOpos_mppo.cpp:4207 y :4212borra el ultimo medio; la variante :4212 pasa iEspecialEMV=1
ProcessKeyMdeP (dos lazos mas)pos_mppo.cpp:4966 y :4981idem while(!DeleteLastMDEP(...))

Sin guarda, al borrarse la fila de PAGO QR quedaban en ETdet las filas del beneficio y en memoria los acumuladores: un ticket pagado despues en efectivo persistia igual el descuento.

6.3 El precedente y la guarda

El criterio ya existia en la casa para tarjetas EMV (pos_mppo.cpp:1563, DeleteLastCardYaAutorizada): una tarjeta ya autorizada no se borra desde la pantalla de medios de pago. La guarda de QR se inserta en pos_mppo.cpp:1581, inmediatamente despues de la EMV y antes de SPbase->Remove(); el cuerpo vive en pos_tic2.cpp:1174.

if(PagoQR_BloquearBorradoMdeP(SPbase, _iIdMdeP, _iIdIdMdeP, cQRDto_TrxId))
	return 1;
  1. EsPagoQR(uId, uIdId) → si no es QR, no hace nada.
  2. Marca de autorizacion = vData de la fila de dE_DMdeP de este ticket; si esa fila no lo trae, cae al acumulador en memoria.
  3. Sin marca → se permite borrar (QR cargado pero nunca autorizado).
  4. Con marca → Error("PAGO QR autorizado: anule via Reversa QR"), log de error y corta.
Devuelve 1, no 0 como la guarda EMV. Copiar el 0 colgaria el POS: la guarda EMV solo corre con iEspecialEMV, cuyo unico llamador (kbDELETE_MEDIO) hace una sola llamada, mientras que la guarda de QR corre siempre y los cuatro lazos while(!DeleteLastMDEP(...)) de :4178, :4198, :4966 y :4981 girarian para siempre con la fila sin borrar. 1 es el mismo valor que DeleteLastMDEP ya devuelve cuando no hay nada que borrar (pos_mppo.cpp:1519-1523), asi que todos los llamadores lo manejan bien: los lazos cortan en la fila QR (el barrido es de abajo hacia arriba, las filas por encima ya se borraron) y las llamadas unicas salen de la pantalla de medios de pago. El ticket nunca queda bloqueado: siempre se puede cerrar con TOTAL, que es lo que corresponde cuando la billetera ya cobro.

6.4 Limpieza cuando el borrado si procede

pos_mppo.cpp:1612 llama a PagoQR_RevertirImputacionMdeP() (pos_tic2.cpp:1198), que:

  1. Saca del detalle las filas del beneficio (TICKET::PagoQR_RemoveFilasDescuento, pos_tic2.cpp:1207), identificadas por el mismo triple con que se escribieron: bAcumulado==1 + iPromocion + cEan + Articulo + iNroTicket. Remove() invalida el cursor, por eso el barrido se reinicia despues de cada baja.
  2. Deshace el recomputo de percepcion con RecomputePercepcionxPromoMdeP(2, ...), que es exactamente lo que hace TICKET::PromocionesMdeP_DeleteMdeP para una promo por medio de pago clasica (pos_prom.cpp:2810).
  3. Limpia los acumuladores del beneficio.

Los totales no se tocan ahi: DeleteLastMDEP ya los devuelve a los valores previos al QR.

Saldo'       = 0 + (G-D) - (-D) = G      (vuelve a deber el bruto)
TotRecDesc'  = TotRecDesc + D   = 0
TotalNeto'   = TotalInicial + 0 = G
TotalCobrar' = G

No se revierte dataRedondeos (lo prorratea PagoQR_ProrratearDataRedondeos, pos_mppo.cpp:3588). Es un no-op sin impresora fiscal, porque CalcularRedondeos() solo lo llena con profile.iAjustaRedondeFiscal && HayImpresoraFiscal. Con impresora fiscal real es un pendiente.

6.5 Que ve y que hace el cajero

  1. Presiona la tecla de borrar medio de pago sobre la fila PAGO QR.
  2. Ve el mensaje PAGO QR autorizado: anule via Reversa QR y la fila queda.
  3. Si tiene que deshacer la operacion, usa la Reversa QR: menu Admin/Tarjetas, tecla kbANULACION_PAGOQR (pos_cara.cpp:1195).
  4. Si no, simplemente cierra el ticket con TOTAL: el pago QR ya esta hecho y el ticket cierra por el importe cobrado.

Limites de la Reversa QR que hay que conocer antes de instruir al cajero:

6.6 Decisiones abiertas

#Decision pendienteEstado
1Despues de una reversa exitosa, quien limpia la marca? Hoy nadie. Opciones: (a) que la reversa limpie vData/cCuenta de la fila del ticket en curso, (b) que la reversa borre directamente la fila de medio de pago, (c) dejarlo asi y que el ticket cierre igual.sin implementar
2Camino automatico del error fiscal 999 (pos_mppo.cpp:4178): antes borraba todos los medios sin preguntar; con la guarda el lazo corta en la fila QR y quedan esa fila y las de abajo. Es el comportamiento deseado, o se prefiere un mensaje explicito al cajero?abierta
3Override de supervisor: no se implemento. El lugar natural seria dentro de PagoQR_BloquearBorradoMdeP, pidiendo clave antes de devolver 0.no implementado
4Recuperacion tras reinicio (F14). Si el ticket queda abierto con un PAGO QR ya autorizado y el POS se reinicia, la fila del beneficio sobrevive (esta en disco, en el detalle) pero la fila del medio de pago no (vive en memoria). Medido en la recuperacion: fPrecioTotal 2475.00 contra 2750.00 cobrados en efectivo, es decir descuento persistido sin el pago que lo justificaba. Antes de este cambio el mismo reinicio dejaba el ticket bruto aunque la billetera ya hubiera cobrado: error del mismo tamano y signo contrario. Hay que definir la politica: reconstruir la fila del medio de pago al recuperar, borrar las filas del beneficio, o bloquear el cierre hasta resolver.NO RESUELTO
5Impresora fiscal: falta revertir dataRedondeos (6.4) y falta el comando fiscal de descuento por medio de pago (PROMOMDEPFISCAL / AJUSTE_DIFERENCIAS, pos_mis2.cpp:1787), que hoy depende de filas de promo. La rama fiscal de TIC_VDET.SCP quedo deliberadamente cortada en PrintRecDescMdeP por la guarda profile.HayImpresoraFiscal.pendiente

7. Archivos fuente a integrar

ArchivoModuloFunciones agregadasFunciones retiradas / cambios
pos_tic2.cppPOS_MAIN PagoQR_FilaIds() (:1095, identidad de la fila en un solo lugar); TICKET::PagoQR_AppendFilasDescuento() (:1153); TICKET::PagoQR_RemoveFilasDescuento() (:1207); PagoQR_BloquearBorradoMdeP() (:1174); PagoQR_RevertirImputacionMdeP() (:1198) Retirada PagoQR_NetearTRESU(). ComputarBinario vuelve a asignar fRecDescMPP = dTotalRecDescMPP sin condicional
pos_mppo.cppPOS_MAIN PagoQR_ImputarDescuento() (:3622); PagoQR_ProrratearDataRedondeos() (:3588); acumuladores dQRDto_Descuento / dQRDto_TotalAntes (POS_EXPORT, es decir _far _export → fuera de DGROUP) y static char _far cQRDto_TrxId[26] Tres llamadas finas: append dentro de PagoQR_ImputarDescuento, guarda de borrado (:1581) y reversa (:1612) en DeleteLastMDEP. El cuerpo de esas dos ultimas vive en pos_tic2.cpp por presupuesto de segmento
pos_misc.cppPOS_MAIN Dump2DDMP (:4717): if(!EsPagoQR(uId,uIdId)) dTotalRecDescMPP += dRecDesc;
pos_inve.cppDLL POS_INVE Retirada PagoQR_ProrratearIVAS() y su llamada en ImprimirIVAS; quedan los comentarios que documentan por que (:4380, :4655)
pos_main.cppPOS_MAIN Los cuatro GetProfile de 564-567 (:4692-4699), junto a los demas
pos_vnta.cppPOS_MAIN PrintRecDescMdeP(): recorre dE_MdeP y por cada fila con dRecDesc != 0 posiciona el registro, setea SF_Status=3 y SF_GranTotal=dRecDesc, y llama a TICK_VAL_DET. Es generica: lista cualquier medio de pago con recargo o descuento, no solo el QR Revive una rama de TIC_VDET.SCP que existia desde 1998 y que nadie disparaba, asi que no hace falta tocar ningun .SCP. Guardas: EstaEnModoCobro, tick!=NULL, VALIDAR(Ticket.TotRecDesc) y profile.HayImpresoraFiscal
pcpos.hheader GLOBALPROFILE: cQRPromo_EAN[20], cQRPromo_PLU[15], iQRPromo_Depto, lQRPromo_PromoNr (:571-574); dos miembros de TICKET (:944-945) profile es _far → FAR_DATA: no consume DGROUP
pos_prof.hheaderLos cuatro ids 564-567 (:672-675)
pos_func.hheaderDeclaraciones de PagoQR_BloquearBorradoMdeP y PagoQR_RevertirImputacionMdeP (:313-315)
pos_main.deflink _dQRDto_Descuento y _dQRDto_TotalAntes en EXPORTS (:164-165) Obligatorio: en este build los simbolos de datos compartidos EXE→DLL se exportan a mano. Sin esto el link de POS_INVE.DLL falla con Undefined symbol _dQRDto_Descuento. Cualquier cambio en el .def obliga a relink de POS_MAIN + implib + regenerar las DLLs
postcpnu.makbuild Macro QRSIM_DEFINE Ver 7.1

7.1 Nota sobre el .mak

postcpnu.mak lleva una macro nueva, declarada en Translator Definitions antes de la definicion de CC:

!ifdef QRSIM_TEST
QRSIM_DEFINE = -DQRSIM_TEST
!else
QRSIM_DEFINE =
!endif
CC = bcc286 -crtdll +postcpnu.cfg $(QRSIM_DEFINE)
En produccion QRSIM_DEFINE queda vacio y CC resulta identico al de siempre (con un espacio de mas al final, inocuo). El build de produccion es el de siempre y no requiere ninguna variante. Verificado: strings sobre el POS_MAIN.EXE de produccion y las 16 DLLs → cero ocurrencias de material de prueba; las unicas cadenas propias que quedan son las de log de produccion ([QRDTO] D=..., [QRDTO] RECHAZADO ..., [QRDTO] ya imputado ..., [QRFILA] r=... D=... bruto=... filas=... res=..., [QRDTO] fila QR eliminada, descuento revertido ..., [QRDTO] borrado bloqueado trx=..., [QRDTO] linea Rec/Desc MdeP ...).

Trampa de make a tener presente al integrar: Borland MAKE decide recompilar por fecha de archivo, no por flags. Al alternar configuraciones de build sin tocar los .cpp, los .obj quedan "al dia" con la configuracion anterior y el link produce un binario que no corresponde. Hay que borrar a mano los .obj afectados (pos_main.obj, pos_mppo.obj, pos_vnta.obj, pos_tic2.obj, pos_misc.obj) o tocar los .cpp para adelantar su fecha.

7.2 Presupuesto de segmentos

SegmentoProduccionLibre
DGROUPFF42H (65346)190
POS_MPPO_TEXTFD45H (64837)699
POS_TIC2_TEXT87D3H (34771)30765
POS_VNTA_TEXTE06EH (57454)8082
POS_MAIN_TEXTE57FH (58751)6785
POS_MISC_TEXTCBAAH (52138)13398
Advertencia: desborde silencioso de 64K. El linker no avisa cuando un segmento de codigo envuelve. Durante este desarrollo se vio dos veces: una version de PrintRecDescMdeP puesta en pos_mppo.cpp hizo que el MAP mostrara POS_MPPO_TEXT pasando de FEDDH (65245) a 00B7H; mas tarde aparecio un POS_MPPO_TEXT 0001013F. Por eso PrintRecDescMdeP vive en pos_vnta.cpp y todo el cuerpo nuevo vive en pos_tic2.cpp (30 KB libres), dejando en pos_mppo.cpp solo las llamadas y los extern. Hay que revisar pos_main.map despues de cualquier cambio en pos_mppo.cpp. Todos los literales nuevos son static char _far / static double _far, por eso DGROUP no se movio.

8. Resultados de validacion

Corridas end-to-end sobre una copia real de bases, Z 20260903. G = bruto de catalogo, D = beneficio, "neto persistido" = dE_TResu.fPrecioTotal.

EscenarioTicketBruto GDNeto persistidoFilasEstado
Un item 21%, beneficio 10%382750.00275.002475.001PASS
21% + 10.5% + exento394739.00473.904265.103PASS
Tres articulos con impuestos internos403230.00323.002907.001PASS
QR parcial (1000.00) + EFECTIVO442750.00100.002650.001PASS
15 items, tasas mezcladas, centavos feos418776.46877.657898.813PASS
Beneficio 33%, buckets sub-centavo424739.001563.873175.133PASS
Beneficio 100% (r == 1)434739.004739.000.003PASS
Guarda 4: beneficio mayor que el total452750.000.00 (rechazado)2750.000PASS
Solo EFECTIVO (regresion)472750.000.002750.000PASS
Recuperacion de ticket abierto462750.000.002750.000PASS
Control de percepcion IIBB 3%, sin QR482750.000.002814.360PASS
Percepcion IIBB + beneficio QR502814.36281.442532.921PASS
Promo por medio de pago + beneficio QR522750.00266.752400.751 + 1 de promoPASS
Intento de borrado de un PAGO QR autorizado2750.00275.001guarda OK

Controles verificados en cada corrida: total persistido == cobrado; IVA por tasa == ROUND(neto * tasa); fila de dE_DDMP con dImporte y dRecDesc correctos y fRecDescMPP == 0; suma de items de dB_TDeta == bruto de catalogo; suma de las filas del beneficio == -D; una fila por tasa presente y ninguna de mas; los cuatro campos de identidad de la fila iguales a los perfiles; suma del detalle == fPrecioTotal; ticket impreso y QR fiscal coherentes; sin rechazos ni lineas de log inesperadas.

Casos destacados. Percepcion + QR: los buckets de venta dan 1930.69 + 405.44 + 138.87 = 2475.00, identicos al split sin percepcion; la percepcion queda en 57.92 y fPrecioTotal 2532.92 == dE_DDMP.dImporte. Promo por medio de pago + QR: la promo dispara antes (-82.50), el beneficio se aplica sobre la base ya descontada (2750.00 - 82.50 - 266.75 = 2400.75) y el ticket impreso cierra exactamente.

8.1 Lo que NO esta cubierto

HuecoPor que
Beneficio de 0% realLa guarda 1 (!VALIDAR(importe_recdesc)) no se pudo alcanzar en el entorno de prueba. Necesita una autorizacion real con importe_recdesc == 0
Idempotencia (guarda 5)Cada autorizacion trae un id unico por ticket, y una segunda fila de PAGO QR la rechaza antes ActiveDMdeP(). La unica via restante seria un reintento dentro del lazo de autorizacion
Nota de Credito sobre un ticket con beneficioNo se pudo ejercitar. Con la fila de detalle deberia ajustar sola, pero hay que verificarlo antes de habilitarla sobre estos tickets
Reversa QRNo se pudo ejercitar en el entorno de prueba
Impresora fiscal realSin impresora fiscal dataRedondeos queda en cero y el ajuste es un no-op; la rama fiscal de TIC_VDET.SCP quedo deliberadamente cortada. Falta el comando fiscal de descuento por medio de pago
Percepcion ComInd e IVASolo se ejercito IIBB. Los tipos ttPERCEPCIONCOMIND y ttPERCEPCIONIVA* nunca corrieron
Impuestos internos en mas de un bucket de IVALimite del catalogo usado: los articulos con impuestos internos son todos 21%. Los caminos fImpInterno2 / fImpInterno3 quedan sin ejercitar
Dos pagos QR con beneficio en el mismo ticketdQRDto_TotalAntes se fija en el primero, asi que el ratio del segundo saldria sobre la base original. Hoy ActiveDMdeP() lo impide
Camino de limpieza PagoQR_RevertirImputacionMdePLa politica decidida es bloquear el borrado de una fila autorizada, asi que el escenario nunca llega a ejecutarlo. Verificado solo por lectura de codigo

9. Bugs encontrados y corregidos durante el desarrollo

BugCausa raiz y correccion
Al 100% de beneficio el neteo se apagaba soloEl rango del ratio era el intervalo abierto (0,1) y D == G da r == 1.0 exacto → no-op silencioso, con el resumen en bruto contra un comprobante que declaraba 0.00. Corregido con limite superior cerrado 0 < r <= 1.0 + 1e-9 y clamp a 1.0.
La percepcion se recortaba dos vecesEl objetivo de neteo se calculaba como bruto - D_completo, pero el bruto ya excluye la percepcion mientras que D se calculo sobre un total con percepcion, y la percepcion ya habia sido reescalada por separado. Resultado: 6.44 menos que lo cobrado. Corregido con objetivo = bruto * (1 - r); es un no-op bit a bit para todo ticket sin percepcion.
Orden de llamada: la fila se calculaba sobre la percepcion ya reducidaEl append quedo despues del recomputo de percepcion y leia Ticket.Percepcion ya bajada: 275.65 en vez de 275.00, 65 centavos de diferencia contra lo cobrado. Corregido moviendo la llamada antes, con comentario //OJO ORDEN. Sin percepciones el orden es indiferente, por eso ningun otro escenario lo detecto.
fRecDescMPP restado dos vecesCon las filas de detalle el total ya viene neto; acumular ademas el dRecDesc del QR hacia que el camino de recuperacion del ticket lo restara de nuevo. Corregido excluyendo el PAGO QR en Dump2DDMP. El monto sigue disponible en dE_DDMP.dRecDesc.
El descuento se habria aplicado dos veces al imprimirPagoQR_ProrratearIVAS() prorrateaba dentro de ImprimirIVAS, que a partir de la migracion arranca de un ComputarBinario que ya devuelve el neto. Corregido retirando la funcion y su llamada; no fue limpieza, fue un error de doble aplicacion.
Cadenas de desarrollo en el binario de produccionUn literal de ruta quedaba declarado fuera de todo #ifdef aunque su unico consumidor si estaba condicionado (dato muerto pero presente), y una funcion de produccion arrastraba un prefijo de log heredado. Corregidos ambos; verificado con strings sobre el EXE y las 16 DLLs de produccion.
Desborde silencioso de POS_MPPO_TEXTUna funcion nueva puesta en pos_mppo.cpp hizo envolver el segmento de codigo de 64K sin que el linker diera ningun error. Corregido moviendo el cuerpo a modulos con holgura. Riesgo permanente: hay que leer el MAP despues de cada cambio en ese modulo.