Appearance
Conceptos de Notas - Documentación Técnica Backend
Módulo: ventas Feature: Conceptos para notas de crédito y débito Tipo: Resource (CRUD) Fecha: 2026-02-11
Referencia de Negocio
Conceptos de Notas - Requisitos de Negocio
Descripción General
Sistema de gestión de conceptos (motivos) para notas de crédito y notas de débito en ventas. Implementa una arquitectura polimórfica donde un único controlador maneja ambos tipos de conceptos mediante discriminación por código de tipo de comprobante.
Los conceptos se utilizan para clasificar los motivos de emisión de notas de ajuste (ej: "Anulación", "Descuento", "Bonificación", "Interés"). Cada concepto puede asociarse opcionalmente con una cuenta contable para integración con el módulo de Contabilidad.
Características distintivas:
- Polimorfismo por código: Mismo controller/modelo base, discriminación dinámica
- Protección de integridad: No permite eliminar el último concepto
- Integración Contabilidad: Enriquecimiento automático con datos de cuentas
- Selección automática: Método especial para obtener concepto de anulación
Arquitectura Implementada
1. Capa API
Archivo Legacy: backend/mod-ventas/concepto-comprobante.php
Endpoint Base: /backend/mod-ventas/concepto-comprobante.php
Métodos HTTP:
GET - Listar conceptos por tipo de comprobante
- Query Params:
codigo(int, required): Código ARCA del tipo de comprobante- Notas de Crédito:
3(A),8(B),13(C) - Notas de Débito:
2(A),7(B),12(C)
- Notas de Crédito:
- Response:
200 OK- Array de conceptos con cuenta contable enriquecida
- Query Params:
POST - Crear nuevo concepto
- Body:
codigo(int, required): Código ARCAnombre(string, required, max 50): Descripción del conceptotipo(string, required, length 1): Tipo de conceptocuenta_contable(int, optional): ID de cuenta contable
- Response:
200 OK- ID del concepto creado
- Body:
PUT - Actualizar concepto
- Body:
codigo(int, required)id(int, required)nombre(string, optional)tipo(string, optional)cuenta_contable(int, optional)
- Response:
204 No Content
- Body:
DELETE - Eliminar concepto (soft delete)
- Body:
codigo(int, required)id(int, required)
- Response:
204 No Content - Error:
400 Bad Requestsi es el último concepto
- Body:
Middleware Stack:
- JWT Authentication (extrae payload)
- Schema setting (establece
search_path)
⚠️ NOTA: Este endpoint utiliza arquitectura legacy (pre-Slim Framework). Pendiente migración a Slim con middleware de validación.
2. Capa Controller
Archivo: controller/modulo-venta/ConceptoComprobanteController.php
Responsabilidades:
- Validar presencia de parámetros requeridos
- Discriminar dinámicamente entre
ConceptoNotaCreditoyConceptoNotaDebitosegún código - Enriquecer respuestas con datos de cuenta contable (si módulo Contabilidad activo)
- Delegar operaciones CRUD al modelo correspondiente
Métodos:
getAll(array $options): ?array- Valida
codigorequerido - Selecciona modelo por código
- Ejecuta
getAll()en modelo - Si módulo Contabilidad activo, enriquece con datos de
CuentaModel - Retorna array de
ConceptoComprobanteDTO
- Valida
insert(array $options): string- Valida
codigo,nombre,tiporequeridos - Selecciona modelo
- Ejecuta
insert()en modelo - Retorna ID del concepto creado
- Valida
update(array $options): bool- Valida
codigo,idrequeridos - Selecciona modelo
- Ejecuta
update()en modelo - Retorna booleano de éxito
- Valida
delete(array $data): bool- Valida
codigo,idrequeridos - Selecciona modelo
- Ejecuta
delete()en modelo - Retorna booleano de éxito
- Valida
Método Privado:
getModelByCodigo(int $codigo): ConceptoComprobante- Utiliza
matchexpression para discriminación polimórfica - Si código en
TipoComprobante::getCodes(NOTA_DE_CREDITO)→ConceptoNotaCredito - Si código en
TipoComprobante::getCodes(NOTA_DE_DEBITO)→ConceptoNotaDebito - Caso contrario →
null
- Utiliza
Dependencias:
ConceptoNotaCredito(model)ConceptoNotaDebito(model)CuentaModel(Contabilidad, condicional)Config(para verificar módulos activos)TipoComprobante(Constants)NombreTipoComprobante(Enum)
3. Capa Model
3.1 ConceptoComprobante (Base)
Archivo: models/modulo-venta/ConceptoComprobante.php
Herencia: extends Model
Constructor:
- Recibe
PDO $connystring $table - Pasa tabla dinámica al constructor padre
- Define reglas de validación
Validaciones (Model Layer):
php
'nombre' => 'required|max:50',
'cuenta_contable' => 'integer',
'tipo' => 'required|max:1'Métodos CRUD:
getAll(): array- Query:
SELECT id, descri as nombre, cuenta as cuenta_contable, tipcon as tipo FROM {table} WHERE deleted_at IS NULL - Mapea resultados a
ConceptoComprobanteDTO - Retorna array vacío si no hay registros
- Query:
insert(array $data): string- Valida datos con
self::validate() - Query:
INSERT INTO {table} (descri, tipcon, cuenta) VALUES (:nombre, :tipo, :cuenta_contable) cuenta_contableopcional (null si no provisto)- Retorna
lastInsertId()
- Valida datos con
update(array $data): bool- Valida datos con
self::validate() - Query:
UPDATE {table} SET descri = :nombre, tipcon = :tipo, cuenta = :cuenta_contable WHERE id = :id - Retorna
truesirowCount() > 0
- Valida datos con
delete(int $id): bool- Soft Delete con protección
- Query condicional: Solo permite borrado si hay más de 1 registro activo
- Query:
UPDATE {table} SET deleted_at = CURRENT_TIMESTAMP WHERE id = :id AND (SELECT COUNT(*) FROM {table} WHERE deleted_at IS NULL) > 1 - Lanza
BadRequestsirowCount() === 0(último registro o no encontrado) - Retorna
truesi éxito
Características de implementación:
- Tabla dinámica: El nombre de tabla se inyecta en constructor (polimorfismo)
- Soft deletes: Usa
deleted_at IS NULLen todas las queries de lectura - Protección de integridad: No permite eliminar el último concepto de cada tipo
- Mapeo de columnas:
descri→nombre,tipcon→tipo,cuenta→cuenta_contable
3.2 ConceptoNotaCredito
Archivo: models/modulo-venta/ConceptoNotaCredito.php
Herencia: extends ConceptoComprobante
Constructor:
php
public function __construct(PDO $conn) {
parent::__construct($conn, 'concecre');
}Método Adicional:
getConceptoAnulacion(): array- Propósito: Selección automática de concepto para procesos de anulación
- Lógica:
- Prioridad 1: Buscar concepto con "anulacion" en descripción (case-insensitive)
- Query:
SELECT id, descri as descripcion FROM concecre WHERE LOWER(descri) LIKE '%anulacion%' AND deleted_at IS NULL LIMIT 1
- Query:
- Fallback: Primer registro activo ordenado por ID
- Query:
SELECT id, descri as descripcion FROM concecre WHERE deleted_at IS NULL ORDER BY id LIMIT 1
- Query:
- Error: Lanza
ServerExceptionsi no hay conceptos configurados
- Prioridad 1: Buscar concepto con "anulacion" en descripción (case-insensitive)
- Retorna:
['id' => int, 'descripcion' => string]
Uso típico: Este método es utilizado por servicios de anulación automática de facturas/recibos para obtener el concepto por defecto sin intervención del usuario.
3.3 ConceptoNotaDebito
Archivo: models/modulo-venta/ConceptoNotaDebito.php
Herencia: extends ConceptoComprobante
Constructor:
php
public function __construct(PDO $conn) {
parent::__construct($conn, 'concedeb');
}Sin métodos adicionales: Utiliza todos los métodos heredados de ConceptoComprobante.
4. Capa Resources (DTOs)
Archivo: Resources/Venta/ConceptoComprobante.php
Herencia: extends DTO
Propiedades:
php
public ?int $id;
public ?string $tipo;
public ?string $nombre;
public int|array|null $cuenta_contable; // Union type para soportar enriquecimiento
public ?string $fecha_eliminacion;Características:
cuenta_contablepuede serint(ID) oarray(objeto cuenta enriquecido)- Soporta
fromArray()ytoArray()(heredados deDTO)
5. Constants y Enums
5.1 TipoComprobante (Constants)
Archivo: Resources/Venta/Constants/TipoComprobante.php
Mapeo ARCA:
php
const CODES = [
'NOTA_DE_CREDITO' => [
'A' => 3,
'B' => 8,
'C' => 13
],
'NOTA_DE_DEBITO' => [
'A' => 2,
'B' => 7,
'C' => 12
]
];Métodos relevantes:
getCodes(Comprobante $comprobante): ?array- Retorna array de códigos por tipo- Ejemplo:
getCodes(NOTA_DE_CREDITO)→[3, 8, 13]
- Ejemplo:
Uso en discriminación:
php
in_array($codigo, TipoComprobante::getCodes(NombreTipoComprobante::NOTA_DE_CREDITO))5.2 NombreTipoComprobante (Enum)
Archivo: Resources/Venta/Enums/NombreTipoComprobante.php
php
enum NombreTipoComprobante: string {
case NOTA_DE_CREDITO = 'NOTA_DE_CREDITO';
case NOTA_DE_DEBITO = 'NOTA_DE_DEBITO';
}Esquema de Base de Datos
Tabla: concecre (Conceptos Nota de Crédito)
Nivel de Schema: EMPRESA y SUCURSAL (multi-nivel)
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
| id | SERIAL | PRIMARY KEY, NOT NULL | Identificador único |
| descri | VARCHAR(50) | NOT NULL | Descripción del concepto |
| tipcon | VARCHAR(1) | NOT NULL | Tipo de concepto (código interno) |
| cuenta | DECIMAL(10,0) | NULL | ID de cuenta contable (FK virtual) |
| deleted_at | TIMESTAMP | NULL | Fecha de eliminación (soft delete) |
Índices:
- PRIMARY KEY en
id(automático por SERIAL)
Constraints:
descri NOT NULLtipcon NOT NULL
Migración: 20240823200726_new_table_concecre.php
Condición de creación:
- Módulo Ventas habilitado
- Tabla
credito(notas de crédito) existe
Tabla: concedeb (Conceptos Nota de Débito)
Nivel de Schema: EMPRESA y SUCURSAL (multi-nivel)
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
| id | SERIAL | PRIMARY KEY, NOT NULL | Identificador único |
| descri | VARCHAR(50) | NOT NULL | Descripción del concepto |
| tipcon | VARCHAR(1) | NOT NULL | Tipo de concepto (código interno) |
| cuenta | BIGINT | NULL | ID de cuenta contable (FK virtual) |
| deleted_at | TIMESTAMP | NULL | Fecha de eliminación (soft delete) |
Índices:
- PRIMARY KEY en
id(automático por SERIAL)
Constraints:
descri NOT NULLtipcon NOT NULL
⚠️ NOTA: cuenta es BIGINT en concedeb vs DECIMAL(10,0) en concecre - inconsistencia de tipo.
Migración: 20240902144030_new_table_concedeb.php
Condición de creación:
- Módulo Ventas habilitado
- Tabla
debito(notas de débito) existe
Relaciones
Relación virtual con Contabilidad:
concecre.cuenta→cuenta_contable.id(NO FK, relación lógica)concedeb.cuenta→cuenta_contable.id(NO FK, relación lógica)
Enriquecimiento condicional: Si módulo Contabilidad está activo, el controller consulta CuentaModel y enriquece el campo cuenta_contable del DTO con objeto completo de cuenta.
Validaciones Implementadas
Nivel 1: Validación Estructural (Controller)
Ubicación: ConceptoComprobanteController.php
| Operación | Campos Requeridos | Error |
|---|---|---|
| GET | codigo | 400 Bad Request |
| POST | codigo, nombre, tipo | 400 Bad Request |
| PUT | codigo, id | 400 Bad Request |
| DELETE | codigo, id | 400 Bad Request |
Implementación: Validación manual con if (!$options['field'])
Nivel 2: Validación de Modelo (Model)
Ubicación: ConceptoComprobante.php - trait Validatable
Reglas:
php
'nombre' => 'required|max:50',
'cuenta_contable' => 'integer',
'tipo' => 'required|max:1'Ejecución: Llamado en insert() y update() vía self::validate($data)
Nivel 3: Validación de Negocio (Model)
Regla de Integridad: No permitir eliminar el último concepto
Implementación: Query con subquery condicional
sql
UPDATE {table}
SET deleted_at = CURRENT_TIMESTAMP
WHERE id = :id
AND (SELECT COUNT(*) FROM {table} WHERE deleted_at IS NULL) > 1Error: Lanza BadRequest con mensaje "No se puede eliminar el último concepto de comprobante."
Puntos de Integración
1. Integración con Contabilidad
Componente: controller/ConceptoComprobanteController::getAll()
Flujo de enriquecimiento:
- Controller consulta
Config::getOneByKey('PermisosEmpresa') - Si
Modulo::CONTABILIDADestá habilitado (=== 1) - Para cada concepto con
cuenta_contabledefinida:- Consulta
CuentaModel::getById($cuenta_contable, 'min') - Reemplaza ID numérico con objeto cuenta completo
- Consulta
DTO transformado:
php
// Antes del enriquecimiento
['cuenta_contable' => 1234]
// Después del enriquecimiento
['cuenta_contable' => ['id' => 1234, 'nombre' => 'Cuenta X', ...]]2. Integración con Procesos de Anulación
Método: ConceptoNotaCredito::getConceptoAnulacion()
Uso: Servicios de anulación automática (ej: anular factura, anular recibo) obtienen el concepto por defecto sin intervención del usuario.
Flujo:
- Servicio llama
getConceptoAnulacion() - Método busca concepto con "anulacion" en descripción
- Si no encuentra, retorna primer concepto activo
- Si no hay conceptos, lanza excepción
Casos de uso típicos:
- Anulación de factura electrónica (requiere nota de crédito automática)
- Proceso masivo de anulación de comprobantes
Testing
Tests Unitarios
Archivo: Tests/Unit/Venta/ConceptoNotaCreditoTest.php
Cobertura:
testGetConceptoAnulacionReturnsAnulacionConceptIfFound()- Verifica que retorna concepto con "anulacion" si existe
testGetConceptoAnulacionReturnsFirstConceptAsFallbackIfAnulacionNotFound()- Verifica fallback a primer registro si no hay "anulacion"
testGetConceptoAnulacionThrowsExceptionIfNoConceptsAreConfigured()- Verifica excepción
ServerExceptionsi no hay conceptos
- Verifica excepción
Estrategia: Mocking de PDO y PDOStatement para tests unitarios puros
Comando de ejecución:
bash
vendor/bin/phpunit Tests/Unit/Venta/ConceptoNotaCreditoTest.phpTests de Integración
Estado: No implementados
Tests recomendados:
- CRUD completo con base de datos real
- Verificación de soft deletes
- Protección contra eliminación del último concepto
- Enriquecimiento con cuenta contable
- Discriminación polimórfica por código
Rendimiento
Consultas Optimizadas
Queries con filtrado activo:
sql
WHERE deleted_at IS NULLSiempre presente en getAll() para excluir registros eliminados.
Índices sugeridos (no implementados):
sql
-- Mejorar performance de soft deletes
CREATE INDEX idx_concecre_deleted_at ON concecre(deleted_at) WHERE deleted_at IS NULL;
CREATE INDEX idx_concedeb_deleted_at ON concedeb(deleted_at) WHERE deleted_at IS NULL;
-- Optimizar búsqueda de anulación
CREATE INDEX idx_concecre_descri_gin ON concecre USING gin(to_tsvector('spanish', descri));Caching
Estado: No implementado
Oportunidad de mejora:
- Cachear lista de conceptos por tipo (bajo cambio de frecuencia)
- Cache key:
conceptos_notas_{tipo}_{schema} - Invalidación: On insert/update/delete
Seguridad
Sanitización de Entrada
Método: Prepared statements en todos los queries
Ejemplo:
php
$stmt = $this->conn->prepare("SELECT ... WHERE id = :id");
$stmt->execute(['id' => $id]);✅ No hay concatenación directa de variables en SQL
Control de Acceso
Autenticación: JWT verificado en middleware
Autorización: Pendiente de implementación
⚠️ Mejora sugerida: Agregar verificación de permisos específicos por operación:
conceptos_notas.listarconceptos_notas.crearconceptos_notas.editarconceptos_notas.eliminar
Auditoría
Estado: No implementado
⚠️ Mejora crítica: Implementar AuditableInterface en un Service layer
Operaciones a auditar:
INSERT- Creación de conceptoUPDATE- Modificación de conceptoDELETE- Eliminación (soft) de concepto
Ejemplo de implementación sugerida:
php
$this->registrarAuditoria(
"INSERT",
"VENTAS",
$this->model->getTable(),
$conceptoId
);Deuda Técnica
1. Arquitectura Legacy
Problema: Endpoint usa arquitectura pre-Slim (direct PHP file)
Impacto:
- No hay validación centralizada (Validator middleware)
- Manejo de errores inconsistente
- Difícil testing de endpoints
Solución sugerida: Migrar a Slim Framework con:
php
// Routes/Ventas/ConceptoComprobanteRoutes.php
$group->get('', [ConceptoComprobanteController::class, 'getAll'])
->add(new ConceptoComprobanteValidator('get'));2. Inconsistencia de Tipos en Schema
Problema:
concecre.cuenta→ DECIMAL(10,0)concedeb.cuenta→ BIGINT
Impacto: Posible inconsistencia en queries cross-table
Solución: Normalizar ambos a BIGINT mediante migración
3. Falta de Foreign Keys
Problema: cuenta no tiene FK a cuenta_contable
Impacto:
- Posible referencia a cuentas inexistentes
- Sin eliminación en cascada
Solución: Agregar FK con ON DELETE SET NULL
sql
ALTER TABLE concecre ADD CONSTRAINT fk_concecre_cuenta
FOREIGN KEY (cuenta) REFERENCES cuenta_contable(id)
ON DELETE SET NULL;4. Falta de Service Layer
Problema: Lógica de negocio en Controller y Model
Impacto:
- No hay transacciones explícitas
- No hay auditoría
- Difícil reutilización de lógica
Solución: Crear ConceptoComprobanteService con:
- Manejo de transacciones
- Auditoría implementada
- Lógica de validación compleja
5. Sin Validador Dedicado
Problema: Validación manual en controller
Impacto: Código repetitivo, mensajes de error inconsistentes
Solución: Crear ConceptoComprobanteValidator middleware
Referencias
- Conceptos de Notas - Requisitos de Negocio
- Conceptos de Notas - Documentación Frontend
- Preguntas Pendientes
Última actualización: 2026-02-11 Versión del sistema: 3.13.1 Estado: Documentación retrospectiva completada
⚠️ NOTA IMPORTANTE: Esta documentación fue generada automáticamente analizando el código implementado. Validar cambios futuros contra este baseline.