Skip to content

Fase 4 — Configuración de Gateway de Pagos (ERP)

⚠️ DOCUMENTACIÓN RETROSPECTIVA — Generada a partir de código implementado el 2026-04-27.

Módulo: Portal de Clientes — ERP Integration Tipo: View Estado: Implementado Fecha: 2026-04-27


Descripción

Vista del ERP (bautista-app) que permite a un operador configurar, por sucursal activa, el gateway de pagos online que utilizará el Portal de Clientes (PayPerTIC, MercadoPago o "sin pagos online"). La configuración se persiste en la tabla genérica data_config mediante el endpoint config/data, usando claves namespaced bajo el prefijo portal.gateway.*.

La sección se monta dentro de la pantalla "Configuración del Sistema" del módulo de Configuración, junto a la sección de SMTP, y es de uso exclusivo del operador del ERP. El cliente final del portal nunca interactúa con esta vista.

Documento de negocio asociado: gateway-config-view.md.


Ubicación en el ERP

AspectoDetalle
Repositoriobautista-app
Módulo del ERPConfiguración del Sistema
Vista contenedoraSistemaConfigView
Acceso del operadorInicio → Configuración → Configuración del Sistema
Posición dentro de la vistaSegunda sección plegable, debajo de "Configuración SMTP"
Estado por defectodefaultExpanded={false} (plegada)

La vista se entrega con el patrón de layout estándar del frontend: PageWrapper + PageHeader + PageContent, con cada sección encapsulada en un FormSection (ts/core/components/form/layout/FormSection).


Componentes implementados

GatewayConfigSection

AspectoDetalle
Ubicaciónbautista-app/ts/mod-config/Sistema/components/GatewayConfigSection.tsx
TipoComponente funcional React
PropsNinguna — el componente se autoabastece de contexto y queries
RetornoReact.JSX.Element | null
data-testid raízgateway-config-section

Responsabilidades:

  • Cargar la configuración actual de gateway desde la API (GET config/data) vía TanStack Query.
  • Detectar si la sucursal tiene la configuración base inicializada y, en caso contrario, ocultarse retornando null.
  • Renderizar un formulario controlado por React Hook Form con resolver Zod para validar los campos.
  • Aplicar render condicional del campo apiSecret solo cuando el gateway seleccionado es mercadopago.
  • Limpiar automáticamente apiSecret cuando el operador deja de seleccionar mercadopago.
  • Toggle de visibilidad individual por campo de credencial (apiKey, apiSecret, webhookToken) con íconos Visibility / VisibilityOff de Material UI.
  • Mostrar feedback al usuario mediante showToast de ToastNotifications en éxito y error.
  • Mostrar el nombre de la sucursal activa en el encabezado, derivado de useConfig().usuario.schema y mapeado por mapSucursalToOption.

Comportamiento cuando no hay seed:

  • El servicio retorna null cuando la clave portal.gateway.nombre no está presente en el diccionario de configuración devuelto por config/data.
  • En ese caso, después del primer isLoading === false, el componente retorna null (early return en línea 152-154 del componente).
  • Esto implica que el FormSection que lo envuelve queda con un body vacío. La presencia de la sección en el árbol depende de SistemaConfigView, no del componente.

Gateways soportados:

Valor del campo nombreEtiqueta UIRequiere apiKeyRequiere apiSecretRequiere webhookToken
'' (vacío)"Sin pagos online"NoNoNo
paypertic"PayPerTIC"No
mercadopago"MercadoPago"

Los valores válidos vienen del literal GATEWAY_OPTIONS definido en el schema (['', 'paypertic', 'mercadopago']).

Estados internos (useState):

  • showApiKey, showApiSecret, showWebhookToken: booleans independientes para alternar visibilidad de cada credencial.

Integraciones externas dentro del componente:

Hook / utilidadOrigenUso
useConfigcore/context/ConfigContextLectura del schema de la sucursal activa
mapSucursalToOptioncore/utilsConvierte schema (suc0001) a etiqueta legible
useForm + Controllerreact-hook-formForm controlado
zodResolver@hookform/resolvers/zodValidación con Zod
useQuery / useMutation@tanstack/react-queryServer state
showToastcore/components/ToastNotificationsNotificaciones

SistemaConfigView

AspectoDetalle
Ubicaciónbautista-app/ts/mod-config/Sistema/views/SistemaConfigView.tsx
TipoComponente funcional React (default export)
PropsNinguna

Responsabilidades:

  • Componer el layout de página (PageWrapper + PageHeader + PageContent).
  • Definir los breadcrumbs: InicioConfiguración del Sistema.
  • Apilar dos FormSection:
    1. "Configuración SMTP" (defaultExpanded={true}) → renderiza <SmtpConfigSection />.
    2. "Configuración del Gateway de Pagos" (defaultExpanded={false}) → renderiza <GatewayConfigSection />.

La integración entre SmtpConfigSection y GatewayConfigSection es por simple coexistencia bajo el mismo PageContent. Cada sección gestiona su propio fetch, su propio formulario y su propia mutación de manera independiente; no comparten state ni cache.


Schema de validación (gatewayConfig.schema.ts)

Ubicación: bautista-app/ts/mod-config/Sistema/schemas/gatewayConfig.schema.ts

Constantes y tipos exportados

SímboloTipoDescripción
GATEWAY_OPTIONSreadonly ['', 'paypertic', 'mercadopago']Tupla as const con los valores aceptados del campo nombre
GatewayName(typeof GATEWAY_OPTIONS)[number]Unión de literales: '' | 'paypertic' | 'mercadopago'
gatewayConfigSchemaz.ZodEffects<...>Schema Zod con refinamiento condicional
GatewayConfigSchemaz.infer<typeof gatewayConfigSchema>Tipo inferido para el formulario

Campos del schema

CampoTipo ZodNotas
nombrez.enum(GATEWAY_OPTIONS)Acepta exactamente los tres valores literales
apiKeyz.string()Validado condicionalmente por superRefine
apiSecretz.string()Validado condicionalmente por superRefine
webhookTokenz.string()Validado condicionalmente por superRefine

Validaciones condicionales (superRefine)

La validación principal vive en el bloque superRefine:

CondiciónCampo afectadoMensaje
nombre === ''Se omiten todas las validaciones de credenciales (early return)
nombre !== '' y apiKey.length === 0apiKey"El api_key es requerido"
nombre !== '' y webhookToken.length === 0webhookToken"El webhook_token es requerido"
nombre === 'mercadopago' y apiSecret.length === 0apiSecret"El api_secret es requerido para MercadoPago"

Implicancias:

  • Cuando nombre === '' se permite guardar con todas las credenciales vacías (deshabilitar pagos).
  • apiSecret solo es exigido para MercadoPago. Para PayPerTIC el campo no se renderiza (render condicional en el componente) y el valor remanente se limpia vía useEffect.
  • No se aplican formatos (length mínima distinta, regex, etc.). Solo presencia.

Service (gatewayConfig.service.ts)

Ubicación: bautista-app/ts/mod-config/Sistema/services/gatewayConfig.service.ts

Constantes y tipos exportados

SímboloTipoDescripción
GATEWAY_KEYSreadonly tuple of 4 stringsLista exhaustiva de claves data_config que maneja el módulo
GatewayKey(typeof GATEWAY_KEYS)[number]Unión de las 4 claves
GatewayConfigValuesRecord<GatewayKey, string>Diccionario completo de claves → valores string
GatewayConfigReadResultGatewayConfigValues | nullResultado de lectura: null cuando no hay seed

Claves de configuración manejadas (portal.gateway.*)

Clave data_configCampo del formPropósito
portal.gateway.nombrenombreIdentificador del gateway ('', paypertic, mercadopago)
portal.gateway.api_keyapiKeyCredencial principal del gateway
portal.gateway.api_secretapiSecretCredencial secreta (solo MercadoPago)
portal.gateway.webhook_tokenwebhookTokenToken compartido para validar webhooks entrantes

El service nunca lee ni escribe claves fuera de portal.gateway.*. No interfiere con la configuración SMTP ni con cualquier otra clave del sistema.

Métodos

GatewayConfigService.getGatewayConfig()

  • Firma: () => Promise<GatewayConfigReadResult>
  • HTTP: GET config/data
  • Forma de la respuesta esperada: ApiResponse<Record<string, string | null>>.
  • Retorna null cuando la clave portal.gateway.nombre no está presente en data (criterio: presencia de la clave, no su valor — un valor de string vacío sigue indicando seed corrido).
  • Retorna GatewayConfigValues cuando la clave existe; cualquier credencial faltante en la respuesta se normaliza a string vacío via ?? ''.
  • Usado dentro del componente como queryFn con queryKey: ['gateway-config'].

GatewayConfigService.saveGatewayConfig(values)

  • Firma: (values: GatewayConfigValues) => Promise<void>
  • HTTP: PUT config/data
  • Body: { claves: { 'portal.gateway.nombre': ..., 'portal.gateway.api_key': ..., 'portal.gateway.api_secret': ..., 'portal.gateway.webhook_token': ... } }
  • Solo persiste las 4 claves del namespace portal.gateway.*. Cualquier otra clave preexistente en data_config queda intacta porque el endpoint del backend hace upsert por clave.
  • No retorna datos al consumidor; el componente maneja éxito/error por el ciclo de TanStack Query (onSuccess/onError de la mutación).

Flujo de datos

PasoActorAcción
1GatewayConfigSection (mount)Llama a useQuery({ queryKey: ['gateway-config'], queryFn: GatewayConfigService.getGatewayConfig })
2GatewayConfigService.getGatewayConfigGET config/data y filtra el dict por presencia de portal.gateway.nombre
3Componente (rama A — sin seed)Si !isLoading && data === null → retorna null y la sección queda vacía
4Componente (rama B — con seed)useEffect con dependencia gatewayConfig ejecuta reset(configToFormValues(gatewayConfig)) para hidratar React Hook Form
5OperadorEdita campos. watch('nombre') dispara useEffect que limpia apiSecret si el valor distinto de mercadopago
6OperadorSubmit (handleSubmit(onSubmit)); el resolver Zod ejecuta gatewayConfigSchema.superRefine
7Validación fallaErrores se asignan al formState; disabled={hasErrors} deshabilita el botón "Guardar Gateway"
8Validación OKonSubmit mapea con formToConfigValues y llama saveMutation.mutate(...)
9useMutation.mutationFnEjecuta GatewayConfigService.saveGatewayConfig(values)PUT config/data
10onSuccess / onErrorMuestra toast con showToast.success(...) o showToast.error(...)

Helpers internos del componente:

FunciónDirecciónResponsabilidad
isGatewayName(value)guardType guard que devuelve true si el string está en GATEWAY_OPTIONS
configToFormValues(config)API → formNormaliza el dict (portal.gateway.*) a la forma del schema; defaultea a ''
formToConfigValues(data)form → APIMapea los campos del form al dict con claves portal.gateway.*

Tests

Al momento de generar esta documentación retrospectiva, no se encontraron archivos de test (*.test.tsx, *.test.ts) dentro de bautista-app/ts/mod-config/Sistema/ para los artefactos de gateway (GatewayConfigSection, gatewayConfig.schema, gatewayConfig.service).

⚠️ Pendiente de validación: Si los tests viven en otra ubicación (por ejemplo bajo un tests/ global del repo) o aún no fueron escritos, conviene confirmar con el equipo y, en su caso, completar esta sección con el detalle de cobertura.

Las áreas razonablemente testeables identificadas a partir del código son:

ÁreaCasos sugeridos
gatewayConfig.schemaAceptación de nombre = '' sin credenciales; rechazo de apiKey vacío con nombre = 'paypertic'; rechazo de apiSecret vacío con nombre = 'mercadopago'; aceptación de apiSecret vacío con nombre = 'paypertic'
gatewayConfig.service.getGatewayConfigRetorno null cuando la clave portal.gateway.nombre no está presente; mapeo correcto cuando todas las claves vienen; defaulteo a '' cuando vienen como null
gatewayConfig.service.saveGatewayConfigBody enviado con exactamente las 4 claves portal.gateway.*
GatewayConfigSectionRender null cuando service devuelve null; render del campo apiSecret solo con MercadoPago; limpieza de apiSecret al cambiar de gateway; toggle de visibilidad por campo; submit habilita/deshabilita según hasErrors

Ver también


⚠️ NOTA IMPORTANTE: Validar con stakeholders antes de considerar final. Especialmente: confirmar la existencia (o ausencia) y ubicación de tests automatizados, y verificar el contrato exacto del endpoint config/data en el backend.