# Exportación remota de Hoja de Distribución

La búsqueda del repositorio no mostró un sustituto web demostrable para
`cmdInternet_Click`, `SendMovimientos` y `ODBCInternet`. Se implementó la
opción B mediante `HojaExportacionService`.

## Configuración

Defina fuera del repositorio:

```yaml
parameters:
  hoja_exportacion_url: 'https://receptor-qa/hoja'
  hoja_exportacion_token: 'secreto-de-qa'
```

Los valores del archivo `.dist` están vacíos y no contienen secretos.

## Contrato

El envío usa JSON con lote, hoja, tiendas, documentos activos y movimientos.
Incluye encabezados `Authorization: Bearer …` e `Idempotency-Key`. El receptor
debe responder JSON con el mismo `lote` y `confirmado=true`.

Estados que devuelve `estado()`: `SIN_DOCUMENTOS`, `PENDIENTE` y `CONFIRMADO`,
calculados a partir de `maedoc.exportado` de los documentos activos de la hoja
(se excluye la bodega: lo que viaja son las entradas de tienda). Un fallo
mantiene `exportado='N'`, así que la hoja vuelve a aparecer como `PENDIENTE` y se
puede reintentar. Sólo una respuesta con `confirmado=true` y el mismo `lote`
actualiza a `'S'`, y ese `UPDATE` exige que el documento siga activo y sin
exportar, de modo que dos envíos simultáneos no lo confirman dos veces.

Reintentar la misma selección reutiliza el mismo token de lote, porque se deriva
de un SHA-256 de tipodoc, hoja y los ids de documento ordenados. Ese token viaja
como `Idempotency-Key`: el receptor puede descartar el duplicado.

La acción requiere el permiso `exportar_remotamente` y token CSRF. No hay SQL
remoto, borrado preventivo ni credenciales embebidas.

## Estado de esta migración: PARCIAL

**Decisión tomada (2026-08-05):** se mantiene el diseño actual, sin tabla de
outbox dedicada. Se contempló (`hoja_exportacion_lote` y
`hoja_exportacion_detalle`), se retiró en `8f5694ebc` al decidir reutilizar la
bitácora, y se confirmó mantenerlo así: el historial de `estado()` sigue
reconstruyéndose de las entradas `[HOJA_EXPORTACION]` de la bitácora (resultado,
lote, tiendas, motivo del error), sin contador de intentos ni almacenamiento
estructurado de solicitud/respuesta. No se construye la tabla dedicada salvo que
se decida lo contrario más adelante.

Lo que sigue pendiente, y por qué la fila no puede pasar a `IMPLEMENTADO`:

1. **Contrato HTTP probado de punta a punta contra un receptor simulado, no
   contra uno real.** `HojaExportacionRondaCompletaTest` levanta un receptor en
   memoria (Guzzle `MockHandler`, sin red) y ejercita: envío con
   `Authorization`/`Idempotency-Key` correctos, confirmación con el mismo lote
   de vuelta, `exportado='S'` sólo tras confirmar, rollback completo si el
   receptor no confirma o responde error, reintento de la misma selección sin
   volver a tocar la red, y la condición de carrera real (otro proceso confirma
   uno de los documentos mientras el nuestro está en vuelo: la transacción debe
   revertirse completa, no dejar un documento confirmado y otro no). Esa
   prueba encontró y corrigió un defecto real: Guzzle lanza excepción en 4xx/5xx
   por defecto, así que el chequeo manual `if ($codigo < 200 || $codigo >= 300)`
   nunca se alcanzaba — quedó código muerto. Se agregó `http_errors: false` para
   que el mensaje de dominio ("El receptor respondió HTTP …") sí se genere.
   Lo que sigue sin probarse es un **receptor de verdad**: nadie ha confirmado
   `hoja_exportacion_url` contra un servicio real en QA.
2. **Pendiente de confirmación del negocio, sin fecha:** si esta exportación
   sigue en uso. VB6 escribía por ODBC directo contra un MySQL remoto; hay que
   confirmar con el cliente si ese canal continúa vigente o si ya lo sustituyó
   el módulo de sincronización (`frmSincronizar` en el legado usa
   `maetie.usahoja`, que es otra bandera distinta a la de la hoja). Se preguntó
   explícitamente y la respuesta fue "no se sabe, hay que confirmarlo con el
   cliente" — queda anotado aquí para no volver a inventar una respuesta.

Mientras el punto 2 no se cierre, y mientras no haya un receptor real
verificado, la fila de exportación remota queda como `PARCIAL` en la matriz de
paridad. Si el negocio confirma que el canal ya no se usa, la fila pasa a
`REEMPLAZADO FUNCIONALMENTE` documentando el proceso sustituto; si confirma que
sí se usa, hay que levantar el receptor en QA y repetir la prueba con la URL
real.
