From a77761f2e51edb7702716ab47d1f0d000449d3e3 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Wed, 4 Mar 2026 10:41:37 -0600 Subject: [PATCH] Enmascaramiento y regax para datos sensibles en los agentes aduanales --- .../api/v1/modules/a76/customs_brokers/dto.py | 32 ++-- .../modules/a76/customs_brokers/services.py | 19 ++- backend/core/error_handlers.py | 112 +++++++++++++- backend/main.py | 25 --- .../customs_brokers/create-dialog.svelte | 144 +++++++++++++++--- .../customs_brokers/edit-dialog.svelte | 86 +++++++++-- .../customs_brokers/edit/[[id]]/+page.svelte | 85 ++++++++++- 7 files changed, 420 insertions(+), 83 deletions(-) diff --git a/backend/api/v1/modules/a76/customs_brokers/dto.py b/backend/api/v1/modules/a76/customs_brokers/dto.py index 9f1309c7..64bbf65c 100644 --- a/backend/api/v1/modules/a76/customs_brokers/dto.py +++ b/backend/api/v1/modules/a76/customs_brokers/dto.py @@ -12,16 +12,16 @@ class CustomsBrokerBaseDTO(BaseModel): postal_code: Optional[str] = None city: Optional[str] = None state: Optional[str] = None - phone: Optional[str] = None + phone: Optional[str] = Field(None, pattern=r"^$|^[\d\s\-\+\(\)]+$") fax: Optional[str] = None - email: Optional[str] = None + email: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") country: Optional[str] = None - tax_id: Optional[str] = None - personal_id: Optional[str] = None + tax_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$") + personal_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](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)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$") position: Optional[str] = None - license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$") + license: Optional[str] = Field(None, max_length=4, pattern=r"^$|^[0-9]*[1-9][0-9]*$") company: Optional[str] = None - contact: Optional[str] = None + contact: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]+$") class CustomsBrokerCreateDTO(CustomsBrokerBaseDTO): @@ -58,16 +58,16 @@ class CustomsBrokerDTO(BaseModel): postal_code: Optional[str] = None city: Optional[str] = None state: Optional[str] = None - phone: Optional[str] = None + phone: Optional[str] = Field(None, pattern=r"^$|^[\d\s\-\+\(\)]+$") fax: Optional[str] = None - email: Optional[str] = None + email: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") country: Optional[str] = None - tax_id: Optional[str] = None - personal_id: Optional[str] = None + tax_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$") + personal_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](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)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$") position: Optional[str] = None - license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$") + license: Optional[str] = Field(None, max_length=4, pattern=r"^$|^[0-9]*[1-9][0-9]*$") company: Optional[str] = None - contact: Optional[str] = None + contact: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]+$") tenant_id: str company_id: str @@ -109,14 +109,14 @@ class CustomsBrokerPersonnelDTO(BaseModel): broker_key: str = Field(..., max_length=5, pattern=r"^[a-zA-Z0-9]+$") line: int name: Optional[str] = None - tax_id: Optional[str] = None - personal_id: Optional[str] = None + tax_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$") + personal_id: Optional[str] = Field(None, pattern=r"^$|^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](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)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$") position: Optional[str] = None - license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$") + license: Optional[str] = Field(None, max_length=4, pattern=r"^$|^[0-9]*[1-9][0-9]*$") first_name: Optional[str] = None last_name: Optional[str] = None middle_name: Optional[str] = None - email: Optional[str] = None + email: Optional[str] = Field(None, pattern=r"^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") tenant_id: Optional[int] = None company_id: Optional[int] = None diff --git a/backend/api/v1/modules/a76/customs_brokers/services.py b/backend/api/v1/modules/a76/customs_brokers/services.py index dc00f352..01cb13e2 100644 --- a/backend/api/v1/modules/a76/customs_brokers/services.py +++ b/backend/api/v1/modules/a76/customs_brokers/services.py @@ -1,4 +1,5 @@ from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError from . import dto, models @@ -45,9 +46,13 @@ class CustomsBrokerService: new_broker = models.CustomsBroker(**broker_dict) db.add(new_broker) - db.commit() - db.refresh(new_broker) - return new_broker + try: + db.commit() + db.refresh(new_broker) + return new_broker + except IntegrityError: + db.rollback() + raise ValueError("La clave del agente ya existe o hay datos duplicados.") @staticmethod def update(db: Session, broker_key: str, tenant_id: int, broker_data: dto.CustomsBrokerUpdateDTO, company_id: int): @@ -56,8 +61,12 @@ class CustomsBrokerService: if broker: for key, value in broker_data.model_dump(exclude_unset=True).items(): setattr(broker, key, value) - db.commit() - db.refresh(broker) + try: + db.commit() + db.refresh(broker) + except IntegrityError: + db.rollback() + raise ValueError("Los datos duplicados no pueden ser guardados o hay un conflicto de integridad.") return broker @staticmethod diff --git a/backend/core/error_handlers.py b/backend/core/error_handlers.py index c3c4c90c..3cbd397c 100644 --- a/backend/core/error_handlers.py +++ b/backend/core/error_handlers.py @@ -8,7 +8,9 @@ from typing import Any, Dict from fastapi import Request, status, HTTPException from fastapi.responses import JSONResponse from fastapi.encoders import jsonable_encoder +from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError from sqlalchemy.exc import IntegrityError, SQLAlchemyError from .config import settings @@ -57,20 +59,69 @@ async def base_exception_handler( return response +# Mapa de campos técnicos a nombres legibles en español +_FIELD_LABELS: Dict[str, str] = { + "broker_key": "Clave del Agente", + "license": "Patente", + "tax_id": "RFC", + "personal_id": "CURP", + "email": "Correo Electrónico", + "phone": "Teléfono", + "fax": "Fax", + "contact": "Nombre de Contacto", + "name": "Nombre / Razón Social", + "address": "Dirección", + "postal_code": "Código Postal", + "city": "Ciudad", + "state": "Estado", + "country": "País", +} + +_FIELD_PATTERN_MESSAGES: Dict[str, str] = { + "broker_key": "La Clave del Agente solo puede contener letras y números (máx. 5 caracteres).", + "license": "La Patente debe ser un número entre 1 y 9999 (no puede ser 0 ni contener letras).", + "tax_id": "El RFC no tiene el formato correcto. Ejemplo válido: XAXX010101000.", + "personal_id": "La CURP no tiene el formato correcto. Debe tener 18 caracteres alfanuméricos.", + "email": "El correo electrónico no tiene un formato válido. Ejemplo: usuario@dominio.com.", + "phone": "El teléfono solo puede contener dígitos, espacios y los símbolos: +, -, (, ).", + "contact": "El nombre de contacto contiene caracteres no permitidos. Use solo letras, números y puntuación básica.", +} + + +def _friendly_message(field_key: str, error_type: str) -> str: + """Devuelve un mensaje de error legible en español según el campo y tipo de error.""" + if error_type in ("string_pattern_mismatch", "value_error"): + return _FIELD_PATTERN_MESSAGES.get( + field_key, + f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' contiene un valor con formato inválido.", + ) + if error_type == "string_too_long": + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' excede la longitud máxima permitida." + if error_type == "string_too_short": + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' es demasiado corto." + if error_type in ("missing", "value_error.missing"): + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' es obligatorio." + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' contiene un valor inválido." + + async def validation_exception_handler( request: Request, exc: RequestValidationError, ) -> JSONResponse: """ - Manejador para errores de validación de Pydantic/FastAPI + Manejador para errores de validación de Pydantic/FastAPI. + Devuelve mensajes legibles en español. """ errors = [] for error in exc.errors(): - field = ".".join(str(loc) for loc in error["loc"] if loc != "body") + loc_parts = [str(loc) for loc in error["loc"] if loc != "body"] + field = ".".join(loc_parts) + field_key = loc_parts[-1] if loc_parts else "" + errors.append( { "field": field, - "message": error["msg"], + "message": _friendly_message(field_key, error["type"]), "type": error["type"], } ) @@ -80,11 +131,63 @@ async def validation_exception_handler( extra={"errors": errors}, ) + summary = ( + errors[0]["message"] + if len(errors) == 1 + else f"Hay {len(errors)} errores de validación: " + " | ".join(e["message"] for e in errors) + ) + response = JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={ "error": "VALIDATION_ERROR", - "message": "Error de validación en los datos recibidos", + "message": summary, + "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, + "errors": errors, + }, + ) + for k, v in _cors_headers(request).items(): + response.headers[k] = v + return response + + +async def inner_validation_exception_handler( + request: Request, + exc: ValidationError, +) -> JSONResponse: + """ + Manejador para errores de validación de Pydantic lanzados internamente (como en tenant_crud_routes). + """ + errors = [] + for error in exc.errors(): + loc_parts = [str(loc) for loc in error["loc"] if loc != "body"] + field = ".".join(loc_parts) + field_key = loc_parts[-1] if loc_parts else "" + + errors.append( + { + "field": field, + "message": _friendly_message(field_key, error["type"]), + "type": error["type"], + } + ) + + logger.warning( + f"Inner Validation Error en {request.url.path}", + extra={"errors": errors}, + ) + + summary = ( + errors[0]["message"] + if len(errors) == 1 + else f"Hay {len(errors)} errores de validación: " + " | ".join(e["message"] for e in errors) + ) + + response = JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "error": "VALIDATION_ERROR", + "message": summary, "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, "errors": errors, }, @@ -221,6 +324,7 @@ def register_exception_handlers(app) -> None: app.add_exception_handler(BaseAPIException, base_exception_handler) app.add_exception_handler(HTTPException, http_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler) + app.add_exception_handler(ValidationError, inner_validation_exception_handler) app.add_exception_handler(IntegrityError, integrity_error_handler) app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler) app.add_exception_handler(Exception, general_exception_handler) diff --git a/backend/main.py b/backend/main.py index b50f92a0..6d6e3ddb 100644 --- a/backend/main.py +++ b/backend/main.py @@ -153,36 +153,11 @@ def _cors_headers_for_request(request: Request): return {} -# Add validation error handler -@app.exception_handler(RequestValidationError) -async def validation_exception_handler(request: Request, exc: RequestValidationError): - logger.error( - f"Validation error for {request.method} {request.url.path}: {exc.errors()}" - ) - logger.error(f"Request body: {await request.body()}") - response = JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": exc.errors(), "body": exc.body}, - ) for k, v in _cors_headers_for_request(request).items(): response.headers[k] = v return response -# Add HTTP exception handler -@app.exception_handler(HTTPException) -async def http_exception_handler(request: Request, exc: HTTPException): - logger.error( - f"HTTP {exc.status_code} for {request.method} {request.url.path}: {exc.detail}" - ) - response = JSONResponse( - status_code=exc.status_code, - content={"detail": exc.detail}, - ) - for k, v in _cors_headers_for_request(request).items(): - response.headers[k] = v - return response - def run_migrations(): subprocess.run(["alembic", "upgrade", "head"], check=True) diff --git a/frontend/src/lib/components/dashboard/customs_brokers/create-dialog.svelte b/frontend/src/lib/components/dashboard/customs_brokers/create-dialog.svelte index 672f019a..c34de920 100644 --- a/frontend/src/lib/components/dashboard/customs_brokers/create-dialog.svelte +++ b/frontend/src/lib/components/dashboard/customs_brokers/create-dialog.svelte @@ -38,6 +38,7 @@ city: '', state: '', country: '', + personal_id: '', tenant_id: '', // Se llenará en el submit o por defecto company_id: '' }; @@ -62,6 +63,7 @@ contact: initialData.contact || '', address: initialData.address || '', postal_code: initialData.postal_code || '', + personal_id: initialData.personal_id || '', city: initialData.city || '', state: initialData.state || '', country: initialData.country || '', @@ -80,6 +82,7 @@ contact: '', address: '', postal_code: '', + personal_id: '', city: '', state: '', country: 'MEX', // Valor por defecto sugerido @@ -116,6 +119,48 @@ loading = false; return; } + if ( + formData.license === '0' || + formData.license === '0000' || + /^0+$/.test(formData.license) + ) { + toast.error('La Patente no puede ser 0'); + loading = false; + return; + } + if (formData.tax_id && !/^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i.test(formData.tax_id)) { + toast.error('El formato del RFC es inválido'); + loading = false; + return; + } + if ( + formData.personal_id && + !/^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](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)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$/i.test( + formData.personal_id + ) + ) { + toast.error('El formato de la CURP es inválido'); + loading = false; + return; + } + if ( + formData.email && + !/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(formData.email) + ) { + toast.error('El formato del correo electrónico es inválido'); + loading = false; + return; + } + if (formData.phone && !/^[\d\s\-\+\(\)]+$/.test(formData.phone)) { + toast.error('El teléfono contiene caracteres no válidos'); + loading = false; + return; + } + if (formData.fax && !/^[\d\s\-\+\(\)]+$/.test(formData.fax)) { + toast.error('El fax contiene caracteres no válidos'); + loading = false; + return; + } // Inyectar company_id si no viene const payload = { ...formData, company_id: companyId }; @@ -125,9 +170,14 @@ toast.success( mode === 'create' ? 'Agente creado correctamente' : 'Agente actualizado correctamente' ); - } catch (error) { + } catch (error: unknown) { console.error(error); - toast.error('Error al guardar el agente aduanal'); + // Intentar extraer el mensaje del servidor si lo hay + let msg = 'Error al guardar el agente aduanal'; + if (error && typeof error === 'object' && 'message' in error) { + msg = (error as { message: string }).message || msg; + } + toast.error(msg); } finally { loading = false; } @@ -135,7 +185,7 @@ - + {mode === 'create' ? 'Nuevo Agente Aduanal' : 'Editar Agente Aduanal'} @@ -148,7 +198,7 @@
-

Identificación

+

Identificación

@@ -156,7 +206,7 @@ id="broker_key" value={formData.broker_key} oninput={(e) => { - const val = e.currentTarget.value.toUpperCase(); + const val = e.currentTarget.value.toUpperCase().replace(/[^A-Z0-9]/g, ''); if (val.length > 5) { brokerKeyError = true; formData.broker_key = val.slice(0, 5); @@ -169,10 +219,11 @@ } else { brokerKeyError = false; formData.broker_key = val; + e.currentTarget.value = formData.broker_key; } }} placeholder="Ej. 550" - maxlength="6" + maxlength={6} class={brokerKeyError ? 'border-red-500 focus-visible:ring-red-500' : ''} disabled={mode === 'edit' || loading} /> @@ -204,7 +255,7 @@ } }} placeholder="Ej. 3421" - maxlength="5" + maxlength={5} class={licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''} disabled={loading} /> @@ -216,7 +267,7 @@
-
+
RFC { + formData.tax_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9&Ñ]/g, '') + .slice(0, 13); + e.currentTarget.value = formData.tax_id; + }} placeholder="RFC de la agencia" + maxlength={13} disabled={loading} />
+ + { + formData.personal_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9]/g, '') + .slice(0, 18); + e.currentTarget.value = formData.personal_id; + }} + placeholder="CURP (Opcional)" + maxlength={18} + disabled={loading} + /> +
+
{ + formData.contact = e.currentTarget.value.replace( + /[^a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]/g, + '' + ); + e.currentTarget.value = formData.contact; + }} placeholder="Persona de contacto" disabled={loading} /> @@ -249,13 +332,33 @@
-

Contacto

+

Contacto

-
+
- + { + formData.phone = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.phone; + }} + disabled={loading} + />
-
+
+ + { + formData.fax = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.fax; + }} + disabled={loading} + /> +
+
@@ -265,7 +368,7 @@
-

Dirección Fiscal

+

Dirección Fiscal

@@ -275,9 +378,16 @@
- { formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); }} /> + { + formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); + }} + />
-
+
diff --git a/frontend/src/lib/components/dashboard/customs_brokers/edit-dialog.svelte b/frontend/src/lib/components/dashboard/customs_brokers/edit-dialog.svelte index 294c7515..fa98ecc3 100644 --- a/frontend/src/lib/components/dashboard/customs_brokers/edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/customs_brokers/edit-dialog.svelte @@ -79,6 +79,41 @@ loading = true; error = null; + // Validaciones Básicas + if (formData.license === '0' || formData.license === '0000' || /^0+$/.test(formData.license)) { + error = 'La Patente no puede ser 0'; + loading = false; + return; + } + if (formData.tax_id && !/^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i.test(formData.tax_id)) { + error = 'El formato del RFC es inválido'; + loading = false; + return; + } + if ( + formData.personal_id && + !/^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](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)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$/i.test( + formData.personal_id + ) + ) { + error = 'El formato de la CURP es inválido'; + loading = false; + return; + } + if ( + formData.email && + !/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(formData.email) + ) { + error = 'El formato del correo electrónico es inválido'; + loading = false; + return; + } + if (formData.phone && !/^[\d\s\-\+\(\)]+$/.test(formData.phone)) { + error = 'El teléfono contiene caracteres no válidos'; + loading = false; + return; + } + try { const payload: CreateCustomsBrokerData = { broker_key: broker.broker_key, // La clave no se edita @@ -138,7 +173,7 @@ - + Editar Agente Aduanal @@ -218,7 +253,7 @@ } }} placeholder="Número de patente" - maxlength="5" + maxlength={5} class={licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''} disabled={loading} /> @@ -251,7 +286,11 @@ { + formData.phone = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.phone; + }} placeholder="Número telefónico" maxlength={30} disabled={loading} @@ -262,7 +301,11 @@ { + formData.fax = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.fax; + }} placeholder="Número de fax" maxlength={30} disabled={loading} @@ -286,7 +329,14 @@ { + formData.contact = e.currentTarget.value.replace( + /[^a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]/g, + '' + ); + e.currentTarget.value = formData.contact; + }} placeholder="Nombre del contacto" maxlength={80} disabled={loading} @@ -318,7 +368,9 @@ placeholder="C.P." maxlength={15} disabled={loading} - oninput={(e) => { formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); }} + oninput={(e) => { + formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); + }} />
@@ -366,9 +418,16 @@ { + formData.tax_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9&Ñ]/g, '') + .slice(0, 13); + e.currentTarget.value = formData.tax_id; + }} placeholder="RFC" - maxlength={30} + maxlength={13} disabled={loading} />
@@ -377,9 +436,16 @@ { + formData.personal_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9]/g, '') + .slice(0, 18); + e.currentTarget.value = formData.personal_id; + }} placeholder="CURP" - maxlength={20} + maxlength={18} disabled={loading} />
diff --git a/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte b/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte index c52512ae..0c001f08 100644 --- a/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte +++ b/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte @@ -228,6 +228,44 @@ return; } + if (formData.license === '0' || formData.license === '0000' || /^0+$/.test(formData.license)) { + error = 'La Patente no puede ser 0'; + toast.error(error); + return; + } + + if (formData.tax_id && !/^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i.test(formData.tax_id)) { + error = 'El formato del RFC es inválido'; + toast.error(error); + return; + } + + if ( + formData.personal_id && + !/^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM](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)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$/i.test( + formData.personal_id + ) + ) { + error = 'El formato de la CURP es inválido'; + toast.error(error); + return; + } + + if ( + formData.email && + !/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(formData.email) + ) { + error = 'El formato del correo electrónico es inválido'; + toast.error(error); + return; + } + + if (formData.phone && !/^[\d\s\-\+\(\)]+$/.test(formData.phone)) { + error = 'El teléfono contiene caracteres no válidos'; + toast.error(error); + return; + } + loading = true; error = null; try { @@ -435,7 +473,14 @@
{ + formData.tax_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9&Ñ]/g, '') + .slice(0, 13); + e.currentTarget.value = formData.tax_id; + }} placeholder="RFC de la empresa" disabled={loading} class="h-10" @@ -444,7 +489,14 @@
{ + formData.personal_id = e.currentTarget.value + .toUpperCase() + .replace(/[^A-Z0-9]/g, '') + .slice(0, 18); + e.currentTarget.value = formData.personal_id; + }} placeholder="CURP si aplica" disabled={loading} class="h-10" @@ -467,7 +519,14 @@
{ + formData.contact = e.currentTarget.value.replace( + /[^a-zA-Z0-9\sñÑáéíóúÁÉÍÓÚ\-\.,]/g, + '' + ); + e.currentTarget.value = formData.contact; + }} placeholder="Nombre del contacto" disabled={loading} class="h-10" @@ -490,7 +549,11 @@
{ + formData.phone = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.phone; + }} placeholder="656-000-0000" disabled={loading} class="h-10" @@ -498,7 +561,15 @@
- + { + formData.fax = e.currentTarget.value.replace(/[^\d\s\-\+\(\)]/g, ''); + e.currentTarget.value = formData.fax; + }} + disabled={loading} + class="h-10" + />
@@ -540,7 +611,9 @@ placeholder="32000" disabled={loading} class="h-10" - oninput={(e) => { formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); }} + oninput={(e) => { + formData.postal_code = e.currentTarget.value.replace(/[^a-zA-Z0-9]/g, ''); + }} />