Appearance
Referencias Contables - Documentación Técnica Backend
⚠️ DOCUMENTACIÓN RETROSPECTIVA - Generada a partir de código implementado el 2026-02-11
Módulo: Ventas Feature: Referencias Contables Fecha: 2026-02-11
Referencia de Negocio
Arquitectura Implementada
Ubicación de Archivos
Controller: controller/modulo-venta/RefContableController.php
Model: models/modulo-venta/RefContable.php
DTO: Resources/Venta/RefContable.php
Legacy Route: backend/ref_con.php
Migration: migrations/migrations/tenancy/20240823200734_new_table_ref_con.php
Seed: migrations/seeds/tenancy/RefCon.php
Patrón Arquitectónico
El sistema implementa un patrón simplificado de 3 capas sin Service Layer:
Legacy Route → Controller → Model → DatabaseCaracterísticas:
- Sin Service Layer (lógica de negocio directamente en Model)
- Sin transacciones explícitas en operaciones
- Sin auditoría implementada
- Legacy endpoint (no integrado a Slim Framework)
API Endpoints
GET (Listar/Obtener)
Endpoint Legacy: backend/ref_con.php (GET)
Operación 1: Obtener por ID
Request:
json
{
"id": 1
}Response (200 OK):
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": {
"id": 1,
"codigo": "GEN",
"nombre": "General",
"impVentas": 41150,
"impCompras": 41150
}
}Operación 2: Obtener por Código
Request:
json
{
"codigo": "GEN"
}Response (200 OK):
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": {
"id": 1,
"codigo": "GEN",
"nombre": "General",
"impVentas": 41150,
"impCompras": 41150
}
}Operación 3: Listar todas
Request:
json
{
"scope": "max",
"filter": "GEN"
}Parámetros Query:
scope(opcional): "min" (solo IDs) | "max" (con datos de cuentas contables expandidos)filter(opcional): Búsqueda por nombre o código (ILIKE). Limita a 10 resultados cuando está presente.
Response (200 OK - scope=min):
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": [
{
"id": 1,
"codigo": "GEN",
"nombre": "General",
"impVentas": 41150,
"impCompras": 41150
}
]
}Response (200 OK - scope=max):
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": [
{
"id": 1,
"codigo": "GEN",
"nombre": "General",
"impVentas": {
"numero": 41150,
"nombre": "Ventas - Productos"
},
"impCompras": {
"numero": 41150,
"nombre": "Ventas - Productos"
}
}
]
}Comportamiento scope=max:
- Por cada referencia, ejecuta consultas adicionales a la tabla
cuentasdel módulo Contabilidad para expandirimpVentaseimpCompras. - Potencial problema N+1 si hay muchas referencias.
POST (Crear)
Endpoint Legacy: backend/ref_con.php (POST)
Request DTO:
json
{
"codigo": "SER",
"nombre": "Servicios",
"impVentas": {
"numero": 41200
},
"impCompras": {
"numero": 51200
}
}Validaciones Estructurales (DTO):
codigo: requerido, max 3 caracteresnombre: requerido, max 20 caracteresimpVentas.numero: integerimpCompras.numero: integer
Validaciones de Negocio (Model):
- Unicidad de código: Verifica que no exista otra referencia con el mismo código.
Response (201 Created):
json
{
"status": 201,
"message": "Datos recibidos correctamente.",
"data": {
"id": 2,
"codigo": "SER",
"nombre": "Servicios",
"impVentas": {
"numero": 41200
},
"impCompras": {
"numero": 51200
}
}
}Response (405 - Duplicado):
json
{
"error": "Ya existe una referencia contable con ese código."
}PUT (Actualizar)
Endpoint Legacy: backend/ref_con.php (PUT)
Request DTO:
json
{
"id": 2,
"codigo": "SRV",
"nombre": "Servicios Profesionales",
"impVentas": {
"numero": 41200
},
"impCompras": {
"numero": 51200
}
}Validaciones de Negocio (Model):
- Unicidad de código: Verifica que no exista otra referencia con el mismo código, excluyendo el ID actual.
Response (204 No Content):
json
{
"status": 204
}Response (405 - Duplicado):
json
{
"error": "Ya existe una referencia contable con ese código."
}Capa de Datos (Model Layer)
RefContable Model
Ubicación: models/modulo-venta/RefContable.php
Responsabilidad: Acceso a datos y mapeo DTO.
Métodos Públicos:
getById(int $id): ?array
- Obtiene referencia contable por ID.
- Validaciones: Verifica que
$idsea entero y esté presente. - Mapeo de columnas:
codigo→codigodenom→nombreimputa→impVentas(cast a bigint)impcom→impCompras(cast a bigint)
- Retorno: Array asociativo o
nullsi no existe.
getByCodigo(string $codigo): ?array
- Obtiene referencia contable por código único.
- Mapeo de columnas: Igual que
getById(). - Retorno: Array asociativo o
nullsi no existe.
getAll(array $options): array
- Lista todas las referencias contables.
- Parámetros:
options['filter']: Búsqueda ILIKE pordenomo LIKE porcodigo. Si está presente, limita a 10 resultados (para autocompletado).
- Mapeo de columnas: Igual que
getById(). - Retorno: Array de referencias o array vacío.
insert(RefContableDTO $data): RefContableDTO
- Crea nueva referencia contable.
- Validación de negocio: Verifica unicidad mediante
getByCodigo(). Lanza excepción si ya existe. - Operación SQL: INSERT con RETURNING ID.
- Bind de parámetros:
:codigo→$data->codigo:nombre→$data->nombre:impVentas→$data->impVentas['numero']:impCompras→$data->impCompras['numero']
- Retorno: DTO actualizado con ID generado.
- Excepción:
InsertErrorsi falla ejecución.
update(RefContableDTO $data): bool
- Actualiza referencia contable existente.
- Validación de negocio: Verifica que no exista otro registro con el mismo código (excluye ID actual).
- Operación SQL: UPDATE por ID.
- Bind de parámetros:
:nombre→$data->nombre:codigo→$data->codigo:impVentas→$data->impVentas['numero']:impCompras→$data->impCompras['numero']:id→$data->id
- Retorno:
truesi ejecución exitosa,falseen caso contrario. - Excepción: Exception con código 405 si código duplicado.
Controller Layer
RefContableController
Ubicación: controller/modulo-venta/RefContableController.php
Responsabilidad: Delegación a Model y expansión de scope.
Constructor:
php
public function __construct(PDO $conn)Métodos Públicos:
getById(int $id)
- Delega a
RefContable::getById().
getByCodigo(string $codigo)
- Delega a
RefContable::getByCodigo().
getAll(array $options)
- Delega a
RefContable::getAll(). - Scope "max": Expande
impVentaseimpComprasconsultando la tablacuentas(módulo Contabilidad) medianteCuenta::getById($id, 'min'). - Problema N+1: Ejecuta consulta adicional por cada referencia cuando scope=max.
insert(RefContableDTO $data)
- Delega a
RefContable::insert().
update(RefContableDTO $data)
- Delega a
RefContable::update().
Esquema de Base de Datos
Tabla: ref_con
Nivel Multi-tenancy: EMPRESA y SUCURSAL
Descripción: Almacena referencias contables para clasificación de productos.
sql
CREATE TABLE ref_con (
codigo VARCHAR(3) NULL, -- Código alfanumérico único (max 3 caracteres)
denom VARCHAR(20) NULL, -- Nombre/denominación (max 20 caracteres)
imputa DECIMAL(10) NULL, -- Cuenta contable para ventas
descri VARCHAR(1) NULL, -- Sin uso actual
impcom DECIMAL(10) NULL, -- Cuenta contable para compras
marca VARCHAR(1) NULL, -- Sin uso actual
id SERIAL PRIMARY KEY -- ID autoincremental
);Índices:
- PRIMARY KEY en
id(automático por SERIAL).
Constraints:
- Ninguna constraint explícita de unicidad en
codigo(validación solo en capa aplicación).
Foreign Keys:
- Ninguna FK definida explícitamente.
- Relación implícita:
imputayimpcomreferencian acuentas.numerodel módulo Contabilidad.
Columnas sin uso:
descri: Definida en schema pero sin uso actual.marca: Definida en schema pero sin uso actual.
Migración:
- Archivo:
20240823200734_new_table_ref_con.php - Tipo: BASE (estructura)
- Niveles: LEVEL_EMPRESA, LEVEL_SUCURSAL
- Condición de ejecución: Requiere módulo Ventas habilitado Y (Contabilidad O Tesorería) O Compras habilitado.
Data Transfer Objects (DTOs)
RefContableDTO
Ubicación: Resources/Venta/RefContable.php
Propiedades:
php
public ?int $id;
public string $codigo; // max 3 caracteres
public string $nombre; // max 20 caracteres
public array|null $impVentas; // {numero: int, nombre: string}
public array|null $impCompras; // {numero: int, nombre: string}Validaciones (via trait Validatable):
id: integercodigo: required, max:3nombre: required, max:20impVentas.numero: integerimpCompras.numero: integer
Constructor:
php
public function __construct(
$codigo,
$nombre,
$impVentas = null,
$impCompras = null,
$id = null
)Métodos heredados:
fromArray(array $data): self- Construcción desde arraytoArray(): array- Conversión a array
Validaciones Implementadas
Validación Estructural (DTO Level)
Ubicación: Resources/Venta/RefContable.php
Reglas:
codigo: required, max:3nombre: required, max:20impVentas.numero: integer (opcional)impCompras.numero: integer (opcional)
Momento: Al construir el DTO desde request (via fromArray()).
Response en caso de falla: HTTP 400 Bad Request (validación ejecutada antes de llegar al controller).
Validación de Negocio (Model Level)
Ubicación: models/modulo-venta/RefContable.php
Reglas:
Unicidad de código (INSERT):
- Verifica mediante
getByCodigo($data->codigo)que no exista otra referencia. - Lanza:
Exception("Ya existe una referencia contable con ese código.", 405).
Unicidad de código (UPDATE):
- Verifica que no exista otra referencia con el mismo código excluyendo el ID actual.
- Lanza:
Exception("Ya existe una referencia contable con ese código.", 405).
Momento: Durante ejecución de insert() o update().
Response en caso de falla: HTTP 405 (código de excepción) con mensaje descriptivo.
Integración con Otros Módulos
Dependencia: Contabilidad (Cuentas)
Relación: Referencias Contables → Cuentas Contables
Propósito:
- Cada referencia vincula con cuentas del plan de cuentas.
imputa: Cuenta para imputación de ventas.impcom: Cuenta para imputación de compras.
Implementación:
- Lazy loading: Cuentas se cargan solo cuando
scope=maxenRefContableController::getAll(). - Model:
Contabilidad\Cuenta - Método:
Cuenta::getById($numero, 'min')
Problema de Performance:
- N+1 queries cuando
scope=max(consulta por cada referencia).
Dependencia: Productos (Ventas)
Relación: Productos → Referencias Contables
Campo: producto.refcon (VARCHAR) almacena el código de referencia (NO el ID).
Seed Behavior:
- Al crear la primera referencia, todos los productos sin referencia (
refcon IS NULL OR refcon = '') se actualizan con el código de la primera referencia disponible. - Archivo:
migrations/seeds/tenancy/RefCon.php
Impacto de cambio de código:
- Si se cambia el código de una referencia, los productos quedan huérfanos (referencian código inexistente).
- No hay cascada: El sistema NO actualiza automáticamente
producto.refconcuando se cambiaref_con.codigo.
Uso en Facturación
Módulos: Ventas, Compras
Propósito: Durante facturación, el sistema obtiene la referencia contable del producto para determinar la cuenta contable de imputación.
Implementación:
- Ventas: Usa
imputa(impVentas) de la referencia. - Compras: Usa
impcom(impCompras) de la referencia.
Referencia en código:
service/CtaCte/MovimientoGananciaService.php: Utilizaimpcompara determinar cuentas contables.
Testing
Cobertura Actual
Unit Tests: No detectados.
Integration Tests: No detectados.
Manual Testing: Sistema en producción requiere testing manual.
Estrategia de Testing Recomendada
Unit Tests (RefContable Model):
Test: insert() - código duplicado
php
public function testInsertDuplicateCodeThrowsException()
{
// Mock: RefContable con código "GEN" ya existe
// Action: insert(RefContableDTO con código "GEN")
// Assert: Exception con mensaje "Ya existe..."
}Test: update() - código duplicado
php
public function testUpdateDuplicateCodeThrowsException()
{
// Mock: Dos referencias (ID 1 código "GEN", ID 2 código "SER")
// Action: update(ID 2 con código "GEN")
// Assert: Exception con mensaje "Ya existe..."
}Test: getAll() con filter
php
public function testGetAllWithFilterLimitsTo10Results()
{
// Mock: 15 referencias en base de datos
// Action: getAll(['filter' => 'GEN'])
// Assert: Retorna máximo 10 resultados
}Integration Tests (RefContableController):
Test: scope=max expande cuentas contables
php
public function testGetAllScopeMaxExpandsAccounts()
{
// Setup: Insertar referencia con impVentas=41150
// Setup: Insertar cuenta 41150 en módulo Contabilidad
// Action: getAll(['scope' => 'max'])
// Assert: impVentas es array con {numero, nombre}
}Test: N+1 query problem
php
public function testGetAllScopeMaxHasN1QueryProblem()
{
// Setup: Insertar 3 referencias
// Action: getAll(['scope' => 'max']) con query profiler
// Assert: Se ejecutan 1 + (3 * 2) = 7 queries (N+1 problem)
}Performance
Análisis de Performance
Operaciones de lectura:
getById(): Query única por ID (PRIMARY KEY) - O(1) - Óptimo.getByCodigo(): Query única sin índice - O(n) - Subóptimo para tablas grandes.getAll()sin filter: Full table scan - O(n) - Aceptable para tabla pequeña.getAll()con filter: ILIKE sin índice - O(n) - Subóptimo.
Problema N+1:
RefContableController::getAll()conscope=maxejecuta 1 query inicial + 2 queries por referencia (una porimpVentas, otra porimpCompras).- Para 10 referencias: 21 queries.
- Solución recomendada: JOIN con
cuentasen query inicial.
Operaciones de escritura:
insert(): Query única + validación de unicidad (1 SELECT + 1 INSERT) - O(1).update(): Query única + validación de unicidad (1 SELECT + 1 UPDATE) - O(1).
Optimizaciones Recomendadas
1. Índice en columna codigo
sql
CREATE UNIQUE INDEX idx_ref_con_codigo ON ref_con(codigo);Beneficios:
- Acelera
getByCodigo()de O(n) a O(log n). - Garantiza unicidad a nivel base de datos (constraint).
- Acelera validaciones de unicidad en
insert()yupdate().
2. JOIN en getAll() scope=max
Query actual (N+1):
php
// 1 query inicial
SELECT * FROM ref_con;
// N queries adicionales
foreach ($refs as $ref) {
SELECT * FROM cuentas WHERE numero = :impVentas;
SELECT * FROM cuentas WHERE numero = :impCompras;
}Query optimizada (1 sola query):
sql
SELECT
rc.id,
rc.codigo,
rc.denom as nombre,
rc.imputa,
cv.numero as impVentas_numero,
cv.nombre as impVentas_nombre,
rc.impcom,
cc.numero as impCompras_numero,
cc.nombre as impCompras_nombre
FROM ref_con rc
LEFT JOIN cuentas cv ON rc.imputa = cv.numero
LEFT JOIN cuentas cc ON rc.impcom = cc.numero;3. Cache para referencias contables
Estrategia:
- Tabla raramente modificada (configuración del sistema).
- Cachear resultado de
getAll()en memoria (Redis, Memcached). - Invalidar cache en
insert()yupdate().
Seguridad
Autenticación y Autorización
Autenticación: JWT
Validación:
- Archivo legacy
backend/ref_con.phprequiere JWT válido. - Token validado en
auth/JwtHandler.php. - Payload contiene
dbyschemapara multi-tenancy.
Autorización:
- No implementada en backend.
- Control de permisos delegado a frontend (permiso
VENTAS_BASES_REF-CONT, id=15).
Prevención de SQL Injection
Implementación:
- Todas las queries usan prepared statements con binding de parámetros.
- Ejemplo:
$stmt->execute([':codigo' => $codigo]).
Estado: ✅ Protegido contra SQL injection.
Sanitización de Datos
Input:
- DTO valida tipos y longitudes máximas.
- No se detecta sanitización adicional (trim, stripslashes).
Output:
- JSON encoding automático mediante
getSuccessResponse().
Multi-tenancy
Implementación:
- Payload JWT contiene
schema. Databaseclass configurasearch_pathde PostgreSQL.
Aislamiento:
- Cada tenant (EMPRESA/SUCURSAL) tiene su propia tabla
ref_con. - Referencias NO compartidas entre tenants.
Auditoría
Estado Actual
Implementación: ❌ No implementada.
Impacto:
- No se registran operaciones de creación, modificación o eliminación.
- No hay trazabilidad de cambios.
- No se identifica qué usuario realizó qué operación.
Implementación Recomendada
Patrón estándar: Bautista Backend usa AuditableInterface + Auditable trait.
Cambios necesarios:
1. Migrar a Service Layer:
php
class RefContableService implements AuditableInterface
{
use Conectable, Auditable;
public function insert(RefContableDTO $data): RefContableDTO
{
$this->connections->beginTransaction('oficial');
try {
$result = $this->model->insert($data);
$this->registrarAuditoria(
"INSERT",
"VENTAS",
"ref_con",
$result->id
);
$this->connections->commit('oficial');
return $result;
} catch (Exception $e) {
$this->connections->rollback('oficial');
throw $e;
}
}
}2. Registrar operaciones:
- INSERT: Al crear nueva referencia.
- UPDATE: Al modificar referencia.
- (DELETE: Si se implementa soft delete en el futuro).
3. Información auditada:
- Usuario (desde JWT payload).
- Timestamp.
- Operación (INSERT/UPDATE/DELETE).
- Tabla (
ref_con). - ID del registro.
- Módulo (VENTAS).
Problemas Identificados
🔴 Críticos (Afectan funcionalidad)
1. Cambio de código rompe relación con productos
- Problema:
producto.refconalmacena código (VARCHAR), no ID. - Impacto: Si se cambia
ref_con.codigo, productos quedan huérfanos. - Solución recomendada:
- Opción A: Agregar FK
producto.refcon_id(integer) y migrar datos. - Opción B: Trigger o procedimiento que actualice
producto.refconcuando cambiaref_con.codigo. - Opción C: Prohibir cambio de código (solo UPDATE de
denom,imputa,impcom).
- Opción A: Agregar FK
2. Sin constraint de unicidad en base de datos
- Problema: Unicidad de
codigosolo validada en aplicación. - Impacto: Race conditions pueden generar códigos duplicados.
- Solución:
CREATE UNIQUE INDEX idx_ref_con_codigo ON ref_con(codigo);
🟡 Importantes (Afectan calidad)
3. N+1 query problem en scope=max
- Problema:
getAll()ejecuta queries adicionales por cada referencia. - Impacto: Performance degradada con muchas referencias.
- Solución: JOIN con
cuentasen query inicial.
4. Sin auditoría
- Problema: No se registran operaciones CUD.
- Impacto: Sin trazabilidad de cambios.
- Solución: Implementar Service Layer con
AuditableInterface.
5. Sin transacciones explícitas
- Problema: Operaciones de escritura sin BEGIN/COMMIT.
- Impacto: Potencial inconsistencia en caso de fallo parcial.
- Solución: Service Layer con
ConnectionManager.
6. Controller con lógica de negocio
- Problema: Controller expande
scope=max(violación de responsabilidad). - Impacto: Dificulta testing y mantenibilidad.
- Solución: Mover lógica a Service Layer.
🟢 Menores (Mejoras)
7. Columnas sin uso en schema
- Problema:
descriymarcadefinidas pero sin uso. - Impacto: Confusión y desperdicio de espacio.
- Solución: Documentar propósito o eliminar en migración futura.
8. Endpoint legacy (no Slim)
- Problema:
backend/ref_con.phpno integrado a Slim Framework. - Impacto: Inconsistencia arquitectónica.
- Solución: Migrar a Slim Routes con middleware.
9. Sin paginación
- Problema:
getAll()retorna todas las referencias. - Impacto: Ineficiente si crece la tabla.
- Solución: Implementar paginación (patrón
PaginatedResponse).
Migración a Arquitectura Estándar
Roadmap Recomendado
Fase 1: Sin Breaking Changes
1.1. Agregar índice de unicidad
sql
CREATE UNIQUE INDEX idx_ref_con_codigo ON ref_con(codigo);1.2. Optimizar getAll() scope=max
- Implementar JOIN con
cuentas. - Mantener compatibilidad con response actual.
1.3. Agregar tests
- Unit tests para Model.
- Integration tests para Controller.
Fase 2: Migración a Service Layer
2.1. Crear RefContableService
- Implementar
AuditableInterface. - Mover lógica de validación desde Model.
- Agregar transacciones explícitas.
2.2. Actualizar Controller
- Delegar a Service en lugar de Model.
- Eliminar lógica de negocio (scope expansion).
2.3. Migrar a Slim Route
- Crear
Routes/Venta/RefContableRoute.php. - Agregar validators como middleware.
- Deprecar
backend/ref_con.php.
Fase 3: Mejoras Estructurales
3.1. Migrar relación Producto → RefContable
- Agregar columna
producto.refcon_id(integer, FK). - Migrar datos:
UPDATE producto SET refcon_id = (SELECT id FROM ref_con WHERE codigo = producto.refcon). - Deprecar columna
producto.refcon(VARCHAR).
3.2. Soft Delete
- Agregar columna
deleted_at. - Implementar
RefContableService::delete().
3.3. Paginación
- Implementar patrón
PaginatedResponse. - Mantener compatibilidad con
filterpara autocompletado.
Preguntas Técnicas Pendientes
⚠️ Aclaraciones Requeridas: Hay aspectos técnicos que requieren validación.
Ver: Preguntas sobre Referencias Contables
Referencias
⚠️ NOTA IMPORTANTE: Esta documentación fue generada automáticamente analizando el código implementado. Validar cambios futuros contra este baseline.