Appearance
Lista de Precios - Automatico por Rango - Documentacion Tecnica Backend
DOCUMENTACION RETROSPECTIVA - Generada a partir de codigo implementado el 2026-02-11
Modulo: Ventas Feature: Lista de Precios - Automatico por Rango Fecha: 2026-02-11
Referencia de Negocio
Arquitectura Implementada
Archivos del Backend
API Layer:
- Route:
Routes/Venta/ListaPrecioRoute.php - Controller:
controller/modulo-venta/ListaPrecioController.php
Service Layer:
- Service:
service/Venta/ListaPrecioService.php
Model Layer:
- Model:
models/modulo-venta/ListaPrecio.php
Resources/DTOs:
- DTO:
Resources/Venta/ListaPrecio.php - Enum:
Resources/Venta/Enums/TipoPrecio.php
Database:
- Migration:
migrations/tenancy/20240823200743_new_table_precios.php - Tabla:
precios
Flujo de la Peticion
Request → Route (sin Validator) → Controller → Service → Model → DatabaseCaracteristicas del flujo:
- Sin validacion estructural: No hay validator middleware aplicado en las rutas
- Validacion en DTO: La validacion estructural se realiza en el constructor del DTO
ListaPrecio - Validacion de negocio en Service: El service valida datos requeridos segun el metodo
- Transacciones en Service: El service maneja transacciones para operaciones atomicas
API Endpoints
GET /api/mod-ventas/lista-precio
Descripcion: Obtiene listas de precios con filtros opcionales
Responsabilidades:
- Recibir parametros de filtrado via body
- Invocar al service para consultar precios
- Retornar array de precios
Request Body (opcional):
json
{
"producto": 123, // int: Filtrar por producto especifico
"producto": [100, 200], // array: Rango de productos
"lista": 1 // int: Filtrar por lista especifica
}Response DTO:
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": [
{
"lista": 1,
"precio": 150.50,
"tipo_precio": "N",
"id_producto": 123
}
]
}Status codes:
200: Consulta exitosa (puede retornar array vacio)
POST /api/mod-ventas/lista-precio
Descripcion: Operacion multi-proposito para insertar precio individual o generar listas automaticas
Responsabilidades:
- Determinar metodo de operacion segun campo
methoden body - Para
por_rango: Generar lista por porcentaje de variacion sobre lista origen - Para
ganancia-margen: Generar lista por margen de ganancia sobre costo - Para operacion default: Insertar precio individual
- Validar datos requeridos segun metodo
- Ejecutar transacciones atomicas para generaciones masivas
- Retornar resultado de la operacion
Request DTO - Metodo "por_rango":
json
{
"method": "por_rango",
"origen": 1,
"destino": 2,
"listas": [
{
"id": 123,
"precio": 180.75
},
{
"id": 124,
"precio": 220.00
}
]
}Request DTO - Metodo "ganancia-margen":
json
{
"method": "ganancia-margen",
"data": {
"agrupacionDesde": 1,
"agrupacionHasta": 10,
"porcentaje": 30.0,
"precioFinal": true,
"lista": 2
}
}Request DTO - Insercion individual:
json
{
"lista": 1,
"precio": 150.50,
"tipo_precio": "N",
"id_producto": 123
}Response DTO:
json
{
"status": 201,
"message": "Datos recibidos correctamente.",
"data": [
{
"lista": 2,
"precio": 180.75,
"tipo_precio": "N",
"id_producto": 123
}
]
}Status codes:
201: Creacion/actualizacion exitosa400: Faltan datos requeridos (BadRequest)500: Error en transaccion o insercion
PUT /api/mod-ventas/lista-precio
Descripcion: Actualiza un precio existente en una lista
Responsabilidades:
- Validar estructura del DTO
- Invocar service para actualizar
- Retornar confirmacion
Request DTO:
json
{
"lista": 1,
"precio": 160.00,
"tipo_precio": "F",
"id_producto": 123
}Response DTO:
json
{
"status": 200,
"message": "Datos recibidos correctamente."
}Status codes:
200: Actualizacion exitosa400: Datos invalidos
DELETE /api/mod-ventas/lista-precio
Descripcion: Elimina un precio de una lista (delete fisico, no soft delete)
Responsabilidades:
- Validar que se proporcionen
productoylista - Invocar service para eliminar
- Retornar confirmacion
Request Body:
json
{
"producto": 123,
"lista": 1
}Response DTO:
json
{
"status": 200,
"message": "Datos recibidos correctamente."
}Status codes:
200: Eliminacion exitosa400: Faltan datos requeridos (producto o lista)
Capa de Servicio
ListaPrecioService
Archivo: service/Venta/ListaPrecioService.php
Constructor:
php
public function __construct(PDO $conn)Dependencias:
PDO $conn: Conexion a base de datosListaPrecio(Model): Modelo para acceso a datosProductoController: Para obtener productos por rubro (usado en metodo ganancia-margen)
Metodos publicos:
getAll(array $options): array
Proposito: Obtener listas de precios con filtros opcionales
Parametros:
$options['producto']: int o array de int (producto especifico o rango)$options['lista']: int (lista especifica)
Retorna: Array de ListaPrecioDTO
Logica:
- Delega al modelo la consulta con filtros
generarListaPrecioPorRango(int $origen, int $destino, array $listas): array
Proposito: Generar lista de precios destino a partir de lista origen con precios calculados
Parametros:
$origen: ID de lista de precios de origen$destino: ID de lista de precios destino$listas: Array de objetos['id' => producto_id, 'precio' => precio_calculado]
Retorna: Array de ListaPrecioDTO insertados/actualizados
Logica de negocio:
- Inicia transaccion
- Para cada item en
$listas:- Consulta si el producto ya existe en lista destino
- Consulta el
tipo_preciode la lista origen - Crea DTO con precio calculado y tipo heredado
- Si existe en destino: actualiza precio
- Si no existe: inserta nuevo precio
- Commit si todo exitoso, rollback si falla
Transaccionalidad: Si Atomicidad: Todas las operaciones se revierten si falla una
generarListaMargenGanancia($agrupacion_desde, $agrupacion_hasta, $porcentaje, $calcula_precio_final, $lista): bool
Proposito: Generar lista de precios calculando desde costo del producto con porcentaje de ganancia
Parametros:
$agrupacion_desde: ID de rubro inicial$agrupacion_hasta: ID de rubro final$porcentaje: Porcentaje de ganancia (nullable, si null usa porcentaje individual del producto)$calcula_precio_final: bool, si true calcula precio con IVA e impuestos$lista: ID de lista destino
Retorna: bool (true si al menos un producto fue procesado, false si ninguno)
Logica de negocio:
- Obtiene todos los productos del rango de rubros via
ProductoController::getAll() - Para cada producto:
- Omite si
costo <= 0 - Omite si no hay porcentaje global y el producto no tiene
porc_gananciadefinido - Calcula
precio_base = costo + costo * (porcentaje / 100) - Si
$calcula_precio_final == true:- Usa clase
Itemdel dominio (Domain\Ventas\Facturacion\Item) - Agrega IVA como ajuste si producto tiene
categoria_iva - Agrega impuesto interno si producto tiene
imp_interno - Calcula precio final con impuestos
- Marca
tipo_precio = "F"
- Usa clase
- Si
$calcula_precio_final == false:- Usa
precio_basedirectamente - Marca
tipo_precio = "N"
- Usa
- Inserta o actualiza en lista destino
- Omite si
- Retorna
truesi al menos un producto fue procesado,falseen caso contrario
Transaccionalidad: No (sin transaccion explicita) Uso de Domain Layer: Si, utiliza Item, Ajuste, enums TipoAjuste, TipoValor, TipoPrecio
insert(ListaPrecioDTO $data): ?ListaPrecioDTO
Proposito: Insertar precio individual
Retorna: DTO insertado o null si falla
Logica: Delega al modelo
update(ListaPrecioDTO $data): ?ListaPrecioDTO
Proposito: Actualizar precio individual
Retorna: DTO actualizado o null si falla
Logica: Delega al modelo
delete(int $producto, int $lista): bool
Proposito: Eliminar precio de una lista
Retorna: bool (exito/fallo)
Logica: Delega al modelo
Capa de Modelo (Data Access)
ListaPrecio Model
Archivo: models/modulo-venta/ListaPrecio.php
Tabla: precios
Constructor:
php
public function __construct(PDO $conn)Metodos:
getAll(array $options): array
SQL dinamico:
sql
SELECT lista::int, precio, tippre as tipo_precio
FROM precios
WHERE {clausulas dinamicas}Filtros soportados:
numero = :numero(producto especifico)numero BETWEEN :desde AND :hasta(rango de productos)lista = :lista(lista especifica)
Mapeo: Array de ListaPrecioDTO
insert(ListaPrecioDTO $data): ?ListaPrecioDTO
SQL:
sql
INSERT INTO precios (lista, numero, precio, tippre)
VALUES(:lista, :numero, :precio, :tipo_precio)Mapeo: Retorna el mismo DTO si exitoso, null si falla
update(ListaPrecioDTO $data): ?ListaPrecioDTO
SQL:
sql
UPDATE precios
SET precio = :precio, tippre = :tipo_precio
WHERE lista = :lista AND numero = :numeroMapeo: Retorna el mismo DTO si exitoso, null si falla
delete(int $producto, int $lista): bool
SQL:
sql
DELETE FROM precios
WHERE lista = :lista AND numero = :numeroTipo de delete: Fisico (no soft delete)
getByProductosYLista(array $productosIds, int $listaId): array
Proposito: Batch loading para evitar N+1 queries
SQL:
sql
SELECT numero::int as producto, lista::int, precio, tippre as tipo_precio
FROM precios
WHERE numero = ANY(:productos) AND lista = :listaPostgreSQL feature: Usa sintaxis ANY() con array PostgreSQL
Mapeo: Array indexado por id_producto para acceso O(1)
Uso: Optimizacion de performance cuando se necesitan precios de multiples productos
Esquema de Base de Datos
Tabla: precios
Nivel de Tenancy: EMPRESA y SUCURSAL (configurable)
Migracion: 20240823200743_new_table_precios.php
Tipo de Migracion: BASE
Esquema SQL:
sql
CREATE TABLE precios (
lista VARCHAR(3) NOT NULL,
numero DECIMAL(6,0) NOT NULL,
precio DECIMAL(16,5),
tippre VARCHAR(1),
prefin DECIMAL(16,5)
);
CREATE INDEX fki_Articulo ON precios (numero);Columnas:
| Campo | Tipo | Constraints | Descripcion |
|---|---|---|---|
| lista | VARCHAR(3) | NOT NULL | ID de la lista de precios |
| numero | DECIMAL(6,0) | NOT NULL | Codigo de producto (FK logica a producto.numero) |
| precio | DECIMAL(16,5) | NULL | Precio del producto en esta lista |
| tippre | VARCHAR(1) | NULL | Tipo de precio: 'N' (Neto) o 'F' (Final) |
| prefin | DECIMAL(16,5) | NULL | Campo sin uso actual |
Indices:
fki_Articuloen columnanumero(para joins con tabla producto)
Clave Primaria: Ninguna definida (ver preguntas pendientes)
Foreign Keys: Ninguna definida explicitamente (FK logicas)
Condicion de ejecucion:
- Se crea solo si el modulo Ventas o CRM esta habilitado
- Se crea solo si la tabla
productoexiste previamente
Niveles configurables:
- Por defecto:
LEVEL_EMPRESAyLEVEL_SUCURSAL - Puede configurarse dinamicamente por empresa via
configuracion_niveles_tablas
Recursos/DTOs
ListaPrecio DTO
Archivo: Resources/Venta/ListaPrecio.php
Proposito: Data Transfer Object para precios de lista con validacion integrada
Propiedades:
php
public float $precio;
public string $tipo_precio;
public ?int $id_producto;
public int $lista;Constructor:
php
public function __construct(
$lista,
$precio,
$id_producto = null,
$tipo_precio = null
)Validaciones integradas:
| Campo | Reglas | Descripcion |
|---|---|---|
| precio | required, numeric | Debe ser numerico y requerido |
| tipo_precio | required, max:1, enum | Debe ser 'N' o 'F' (validado contra TipoPrecio enum) |
| lista | required, integer | Debe ser entero y requerido |
| id_producto | integer | Debe ser entero si se proporciona |
Valor por defecto: tipo_precio es TipoPrecio::NETO->value ('N') si no se proporciona
Metodos heredados (de DTO):
fromArray(array $data): self: Instanciar desde arraytoArray(): array: Convertir a arrayvalidate(array $data, array $rules): Validador interno
TipoPrecio Enum
Archivo: Resources/Venta/Enums/TipoPrecio.php
Tipo: Backed enum (string)
Valores:
| Case | Value | Descripcion |
|---|---|---|
| NETO | 'N' | Precio neto sin impuestos |
| FINAL | 'F' | Precio final con impuestos incluidos |
Uso: Validacion de tipo de precio en DTO y asignacion de tipo en calculos
Validaciones
Validacion Estructural
Ubicacion: Constructor de ListaPrecio DTO
Responsabilidad: Validar tipos de datos, formatos, valores permitidos
Reglas aplicadas:
precio: numerico, requeridotipo_precio: requerido, max 1 caracter, debe ser 'N' o 'F'lista: entero, requeridoid_producto: entero si se proporciona
Excepcion: Lanza excepcion de validacion si falla
Validacion de Negocio
Ubicacion: ListaPrecioService y ListaPrecioController
Responsabilidad: Validar condiciones de negocio segun metodo
Validaciones por metodo:
Metodo "por_rango":
- Valida que existan campos
destino,origen,listas - Lanza
BadRequestsi faltan datos
Metodo "ganancia-margen":
- Valida que al menos un producto sea procesado
- Lanza
Exceptioncon codigo 201 si ningun producto cumple condiciones - No valida rangos o porcentajes (confia en frontend)
DELETE:
- Valida que existan
productoylistaen body - Lanza
BadRequestsi faltan
Integracion con Otros Modulos
Dependencia de Producto
Relacion: Tabla precios tiene FK logica a producto.numero
Uso:
- Metodo
generarListaMargenGananciaobtiene productos viaProductoController::getAll() - Consulta datos:
costo,porc_ganancia,categoria_iva,imp_interno,tipo_imp
Acoplamiento: ListaPrecioService depende de ProductoController (no ideal, deberia ser service)
Uso de Domain Layer
Clases utilizadas:
Domain\Ventas\Facturacion\Item: Calculo de precio final con impuestosDomain\Ventas\Facturacion\Ajuste: Representacion de ajustes (IVA, impuestos internos)Resources\Venta\Enums\TipoAjuste: Enum para tipos de ajusteResources\Venta\Enums\TipoValor: Enum para tipo de valor (porcentaje o fijo)
Contexto de uso: Solo en generarListaMargenGanancia cuando $calcula_precio_final == true
Logica aplicada:
- Instanciar
Itemcon precio base y tipo neto - Agregar ajuste de IVA si producto tiene categoria_iva
- Agregar ajuste de impuesto interno si producto tiene imp_interno
- Invocar
calculate()para obtener precio final
Estrategia de Testing
Testing de Unidad
Componentes a probar:
ListaPrecio Model:
getAll()con diferentes combinaciones de filtrosinsert()con datos validosupdate()con registro existentedelete()con registro existentegetByProductosYLista()para batch loading
ListaPrecioService:
generarListaPrecioPorRango()con productos existentes/no existentes en destinogenerarListaMargenGanancia()con productos con/sin costo, con/sin porcentaje- Validar atomicidad de transaccion en
generarListaPrecioPorRango() - Validar calculo de precio final con impuestos
ListaPrecio DTO:
- Validacion de tipo_precio contra enum
- Valor por defecto de tipo_precio
- Validacion de precio numerico
Mocks necesarios:
PDO: Para aislar tests de base de datosProductoController: Para evitar dependencia en tests de service
Testing de Integracion
Escenarios a probar:
Generacion por rango:
- Generar lista destino vacia desde lista origen con precios
- Actualizar lista destino existente con nuevos precios
- Transaccion rollback si falla una insercion
- Herencia correcta de tipo_precio de lista origen
Generacion por margen de ganancia:
- Calcular precio neto desde costo con porcentaje global
- Calcular precio neto desde costo con porcentaje individual del producto
- Calcular precio final incluyendo IVA e impuestos internos
- Omitir productos sin costo o sin porcentaje de ganancia
- Retornar false si ningun producto fue procesado
CRUD individual:
- Insertar precio nuevo
- Actualizar precio existente
- Eliminar precio existente
- Consultar con filtros combinados
Database setup necesario:
- Tabla
preciosvacia o con datos de prueba - Tabla
productocon productos de prueba (para generacion por margen) - Datos de categoria_iva e imp_interno para tests de precio final
Consideraciones de Performance
Problema N+1 Queries
Problema identificado: En generarListaPrecioPorRango(), se ejecuta una consulta por producto:
php
foreach ($listas as $lista) {
$listasRegistradas = $this->getAll(['lista' => $destino, 'producto' => $id]);
$listaOrigen = $this->getAll(['lista' => $origen, 'producto' => $id]);
// ...
}Impacto: Si hay 100 productos, se ejecutan 200 consultas SELECT
Solucion implementada: El modelo tiene metodo getByProductosYLista() que usa ANY() para batch loading, pero no se usa en el service
Recomendacion: Refactorizar generarListaPrecioPorRango() para usar batch loading
Indices
Indice existente: fki_Articulo en columna numero
Uso: Acelera joins con tabla producto y consultas filtradas por producto
Indices faltantes:
- Indice en columna
lista(filtro frecuente) - Indice compuesto
(lista, numero)para optimizar consultas combinadas
Transacciones
Uso actual:
generarListaPrecioPorRango(): Usa transaccion explicita (begin/commit/rollback)generarListaMargenGanancia(): No usa transaccion (vulnerabilidad de consistencia)
Recomendacion: Agregar transaccion en generarListaMargenGanancia() para atomicidad
Consideraciones de Seguridad
Validacion de Input
Fortalezas:
- DTO valida tipos de datos y enum de
tipo_precio - Uso de prepared statements en todas las consultas SQL
Debilidades:
- No valida rangos de precios (negativos, extremadamente altos)
- No valida existencia de lista origen/destino antes de generar
- No valida permisos a nivel de endpoint (depende de middleware externo)
SQL Injection
Estado: Protegido via prepared statements en todo el codigo
Ejemplo correcto:
php
$stmt = $this->conn->prepare("SELECT * FROM precios WHERE lista = :lista");
$stmt->execute(['lista' => $lista]);Auditoria
Estado actual: No implementada
Operaciones sin auditar:
- Generacion de listas de precios por rango
- Generacion de listas por margen de ganancia
- Modificaciones individuales de precios
- Eliminacion de precios
Recomendacion: Implementar AuditableInterface y Auditable trait en ListaPrecioService
Preguntas Tecnicas Pendientes
Aclaraciones Requeridas: Hay aspectos tecnicos que requieren validacion.
Ver: Preguntas sobre Lista de Precios Rango
Preguntas tecnicas identificadas:
- Ausencia de validador middleware en rutas
- Calculo de precios en frontend vs backend
- Ausencia de clave primaria en tabla
precios - Vulnerabilidad N+1 queries en
generarListaPrecioPorRango - Falta de transaccion en
generarListaMargenGanancia - Falta de auditoria en operaciones criticas
- Acoplamiento de
ListaPrecioServiceconProductoController
Referencias
NOTA IMPORTANTE: Esta documentacion fue generada automaticamente analizando el codigo implementado. Validar cambios futuros contra este baseline.