Appearance
Artículos/Productos - Documentación Técnica Backend
Módulo: Ventas Feature: Gestión de Productos (CRUD) Fecha: 2026-02-11
Arquitectura Implementada
Patrón: Arquitectura Legacy (No sigue 5-layer DDD estándar)
- Endpoint Legacy:
/backend/producto.php - Controller:
controller/modulo-venta/ProductoController.php - Service:
service/Venta/ProductoService.php(solo consultas) - Model:
models/modulo-venta/Producto.php - DTO:
Resources/Venta/Producto.php
Nota: Esta implementación es legacy y NO sigue la arquitectura 5-layer DDD documentada en bautista-backend-architecture. Los endpoints están en archivos PHP planos en /backend/ en lugar de usar Slim Framework RouteCollectorProxy. Las operaciones CUD (Create, Update, Delete) se ejecutan en el Controller en lugar del Service layer.
API Endpoints
GET /backend/producto
Descripción: Consulta de productos con múltiples modos de operación
Parámetros Query:
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| scope | string | No | Nivel de detalle: 'min', 'max', 'lista_precio', 'stock', 'facturacion' (default: 'min') |
| activo | boolean | No | Filtrar por estado activo (default: true) |
| pageIndex | int | No | Índice de página para paginación |
| pageSize | int | No | Tamaño de página para paginación |
| serverSide | boolean | No | Activar modo Server-Side Rendering de DataTables |
| id | int | No | Buscar por ID de producto |
| codigo_comercial | string | No | Buscar por código comercial |
| filter | string | No | Búsqueda global por nombre, ID o código |
| exclude | array | No | IDs de productos a excluir |
| columnFilter | array | No | Filtros por columna específica |
| order | array | No | Ordenamiento personalizado |
| lista | int | No | ID de lista de precios (agrega JOIN con PRECIOS) |
Modos de Operación:
1. Paginado (pageIndex + pageSize)
http
GET /backend/producto?pageIndex=0&pageSize=20&scope=minRespuesta:
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": {
"data": [...],
"meta": {
"totalRowCount": 150,
"filteredRowCount": 150,
"pageCount": 8,
"pageIndex": 0,
"pageSize": 20
}
}
}2. Server-Side Rendering (DataTables)
http
GET /backend/producto?serverSide=true&draw=1&start=0&length=10&search[value]=cafe3. Búsqueda por ID o Código
http
GET /backend/producto?id=123&scope=max
GET /backend/producto?codigo_comercial=78912345678904. Listado simple
http
GET /backend/producto?scope=min&activo=trueResponse DTO (scope='max'):
typescript
{
id: number;
nombre: string;
codigo_comercial: string;
stock: number;
bonificacion: number;
tipo_imp: 'P' | 'F'; // P=Porcentual, F=Fijo
imp_interno: number;
costo: number;
descripcion: string;
rubro: { id: number; nombre: string };
linea: { id: number; nombre: string };
ref_con: { codigo: string; nombre: string } | null;
maneja_stock: 'S' | 'N';
categoria_iva: { codigo: number; descripcion: string };
punto_pedido: number;
proveedor: { id: number; nombre: string } | null;
sincroniza_web: boolean;
stock_web: number;
listas: Array<{ lista: number; precio: number; tipo: 'F' | 'N' }>;
comision: number;
manejo_precios_facturacion: boolean;
activo: boolean;
porc_ganancia: number;
ubicacion: { id: number; ruta: string } | null;
membresia: { // Solo si módulo membresías está habilitado
id_producto: number;
meses: number;
// ... otros campos de membresía
} | null;
}Scopes Disponibles:
| Scope | Campos Incluidos | Uso |
|---|---|---|
| min | id, nombre, codigo_comercial, activo | Autocompletes, selects |
| lista_precio | id, nombre, codigo_comercial | Listas de precios |
| stock | id, nombre, stock, maneja_stock | Control de inventario |
| facturacion | id, nombre, codigo_comercial, costo, stock, maneja_stock, bonificacion, impuestos, categoria_iva, comision | Facturación |
| max | Todos los campos + relaciones hidratadas | Formularios, detalle |
Status Codes:
200 OK- Consulta exitosa400 Bad Request- Parámetros inválidos401 Unauthorized- No autenticado500 Internal Server Error- Error del servidor
POST /backend/producto
Descripción: Crear nuevo producto con listas de precios y extensión de membresía (opcional)
Request Body:
json
{
"nombre": "Café Colombia 500g",
"codigo_comercial": "7891234567890",
"costo": 1500.00,
"rubro": { "id": 1 },
"linea": { "id": 5 },
"categoria_iva": { "codigo": 1 },
"maneja_stock": "S",
"punto_pedido": 10,
"proveedor": { "id": 25 },
"descripcion": "Café premium origen Colombia",
"sincroniza_web": true,
"stock_web": 100,
"comision": 10.5,
"manejo_precios_facturacion": true,
"porc_ganancia": 30.0,
"ubicacion": { "id": 3 },
"listas": [
{ "lista": 1, "precio": 2000.00, "tipo": "F" },
{ "lista": 2, "precio": 1800.00, "tipo": "F" }
],
"membresia": { // Opcional - solo si módulo habilitado
"meses": 12
}
}Proceso de Inserción (Controller):
- Iniciar transacción
- Procesar ubicación (crear si es nueva o usar existente)
- Transformar datos:
rubro.id→rubrolinea.id→lineacategoria_iva.codigo→categoria_ivaref_con.codigo→ref_con
- Insertar producto (Model.insert())
- Insertar listas de precios (ListaPrecioService)
- Insertar extensión membresía (si datos presentes y módulo habilitado)
- Commit transacción
Response:
json
{
"status": 201,
"message": "Datos recibidos correctamente.",
"data": {
"id": 456
}
}Status Codes:
201 Created- Producto creado exitosamente400 Bad Request- Validación estructural fallida405 Method Not Allowed- Código comercial duplicado500 Internal Server Error- Error en transacción
PUT /backend/producto
Descripción: Actualizar producto existente, sus listas de precios y extensión de membresía
Request Body:
json
{
"id": 456,
"nombre": "Café Colombia 500g Premium",
"codigo_comercial": "7891234567890",
"costo": 1600.00,
"rubro": { "id": 1 },
"linea": { "id": 5 },
"categoria_iva": { "codigo": 1 },
"maneja_stock": "S",
"punto_pedido": 15,
"proveedor": { "id": 25 },
"descripcion": "Café premium origen Colombia - Edición especial",
"sincroniza_web": true,
"stock_web": 150,
"comision": 12.0,
"activo": true,
"manejo_precios_facturacion": true,
"porc_ganancia": 35.0,
"ubicacion": { "id": 4 },
"listas": [
{ "lista": 1, "precio": 2200.00, "tipo": "F" },
{ "lista": 2, "precio": 2000.00, "tipo": "F" },
{ "lista": 3, "precio": 1900.00, "tipo": "F" }
],
"membresia": { // Si se omite y existe, se elimina la extensión
"meses": 18
}
}Proceso de Actualización (Controller):
- Iniciar transacción
- Procesar ubicación
- Actualizar producto (Model.update())
- Sincronizar listas de precios:
- Obtener listas registradas actuales
- Para cada lista en petición:
- Si existe: actualizar
- Si no existe: insertar
- Eliminar listas no presentes en petición
- Gestionar extensión membresía (si módulo habilitado):
- Si vienen datos y existe extensión: actualizar (partialUpdate)
- Si vienen datos y NO existe: insertar (requiere validación completa)
- Si NO vienen datos y existe: eliminar
- Limpiar ubicaciones no usadas
- Commit transacción
Response:
json
{
"status": 204,
"message": "Datos recibidos correctamente."
}Status Codes:
204 No Content- Actualización exitosa400 Bad Request- Datos inválidos405 Method Not Allowed- Código comercial duplicado (diferente producto)500 Internal Server Error- Error en transacción
Capa de Servicio
ProductoService
Archivo: service/Venta/ProductoService.php
Responsabilidad: Servicio de consulta de productos (NO maneja CUD operations - esas están en Controller)
Constructor:
php
public function __construct(ModelFactory $modelFactory)Métodos Públicos:
getAll(array $options, string $scope, bool $activo): array
Obtiene todos los productos aplicando filtros opcionales.
Parámetros:
$options: Filtros (filter, exclude, lista, etc.)$scope: 'min', 'max', 'facturacion', etc.$activo: Filtrar por productos activos
Retorna: Array de productos según scope
Enriquecimiento: Si scope='max' y módulo membresías habilitado, agrega campo 'membresia' en bulk
getAllPaginated(array $options, string $scope, bool $activo): array
Obtiene productos paginados con soporte de filtros y ordenamiento.
Parámetros:
$options: Filtros + pageIndex, pageSize, order, columnFilter$scope: Nivel de detalle$activo: Estado del producto
Retorna:
php
[
'data' => [...productos...],
'meta' => [
'totalRowCount' => int,
'filteredRowCount' => int,
'pageCount' => int,
'pageIndex' => int,
'pageSize' => int
]
]getByIds(array $ids, string $scope): array
Obtiene múltiples productos por sus IDs.
getById(int $id, string $scope): ?Producto
Obtiene un producto por su ID.
Métodos Privados:
isMembresiasModuleEnabled(): bool
Verifica si el módulo de membresías está habilitado leyendo configuración desde sistema.PermisosEmpresa.modulo_membresias.
enrichProductoWithMembresia(mixed $producto): mixed
Enriquece un solo producto con datos de extensión de membresía.
enrichProductosWithMembresia(array $productos): array
Enriquece múltiples productos usando consulta bulk (una sola query) para mejor performance.
Lógica de Negocio
Generación de ID
Método: Producto::getNewId()
Utiliza MAX(NUMERO) + 1 para generar nuevo ID de producto.
⚠️ RIESGO DE CONCURRENCIA DETECTADO:
La generación de ID NO está dentro de la transacción principal. Uso de MAX+1 sin lock puede generar duplicados en escenarios de alta concurrencia.
php
// En Controller.insert() - línea 1061
$data['id'] = $this->model->getNewId(); // Fuera de transacción
$this->conn->beginTransaction(); // Transacción inicia DESPUÉS
try {
// ... inserción ...
}Recomendación: Mover getNewId() dentro de transacción o usar secuencias PostgreSQL.
Validación de Duplicados
Código comercial único:
Antes de insertar/actualizar, se verifica que codigo_comercial no esté en uso por otro producto.
php
// En insert
$res = $this->getOne(['codigo_comercial' => $producto->codigo_comercial], 'min');
if (!empty($res)) {
throw new Exception("Ya existe un producto con ese codigo comercial", 405);
}
// En update
$res = $this->getOne(['codigo_comercial' => $producto->codigo_comercial], 'min');
if ($res && $res->id !== $producto->id) {
throw new Exception("Ya existe un producto con ese codigo comercial", 405);
}Gestión de Listas de Precios
Tabla: precios (sin PRIMARY KEY compuesta explícita)
Sincronización en Update:
- Obtener listas registradas actuales
- Recorrer listas de petición:
- Si lista existe → UPDATE
- Si lista NO existe → INSERT
- Calcular diferencia (listas registradas - listas petición)
- Eliminar listas sobrantes
Servicio: ListaPrecioService maneja operaciones CRUD sobre precios
Gestión de Ubicaciones
Servicio: UbicacionService
Método: procesarUbicacion(array|int $ubicacion): ?int
- Si recibe ID: retorna ID
- Si recibe array (nueva ubicación): crea ubicación jerárquica y retorna ID
Limpieza de Ubicaciones No Usadas:
Después de actualizar producto, se obtienen todas las ubicaciones en uso y se eliminan las que no tienen hijas ni están siendo utilizadas.
php
$idsUbicacionUtilizadas = $this->model->getIdsUbicacionesUsadas();
if ($idsUbicacionUtilizadas) {
$serviceUbicacion->eliminarNoUsadas($idsUbicacionUtilizadas);
}Integración con Módulo Membresías
Verificación de Módulo Habilitado:
php
$config = new Config($conn);
$permisos = $config->getOneByKey('PermisosEmpresa');
$permisosArray = json_decode($permisos, true);
$enabled = $permisosArray['modulo_membresias'] === 1;Modelo: MembresiaExtProducto (Doctrine/modulo Membresia)
Validadores:
MembresiaExtProductoValidator(inserción completa)MembresiaExtProductoPartialValidator(actualización parcial)
En Insert:
- Si módulo habilitado Y hay datos
membresiaen petición:- Validar con
MembresiaExtProductoValidator - Insertar extensión
- Validar con
En Update:
- Si módulo habilitado:
- Si vienen datos Y existe extensión →
partialUpdate() - Si vienen datos Y NO existe → validar completo +
insert() - Si NO vienen datos Y existe →
delete()
- Si vienen datos Y existe extensión →
Enriquecimiento en Consultas:
Si scope='max' y módulo habilitado:
- Controller/Service agregan campo
membresiaa cada producto - Uso de consulta bulk (
getByIds()) para evitar N+1 queries
Esquema de Base de Datos
Tabla: producto
Nivel Multi-Tenancy: EMPRESA, SUCURSAL
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
| numero | DECIMAL(6,0) | PRIMARY KEY | ID único del producto |
| codcom | VARCHAR(14) | Código comercial de barras | |
| denom | VARCHAR(50) | Nombre del producto | |
| denom1 | VARCHAR(400) | Descripción extendida del producto | |
| costo | DECIMAL(16,5) | Costo del producto | |
| rubro | DECIMAL(6,0) | FK → lrubro.rubro | Rubro del producto |
| linea | DECIMAL(6,0) | FK → linea.linea | Línea del producto |
| mstock | VARCHAR(1) | Marca de movimiento de stock ('S'/'N') | |
| stoact | DECIMAL(11,3) | Stock actual del producto | |
| punped | DECIMAL(16,5) | Punto de pedido (stock mínimo) | |
| ivacod | DECIMAL(2,0) | FK → ivaart.codcat | Categoría IVA del producto |
| refcon | VARCHAR(3) | FK → ref_con.codigo | Referencia contable |
| prove | DECIMAL(6,0) | FK → cpdprov.cnro | Proveedor predeterminado |
| aimi | DECIMAL(16,5) | Importe/porcentaje impuesto interno | |
| imintipo | VARCHAR(1) | Tipo impuesto interno ('P'=Porcentual, 'F'=Fijo) | |
| bonfija | DECIMAL(16,5) | DEFAULT 0 | Bonificación fija del producto |
| comven | DECIMAL(16,5) | Comisión de venta (porcentaje) | |
| porgan | DECIMAL(16,5) | Porcentaje de ganancia | |
| prefac | VARCHAR(1) | Marca manejo precios facturación ('S'/'N') | |
| activo | BOOLEAN | DEFAULT true | Producto activo |
| activo_web | BOOLEAN | DEFAULT false | Sincronizar con e-commerce |
| stock_web | INTEGER | Stock a mostrar en web | |
| ubicacion | INTEGER | FK → ubicaciones.id | Ubicación física del producto |
Campos Sin Uso (legacy):
- grupo, tipo, envase, kilosxpiez, precioxkil, iva, ptfa, tipfac, codagr, facpro, stopro, aiva, ticket, consigna, recfin, id_estado
Índices:
- PRIMARY KEY:
numero
Foreign Keys:
rubro→lrubro(rubro)ON DELETE RESTRICT ON UPDATE CASCADElinea→linea(linea)ON DELETE RESTRICT ON UPDATE CASCADEivacod→ivaart(codcat)ON DELETE RESTRICT ON UPDATE CASCADErefcon→ref_con(codigo)ON DELETE SET NULL ON UPDATE CASCADEprove→cpdprov(cnro)ON DELETE SET NULL ON UPDATE CASCADEubicacion→ubicaciones(id)ON DELETE RESTRICT ON UPDATE CASCADE
Tabla: precios
Nivel Multi-Tenancy: EMPRESA, SUCURSAL
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
| lista | VARCHAR(3) | NOT NULL | Código de lista de precios |
| numero | DECIMAL(6,0) | NOT NULL, FK → producto.numero | Código de producto |
| precio | DECIMAL(16,5) | Precio en esta lista | |
| tippre | VARCHAR(1) | Tipo de precio ('F'=Final, 'N'=Neto) | |
| prefin | DECIMAL(16,5) | Precio final (sin uso) |
Índices:
- INDEX:
fki_Articuloonnumero
Nota: No tiene PRIMARY KEY compuesta definida explícitamente. La combinación (lista, numero) funciona como clave natural.
Tabla: membresia_ext_producto (Módulo Membresías)
Nivel Multi-Tenancy: EMPRESA, SUCURSAL
Extensión para productos de membresía (solo si módulo habilitado).
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
| id_producto | INTEGER | PRIMARY KEY, FK → producto.numero | ID del producto |
| meses | INTEGER | NOT NULL | Duración de membresía en meses |
| ... | Otros campos específicos de membresía |
Capa de Datos
Producto Model
Archivo: models/modulo-venta/Producto.php
Hereda: Model (base model con validación y soft delete)
Responsabilidades:
- Acceso a datos de productos
- Queries con filtros complejos
- Paginación
- Hidratación de relaciones
- Generación de IDs
Métodos Principales:
getPaginated(array $options): array
Paginación avanzada con soporte de:
- Filtros globales y por columna
- Ordenamiento multi-columna
- Exclusión de IDs
- Join condicional con PRECIOS (si se especifica lista)
- Hidratación de relaciones según scope
getAll(array $options, string $scope, bool $activo): array
Consulta de todos los productos con filtros:
- Búsqueda por texto (nombre, ID)
- Filtros por rubro (individual o rango)
- Filtros por proveedor (individual o rango)
- Filtros por rango de IDs
- Exclusión de productos
- Límite de 10 para autocompletes
getOne(array $data, string $scope, bool $activo): ?ProductoDTO
Consulta individual por ID o código comercial.
Incluye:
- Hidratación completa de relaciones
- Listas de precios (todas o filtrada por lista)
- Ubicación con ruta jerárquica
insert(array $data): ?ProductoDTO
Inserción de producto:
- Validación estructural (
Model::validate()) - Verificación duplicado código comercial
- Auto-generación ID
- Valores por defecto (sincroniza_web, manejo_precios_facturacion)
update(array $data): ?ProductoDTO
Actualización de producto:
- Validación estructural
- Verificación duplicado código comercial (excluyendo mismo producto)
- Actualización de todos los campos
updateStockProducto(int $id, float $cantidad, bool $ingreso): bool
Actualiza stock actual sumando o restando cantidad.
updateCostoProducto(int $id, float $costo): bool
Actualiza costo del producto.
getByIds(array $ids, string $scope): array
Consulta bulk por múltiples IDs (para evitar N+1 queries).
getProductosSSR(array $data): array
Server-Side Rendering para DataTables con:
- Búsqueda por nombre, ID, código
- Ordenamiento dinámico
- Paginación
- Exclusión de IDs
Validaciones de Modelo
Reglas (definidas en Producto::$rules):
| Campo | Validación |
|---|---|
| id | required, integer |
| nombre | required, max:50 |
| codigo_comercial | required, max:14 |
| stock | integer |
| bonificacion | numeric |
| tipo_imp | max:1, in:['P', 'F'] |
| imp_interno | numeric |
| costo | numeric |
| descripcion | max:400 |
| rubro | required, integer |
| linea | required, integer |
| ref_con | max:3 |
| maneja_stock | required, max:1 |
| categoria_iva | required, integer |
| punto_pedido | integer |
| proveedor | integer |
| sincroniza_web | required, boolean |
| stock_web | integer |
| comision | numeric, max:100 |
| manejo_precios_facturacion | required, boolean |
| activo | boolean |
| porc_ganancia | numeric |
| ubicacion | numeric |
Scopes de Consulta
Definidos en Producto::$scopes:
| Scope | Campos SQL | Uso |
|---|---|---|
| min | id, nombre, codigo_comercial, activo | Selects, autocompletes |
| lista_precio | id, nombre, codigo_comercial | Gestión de listas |
| stock | id, nombre, stock, maneja_stock | Control inventario |
| facturacion | id, nombre, codigo_comercial, costo, stock, maneja_stock, bonificacion, impuestos, categoria_iva, comision | Proceso de facturación |
| max | Todos los campos disponibles | Formularios, detalle completo |
Hidratación de Relaciones
Método: hydrateProducto(array $producto, string $scope): array
Para scopes diferentes de 'min', hidrata:
categoria_iva→ DTO de CategoriaIvarubro→ DTO de Rubrolinea→ DTO de Linea (con contexto de rubro)ref_con→ DTO de RefContableproveedor→ DTO de Proveedorubicacion→ DTO de Ubicacion (con ruta jerárquica)
Además convierte tipos:
- Decimales a float (bonificacion, imp_interno, costo, comision, porc_ganancia)
- Enteros (stock, punto_pedido, stock_web)
Integración con Otros Módulos
Módulo Membresías (Opcional)
Condición: sistema.PermisosEmpresa.modulo_membresias = 1
Modelo: Membresia\Infrastructure\Persistence\Models\MembresiaExtProducto
Operaciones:
insert(array $data): Crear extensiónpartialUpdate(int $id, array $data): Actualizar parcialmentedelete(int $id): Eliminar extensióngetById(int $id): Obtener por ID productogetByIds(array $ids): Obtener múltiples (bulk)exists(int $id): Verificar existencia
Impacto en Performance:
- Enriquecimiento bulk evita N+1 queries
- Solo se carga si scope='max' y módulo habilitado
Módulo Stock
Método: Producto::updateStockProducto(int $id, float $cantidad, bool $ingreso)
Llamado desde otros módulos al registrar movimientos de stock:
- Compras (ingresos)
- Ventas (egresos)
- Ajustes de inventario
Módulo Facturación
Scope: 'facturacion'
Campos críticos para facturación:
costo: Cálculo de gananciacategoria_iva: Cálculo de impuestosbonificacion: Aplicación de descuentosimp_interno: Impuestos internosimintipo: Tipo de impuesto ('P'=%, 'F'=fijo)manejo_precios_facturacion: Control de precios especialescomision: Comisión del vendedor
Performance
Índices Implementados
Tabla producto:
- PRIMARY KEY:
numero(búsqueda por ID)
⚠️ ÍNDICES FALTANTES RECOMENDADOS:
codcom(búsquedas por código comercial frecuentes)activo(filtro común en todas las consultas)rubro, linea(filtros frecuentes en listados)
Tabla precios:
- INDEX:
fki_Articuloonnumero(JOIN con productos)
⚠️ ÍNDICE COMPUESTO RECOMENDADO:
(numero, lista)para búsquedas específicas de precio
Problema N+1 Queries
RESUELTO en consultas con scope='max':
Service/Controller: Uso de enrichProductosWithMembresia() que:
- Extrae todos los IDs de productos
- Realiza UNA consulta bulk:
MembresiaExtProducto::getByIds($productIds) - Mapea resultados a cada producto
RESUELTO en hidratación de relaciones:
Cada producto hidrata sus relaciones individualmente (rubro, linea, proveedor, etc.) pero esto se hace una sola vez en hydrateProductos() que procesa el array completo.
⚠️ POTENCIAL PROBLEMA: Si se consultan muchos productos con scope='max', cada uno ejecuta consultas individuales para rubro, linea, etc. Considerar implementar hidratación bulk para esas relaciones también.
Caching
NO IMPLEMENTADO
El sistema no utiliza caching de productos. Todas las consultas van directo a base de datos.
Recomendación: Implementar caching para:
- Productos activos con scope='min' (autocompletes)
- Listas de precios por producto (consultas frecuentes en facturación)
- Configuración de módulo membresías (se consulta en cada request)
Seguridad
Autenticación
Middleware: AuthMiddleware (en /backend/auth/JwtHandler.php)
Validación de token JWT antes de procesar request.
Sanitización de Entrada
Prepared Statements: ✅ Todas las queries utilizan placeholders
Ejemplo:
php
$sql = "SELECT * FROM producto WHERE numero = :id";
$stmt = $this->conn->prepare($sql);
$stmt->execute(['id' => $id]);Validación de Tipos: ✅ Conversión explícita de tipos antes de binding
php
$stmt->bindValue(':numero', $producto->id); // int
$stmt->bindValue(':activo_web', $producto->sincroniza_web, PDO::PARAM_BOOL);Validación de Duplicados
Verificación de codigo_comercial único antes de insertar/actualizar evita conflictos.
Transacciones
Operaciones CUD: ✅ Todas envueltas en transacciones
php
$this->conn->beginTransaction();
try {
// Múltiples operaciones
$this->conn->commit();
} catch (Exception $e) {
$this->conn->rollBack();
throw $e;
}⚠️ PROBLEMA: Generación de ID (getNewId()) fuera de transacción en insert().
Permisos
NO IMPLEMENTADO A NIVEL DE CÓDIGO
No hay verificación explícita de permisos de usuario en el código. La seguridad depende únicamente de autenticación JWT.
Recomendación: Agregar verificación de permisos por operación (crear, modificar, eliminar productos).
Auditoría
NO IMPLEMENTADO
No hay registro de auditoría de operaciones sobre productos.
Recomendación: Implementar AuditableInterface y usar Auditable trait para registrar:
- Quién creó/modificó productos
- Cuándo se realizaron cambios
- Qué datos cambiaron
Preguntas Técnicas Pendientes
Durante el análisis del código se identificaron los siguientes aspectos que requieren aclaración:
1. Concurrencia en Generación de ID
Observación: El método getNewId() usa MAX(NUMERO) + 1 y se llama FUERA de la transacción en ProductoController::insert().
php
// Línea 1061 - FUERA de transacción
$data['id'] = $this->model->getNewId();
$this->conn->beginTransaction(); // Transacción inicia DESPUÉSPregunta: ¿Han experimentado duplicados de ID en ambientes con alta concurrencia? ¿Debería moverse dentro de la transacción o migrar a secuencias PostgreSQL?
Impacto: Riesgo de duplicados si dos procesos llaman getNewId() simultáneamente antes de insertar.
2. Ausencia de Endpoint DELETE
Observación: No existe endpoint para eliminar productos. Solo se implementa soft delete a nivel de modelo pero no está expuesto en API.
Pregunta: ¿La eliminación de productos se hace desde otro lugar? ¿Debería implementarse soft delete vía API PUT actualizando campo activo?
Impacto: Afecta la documentación de operaciones CRUD disponibles.
3. Validador de Productos No Encontrado
Observación: No existe ProductoValidator en /Validators/Venta/. Las validaciones se hacen a nivel de modelo usando reglas de validación.
Pregunta: ¿Es intencional no tener validador middleware para productos? ¿Las validaciones de negocio adicionales se hacen solo en Controller?
Impacto: No hay validación estructural en middleware layer antes de llegar a Controller.
4. Arquitectura Legacy vs Nueva
Observación: Este recurso usa arquitectura legacy (/backend/producto.php) mientras otros recursos nuevos usan Slim RouteCollectorProxy.
Pregunta: ¿Está planificada la migración de productos a nueva arquitectura 5-layer DDD? ¿Hay restricciones que impiden la migración?
Impacto: Inconsistencia arquitectónica entre módulos.
5. PRIMARY KEY en Tabla precios
Observación: La tabla precios no tiene PRIMARY KEY explícita. La combinación (lista, numero) funciona como clave natural pero no está definida como constraint.
Pregunta: ¿Debería agregarse PRIMARY KEY (lista, numero) a la tabla? ¿Hay razón para no tenerla?
Impacto: Potencial duplicación de registros si no se controla a nivel aplicación.
6. Índices Faltantes
Observación: La tabla producto solo tiene índice en numero (PK). Consultas frecuentes por codcom y activo no tienen índices.
Pregunta: ¿Hay problemas de performance en consultas? ¿Deberían agregarse índices en codcom, activo, y compuestos en (rubro, linea)?
Impacto: Performance en consultas grandes con filtros.
7. Gestión de Stock Web
Observación: Campos activo_web y stock_web para sincronización con e-commerce, pero no se ve integración con sistema externo.
Pregunta: ¿Cómo se sincronizan estos datos con la web? ¿Hay proceso batch o webhook? ¿Dónde está implementado?
Impacto: Documentación de integraciones externas.
8. Campo prefac (Manejo Precios Facturación)
Observación: Campo prefac se mapea a manejo_precios_facturacion con conversión 'S'/'N' a boolean.
Pregunta: ¿Qué efecto tiene este campo en el proceso de facturación? ¿Permite precios especiales fuera de listas?
Impacto: Documentación de reglas de negocio en facturación.
9. Limpieza de Ubicaciones
Observación: Después de update, se llama UbicacionService::eliminarNoUsadas() para limpiar ubicaciones huérfanas.
Pregunta: ¿Este proceso puede fallar si otra tabla referencia la ubicación? ¿Hay constraint de integridad referencial?
Impacto: Potencial error en transacción de update.
10. Módulo Membresías - Configuración por Empresa
Observación: La habilitación del módulo se lee de sistema.PermisosEmpresa en cada request.
Pregunta: ¿Esta configuración se cachea en algún lugar? ¿Es la misma para todos los schemas (sucursales/cajas)?
Impacto: Performance al consultar config en cada operación.
Referencias
- Requisitos de Negocio (pendiente de creación)
- Documentación Frontend (pendiente de creación)
- Arquitectura Backend
- Skill: bautista-backend-architecture
⚠️ NOTA IMPORTANTE: Esta documentación fue generada automáticamente analizando el código implementado. Validar cambios futuros contra este baseline. Las "Preguntas Técnicas Pendientes" requieren respuestas de stakeholders técnicos para completar la documentación.