Skip to content

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ámetroTipoRequeridoDescripción
scopestringNoNivel de detalle: 'min', 'max', 'lista_precio', 'stock', 'facturacion' (default: 'min')
activobooleanNoFiltrar por estado activo (default: true)
pageIndexintNoÍndice de página para paginación
pageSizeintNoTamaño de página para paginación
serverSidebooleanNoActivar modo Server-Side Rendering de DataTables
idintNoBuscar por ID de producto
codigo_comercialstringNoBuscar por código comercial
filterstringNoBúsqueda global por nombre, ID o código
excludearrayNoIDs de productos a excluir
columnFilterarrayNoFiltros por columna específica
orderarrayNoOrdenamiento personalizado
listaintNoID 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=min

Respuesta:

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]=cafe

3. Búsqueda por ID o Código

http
GET /backend/producto?id=123&scope=max
GET /backend/producto?codigo_comercial=7891234567890

4. Listado simple

http
GET /backend/producto?scope=min&activo=true

Response 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:

ScopeCampos IncluidosUso
minid, nombre, codigo_comercial, activoAutocompletes, selects
lista_precioid, nombre, codigo_comercialListas de precios
stockid, nombre, stock, maneja_stockControl de inventario
facturacionid, nombre, codigo_comercial, costo, stock, maneja_stock, bonificacion, impuestos, categoria_iva, comisionFacturación
maxTodos los campos + relaciones hidratadasFormularios, detalle

Status Codes:

  • 200 OK - Consulta exitosa
  • 400 Bad Request - Parámetros inválidos
  • 401 Unauthorized - No autenticado
  • 500 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):

  1. Iniciar transacción
  2. Procesar ubicación (crear si es nueva o usar existente)
  3. Transformar datos:
    • rubro.idrubro
    • linea.idlinea
    • categoria_iva.codigocategoria_iva
    • ref_con.codigoref_con
  4. Insertar producto (Model.insert())
  5. Insertar listas de precios (ListaPrecioService)
  6. Insertar extensión membresía (si datos presentes y módulo habilitado)
  7. Commit transacción

Response:

json
{
  "status": 201,
  "message": "Datos recibidos correctamente.",
  "data": {
    "id": 456
  }
}

Status Codes:

  • 201 Created - Producto creado exitosamente
  • 400 Bad Request - Validación estructural fallida
  • 405 Method Not Allowed - Código comercial duplicado
  • 500 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):

  1. Iniciar transacción
  2. Procesar ubicación
  3. Actualizar producto (Model.update())
  4. 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
  5. 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
  6. Limpiar ubicaciones no usadas
  7. Commit transacción

Response:

json
{
  "status": 204,
  "message": "Datos recibidos correctamente."
}

Status Codes:

  • 204 No Content - Actualización exitosa
  • 400 Bad Request - Datos inválidos
  • 405 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:

  1. Obtener listas registradas actuales
  2. Recorrer listas de petición:
    • Si lista existe → UPDATE
    • Si lista NO existe → INSERT
  3. Calcular diferencia (listas registradas - listas petición)
  4. 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 membresia en petición:
    • Validar con MembresiaExtProductoValidator
    • Insertar extensión

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()

Enriquecimiento en Consultas:

Si scope='max' y módulo habilitado:

  • Controller/Service agregan campo membresia a cada producto
  • Uso de consulta bulk (getByIds()) para evitar N+1 queries

Esquema de Base de Datos

Tabla: producto

Nivel Multi-Tenancy: EMPRESA, SUCURSAL

CampoTipoConstraintsDescripción
numeroDECIMAL(6,0)PRIMARY KEYID único del producto
codcomVARCHAR(14)Código comercial de barras
denomVARCHAR(50)Nombre del producto
denom1VARCHAR(400)Descripción extendida del producto
costoDECIMAL(16,5)Costo del producto
rubroDECIMAL(6,0)FK → lrubro.rubroRubro del producto
lineaDECIMAL(6,0)FK → linea.lineaLínea del producto
mstockVARCHAR(1)Marca de movimiento de stock ('S'/'N')
stoactDECIMAL(11,3)Stock actual del producto
punpedDECIMAL(16,5)Punto de pedido (stock mínimo)
ivacodDECIMAL(2,0)FK → ivaart.codcatCategoría IVA del producto
refconVARCHAR(3)FK → ref_con.codigoReferencia contable
proveDECIMAL(6,0)FK → cpdprov.cnroProveedor predeterminado
aimiDECIMAL(16,5)Importe/porcentaje impuesto interno
imintipoVARCHAR(1)Tipo impuesto interno ('P'=Porcentual, 'F'=Fijo)
bonfijaDECIMAL(16,5)DEFAULT 0Bonificación fija del producto
comvenDECIMAL(16,5)Comisión de venta (porcentaje)
porganDECIMAL(16,5)Porcentaje de ganancia
prefacVARCHAR(1)Marca manejo precios facturación ('S'/'N')
activoBOOLEANDEFAULT trueProducto activo
activo_webBOOLEANDEFAULT falseSincronizar con e-commerce
stock_webINTEGERStock a mostrar en web
ubicacionINTEGERFK → ubicaciones.idUbicació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:

  • rubrolrubro(rubro) ON DELETE RESTRICT ON UPDATE CASCADE
  • linealinea(linea) ON DELETE RESTRICT ON UPDATE CASCADE
  • ivacodivaart(codcat) ON DELETE RESTRICT ON UPDATE CASCADE
  • refconref_con(codigo) ON DELETE SET NULL ON UPDATE CASCADE
  • provecpdprov(cnro) ON DELETE SET NULL ON UPDATE CASCADE
  • ubicacionubicaciones(id) ON DELETE RESTRICT ON UPDATE CASCADE

Tabla: precios

Nivel Multi-Tenancy: EMPRESA, SUCURSAL

CampoTipoConstraintsDescripción
listaVARCHAR(3)NOT NULLCódigo de lista de precios
numeroDECIMAL(6,0)NOT NULL, FK → producto.numeroCódigo de producto
precioDECIMAL(16,5)Precio en esta lista
tippreVARCHAR(1)Tipo de precio ('F'=Final, 'N'=Neto)
prefinDECIMAL(16,5)Precio final (sin uso)

Índices:

  • INDEX: fki_Articulo on numero

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).

CampoTipoConstraintsDescripción
id_productoINTEGERPRIMARY KEY, FK → producto.numeroID del producto
mesesINTEGERNOT NULLDuració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):

CampoValidación
idrequired, integer
nombrerequired, max:50
codigo_comercialrequired, max:14
stockinteger
bonificacionnumeric
tipo_impmax:1, in:['P', 'F']
imp_internonumeric
costonumeric
descripcionmax:400
rubrorequired, integer
linearequired, integer
ref_conmax:3
maneja_stockrequired, max:1
categoria_ivarequired, integer
punto_pedidointeger
proveedorinteger
sincroniza_webrequired, boolean
stock_webinteger
comisionnumeric, max:100
manejo_precios_facturacionrequired, boolean
activoboolean
porc_ganancianumeric
ubicacionnumeric

Scopes de Consulta

Definidos en Producto::$scopes:

ScopeCampos SQLUso
minid, nombre, codigo_comercial, activoSelects, autocompletes
lista_precioid, nombre, codigo_comercialGestión de listas
stockid, nombre, stock, maneja_stockControl inventario
facturacionid, nombre, codigo_comercial, costo, stock, maneja_stock, bonificacion, impuestos, categoria_iva, comisionProceso de facturación
maxTodos los campos disponiblesFormularios, detalle completo

Hidratación de Relaciones

Método: hydrateProducto(array $producto, string $scope): array

Para scopes diferentes de 'min', hidrata:

  • categoria_iva → DTO de CategoriaIva
  • rubro → DTO de Rubro
  • linea → DTO de Linea (con contexto de rubro)
  • ref_con → DTO de RefContable
  • proveedor → DTO de Proveedor
  • ubicacion → 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ón
  • partialUpdate(int $id, array $data): Actualizar parcialmente
  • delete(int $id): Eliminar extensión
  • getById(int $id): Obtener por ID producto
  • getByIds(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 ganancia
  • categoria_iva: Cálculo de impuestos
  • bonificacion: Aplicación de descuentos
  • imp_interno: Impuestos internos
  • imintipo: Tipo de impuesto ('P'=%, 'F'=fijo)
  • manejo_precios_facturacion: Control de precios especiales
  • comision: 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_Articulo on numero (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:

  1. Extrae todos los IDs de productos
  2. Realiza UNA consulta bulk: MembresiaExtProducto::getByIds($productIds)
  3. 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ÉS

Pregunta: ¿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


⚠️ 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.