Skip to content

Arquitectura Backend - Portal API

Resumen

El backend del Portal PWA extiende el servidor Slim Framework existente con nuevos endpoints bajo el namespace /portal/*, reutilizando modelos y servicios existentes.

Estructura de Capas

┌─────────────────────────────────────────────────────────┐
│  Routes (/portal/*)                                     │
│  - Definición de endpoints                             │
│  - Middleware: PortalAuth → Connection                 │
└──────────────────────────┬──────────────────────────────┘

┌──────────────────────────▼──────────────────────────────┐
│  Controllers                                            │
│  - PortalAuthController                                 │
│  - PortalCtaCteController                               │
│  - PortalPagosController                                │
│  - PortalCuponesController                              │
│  Solo manejo HTTP, delega a Services                    │
└──────────────────────────┬──────────────────────────────┘

┌──────────────────────────▼──────────────────────────────┐
│  Services (Lógica de Negocio)                           │
│  - ClientIdentificationService                          │
│  - PortalCtaCteService (reutiliza CuentaCorriente)      │
│  - PaymentGatewayService (multi-gateway)                │
│  - CuponPagoService                                     │
│  Lógica + transacciones + auditoría                     │
└──────────────────────────┬──────────────────────────────┘

┌──────────────────────────▼──────────────────────────────┐
│  Models (Acceso a Datos)                                │
│  Nuevos:                                                │
│  - TenantDomainModel                                    │
│  - PortalClientModel                                    │
│  - PortalPaymentModel                                   │
│  - PortalCuponModel                                     │
│  Reutilizados:                                          │
│  - Cliente                                              │
│  - CuentaCorriente                                      │
└──────────────────────────┬──────────────────────────────┘

┌──────────────────────────▼──────────────────────────────┐
│  Database (PostgreSQL Multi-Tenant)                     │
│  - DB ini: tenant_domains                               │
│  - DB {tenant}: portal_clients, portal_payments, etc.   │
└─────────────────────────────────────────────────────────┘

Middleware

PortalAuthMiddleware

Responsabilidad: Resolver tenant por dominio + autenticar cliente

Flujo:

  1. Extraer host del request (ej: ctacte.empresaA.com.ar)
  2. Buscar en tenant_domains por dominio
  3. Inyectar tenant_context en request (tenant_id, database, schema)
  4. Si ruta protegida, verificar sesión/token del cliente
  5. Inyectar cliente_context en request
  6. Delegar a ConnectionMiddleware

Ventajas:

  • Más simple que JWT de empresa (no requiere machine tokens)
  • Reutiliza ConnectionMiddleware existente
  • Session PHP o JWT ligero de cliente

ConnectionMiddleware

Reutiliza middleware existente: Configura ConnectionManager con database y schema del tenant.

Endpoints

Autenticación

POST /portal/auth/identify-client

Propósito: Identificar cliente sin contraseña (DNI/CUIT/ID).

Request:

json
{
  "identifier": "12345678",
  "identifier_type": "dni"  // dni, cuit, cliente_id
}

Response:

json
{
  "success": true,
  "data": {
    "cliente_id": 123,
    "nombre": "Juan Pérez",
    "email": "juan@example.com",
    "has_ctacte": true
  },
  "session_token": "..."
}

Cuenta Corriente

GET /portal/mi-cuenta

Propósito: Resumen del estado de cuenta del cliente.

Parámetros: cliente_id (query param)

Response:

json
{
  "cliente_id": 123,
  "nombre": "Juan Pérez",
  "saldo_total": 15000.00,
  "facturas_vencidas": 3,
  "ultimo_pago": {
    "fecha": "2026-01-15",
    "monto": 5000.00
  }
}

GET /portal/deudas

Propósito: Listar todas las facturas pendientes del cliente.

Parámetros: cliente_id (query param)

Response:

json
{
  "data": [
    {
      "id": "uuid-1234",
      "tipo": "Factura A",
      "numero": 123,
      "fecha": "2026-01-01",
      "vencimiento": "2026-01-31",
      "monto": 10000.00,
      "saldo": 10000.00,
      "dias_vencido": 0,
      "esta_vencido": false
    }
  ]
}

Pagos

POST /portal/pagos/iniciar

Propósito: Iniciar pago online y obtener URL de redirección al gateway.

Request:

json
{
  "cliente_id": 123,
  "facturas": [
    {"id": "uuid-1234", "monto": 10000.00},
    {"id": "uuid-5678", "monto": 5000.00}
  ],
  "total": 15000.00
}

Response:

json
{
  "payment_id": "uuid-payment-123",
  "redirect_url": "https://gateway.com/checkout?...",
  "payment_method": "mercadopago"
}

Flujo:

  1. Crear registro en portal_payments con estado "pending"
  2. Llamar adapter del gateway configurado
  3. Obtener URL de checkout del gateway
  4. Retornar URL para redirigir al cliente

POST /portal/pagos/webhook

Propósito: Recibir notificaciones del gateway cuando el pago cambia de estado.

Request: Variable según gateway (MercadoPago, PagoTIC, etc.)

Headers importantes:

  • x-signature: Firma del webhook (validación de seguridad)
  • x-request-id: ID único del request (idempotencia)

Response:

json
{
  "success": true
}

Flujo:

  1. Validar firma del webhook según adapter del gateway
  2. Verificar idempotencia (no procesar dos veces)
  3. Si pago aprobado:
    • Actualizar portal_payments.status = 'approved'
    • Crear recibo en ordcta usando ReciboRelationsService
    • Vincular recibo con payment
  4. Si pago rechazado:
    • Actualizar portal_payments.status = 'rejected'

GET /portal/pagos/historial

Propósito: Ver historial de pagos del cliente.

Parámetros: cliente_id (query param)

Response:

json
{
  "data": [
    {
      "id": "uuid-payment-123",
      "fecha": "2026-01-20",
      "metodo": "online",
      "monto": 15000.00,
      "estado": "approved",
      "facturas_pagadas": [
        {"tipo": "Factura A", "numero": 123, "monto": 10000.00}
      ],
      "recibo_numero": "REC-00123"
    }
  ]
}

Cupones

POST /portal/cupones/generar

Propósito: Generar cupón de pago con código de barras para pago físico.

Request:

json
{
  "cliente_id": 123,
  "facturas": [
    {"id": "uuid-1234", "tipo": "Factura A", "numero": 123, "monto": 10000.00}
  ],
  "total": 10000.00,
  "dias_vencimiento": 30
}

Response:

json
{
  "cupon_id": "uuid-cupon-123",
  "codigo_barras": "0001056789202601274",
  "monto": 10000.00,
  "fecha_vencimiento": "2026-02-27",
  "facturas": [...]
}

Flujo:

  1. Generar código de barras único (ITF - 19 dígitos)
  2. Calcular fecha de vencimiento
  3. Crear registro en portal_cupones
  4. Retornar datos del cupón

Formato código de barras:

SUCU(4) + CLIENTE(6) + TIMESTAMP(8) + DV(1) = 19 dígitos
0001   + 056789    + 20260127    + 4     = 0001056789202601274

GET /portal/cupones/cliente/

Propósito: Listar cupones del cliente con filtros.

Parámetros (query):

  • estado: pending, used, expired, cancelled (opcional)
  • limit: Número de registros (default: 50)
  • offset: Offset para paginación (default: 0)

Response:

json
{
  "cupones": [
    {
      "cupon_id": "uuid-cupon-123",
      "codigo_barras": "0001056789202601274",
      "monto": 10000.00,
      "estado": "pending",
      "fecha_generacion": "2026-01-27",
      "fecha_vencimiento": "2026-02-27",
      "recibo_id": null
    }
  ],
  "total": 10,
  "has_more": false
}

GET /portal/cupones/

Propósito: Obtener detalle de cupón por código de barras.

Response:

json
{
  "cupon_id": "uuid-cupon-123",
  "cliente_id": 123,
  "cliente_nombre": "Juan Pérez",
  "codigo_barras": "0001056789202601274",
  "monto": 10000.00,
  "estado": "pending",
  "fecha_generacion": "2026-01-27",
  "fecha_vencimiento": "2026-02-27",
  "facturas": [...]
}

Servicios (Lógica de Negocio)

ClientIdentificationService

Responsabilidad: Identificar y autenticar clientes.

Flujo de identificación:

  1. Conectar a DB del tenant
  2. Buscar en portal_clients por DNI/CUIT
  3. Si no existe, buscar en ordcon (tabla existente de clientes)
  4. Crear registro en portal_clients si es primera vez
  5. Verificar si está bloqueado por intentos fallidos
  6. Actualizar last_login
  7. Retornar datos del cliente

Validaciones:

  • El identificador debe existir en ordcon
  • El cliente debe pertenecer al tenant actual
  • No debe estar bloqueado temporalmente

PortalCtaCteService

Responsabilidad: Consultar cuenta corriente del cliente.

Métodos principales:

  • getDeudas(): Obtener facturas pendientes
  • getMiCuenta(): Resumen de cuenta

Reutilización:

  • Usa CuentaCorriente model existente
  • Enriquece datos con dias_vencido, esta_vencido
  • Calcula saldo total y cantidad de facturas vencidas

PaymentGatewayService

Responsabilidad: Orquestación de pagos online con múltiples gateways.

Métodos principales:

  • iniciarPago(): Crear pago y obtener URL de checkout
  • procesarWebhook(): Procesar notificaciones del gateway
  • getHistorial(): Obtener historial de pagos

Características:

  • Patrón Adapter para múltiples gateways (MercadoPago, PagoTIC, etc.)
  • Factory pattern para seleccionar adapter según configuración
  • Validación de firma de webhook específica por gateway
  • Idempotencia: verifica portal_payments.external_id
  • Acreditación automática: llama ReciboRelationsService cuando se aprueba
  • Configuración por tenant en ini.sistemas

CuponPagoService

Responsabilidad: Generación y validación de cupones de pago.

Métodos principales:

  • generarCupon(): Crear cupón con código de barras
  • validarCupon(): Validar cupón antes de usar

Generación de código ITF (19 dígitos):

SUCU(4) + CLIENTE(6) + TIMESTAMP(8) + DV(1)
  • SUCU: Código de sucursal (4 dígitos)
  • CLIENTE: ID del cliente (6 dígitos)
  • TIMESTAMP: Fecha actual AAAAMMDD (8 dígitos)
  • DV: Dígito verificador calculado (1 dígito)

Seguridad

Autenticación

Opción recomendada: Session PHP

Después de identificar cliente:

  • Se guarda en $_SESSION['portal_cliente']
  • Incluye: cliente_id, tenant_id, nombre, expires

Alternativa: JWT Ligero

  • Payload simple con cliente_id, tenant_id, exp
  • Firmado con HS256

Validaciones

  1. Propiedad del cliente: Verificar que cliente pertenece al tenant actual
  2. Rate Limiting:
    • Max 5 intentos de login/minuto por IP
    • Max 100 requests/minuto por dominio
    • Bloqueo automático después de 3 intentos fallidos (15 minutos)
  3. Webhook Security:
    • Validar firma específica por gateway
    • Verificar idempotencia (no procesar dos veces)
    • Loggear todos los payloads recibidos
    • Responder en < 5 segundos

Headers Requeridos

Todos los endpoints protegidos requieren:

  • Authorization: Bearer {session_token}: Token de sesión del cliente
  • X-Client-Domain: Dominio actual (para resolver tenant)

Reutilización de Código Existente

El portal reutiliza ampliamente el backend existente:

Modelos reutilizados:

  • Cliente: Tabla existente de clientes
  • CuentaCorriente: Movimientos de cuenta corriente
  • ReciboRelationsService: Creación de recibos

Middleware reutilizado:

  • ConnectionMiddleware: Configuración de conexión multi-tenant

Servicios reutilizados:

  • ReciboRelationsService: Crear recibos cuando se aprueba un pago

Próximos Pasos

  1. Ver middleware/portal-auth-middleware.md
  2. Revisar services/ para detalles de cada servicio
  3. Consultar database/schema.md para estructura de datos