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— elaccount_numberya existe en la lista del usuario.duplicate_alias— elaliaschoca 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:
- Upload →
pending(oparsingcuando el worker lo toma). - Parse →
preview_readycon filas distribuidas en buckets. UI sondea cada 2s. - Preview →
GET /v1/beneficiaries/imports/{id}/previewoGET /v1/validations/imports/{id}/preview— paginado, filtrable porbuckets[]. - Edit →
PATCH .../rows/{row_id}re-correRowProcessorsobre los campos editados; la fila puede saltar decorrectableavalid(o al revés). - Commit →
POST .../commitinserta solo filasvalidycorrectable. El bucket de duplicados nunca se ingiere. - Cancel →
DELETE /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.