diff --git a/backend/api/v1/modules/a76/layouts_csv/customs_brokers/common/common_validators.py b/backend/api/v1/modules/a76/layouts_csv/customs_brokers/common/common_validators.py index 65292796..24b2c573 100644 --- a/backend/api/v1/modules/a76/layouts_csv/customs_brokers/common/common_validators.py +++ b/backend/api/v1/modules/a76/layouts_csv/customs_brokers/common/common_validators.py @@ -11,6 +11,16 @@ LICENSE_MAX = 4 RFC_MAX = 30 CURP_MAX = 19 +# Formato RFC/CURP (paridad con flujo manual dashboard/customs_brokers) +RFC_PATTERN = re.compile(r"^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$", re.IGNORECASE) +# CURP: 18 caracteres (inicial, vocal, 2 letras, fecha, sexo, estado, 3 consonantes, homoclave) +CURP_PATTERN = re.compile( + r"^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM]" + r"(AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)" + r"[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$", + re.IGNORECASE, +) + def check_required_broker_key(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]: """Col B: Clave de Agente Aduanal obligatoria, máx 5 caracteres.""" @@ -174,6 +184,38 @@ def check_curp_max(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any return None +def check_rfc_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]: + """Col E: RFC opcional; si tiene valor, debe cumplir formato RFC (ej. XAXX010101XXX).""" + val = (row.get("RFC") or "").strip() + if not val: + return None + if not RFC_PATTERN.match(val): + clave = (row.get("CLAVE") or "").strip() + return { + "line": line_num, + "col": "RFC", + "msg": "Error: (Col. E) El formato del RFC es inválido.", + "solution": "Capturar en la columna E un RFC con formato válido (ej. XAXX010101XXX).", + } + return None + + +def check_curp_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]: + """Col N: CURP/PERSONAL_ID opcional; si tiene valor, debe cumplir formato CURP de 18 caracteres.""" + val = (row.get("PERSONAL_ID") or "").strip() + if not val: + return None + if not CURP_PATTERN.match(val): + clave = (row.get("CLAVE") or "").strip() + return { + "line": line_num, + "col": "PERSONAL_ID", + "msg": "Error: (Col. N) El formato de la CURP es inválido.", + "solution": "Capturar en la columna N un CURP con formato válido de 18 caracteres.", + } + return None + + def check_pais_catalogo( row: Dict[str, Any], line_num: int, diff --git a/backend/api/v1/modules/a76/layouts_csv/customs_brokers/validators/common.py b/backend/api/v1/modules/a76/layouts_csv/customs_brokers/validators/common.py index 867bbe70..8edfdefd 100644 --- a/backend/api/v1/modules/a76/layouts_csv/customs_brokers/validators/common.py +++ b/backend/api/v1/modules/a76/layouts_csv/customs_brokers/validators/common.py @@ -12,6 +12,8 @@ from ..common.common_validators import ( check_patente_obligatoria_si_mex, check_rfc_max, check_curp_max, + check_rfc_format, + check_curp_format, check_pais_catalogo, ) @@ -40,9 +42,15 @@ def validaciones_agente_aduanal( if err: return err err = check_rfc_max(row, line_num) + if err: + return err + err = check_rfc_format(row, line_num) if err: return err err = check_curp_max(row, line_num) + if err: + return err + err = check_curp_format(row, line_num) if err: return err err = check_pais_catalogo(row, line_num, valid_country_m3)