Skip to content

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

Requisitos 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 → Database

Caracterí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 cuentas del módulo Contabilidad para expandir impVentas e impCompras.
  • 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 caracteres
  • nombre: requerido, max 20 caracteres
  • impVentas.numero: integer
  • impCompras.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 $id sea entero y esté presente.
  • Mapeo de columnas:
    • codigocodigo
    • denomnombre
    • imputaimpVentas (cast a bigint)
    • impcomimpCompras (cast a bigint)
  • Retorno: Array asociativo o null si no existe.

getByCodigo(string $codigo): ?array

  • Obtiene referencia contable por código único.
  • Mapeo de columnas: Igual que getById().
  • Retorno: Array asociativo o null si no existe.

getAll(array $options): array

  • Lista todas las referencias contables.
  • Parámetros:
    • options['filter']: Búsqueda ILIKE por denom o LIKE por codigo. 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: InsertError si 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: true si ejecución exitosa, false en 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 impVentas e impCompras consultando la tabla cuentas (módulo Contabilidad) mediante Cuenta::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: imputa y impcom referencian a cuentas.numero del 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: integer
  • codigo: required, max:3
  • nombre: required, max:20
  • impVentas.numero: integer
  • impCompras.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 array
  • toArray(): array - Conversión a array

Validaciones Implementadas

Validación Estructural (DTO Level)

Ubicación: Resources/Venta/RefContable.php

Reglas:

  • codigo: required, max:3
  • nombre: required, max:20
  • impVentas.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=max en RefContableController::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.refcon cuando se cambia ref_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: Utiliza impcom para 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() con scope=max ejecuta 1 query inicial + 2 queries por referencia (una por impVentas, otra por impCompras).
  • Para 10 referencias: 21 queries.
  • Solución recomendada: JOIN con cuentas en 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() y update().

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() y update().

Seguridad

Autenticación y Autorización

Autenticación: JWT

Validación:

  • Archivo legacy backend/ref_con.php requiere JWT válido.
  • Token validado en auth/JwtHandler.php.
  • Payload contiene db y schema para 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.
  • Database class configura search_path de 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.refcon almacena 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.refcon cuando cambia ref_con.codigo.
    • Opción C: Prohibir cambio de código (solo UPDATE de denom, imputa, impcom).

2. Sin constraint de unicidad en base de datos

  • Problema: Unicidad de codigo solo 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 cuentas en 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: descri y marca definidas 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.php no 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 filter para 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.