# Módulo Cliente — Cliente · API Reference

Base URL: `/api`  
Todos los endpoints requieren `Authorization: Bearer <token>` y el permiso correspondiente.  
Prefijo real: `/api/cliente/cliente/`

---

## Empresa activa

Los clientes se relacionan con la empresa activa **a través del grupo empresarial**.  
El `idEmpresa` nunca se envía en el body — el backend lo resuelve vía `empresa_activa_id()`.

Para pruebas en Postman incluye la empresa activa en el header:

```
X-Empresa-Id: 2
```

---

## IDs en base64

El endpoint `POST /listar` devuelve `idCliente` **codificado en base64** (via Resource).  
`GET /cargar-datos-edicion/{id}` devuelve el entero crudo en `data.idCliente`.

```js
// Para reutilizar el ID en un form de edición:
$('#form-idCliente').val(btoa(String(d.idCliente)));
```

---

## Código auto-generado

El campo `clienteCodigo` sigue el formato:

```
Q{YY}-{NNN}     →     Q26-001, Q26-002, Q27-001 …
```

- `YY` = últimos 2 dígitos del año en curso
- `NNN` = correlativo de 3 dígitos, reinicia cada año
- El backend lo genera al crear. **No se envía en el body** ni se puede modificar después.

---

## Dos modos de creación

| Endpoint | Uso | Beneficiario fiscal |
|---|---|---|
| `POST /guardar-rapido` | Alta rápida solo con campos básicos | No aplica |
| `POST /guardar` | Alta/edición completa | Sí, si se envía `idRegimenFiscal` |

---

## Endpoints

| Método | URL | Permiso | Descripción |
|---|---|---|---|
| `GET` | `/api/cliente/cliente/catalogo` | `cliente.cliente.catalogo` | Selects del formulario |
| `POST` | `/api/cliente/cliente/listar` | `cliente.cliente.listar` | Lista paginada (DataTable) |
| `GET` | `/api/cliente/cliente/cargar-datos-edicion/{id}` | `cliente.cliente.cargarDatosEdicion` | Datos para edición |
| `POST` | `/api/cliente/cliente/guardar-rapido` | `cliente.cliente.guardarRapido` | Crear cliente rápido |
| `POST` | `/api/cliente/cliente/guardar` | `cliente.cliente.guardar` | Crear o actualizar completo |
| `GET` | `/api/cliente/cliente/colonias-por-cp` | `cliente.cliente.coloniasporCP` | Búsqueda de colonias por CP |
| `POST` | `/api/cliente/cliente/cambiar-estado` | `cliente.cliente.cambiarEstado` | Activar / Suspender / Cancelar |
| `GET` | `/api/cliente/cliente/detalle/{id}` | `cliente.cliente.detalle` | Detalle del cliente |
| `POST` | `/api/cliente/cliente/exportar-excel` | `cliente.cliente.exportarExcel` | Exportar a Excel |
| `POST` | `/api/cliente/cliente/exportar-pdf` | `cliente.cliente.exportarPdf` | Exportar a PDF |

---

## `GET /catalogo`

Devuelve todos los selects necesarios para los formularios. Llamar una vez al abrir el form.

**Respuesta:**
```json
{
  "success": true,
  "code":    200,
  "data": {
    "grupos": [
      { "idGrupoEmpresarial": 1, "grupoEmpresarialCodigo": "GE26-001", "grupoEmpresarialAlias": "Grupo Norte" }
    ],
    "paises": [
      { "idPais": 1, "paisAlias": "México" }
    ],
    "sectores": [
      { "idSelector": 1, "sectorCodigo": "01", "sectorAlias": "Manufactura" }
    ],
    "responsables": [
      { "idUsuario": 5, "usuarioNombre": "Carlos López", "usuarioAlias": "clopez" }
    ],
    "regimenes": [
      { "idRegimenFiscal": 1, "regimenFiscalClave": "601", "regimenFiscalAlias": "General de Ley Personas Morales" }
    ]
  }
}
```

> `responsables` son usuarios con empresa QSR (id=2) y **sin** empresa FSG (id=1).

---

## `POST /listar`

**Body (Postman):**
```json
{
  "perPage":                10,
  "page":                   1,
  "searchQuery":            "Q26",
  "clienteCodigo":          "Q26-001",
  "clienteNombre":          "Empresa",
  "clienteRazonSocial":     "SA de CV",
  "clienteTipo":            "Sistema de Gestión",
  "clienteFaseContratacion":"Cliente",
  "estado":                 "Activo",
  "sort_field":             "clienteNombre",
  "sort_direction":         "asc"
}
```

**Respuesta:**
```json
{
  "success":         true,
  "code":            200,
  "data": [
    {
      "idCliente":              "MQ==",
      "clienteCodigo":          "Q26-001",
      "clienteNombre":          "Empresa SA de CV",
      "clienteRazonSocial":     "Empresa SA de CV",
      "clienteTipo":            "Sistema de Gestión",
      "clienteFaseContratacion":"Cliente",
      "clienteEspecial":        "No",
      "clienteEstado":          "Activo",
      "grupoEmpresarialAlias":  "Grupo Norte"
    }
  ],
  "lastPage":        1,
  "totalData":       1,
  "currentPage":     1
}
```

> `idCliente` viene en **base64** desde el Resource.

**Valores válidos para `estado`:** `Activo` | `Suspendido` | `Cancelado` | `no-activo` (todos los no activos)

---

## `GET /cargar-datos-edicion/{id}`

`{id}` = base64 del ID (viene del `listar`).

**Respuesta:**
```json
{
  "success": true,
  "code":    200,
  "data": {
    "idCliente":              1,
    "clienteCodigo":          "Q26-001",
    "clienteNombre":          "Empresa SA de CV",
    "clienteRazonSocial":     "Empresa SA de CV",
    "clienteRFC":             "EMP260101ABC",
    "clienteWeb":             "https://empresa.com",
    "clienteTipo":            "Sistema de Gestión",
    "clienteFaseContratacion":"Cliente",
    "clienteEspecial":        "No",
    "clienteMedioContacto":   "Email",
    "clienteFechaContacto":   "2026-01-15",
    "clienteSectores":        [1, 2],
    "clienteEstado":          "Activo",
    "idPais":                 1,
    "idGrupoEmpresarial":     1,
    "clienteIdResponsableQSR":5
  },
  "beneficiario": {
    "idBeneficiarioFiscal":          1,
    "idCliente":                     1,
    "idRegimenFiscal":               1,
    "beneficiarioFiscalRazonSocial": "Empresa SA de CV",
    "beneficiarioFiscalRFC":         "EMP260101ABC",
    "beneficiarioFiscalUsoCFDI":     "G03",
    "beneficiarioFiscalEmail":       "facturacion@empresa.com",
    "beneficiarioFiscalTelefono":    "5512345678",
    "beneficiarioFiscalTipoVialidad":"Calle",
    "beneficiarioFiscalNombreVialidad":"Insurgentes Sur",
    "beneficiarioFiscalNumeroExterior":"1234",
    "beneficiarioFiscalNumeroInterior":"P2",
    "beneficiarioFiscalEstado":      "Activo",
    "idColonia":                     305,
    "colonia": {
      "idColonia":    305,
      "coloniaAlias": "Del Valle",
      "coloniaCP":    "03100",
      "delegacion": {
        "idDelegacion":    12,
        "delegacionAlias": "Benito Juárez",
        "estado": {
          "idEstado":    9,
          "estadoAlias": "Ciudad de México",
          "pais": {
            "idPais":    1,
            "paisAlias": "México"
          }
        }
      }
    }
  }
}
```

> `beneficiario` puede ser `null` si el cliente aún no tiene beneficiario fiscal registrado.  
> `clienteSectores` es un arreglo de IDs enteros. `idCliente` en `data` es **entero** (modelo crudo).

---

## `GET /colonias-por-cp`

Busca colonias cuyo código postal **empieza con** el valor enviado. Usado para el autocompletado de domicilio fiscal con cascada País → Estado → Delegación → Colonia.

**Query params:**

| Param | Tipo | Mínimo | Descripción |
|---|---|---|---|
| `cp` | string | 2 chars | Prefijo del código postal |

**Ejemplo:** `GET /api/cliente/cliente/colonias-por-cp?cp=07412`

**Respuesta:**
```json
{
  "success": true,
  "data": [
    {
      "idColonia":        305,
      "coloniaNombre":    "Gustavo A. Madero",
      "coloniaCP":        "07412",
      "idDelegacion":     8,
      "delegacionNombre": "Gustavo A. Madero",
      "idEstado":         9,
      "estadoNombre":     "Ciudad de México",
      "idPais":           1,
      "paisNombre":       "México",
      "label":            "Gustavo A. Madero — Gustavo A. Madero, Ciudad de México (07412)"
    }
  ]
}
```

> Máximo **50 resultados** ordenados por `coloniaCP` y `coloniaAlias`.  
> El JS filtra los resultados en cliente para construir la cascada de selects.  
> Con 2 dígitos pueden venir muchas colonias; recomendar al usuario tipear el CP completo (5 dígitos).

**Flujo JS de cascada:**
1. CP input (min 2 chars, debounce 350ms) → llama al endpoint
2. Poblar `#form-bf-selPais` con países únicos; autoseleccionar si solo hay 1
3. Al seleccionar País → poblar `#form-bf-selEstado` (filtrado client-side)
4. Al seleccionar Estado → poblar `#form-bf-selDelegacion`
5. Al seleccionar Delegación → poblar `#form-bf-selColonia`
6. El select `#form-bf-selColonia` tiene `name="idColonia"` → se envía directamente al backend

---

## `POST /guardar-rapido` — Crear rápido

Solo para **creación**. No permite editar ni guardar beneficiario fiscal.

**Body:**
```json
{
  "clienteNombre":          "Empresa del Norte SA",
  "clienteRazonSocial":     "Empresa del Norte SA de CV",
  "clienteRFC":             "ENO260101XYZ",
  "idPais":                 1,
  "clienteTipo":            "Sistema de Gestión",
  "clienteFaseContratacion":"Pre Cliente",
  "idGrupoEmpresarial":     1,
  "clienteIdResponsableQSR":5,
  "clienteFechaContacto":   "2026-06-12",
  "clienteMedioContacto":   "Email",
  "clienteSectores":        [1, 2],
  "clienteEspecial":        "No"
}
```

**Respuesta:**
```json
{
  "success": true,
  "code":    200,
  "message": "Cliente registrado correctamente (rápido)",
  "data": {
    "idCliente":     1,
    "clienteCodigo": "Q26-001",
    "clienteEstado": "Activo"
  }
}
```

| Campo | Regla |
|---|---|
| `clienteNombre` | Requerido, único en `cli_cliente`, máx. 150 |
| `idGrupoEmpresarial` | Requerido, existe en `cli_grupoempresarial` |
| `clienteRFC` | Opcional, regex RFC mexicano/extranjero, máx. 13 |
| `clienteWeb` | Opcional, URL válida, máx. 255 |
| `clienteSectores[]` | Opcional, arreglo de IDs enteros |
| `clienteEspecial` | Opcional, `"Sí"` o `"No"` (default `"No"`) |

---

## `POST /guardar` — Crear o actualizar completo

Crea **o** edita el cliente. Si se envía `idRegimenFiscal`, también crea/edita el beneficiario fiscal en la misma transacción.

### Crear (sin `idCliente`)

```json
{
  "clienteNombre":                   "Empresa SA de CV",
  "clienteRazonSocial":              "Empresa SA de CV",
  "clienteRFC":                      "EMP260101ABC",
  "clienteWeb":                      "https://empresa.com",
  "idPais":                          1,
  "clienteTipo":                     "Sistema de Gestión",
  "clienteFaseContratacion":         "Cliente",
  "idGrupoEmpresarial":              1,
  "clienteIdResponsableQSR":         5,
  "clienteFechaContacto":            "2026-06-12",
  "clienteMedioContacto":            "Email",
  "clienteSectores":                 [1, 2],
  "clienteEspecial":                 "No",
  "idRegimenFiscal":                 1,
  "beneficiarioFiscalRazonSocial":   "Empresa SA de CV",
  "beneficiarioFiscalRFC":           "EMP260101ABC",
  "beneficiarioFiscalUsoCFDI":       "G03",
  "beneficiarioFiscalEmail":         "facturacion@empresa.com",
  "beneficiarioFiscalTelefono":      "5512345678",
  "idColonia":                       305,
  "beneficiarioFiscalTipoVialidad":  "Calle",
  "beneficiarioFiscalNombreVialidad":"Insurgentes Sur",
  "beneficiarioFiscalNumeroExterior":"1234",
  "beneficiarioFiscalNumeroInterior":"P2"
}
```

### Editar (con `idCliente` en base64)

Agrega los IDs en base64:

```json
{
  "idCliente":              "MQ==",
  "idBeneficiarioFiscal":   "Mg==",
  "clienteNombre":          "Empresa SA de CV Actualizada",
  "..."
}
```

> Si `idBeneficiarioFiscal` está vacío pero se envía `idRegimenFiscal`, se **crea** un nuevo beneficiario.  
> Si no se envía `idRegimenFiscal`, el beneficiario fiscal no se toca.

**Respuesta:**
```json
{
  "success": true,
  "code":    200,
  "message": "Cliente registrado correctamente",
  "data":    { "idCliente": 1, "clienteCodigo": "Q26-001", "clienteEstado": "Activo" }
}
```

| Campo beneficiario | Regla |
|---|---|
| `idRegimenFiscal` | Requerido para guardar BF, existe en `fin_regimenfiscal` |
| `beneficiarioFiscalRFC` | Opcional, regex RFC, máx. 13 |
| `beneficiarioFiscalEmail` | Opcional, email válido, máx. 150 |
| `idColonia` | Opcional, existe en `base_colonia` — FK del domicilio fiscal |

---

## `POST /cambiar-estado`

```json
{
  "idCliente": "MQ==",
  "estado":    "Suspendido"
}
```

`estado`: `"Activo"` | `"Suspendido"` | `"Cancelado"`

**Respuesta:**
```json
{
  "success": true,
  "code":    200,
  "message": "Se suspendió el cliente correctamente",
  "data":    { "idCliente": 1, "clienteEstado": "Suspendido" }
}
```

---

## `GET /detalle/{id}`

`{id}` = base64 del ID.

**Respuesta:**
```json
{
  "success": true,
  "code":    200,
  "message": "Cliente encontrado con éxito",
  "data": {
    "idCliente":              1,
    "clienteCodigo":          "Q26-001",
    "clienteNombre":          "Empresa SA de CV",
    "clienteEstado":          "Activo",
    "grupoEmpresarial":       { "idGrupoEmpresarial": 1, "grupoEmpresarialAlias": "Grupo Norte" },
    "pais":                   { "idPais": 1, "paisAlias": "México" },
    "responsableQSR":         { "idUsuario": 5, "usuarioNombre": "Carlos López" }
  }
}
```

---

## Exportar

### `POST /exportar-excel`
### `POST /exportar-pdf`

```json
{
  "estado":       "Activo",
  "tituloEstado": "Activos",
  "output_mode":  "download"
}
```

Acepta los mismos filtros que `POST /listar`. El PDF acepta `output_mode`: `"download"` | `"inline"`.

**Columnas exportadas:**

| Campo | Encabezado |
|---|---|
| `clienteCodigo` | Código |
| `clienteNombre` | Nombre |
| `clienteRazonSocial` | Razón Social |
| `clienteTipo` | Tipo |
| `clienteFaseContratacion` | Fase Contratación |
| `clienteEspecial` | Especial |
| `clienteEstado` | Estado |

---

## Bitácora

| ID acción | Descripción |
|---|---|
| 59 | Crear cliente (rápido o completo) |
| 60 | Editar cliente |
| 61 | Suspender cliente |
| 62 | Cancelar cliente |
| 63 | Activar cliente |

---

## Permisos necesarios (`seg_ruta`)

```sql
INSERT INTO seg_ruta (rutaUrl, rutaPermiso, rutaModulo) VALUES
('/api/cliente/cliente/catalogo',              'cliente.cliente.catalogo',              'cliente'),
('/api/cliente/cliente/listar',                'cliente.cliente.listar',                'cliente'),
('/api/cliente/cliente/cargar-datos-edicion',  'cliente.cliente.cargarDatosEdicion',    'cliente'),
('/api/cliente/cliente/guardar-rapido',        'cliente.cliente.guardarRapido',         'cliente'),
('/api/cliente/cliente/guardar',               'cliente.cliente.guardar',               'cliente'),
('/api/cliente/cliente/colonias-por-cp',       'cliente.cliente.coloniasporCP',         'cliente'),
('/api/cliente/cliente/cambiar-estado',        'cliente.cliente.cambiarEstado',         'cliente'),
('/api/cliente/cliente/detalle',               'cliente.cliente.detalle',               'cliente'),
('/api/cliente/cliente/exportar-excel',        'cliente.cliente.exportarExcel',         'cliente'),
('/api/cliente/cliente/exportar-pdf',          'cliente.cliente.exportarPdf',           'cliente');
```

---

## Tabla `cli_beneficiariofiscal` — migración requerida

```sql
ALTER TABLE cli_beneficiariofiscal
  ADD COLUMN idColonia INT NULL AFTER beneficiarioFiscalTelefono,
  ADD CONSTRAINT fk_bf_colonia FOREIGN KEY (idColonia) REFERENCES base_colonia(idColonia);
```
