Datos y documentos
Análisis Funcional: Pago con documentos — parte 3 de 5. ← El flujo del pago · Índice · Siguiente: La papeleta →
Dos cosas distintas que el operador produce en el mismo momento: datos que tipea y archivos que sube. Se tratan aparte porque tienen dueños distintos —los datos son del reporte regulatorio, los archivos son del legajo de auditoría— y reglas distintas.
2.1 Los campos que completa el operador
El requerimiento #1 de CIS-EC es tajante: "Si Soterex no envía la columna por la API, favor incluir los campos en la pestaña del pago de la tx (Módulo Cajero)".
La migración 2026_08_07_100000_e5a_datos_del_pago_en_transacciones.php agrega 13 columnas, de las cuales 11 las tipea el operador en ventanilla y 2 vienen de Soterex o se derivan.
Los 11 que se cargan en ventanilla
| Campo | Rótulo en pantalla | Para qué sirve |
|---|---|---|
delivery_type | Medio de pago | EFECTIVO o TRANSFERENCIA. Columna AA del reporte. Define qué documentos son obligatorios. |
bank_account | Cuenta bancaria | Columna AB. Solo tiene sentido con transferencia — el backend la descarta si el medio es efectivo. |
identification_type | Tipo de identificación | CI, pasaporte…. Va al reporte y a la papeleta. |
identification_number | N° de identificación | Ídem. Es el dato con el que se coteja al beneficiario. |
identification_country | País de la identificación | Dos letras. Distinto de la nacionalidad y del país de destino. |
beneficiary_address | Dirección | Columna Address del reporte. |
beneficiary_city | Ciudad | Columna City. |
beneficiary_state | Provincia / Estado | Columna State. |
phone_2 | Teléfono adicional | "SOTEREX ENVÍA UN TELÉFONO, AGREGAR UN TELÉFONO 2". Es para poder ubicar al beneficiario. |
email_2 | Mail adicional | "SOTEREX ENVÍA UN MAIL, AGREGAR UN MAIL 2". Ídem. |
notes | Notas | "gestiones de contacto adicional", columna AC. Texto libre, hasta 2000 caracteres. |
El formulario está en frontend/src/components/DatosPagoForm.vue:64-117, y la validación en backend/app/Http/Controllers/TransactionController.php:469-481.
Los 2 que no tipea el operador
| Campo | De dónde sale |
|---|---|
expires_at | La carta al beneficiario indica 60 días de plazo para retirar. |
nationality | La manda Soterex. Distinta de destination_country — es la del beneficiario, no la del giro. |
Para qué sirve todo esto: el reporte regulatorio
De las 48 columnas del reporte que hoy Soterex manda por Excel, 36 se quedan en el reporte transaccional que CIS le presenta al regulador. Buena parte de esas 36 son datos que Soterex no expone por API, y hasta E5a simplemente no existían en el modelo.
El reporte en sí todavía no se implementó — es una entrega propia. Lo que E5a hace es asegurar que cuando llegue, los datos estén. Cargarlos retroactivamente sobre transacciones ya pagadas es imposible: el cliente ya no está.
Tres columnas del reporte se decidieron NO guardar: Payout Currency, Ex. Rate y Beneficiary Destination Amount. Con el cambio de moneda diferido a MVP2, las tres son derivables (USD, 1.0 y el principal). Guardarlas ahora sería persistir tres constantes; se resuelven en el reporte.
Ningún campo es obligatorio
Misma lógica que los documentos (§1.4): todas las columnas son nullable y datosPago() valida solo el formato, nunca la presencia. Se pueden cargar antes o después de pagar.
La única regla de coherencia que sí se aplica: la cuenta bancaria se borra si el medio de pago no es transferencia, tanto en el backend como en el formulario, para no ensuciar el reporte con un dato colgado de un pago en efectivo (TransactionController.php:483-488).
2.2 Los nueve tipos de documento
El catálogo sale del requerimiento #5 y de la hoja Módulo caja, filas 15 a 24. Son constantes en el modelo, no una tabla de configuración — mismo criterio que los motivos de caja: son conceptos del dominio, no algo que un administrador deba poder inventar (backend/app/Models/TransactionDocument.php:27-43).
| Tipo | Rótulo en pantalla | ¿Obligatorio (completitud)? | ¿Bloquea el pago? | ¿Cuándo aplica? |
|---|---|---|---|---|
CARTA | Carta de Soterex | Sí | Sí — ADR-008 | Siempre |
IDENTIFICACION_ANVERSO | Identificación (anverso) | Sí | Sí — ADR-008 | Siempre |
IDENTIFICACION_REVERSO | Identificación (reverso) | Sí | Sí — ADR-008 | Siempre |
SELFIE | Foto del cliente con la identificación | No — "DESEABLE, no es obligatorio" | No | Siempre (opcional) |
PAPELETA_FIRMADA | Papeleta firmada | Sí | No — no puede: se firma después de pagar | Siempre |
FORMULARIO_ENROLAMIENTO | Formulario de enrolamiento | Sí | No | Solo TRANSFERENCIA |
CARTOLA_BANCARIA | Cartola bancaria | Sí | No | Solo TRANSFERENCIA |
SOLICITUD_TRANSFERENCIA | Solicitud de pago por transferencia | Sí | No | Solo TRANSFERENCIA |
CONFIRMACION_ACREDITACION | Confirmación de acreditación | Sí | No | Solo TRANSFERENCIA |
"¿Obligatorio?" y "¿Bloquea el pago?" dejaron de ser la misma pregunta desde ADR-008 (2026-09-12): los ocho no-selfie siguen siendo obligatorios para la completitud posterior (§2.3), pero solo los tres de identificación bloquean confirmar el pago. TransactionDocument::REQUERIDOS_ANTES_DE_PAGAR es ese subconjunto — no reutiliza REQUERIDOS_SIEMPRE directamente porque excluye PAPELETA_FIRMADA a propósito.
Los rótulos son los de frontend/src/services/transactionDocuments.ts:8-18.
La selfie es el único opcional
El cliente lo escribió con esas palabras en la fila 18 de la hoja Módulo caja: junto al ítem "Foto del cliente con la identificación junto al rostro (Selfie)" aclara "*DESEABLE, no es obligatorio". Coincide con la decisión #17 del plan v2, que ya lo había anticipado.
Se refleja en tres lugares:
- No está en
REQUERIDOS_SIEMPRE(TransactionDocument.php:50-55). - En pantalla lleva el sufijo
· opcionaly un ícono neutro en vez del triángulo de advertencia (frontend/src/components/DocumentacionPago.vue:171-183). - Un test explícito garantiza que nunca afecta la completitud.
Los cuatro de transferencia no se muestran en pagos en efectivo
Listarlos siempre daría la impresión de que faltan documentos que nadie espera. La lista de casilleros se arma según el medio de pago elegido (DocumentacionPago.vue:41-49).
2.3 Cómo se calcula la completitud
// backend/app/Models/Transaction.php:99-105
public function faltanDocumentos(): array
{
$requeridos = TransactionDocument::requeridosPara($this->delivery_type);
$subidos = $this->documents->pluck('type')->unique()->all();
return array_values(array_diff($requeridos, $subidos));
}Con EFECTIVO (o sin medio de pago elegido todavía) los requeridos son 4; con TRANSFERENCIA son 8 (TransactionDocument::requeridosPara(), líneas 83-88).
Consecuencia que vale tener presente: cambiar el medio de pago cambia la completitud retroactivamente. Una transacción completa en efectivo pasa a incompleta apenas se la marca como transferencia. Es el comportamiento correcto —los cuatro documentos extra efectivamente hacen falta— pero puede sorprender.
2.4 "Pagada pero incompleta" no es un estado
El cliente lo planteó como una posibilidad abierta: "podría evaluarse la posibilidad de que la tx se quede pagada pero incompleta" (hoja Módulo caja, fila 14). La tentación evidente era agregar un valor más a status.
Se decidió no hacerlo. status sigue siendo ACCEPTED / PAID / CANCELLED, y "incompleta" se deriva de los documentos que la transacción tiene contra los que su medio de pago exige.
Las razones, en orden de peso:
Un estado nuevo obligaría a revisar cada consulta que hoy filtra por
status. Listado, dashboard, reportes, prefondeo, caja, el buscador de la pantalla Pago: todos preguntanstatus = 'PAID'en algún lado. Un cuarto valor los rompe a todos en silencio — el peor tipo de rotura, porque no falla, devuelve de menos.No es una propiedad de la transacción, es una pregunta sobre otra tabla. El estado de una transacción describe qué pasó con la plata. Cuántos escaneos tiene es información de
transaction_documents. Meterlo enstatusmezcla dos ejes que se mueven por separado: una transacción puede volverse completa sin que nada cambie en su pago.Sería un estado que cambia solo. Subir un archivo tendría que mutar el estado de la transacción, y cambiar el medio de pago tendría que mutarlo de vuelta. Un
statusque se recalcula por efectos laterales de otra tabla no es un estado: es una vista con nombre engañoso.
Se expone entonces como dos cosas derivadas, en cada respuesta de la API: documentacion_completa (booleano) y faltan (los tipos que restan). El listado lo ofrece como filtro.
2.5 Los documentos son PII sensible
Es el punto más delicado de la entrega, y conviene decir por qué antes de decir cómo.
Lo que se guarda son documentos de identidad y fotografías de personas migrantes en proceso de salida voluntaria. Una filtración no es un incidente de privacidad genérico: es exponer la identidad y la ubicación de una población vulnerable. El propio material de referencia del proyecto lo trató con ese criterio — el reporte real de 2.957 transacciones y los siete documentos de ejemplo no se subieron al repositorio justamente por eso.
Cinco defensas, todas verificables en el código:
1. Nunca se sirve una URL directa al archivo. El contenido pasa siempre por TransactionDocumentController::download(), que valida permiso y alcance antes de leer el disco. El modelo esconde disk y path de toda serialización:
// backend/app/Models/TransactionDocument.php:100-101
/** El contenido no se serializa nunca: el path es interno. */
protected $hidden = ['disk', 'path'];En el frontend, ni siquiera el preview usa un <img src> apuntando al archivo: se trae como blob con el Bearer y se libera al desmontar (DocumentacionPago.vue:108-130).
2. Alcance por país y por PDV. Ver §1.6. Un operador de la sucursal A no alcanza los documentos de la sucursal B, ni por pantalla ni por API conociendo el id.
3. Cada acceso queda auditado. No solo la subida: la lectura.
// backend/app/Http/Controllers/TransactionDocumentController.php:109-116
AuditLog::record(
user: $user,
action: 'TRANSACTION_DOCUMENT_VIEWED',
entityType: 'transaction_document',
entityId: $documento->id,
transactionId: $transaction->id,
metadata: ['type' => $documento->type],
);Es el registro de quién miró el documento de identidad de una persona — exactamente lo que una auditoría de seguridad va a querer revisar. Y no es gratis: implica que la tabla de auditoría crece con cada preview.
4. La ruta la arma el servidor, siempre. Nada de lo que manda el usuario entra en el path: transacciones/{transaction_id}/{tipo}-{uuid}.{ext}. El nombre original del archivo se conserva para que el operador reconozca lo que subió, pero nunca se usa para construir la ruta (backend/app/Services/TransactionDocumentService.php:39-48). El almacenamiento es un disco documents, local en desarrollo y S3 privado con cifrado at-rest en staging y producción.
5. No hay borrado. No existe endpoint de baja. Un soporte contable no se borra: reemplazarlo es subir otro del mismo tipo, y el anterior queda en la base y en el disco. El modelo ni siquiera tiene updated_at (TransactionDocument.php:20).
Lo que esto NO cubre, y hay que decirlo: no hay expiración ni política de retención, no hay cifrado a nivel aplicación sobre el contenido, y cualquier operador con
transacciones:writeen su PDV puede descargar cualquier documento de ese PDV. La auditoría es detectiva, no preventiva — te dice quién miró, no impide que mire. Ver Preguntas Abiertas #7.
2.6 Qué se acepta al subir
| Regla | Valor | Por qué |
|---|---|---|
| Formatos | PDF, JPG, PNG | "los soportes pueden ser fotos, escaneos o pdfs" (hoja Módulo caja, fila 14) |
| Tamaño máximo | 10 MB | Son escaneos y fotos de teléfono, no videos |
| Validación de tipo | MIME real, no la extensión | Renombrar un ejecutable a .pdf es trivial |
La validación de MIME real usa la regla mimetypes de Laravel, que inspecciona el archivo, y no mimes, que confía en el nombre (TransactionDocumentController.php:52-61 y TransactionDocumentService.php:29-33). Hay un test dedicado a un archivo que dice ser PDF y no lo es.
Si el archivo no pasa, el frontend traduce el 422 a un mensaje concreto en vez de repetir el error del backend: "El archivo no es válido: tiene que ser PDF, JPG o PNG, de hasta 10 MB" (DocumentacionPago.vue:84-87).
2.7 Cómo se ve en pantalla
Los nueve tipos se muestran como una secuencia de pasos numerados (1, 2, 3...) — un stepper visual, no una lista plana. Sigue sin ser un wizard bloqueante: cualquier paso se sube en cualquier orden, incluso días después, y nunca hay un paso deshabilitado ni un "Siguiente" que dependa de completar el anterior. El número es solo para guiar el ojo por la lista; no marca un camino obligatorio, y por eso el conector entre los círculos es siempre de un color neutro, nunca una barra de progreso que se "completa" de arriba hacia abajo. Sigue siendo la misma decisión de §1.4 (R3): el pago no depende de esto.
Cada paso es un círculo numerado que se convierte en tilde cuando el documento está — mismo criterio de siempre: tilde si está, borde de alerta si falta y es obligatorio, borde neutro si es la selfie. Junto al número va el nombre del tipo, el nombre del archivo subido (clickeable, abre el preview) y un botón Subir o Reemplazar.
Arriba a la derecha, un badge dice Completa o Faltan N. Abajo, cuando falta algo, un texto que importa por lo que evita:
"El pago ya quedó registrado. Podés completar los soportes que falten desde acá cuando los tengas — no hace falta rehacer nada." —
DocumentacionPago.vue
Es la traducción a la pantalla de la decisión de §1.4: el operador tiene que entender que la falta de un escaneo es una tarea pendiente, no un pago fallido. Sin esa frase, el badge rojo invita a pensar que algo salió mal y a intentar pagar de nuevo.
Si no se puede ni consultar el estado de los documentos — por ejemplo, una transacción PAID de otro PDV que la búsqueda sí encontró pero cuyos documentos están fuera de alcance (404, §1.6) — la pantalla no muestra el stepper ni el badge: muestra "No se pudo cargar la documentación de esta transacción". Mostrar Faltan 0 en ese caso sería peor que no mostrar nada — se leería como "no falta nada" cuando en realidad no se sabe.
Análisis Funcional: Pago con documentos — parte 3 de 5. ← El flujo del pago · Índice · Siguiente: La papeleta →

