Skip to content

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

Caracteristicas del flujo:

  1. Sin validacion estructural: No hay validator middleware aplicado en las rutas
  2. Validacion en DTO: La validacion estructural se realiza en el constructor del DTO ListaPrecio
  3. Validacion de negocio en Service: El service valida datos requeridos segun el metodo
  4. 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 method en 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 exitosa
  • 400: 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 exitosa
  • 400: 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 producto y lista
  • 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 exitosa
  • 400: 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 datos
  • ListaPrecio (Model): Modelo para acceso a datos
  • ProductoController: 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:

  1. Inicia transaccion
  2. Para cada item en $listas:
    • Consulta si el producto ya existe en lista destino
    • Consulta el tipo_precio de la lista origen
    • Crea DTO con precio calculado y tipo heredado
    • Si existe en destino: actualiza precio
    • Si no existe: inserta nuevo precio
  3. 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:

  1. Obtiene todos los productos del rango de rubros via ProductoController::getAll()
  2. Para cada producto:
    • Omite si costo <= 0
    • Omite si no hay porcentaje global y el producto no tiene porc_ganancia definido
    • Calcula precio_base = costo + costo * (porcentaje / 100)
    • Si $calcula_precio_final == true:
      • Usa clase Item del 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"
    • Si $calcula_precio_final == false:
      • Usa precio_base directamente
      • Marca tipo_precio = "N"
    • Inserta o actualiza en lista destino
  3. Retorna true si al menos un producto fue procesado, false en 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 = :numero

Mapeo: 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 = :numero

Tipo 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 = :lista

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

CampoTipoConstraintsDescripcion
listaVARCHAR(3)NOT NULLID de la lista de precios
numeroDECIMAL(6,0)NOT NULLCodigo de producto (FK logica a producto.numero)
precioDECIMAL(16,5)NULLPrecio del producto en esta lista
tippreVARCHAR(1)NULLTipo de precio: 'N' (Neto) o 'F' (Final)
prefinDECIMAL(16,5)NULLCampo sin uso actual

Indices:

  • fki_Articulo en columna numero (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 producto existe previamente

Niveles configurables:

  • Por defecto: LEVEL_EMPRESA y LEVEL_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:

CampoReglasDescripcion
preciorequired, numericDebe ser numerico y requerido
tipo_preciorequired, max:1, enumDebe ser 'N' o 'F' (validado contra TipoPrecio enum)
listarequired, integerDebe ser entero y requerido
id_productointegerDebe 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 array
  • toArray(): array: Convertir a array
  • validate(array $data, array $rules): Validador interno

TipoPrecio Enum

Archivo: Resources/Venta/Enums/TipoPrecio.php

Tipo: Backed enum (string)

Valores:

CaseValueDescripcion
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, requerido
  • tipo_precio: requerido, max 1 caracter, debe ser 'N' o 'F'
  • lista: entero, requerido
  • id_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 BadRequest si faltan datos

Metodo "ganancia-margen":

  • Valida que al menos un producto sea procesado
  • Lanza Exception con codigo 201 si ningun producto cumple condiciones
  • No valida rangos o porcentajes (confia en frontend)

DELETE:

  • Valida que existan producto y lista en body
  • Lanza BadRequest si faltan

Integracion con Otros Modulos

Dependencia de Producto

Relacion: Tabla precios tiene FK logica a producto.numero

Uso:

  • Metodo generarListaMargenGanancia obtiene productos via ProductoController::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 impuestos
  • Domain\Ventas\Facturacion\Ajuste: Representacion de ajustes (IVA, impuestos internos)
  • Resources\Venta\Enums\TipoAjuste: Enum para tipos de ajuste
  • Resources\Venta\Enums\TipoValor: Enum para tipo de valor (porcentaje o fijo)

Contexto de uso: Solo en generarListaMargenGanancia cuando $calcula_precio_final == true

Logica aplicada:

  1. Instanciar Item con precio base y tipo neto
  2. Agregar ajuste de IVA si producto tiene categoria_iva
  3. Agregar ajuste de impuesto interno si producto tiene imp_interno
  4. Invocar calculate() para obtener precio final

Estrategia de Testing

Testing de Unidad

Componentes a probar:

ListaPrecio Model:

  • getAll() con diferentes combinaciones de filtros
  • insert() con datos validos
  • update() con registro existente
  • delete() con registro existente
  • getByProductosYLista() para batch loading

ListaPrecioService:

  • generarListaPrecioPorRango() con productos existentes/no existentes en destino
  • generarListaMargenGanancia() 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 datos
  • ProductoController: Para evitar dependencia en tests de service

Testing de Integracion

Escenarios a probar:

Generacion por rango:

  1. Generar lista destino vacia desde lista origen con precios
  2. Actualizar lista destino existente con nuevos precios
  3. Transaccion rollback si falla una insercion
  4. Herencia correcta de tipo_precio de lista origen

Generacion por margen de ganancia:

  1. Calcular precio neto desde costo con porcentaje global
  2. Calcular precio neto desde costo con porcentaje individual del producto
  3. Calcular precio final incluyendo IVA e impuestos internos
  4. Omitir productos sin costo o sin porcentaje de ganancia
  5. Retornar false si ningun producto fue procesado

CRUD individual:

  1. Insertar precio nuevo
  2. Actualizar precio existente
  3. Eliminar precio existente
  4. Consultar con filtros combinados

Database setup necesario:

  • Tabla precios vacia o con datos de prueba
  • Tabla producto con 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:

  1. Ausencia de validador middleware en rutas
  2. Calculo de precios en frontend vs backend
  3. Ausencia de clave primaria en tabla precios
  4. Vulnerabilidad N+1 queries en generarListaPrecioPorRango
  5. Falta de transaccion en generarListaMargenGanancia
  6. Falta de auditoria en operaciones criticas
  7. Acoplamiento de ListaPrecioService con ProductoController

Referencias


NOTA IMPORTANTE: Esta documentacion fue generada automaticamente analizando el codigo implementado. Validar cambios futuros contra este baseline.