Appearance
Carteras - Documentación Técnica Backend
⚠️ DOCUMENTACIÓN RETROSPECTIVA - Generada a partir de código implementado el 2026-02-11
Módulo: Ventas Feature: Gestión de Carteras de Clientes Fecha: 2026-02-11
Referencia a Requisitos de Negocio
Requisitos de Negocio - Carteras
Flujo de Arquitectura
Request
↓
[JWT Auth Middleware]
↓
[Connection Middleware] → X-Schema header → search_path
↓
backend/carteras.php (Legacy Router)
↓
CarteraController (controller/general/)
↓
Cartera Model (models/general/)
↓
PostgreSQL Database (tabla: carteras)Nivel de arquitectura: Legacy (pre-DDD)
- No usa Slim Framework (archivo PHP standalone en
backend/) - No usa Service Layer
- Controller delgado (delegación directa a Model)
- Sin Domain Layer
- Sin validadores middleware
API Endpoints
GET /backend/carteras.php
Responsabilidades:
- Listar todas las carteras según tipo (cliente o proveedor)
- Soportar filtros de autocomplete
- Obtener primera/última cartera
- Obtener cartera por ID
Request Query Parameters:
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| tipo | string | Sí | Tipo de cartera: 'cliente' o 'proveedor' |
| id | int | No | Filtrar por ID específico (ccar para clientes, codigo para proveedores) |
| filter | string | No | Búsqueda por nombre o ID (ILIKE, LIMIT 10) |
| first | boolean | No | Obtener primer registro ordenado por ID ASC |
| last | boolean | No | Obtener último registro ordenado por ID DESC |
Lógica de Query:
- tipo='cliente': Consulta tabla
carteras, camposccar(ID),nombre - tipo='proveedor': Consulta tabla
deu_acr, camposcodigo(ID),descri(nombre),imputa(cuenta contable)
Response DTO (tipo='cliente'):
typescript
{
id: number, // ccar::int
nombre: string // nombre
}Response DTO (tipo='proveedor'):
typescript
{
id: number, // codigo::int
nombre: string, // descri
cuenta: number // imputa::bigint
}Status Codes:
200 OK- Consulta exitosa400 Bad Request- Tipo inválido o parámetros incorrectos401 Unauthorized- Token JWT ausente o inválido500 Internal Server Error- Error en la consulta
POST /backend/carteras.php
Responsabilidades:
- Crear nueva cartera de clientes
- Auto-generar ID incremental (MAX(ccar) + 1)
- Insertar registro en tabla
carteras
Request Body:
json
{
"nombre": "string" // Nombre de la cartera (max 30 caracteres)
}Lógica de ID:
sql
SELECT
CASE
WHEN MAX(ccar::int) IS NULL THEN 1
ELSE MAX(ccar::int) + 1
END AS id
FROM carteras⚠️ Problema de Concurrencia Detectado: Usa MAX(ccar) + 1 sin transacción explícita. Potencial race condition en inserciones simultáneas.
Response DTO:
json
{
"status": 201,
"message": "Datos recibidos correctamente.",
"data": {
"id": number // ccar asignado
}
}Status Codes:
201 Created- Cartera creada exitosamente400 Bad Request- Nombre faltante o inválido401 Unauthorized- Token JWT ausente o inválido500 Internal Server Error- Error en INSERT o al obtener MAX(ccar)
PUT /backend/carteras.php
Responsabilidades:
- Actualizar nombre de cartera existente
- Modificar registro por ID (ccar)
Request Body:
json
{
"id": number, // ccar de la cartera
"nombre": "string" // Nuevo nombre (max 30 caracteres)
}SQL Ejecutado:
sql
UPDATE carteras
SET nombre = :nombre
WHERE ccar = :idResponse:
json
{
"status": 204
}Status Codes:
204 No Content- Actualización exitosa (sin cuerpo de respuesta)400 Bad Request- ID o nombre faltante/inválido401 Unauthorized- Token JWT ausente o inválido500 Internal Server Error- Error en UPDATE
Modelo de Datos
Cartera Model
Archivo: models/general/Cartera.php
Métodos Implementados:
getAll(array $array): array- Obtiene carteras según tipo y filtros
- Soporta búsqueda por ID, autocomplete, first/last
- Consulta dinámica (carteras o deu_acr según tipo)
- Sin paginación (LIMIT 10 solo en autocomplete)
insert(array $array): int- Crea nueva cartera
- Genera ID incremental (MAX + 1)
- Retorna ID asignado (ccar)
- No usa transacción explícita
update(array $array): bool- Actualiza nombre de cartera
- Retorna true/false según éxito
- Sin validación de existencia previa
Dependencias:
- PDO connection (inyectado en constructor)
- Tabla
carteras - Tabla
deu_acr(para tipo='proveedor')
Esquema de Base de Datos
Tabla: carteras
Nivel: EMPRESA (1) y SUCURSAL (2) - Configurable dinámicamente
⚠️ Multi-Tenancy Critical: Esta tabla soporta configuración dinámica de niveles.
Niveles por Defecto: [EMPRESA, SUCURSAL]
- Permite tener carteras separadas por sucursal (comportamiento tradicional)
Configuración Común: Solo [EMPRESA]
- Permite compartir carteras entre todas las sucursales
Para configurar:
bash
php manage-table-levels.php --set --database=mi_empresa --table=carteras --levels=1| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
| ccar | SMALLINT | PRIMARY KEY, NOT NULL | Código de cartera (auto-incremental manual) |
| cuenta | DECIMAL(10) | NULL | Código de cuenta contable asociada |
| nombre | VARCHAR(30) | NULL | Nombre descriptivo de la cartera |
Indexes:
- PRIMARY KEY en
ccar
Foreign Keys: Ninguna (referenciada por ordcon.ccar)
Constraints: Ninguna explícita
Tabla: ordcon (Relación con Clientes)
Nivel: EMPRESA (1) y SUCURSAL (2) - Configurable dinámicamente
⚠️ Relación Crítica: La tabla ordcon (clientes) referencia a carteras mediante el campo ccar.
| Campo Relevante | Tipo | Descripción |
|---|---|---|
| cnro | DECIMAL(6) | PRIMARY KEY - Código de cliente |
| ccar | DECIMAL(1) | Referencia a carteras.ccar (sin FK explícita) |
| cven | DECIMAL(3) | Código de vendedor (ordven.cven) |
Relación de Negocio:
- Un cliente (
ordcon) pertenece a UNA cartera (ccar) - Una cartera agrupa MÚLTIPLES clientes
- El vendedor asignado (
cven) trabaja con clientes de su cartera
Sin Foreign Keys Explícitas: La relación ordcon.ccar → carteras.ccar no está implementada como FK en el esquema.
Validaciones Implementadas
Nivel 1: Validaciones Estructurales
⚠️ CRÍTICO - NO IMPLEMENTADAS: No existe Validator middleware para este endpoint.
Validaciones Faltantes:
- Tipo de dato de
nombre(debería ser string, max 30 chars) - Tipo de dato de
id(debería ser int positivo) - Tipo de dato de
tipo(debería ser enum 'cliente'|'proveedor') - Campos requeridos (nombre en POST/PUT, id en PUT)
Validación Manual en Código: Ninguna validación explícita en Controller/Model.
Nivel 2: Validaciones de Negocio
En Model (mínimas):
insert(): Verifica queMAX(ccar)retorne al menos 1 fila (lanzaInsertErrorsi falla)getAll(): Filtra por tipo, pero sin validación de valores permitidos
Validaciones de Negocio NO Implementadas:
- Unicidad de
nombre(se permite duplicados) - Validación de que
ccarno esté en uso antes de UPDATE - Validación de que cartera no tenga clientes asignados antes de eliminar (NO HAY DELETE)
Capa de Servicio
⚠️ NO IMPLEMENTADA: No existe Service Layer para Carteras.
Consecuencias:
- No hay transacciones explícitas
- No hay audit logging (ninguna operación CUD registra auditoría)
- Lógica de negocio mínima (solo CRUD básico)
- Sin orquestación de operaciones complejas
Puntos de Integración
Módulos Relacionados
Ventas:
- Modelo
Cliente(models/modulo-venta/Cliente.php)- Campo
cartera(CLI.CCAR::int) referencia acarteras.ccar - Campo
vendedor(CLI.CVEN::int) referencia aordven.cven
- Campo
Compras:
- Tabla
deu_acr(acreedores/proveedores)- Consultada en
getAll()cuando tipo='proveedor' - Campos:
codigo(ID),descri(nombre),imputa(cuenta)
- Consultada en
Servicios Externos
Ninguno.
Estrategia de Testing
⚠️ NO EXISTEN TESTS: No se encontraron archivos de testing para Carteras.
Tests Recomendados (Unit)
Model Tests (con PHPUnit + mocks):
php
CarteraModelTest::testGetAllClientes()
CarteraModelTest::testGetAllProveedores()
CarteraModelTest::testGetAllConFiltro()
CarteraModelTest::testInsertGeneraIdCorrectamente()
CarteraModelTest::testUpdateModificaNombre()Áreas Críticas a Testear:
- Generación de ID incremental (MAX + 1)
- Filtros de autocomplete (LIMIT 10)
- Manejo de first/last
- Comportamiento cuando no hay resultados
Tests Recomendados (Integration)
Con Base de Datos Real (usando BaseIntegrationTestCase):
php
CarteraIntegrationTest::testInsertConcurrenteGeneraDuplicados()
CarteraIntegrationTest::testGetAllRespetaMultiTenancy()
CarteraIntegrationTest::testUpdateDeCarteraInexistente()Áreas Críticas:
- Race conditions: Inserción simultánea de 2+ carteras
- Multi-tenancy: Schemas diferentes no ven carteras ajenas
- Integridad referencial: Cartera con clientes asignados
Consideraciones de Rendimiento
Índices Implementados
Tabla carteras:
- PRIMARY KEY en
ccar(index automático)
NO EXISTEN índices adicionales:
nombre(usado en ILIKE, escaneo secuencial)
Impacto:
- Búsquedas ILIKE sin índice (aceptable si hay <1000 carteras)
- MAX(ccar) hace full table scan (aceptable para tabla pequeña)
Caching
⚠️ NO IMPLEMENTADO: Sin estrategia de cache.
Oportunidades de Cache:
- Lista de carteras (cambia raramente)
- Autocomplete de carteras (TanStack Query en frontend)
Problemas N+1
NO DETECTADOS: Carteras no hace consultas anidadas.
Potencial N+1 en Consumidores:
- Si frontend carga 100 clientes y cada uno busca cartera por separado
- Recomendación: Implementar batch loading o incluir nombre de cartera en query de clientes
Consideraciones de Seguridad
Sanitización de Entradas
Implementado:
- Prepared statements en todas las queries (previene SQL injection)
- Binding de parámetros con PDO
NO Implementado:
- Validación de tipos (acepta cualquier valor en
nombre) - Escape de caracteres especiales (delegado a PDO)
Permisos
NO IMPLEMENTADOS EXPLÍCITAMENTE: No hay verificación de permisos en el código.
Asume:
- Middleware de autenticación valida JWT (archivo
auth/JwtHandler.php) - No hay verificación de roles/permisos específicos para operaciones CRUD
Recomendación: Agregar verificación de permisos por operación:
CARTERAS_VIEW- GETCARTERAS_CREATE- POSTCARTERAS_EDIT- PUT
Auditoría
⚠️ NO IMPLEMENTADA: Ninguna operación registra auditoría.
Operaciones Sin Auditar:
- INSERT de cartera (debería registrar quién creó y cuándo)
- UPDATE de cartera (debería registrar quién modificó y cuándo)
Recomendación: Migrar a Service Layer con Auditable trait:
php
$this->registrarAuditoria("INSERT", "VENTAS", "carteras", $carteraId);Preguntas Técnicas Pendientes
⚠️ Aclaraciones Requeridas: Hay aspectos técnicos que requieren validación.
Resumen de Preguntas Pendientes:
- ¿Por qué no hay endpoint DELETE? ¿Se pueden eliminar carteras?
- ¿Deberían implementarse validaciones de negocio (unicidad, carteras con clientes)?
- ¿Debería migrarse a arquitectura DDD 5-layer?
- ¿Debería implementarse auditoría de operaciones CUD?
- ¿Cómo resolver race condition en MAX(ccar) + 1?
Limitaciones y Deuda Técnica
Arquitectura Legacy
Problema: No sigue arquitectura DDD 5-layer del backend moderno.
Consecuencias:
- Sin Service Layer (no hay transacciones, no hay auditoría)
- Sin Validators (no hay validación estructural)
- Sin Slim Framework (routing manual en archivo PHP)
- Sin DTOs (arrays asociativos en lugar de objetos tipados)
Esfuerzo de Migración: Medio (2-3 días)
Concurrencia
Problema: Uso de MAX(ccar) + 1 sin transacción explícita.
Escenario de Falla:
- Request A lee MAX(ccar) = 5
- Request B lee MAX(ccar) = 5 (antes de que A haga INSERT)
- Request A inserta ccar=6
- Request B inserta ccar=6 → DUPLICATE KEY ERROR
Solución: Usar SERIAL o SEQUENCE de PostgreSQL, o envolver en transacción con LOCK.
Validaciones
Problema: Sin validadores middleware ni validaciones de negocio.
Consecuencias:
- Se puede insertar
nombrevacío/NULL - Se puede actualizar cartera inexistente sin error
- Se pueden crear carteras duplicadas (mismo nombre)
Esfuerzo de Mejora: Bajo (1 día para validadores + reglas de negocio)
Referencias
- Requisitos de Negocio - Carteras
- Documentación Frontend - Carteras
- Arquitectura Backend - DDD 5-layer
- Multi-Tenancy Patterns
⚠️ NOTA IMPORTANTE: Esta documentación fue generada automáticamente analizando el código implementado. Validar cambios futuros contra este baseline. Se recomienda priorizar la migración a arquitectura DDD y la resolución del problema de concurrencia en generación de IDs.