MexCep expone dos pipelines de import masivo separados pero estructuralmente análogos: uno crea beneficiarios y otro lanza validaciones. Ambos comparten el mismo loop async (Symfony Messenger sobre el stream imports de Redis) y el mismo contrato de UI — el usuario sube un archivo, el sistema lo parsea, el cliente revisa el preview, edita filas dudosas y confirma el commit cuando está conforme.

Beneficiarios

POST /v1/beneficiaries/imports — multipart con file + parse_mode (template o free). Aceptado en cookie o API key. Formatos: csv, xls, xlsx, txt, pdf. Cap de 20 MB.

El parser es híbrido manual+LLM: primero corre CsvParser / XlsxParser / PdfParser para los formatos estructurados o el template estándar. Si ParseResult::shouldEscalateToLlm devuelve true (típicamente un PDF escaneado o un texto libre sin columnas reconocibles), LlmAccountExtractor envía el archivo a Gemini 2.5 Flash y reparsea el output contra el mismo RowProcessor. El LLM nunca persiste filas crudas — cada cuenta extraída pasa por la misma validación CLABE/Luhn que el path manual.

Las filas terminales del parser caen en cinco buckets (src/Beneficiaries/Import/Dto/ParsedRow.php):

  • valid — pasa todas las validaciones y va al commit.
  • correctable — algún campo es inválido pero el usuario puede editarlo en preview.
  • fatal — error que el usuario no puede arreglar (ej. campos obligatorios faltantes).
  • duplicate_account — el account_number ya existe en la lista del usuario.
  • duplicate_alias — el alias choca con otro beneficiario activo del mismo usuario.

Validaciones

POST /v1/validations/imports — multipart con file + upload_type (file para CSV/XLSX/etc. o images para comprobantes) + parse_mode. Cookie-only (los routes usan AuthMethodMiddleware::allow('cookie')), no acepta API key. Formatos file: csv, xls, xlsx, txt, json, zip (texto). Formatos images: PNG/JPG/WebP individuales, multi-select (PHP las recibe como file[] y el server las empaqueta en un ZIP tmp) o un ZIP directo.

Para imports de imágenes, el OCR corre en preview, no en commit — cada imagen pasa por ImageSanitizer (strip EXIF/XMP/ICC) y luego por el mismo pipeline de runOcrPipeline que el OCR single, garantizando paridad byte-por-byte. Las filas con tipo_comprobante reconocido y NormalizationService::normalize completo entran al bucket valid; el resto va a correctable o fatal. Los buckets de este pipeline son cuatro: valid, correctable, fatal, duplicate.

Por cada batch de imágenes el sistema asigna un sticky proxy (ProxyPoolService::allocateBatchSession) para que las N requests a Banxico vayan a través de la misma sesión NAT, y un semáforo de slots (BatchSlotSemaphore, cap default 5 desde bulk_validations.per_batch_concurrent) que limita cuántas filas del mismo batch pueden estar en vuelo simultáneamente — independiente del cap global de concurrencia por usuario.

El flujo preview → commit

Igual para los dos pipelines:

  1. Uploadpending (o parsing cuando el worker lo toma).
  2. Parsepreview_ready con filas distribuidas en buckets. UI sondea cada 2s.
  3. PreviewGET /v1/beneficiaries/imports/{id}/preview o GET /v1/validations/imports/{id}/preview — paginado, filtrable por buckets[].
  4. EditPATCH .../rows/{row_id} re-corre RowProcessor sobre los campos editados; la fila puede saltar de correctable a valid (o al revés).
  5. CommitPOST .../commit inserta solo filas valid y correctable. El bucket de duplicados nunca se ingiere.
  6. CancelDELETE /v1/beneficiaries/imports/{id} (o equivalente en validations) — solo aplica a jobs no terminales.

Un import activo por usuario

Ambos pipelines aplican una regla de 1-in-flight sobre el user_id. ImportJobService::handleUpload valida que no exista otro job en estado pending, parsing, preview_ready o committing; si lo hay, responde 422 import_already_in_flight. La regla aplica por pipeline (un beneficiary import en vuelo NO bloquea un validation import) — son tablas y locks separados.

Cuando un job queda atascado en parsing por más tiempo del esperado (worker muerto sin liberar el lock), las validation imports tienen un guard que detecta el lock stale y lo limpia en el próximo upload del mismo usuario.