Skip to content

Comprobantes Pendientes - Documentación Técnica Backend

Módulo: ventas Feature: Comprobantes Pendientes (Pedidos, Remitos, Presupuestos) Fecha: 2026-02-11

⚠️ DOCUMENTACIÓN RETROSPECTIVA - Generada a partir de código implementado el 2026-02-11


Referencia a Documentación de Negocio


Arquitectura Implementada

Patrón Factory + Strategy

El sistema utiliza un patrón combinado para soportar múltiples tipos de comprobantes pendientes (Pedido, Remito, Presupuesto) de forma extensible:

  • Factory (PendienteServiceFactory): Crea instancias de servicios específicos basándose en el tipo de comprobante
  • Strategy (Clases abstractas): PendienteService define la estructura, subclases implementan comportamiento específico
  • Enum (TipoComprobantePendiente): Define los tipos de comprobantes disponibles

Jerarquía de clases:

PendienteService (abstract)
├── PedidoService (implementado)
├── RemitoService (pendiente)
└── PresupuestoService (pendiente)

ItemPendienteService (abstract)
├── ItemPedidoService (implementado)
├── ItemRemitoService (pendiente)
└── ItemPresupuestoService (pendiente)

Actualmente implementado: Solo PedidoService y ItemPedidoService.

Ubicación de Archivos

Controllers:

  • controller/modulo-venta/Pendiente/PedidoController.php
  • controller/modulo-venta/TipoComprobantePendienteController.php

Services:

  • service/Venta/Pendiente/PendienteService.php (abstract)
  • service/Venta/Pendiente/PedidoService.php
  • service/Venta/Pendiente/ItemPendienteService.php (abstract)
  • service/Venta/Pendiente/ItemPedidoService.php
  • service/Venta/TipoComprobantePendienteService.php

Models:

  • models/modulo-venta/Pendiente/Pendiente.php
  • models/modulo-venta/TipoComprobantePendiente.php

Resources (DTOs):

  • Resources/Venta/Pendiente/Pendiente.php
  • Resources/Venta/Pendiente/PendienteRequest.php
  • Resources/Venta/Pendiente/Pedido.php
  • Resources/Venta/Pendiente/ItemPendiente.php
  • Resources/Venta/Pendiente/ItemPendienteRequest.php
  • Resources/Venta/TipoComprobantePendiente.php
  • Resources/Enums/TipoComprobantePendiente.php

Routes:

  • Routes/Venta/VentaRoutes.php
  • Routes/Venta/Pendiente/PendienteRoutes.php
  • Routes/Venta/Pendiente/PedidoRoute.php
  • Routes/Venta/TipoComprobantePendienteRoute.php

Factory:

  • Factories/Venta/PendienteServiceFactory.php

API Endpoints

Gestión de Tipos de Comprobantes Pendientes

Base Path: /api/mod-ventas/tipo-comprobante-pendiente

GET /api/mod-ventas/tipo-comprobante-pendiente

Responsabilidades:

  • Obtener listado de todos los tipos de comprobantes pendientes configurados
  • Incluye: id, nombre, reporte, numero_comprobante, maneja_stock

Request: Sin parámetros

Response DTO:

{
  "status": 200,
  "message": "...",
  "data": [
    {
      "id": 1,
      "nombre": "Pedido",
      "reporte": "PEDIDO",
      "numero_comprobante": 1523,
      "maneja_stock": true
    }
  ]
}

Status Codes:

  • 200: OK
  • 500: Error interno

PUT /api/mod-ventas/tipo-comprobante-pendiente/{id}

Responsabilidades:

  • Actualizar configuración de un tipo de comprobante pendiente
  • Permite modificar: nombre, reporte, numero_comprobante, maneja_stock
  • Validación dinámica de campos válidos

Request DTO:

json
{
  "nombre": "Pedido Actualizado",
  "maneja_stock": true
}

Response: 204 No Content

Status Codes:

  • 204: Actualizado exitosamente
  • 400: Datos inválidos
  • 404: Tipo no encontrado
  • 500: Error interno

Gestión de Pedidos (Comprobantes Pendientes)

Base Path: /api/mod-ventas/pendiente/pedido

GET /api/mod-ventas/pendiente/pedido

Responsabilidades:

  • Obtener listado de pedidos pendientes
  • Filtrado por cliente (opcional)
  • Filtrado por comprobante (opcional)
  • Soporte de scopes: min (default) o max
  • Solo retorna pedidos no eliminados y no facturados

Query Parameters:

  • cliente (int, opcional): Código de cliente
  • comprobante (string, opcional): Número de comprobante
  • scope (string, opcional): min | max

Scope min: id, codigo_cliente, numero_comprobante, fecha, total

Scope max: Todos los campos del pendiente

Response DTO:

{
  "status": 200,
  "data": [
    {
      "id": 1,
      "codigo_cliente": 10,
      "numero_comprobante": 1500,
      "fecha": "2026-02-10",
      "total": 15000.50
    }
  ]
}

Status Codes:

  • 200: OK
  • 500: Error interno

GET /api/mod-ventas/pendiente/pedido/{id}

Responsabilidades:

  • Obtener detalle completo de un pedido
  • Incluye datos del cliente (min)
  • Si scope=max: Incluye items del pedido con detalle completo

Query Parameters:

  • scope (string, opcional): min | max

Response DTO (scope=max):

{
  "status": 200,
  "data": {
    "id": 1,
    "codigo_cliente": 10,
    "fecha": "2026-02-10",
    "neto": 12000.00,
    "subtotal": 12000.00,
    "total": 11000.00,
    "codigo_vendedor": 5,
    "lista": "1",
    "fecha_entrega": "2026-02-15",
    "comentario": "Urgente",
    "provincia": 1,
    "sucursal": 1,
    "numero_comprobante": 1500,
    "id_preformul": 1,
    "stock": true,
    "cliente": {
      "id": 10,
      "nombre": "Juan Pérez",
      ...
    },
    "items": [
      {
        "id": 1,
        "codigo": 100,
        "cantidad": 10,
        "precio_unitario": 95.00,
        "importe": 950.00,
        "neto_unitario": 100.00,
        "bonificacion": 5.0,
        "monto_bonificacion": 50.00,
        "descripcion": "Producto A",
        "costo": 70.00,
        "manejaStock": 1
      }
    ]
  }
}

Status Codes:

  • 200: OK
  • 404: Pedido no encontrado
  • 500: Error interno

POST /api/mod-ventas/pendiente/pedido

Responsabilidades:

  • Crear un nuevo pedido pendiente
  • Validar estructura de datos (PendienteRequest)
  • Asignar número de comprobante automáticamente
  • Insertar pedido y sus items en transacción
  • Generar movimientos de stock si aplica
  • Incrementar numerador del tipo de comprobante

Request DTO (PendienteRequest):

json
{
  "cliente": 10,
  "vendedor": 5,
  "sucursal": 1,
  "lista": 1,
  "fecha": "2026-02-10",
  "fechaEntrega": "2026-02-15",
  "provincia": 1,
  "comentario": "Urgente",
  "items": [
    {
      "id": 100,
      "nombre": "Producto A",
      "cantidad": 10,
      "precio": 100.00,
      "bonificacion": 5.0
    }
  ]
}

Validaciones estructurales:

  • cliente: NotNull
  • vendedor: NotNull
  • sucursal: NotNull
  • lista: NotNull
  • fecha: NotNull, formato Carbon
  • fechaEntrega: NotNull, formato Carbon
  • provincia: NotNull
  • comentario: Length(max: 100)
  • items: NotNull, array de ItemPendienteRequest

Response DTO:

json
{
  "status": 200,
  "data": {
    "id": 123
  }
}

Status Codes:

  • 200: Creado exitosamente (retorna ID)
  • 400: Datos inválidos (validación estructural)
  • 422: Error de negocio (producto no existe, etc.)
  • 500: Error interno

PUT /api/mod-ventas/pendiente/pedido/{id}

Responsabilidades:

  • Actualizar un pedido existente
  • Reemplaza todos los datos del pedido
  • Elimina y reinserta todos los items
  • Elimina y regenera movimientos de stock si aplica
  • Ejecuta en transacción

Request DTO: Igual que POST (PendienteRequest)

Response: 204 No Content

Status Codes:

  • 204: Actualizado exitosamente
  • 400: Datos inválidos
  • 404: Pedido no encontrado
  • 422: Error de negocio
  • 500: Error interno

DELETE /api/mod-ventas/pendiente/pedido/{id}

Responsabilidades:

  • Eliminar lógicamente un pedido (soft delete)
  • Marca el campo deleted_at con timestamp actual
  • Elimina movimientos de stock asociados si aplica
  • Ejecuta en transacción

Request: Sin body

Response: 204 No Content

Status Codes:

  • 204: Eliminado exitosamente
  • 404: Pedido no encontrado
  • 500: Error interno

Endpoints de Consulta General

Base Path: /api/mod-ventas/pendiente

GET /api/mod-ventas/pendiente

Responsabilidades:

  • Obtener listado de pendientes (todos los tipos)
  • Usado para consultas generales sin filtrar por tipo específico

Query Parameters: Igual que GET de pedidos

Response: Lista de pendientes (sin filtro por tipo)


GET /api/mod-ventas/pendiente/{id}

Responsabilidades:

  • Obtener un pendiente por ID sin especificar el tipo

Response: Datos del pendiente


Capa de Servicio

PendienteService (Abstract)

Ubicación: service/Venta/Pendiente/PendienteService.php

Responsabilidades:

  • Define el contrato para todos los tipos de comprobantes pendientes
  • Implementa lógica común de CRUD con transacciones
  • Gestiona la creación/actualización/eliminación de items
  • Coordina con MovimientoStockService para stock
  • Coordina con TipoComprobantePendienteService para numeración

Métodos abstractos (cada subclase debe implementar):

php
abstract public function getAll(array $options): array;
abstract public function getOne(array $options): PendienteDTO;
abstract protected function getIdTipoComprobante(): int;
abstract protected function mapPendiente(
    PendienteRequest $comprobantePendiente,
    object $tipoComprobantePendiente
): PendienteDTO;
abstract protected function insertItems(
    PendienteDTO $pendiente,
    PendienteRequest $comprobantePendiente
): array;
abstract protected function actualizarStock(
    PendienteDTO $pendiente,
    object $tipoComprobantePendiente,
    array $items
): void;

Métodos públicos implementados:

  • insert(PendienteRequest $comprobantePendiente): int

    • Crea un nuevo comprobante pendiente
    • Flujo:
      1. Obtiene tipo de comprobante pendiente
      2. Mapea datos del request a DTO
      3. Inicia transacción
      4. Inserta comprobante
      5. Inserta items
      6. Actualiza numerador del tipo
      7. Genera movimientos de stock si aplica
      8. Commit transacción
    • Retorna: ID del pendiente creado
    • Excepciones: InsertError en caso de error
  • update(string $id, PendienteRequest $comprobantePendiente): void

    • Actualiza un comprobante pendiente
    • Flujo:
      1. Obtiene tipo de comprobante pendiente
      2. Mapea datos del request
      3. Inicia transacción
      4. Actualiza comprobante
      5. Elimina y reinserta items
      6. Elimina y regenera movimientos de stock
      7. Commit transacción
    • Excepciones: InsertError en caso de error
  • delete(int $id): void

    • Elimina lógicamente un comprobante
    • Flujo:
      1. Inicia transacción
      2. Marca el pendiente como eliminado
      3. Elimina movimientos de stock asociados
      4. Commit transacción
    • Excepciones: InsertError en caso de error
  • patch(int $id, array $datosEditar): void

    • Actualización parcial de campos específicos
    • Delega al modelo

Métodos privados auxiliares:

  • getTipoComprobantePendiente(int $idTipoComprobante)

    • Obtiene configuración del tipo de comprobante
    • Usa TipoComprobantePendienteService
  • actualizarComprobantePendiente(int $idTipoComprobante, int $numeroComprobante): void

    • Incrementa el numerador del tipo de comprobante
    • Ejecuta: numero_comprobante = numeroComprobante + 1
  • tieneModuloStock(): bool

    • Verifica si la empresa tiene el módulo de Stock habilitado
    • Consulta la configuración PermisosEmpresa en tabla config
    • Retorna: true si modulos[STOCK] === true

Dependencias:

  • ConnectionManager: Gestión de conexiones multi-tenant
  • Pendiente (Model): Acceso a datos
  • ItemPendienteService: Gestión de items
  • MovimientoStockService: Gestión de stock
  • TipoComprobantePendienteService: Gestión de tipos
  • Config (Model): Consulta de configuración

Trait utilizado:

  • Conectable: Acceso a ConnectionManager

PedidoService

Ubicación: service/Venta/Pendiente/PedidoService.php

Responsabilidades:

  • Implementación específica para comprobantes de tipo Pedido
  • Define el ID del tipo: TipoComprobantePendiente::PEDIDO (1)
  • Implementa mapeo específico de datos
  • Gestiona creación de items via ItemPedidoService
  • Genera movimientos de stock como EGRESO

Métodos implementados:

  • getAll(array $options): array

    • Filtra por id_preformul = 1 (Pedido)
    • Retorna lista de PedidoDTO
  • getOne(array $options): PedidoDTO

    • Filtra por id_preformul = 1
    • Obtiene datos del cliente (scope min)
    • Si scope=max: Obtiene items con ItemPedidoService
    • Retorna PedidoDTO con cliente e items
  • getIdTipoComprobante(): int

    • Retorna: 1 (TipoComprobantePendiente::PEDIDO)
  • mapPendiente(PendienteRequest $comprobantePendiente, object $tipoComprobantePendiente): PendienteDTO

    • Calcula neto: suma de precio * cantidad de todos los items
    • Calcula bonificación: suma de (bonificacion / 100) * precio * cantidad
    • Calcula total: neto - bonificacion
    • Mapea todos los campos del request al DTO
    • Asigna numero_comprobante del tipo
  • insertItems(PendienteDTO $pendiente, PendienteRequest $comprobantePendiente): array

    • Usa ItemPedidoService para mapear y insertar items
    • Retorna: array de ItemPendienteDTO insertados
  • actualizarStock(PendienteDTO $pendiente, object $tipoComprobantePendiente, array $items): void

    • Itera sobre items
    • Para cada item que manejaStock === 1:
      • Crea MovimientoStock con:
        • posicion: TipoMovimiento::EGRESO
        • concepto: nombre del tipo de comprobante
        • nrocomp: número de comprobante
        • id_prefa: ID del pendiente
        • marca: MarcaOrigen::OFICIAL
    • Inserta movimientos via MovimientoStockService

Dependencias adicionales:

  • Cliente (Model): Obtiene datos del cliente
  • ItemPedidoService: Gestión de items de pedido

ItemPedidoService

Ubicación: service/Venta/Pendiente/ItemPedidoService.php

Responsabilidades:

  • Mapeo de items del request a DTOs
  • Consulta de productos para obtener costo y manejo de stock
  • Cálculo de importes y bonificaciones por item

Métodos:

  • mapItems(PendienteDTO $pendiente, PendienteRequest $comprobantePendiente): array
    • Itera sobre items del request
    • Para cada item:
      1. Consulta producto desde Producto model
      2. Calcula bonificación unitaria: (bonificacion / 100) * precio
      3. Calcula precio unitario: precio - bonificacion_unitaria
      4. Calcula monto bonificación: bonificacion_unitaria * cantidad
      5. Calcula importe: (precio * cantidad) - monto_bonificacion
      6. Crea ItemPendienteDTO con:
        • codigo: ID del producto
        • cantidad
        • precio_unitario: con bonificación aplicada
        • importe: total del item
        • neto_unitario: precio sin bonificación
        • costo: del producto
        • descripcion: nombre del item
        • id_prefa: ID del pendiente
        • manejaStock: del producto
        • bonificacion: porcentaje
        • monto_bonificacion: importe descontado
    • Cache de productos consultados para optimizar
    • Retorna: array de ItemPendienteDTO

Dependencias:

  • Producto (Model): Consulta de productos
  • ConnectionManager: Acceso a base de datos

TipoComprobantePendienteService

Ubicación: service/Venta/TipoComprobantePendienteService.php

Responsabilidades:

  • Gestión de tipos de comprobantes pendientes (configuración)
  • CRUD de tipos

Métodos:

  • getAll(): array

    • Retorna todos los tipos configurados
    • Retorna: TipoComprobantePendienteDTO[]
  • getById(int $id): TipoComprobantePendienteDTO

    • Obtiene un tipo por ID
    • Excepciones: ServerException si no existe
  • update(int $id, array $data): void

    • Actualiza campos específicos de un tipo
    • Valida campos permitidos: id, nombre, reporte, numero_comprobante, maneja_stock
    • Casteo especial: maneja_stock se castea a int para almacenamiento boolean en BD

Dependencias:

  • TipoComprobantePendiente (Model): Acceso a datos

Lógica de Negocio

Asignación Automática de Número de Comprobante

Flujo:

  1. Al crear un pendiente, se consulta el tipo de comprobante
  2. Se obtiene numero_comprobante actual del tipo
  3. Se asigna ese número al pendiente
  4. Se incrementa numero_comprobante del tipo en 1

Problema potencial identificado: ⚠️ No se detectó el uso de transacciones exclusivas o locks para el incremento del numerador. Esto podría causar race conditions en entornos concurrentes.


Generación de Movimientos de Stock

Condiciones:

  1. La empresa debe tener el módulo de Stock habilitado (PermisosEmpresa en config)
  2. El tipo de comprobante debe tener maneja_stock = true
  3. El producto del item debe tener maneja_stock = 1

Flujo:

  • Al crear/actualizar: Se generan movimientos de EGRESO
  • Al actualizar: Se eliminan movimientos anteriores y se regeneran
  • Al eliminar: Se eliminan todos los movimientos asociados

Tipo de movimiento: TipoMovimiento::EGRESO (salida de stock)

Marca: MarcaOrigen::OFICIAL (siempre oficial para pendientes)

Referencia: Los movimientos se vinculan al pendiente mediante id_prefa


Cálculo de Totales

Fórmulas implementadas:

Para el pedido completo:
- neto = suma( precio * cantidad ) de todos los items
- bonificacion_total = suma( (bonificacion / 100) * precio * cantidad )
- total = neto - bonificacion_total
- subtotal = neto

Para cada item:
- bonificacion_unitaria = (porcentaje_bonificacion / 100) * precio
- precio_unitario = precio - bonificacion_unitaria
- monto_bonificacion = bonificacion_unitaria * cantidad
- importe = (precio * cantidad) - monto_bonificacion

Eliminación Lógica (Soft Delete)

Implementación:

  • Campo deleted_at se marca con timestamp actual
  • Los items NO se eliminan (se mantienen)
  • Los movimientos de stock SÍ se eliminan físicamente

Filtros automáticos:

  • Todas las consultas incluyen: WHERE deleted_at IS NULL
  • Todas las consultas incluyen: AND id_factura IS NULL

Esto asegura que solo se muestren pendientes activos y no facturados.


Cancelación de Pendientes

Mecanismo:

  • Campo id_factura se asigna cuando se genera una factura a partir del pendiente
  • El filtro id_factura IS NULL excluye pendientes ya facturados de las consultas

⚠️ NOTA: El flujo de cancelación por facturación NO está implementado en el código de Pendientes analizado. Probablemente se gestiona desde el módulo de Facturación.


Esquema de Base de Datos

Tabla: prefa

Nivel: SUCURSAL o EMPRESA (configurable por empresa mediante configuracion_niveles_tablas)

Descripción: Almacena los comprobantes pendientes (pedidos, remitos, presupuestos)

CampoTipoConstraintsDescripción
idSERIALPRIMARY KEYID único auto-incremental
zfVARCHAR(6)NOT NULLCódigo de cliente
fechaDATENOT NULLFecha del comprobante
netoDECIMAL(16,5)NULLTotal neto (suma precio*cantidad sin bonificación)
subtotalDECIMAL(16,5)NULLSubtotal del comprobante (igual a neto)
totalDECIMAL(16,5)NULLTotal del comprobante (neto - bonificación)
vendDECIMAL(3)NULLCódigo de vendedor
listaVARCHAR(3)NULLCódigo de lista de precios
vencimientoDATENULLFecha de vencimiento del comprobante
fecha_entregaDATENULLFecha de entrega programada
ivaDECIMAL(16,5)NULLImporte total de IVA
financDECIMAL(16,5)NULL⚠️ Sin uso actualmente
bonifDECIMAL(16,5)NULL⚠️ Sin uso actualmente
comenVARCHAR(100)NULLComentario del comprobante
provinciaDECIMAL(2)NULLCódigo de provincia del cliente
sucSMALLINTNOT NULLCódigo de sucursal emisora
nrocompBIGINTNOT NULLNúmero de comprobante
iminDECIMAL(16,5)NULL⚠️ Sin uso actualmente
id_preformulINTEGERNOT NULL, FKID del tipo de comprobante (FK a preformul)
deleted_atTIMESTAMPNULLTimestamp de eliminación lógica
id_facturaINTEGERNULLID de la factura que canceló el pendiente
modoSMALLINTNULLModo de facturación: 0=Prueba, 1=Oficial, 2=Consolidado
stockBOOLEANNOT NULLFlag que indica si registró movimiento de stock

Índices: PRIMARY KEY (id)

Foreign Keys:

  • id_preformulpreformul(id) ON DELETE RESTRICT ON UPDATE CASCADE

Constraints:

  • Soft delete: deleted_at IS NULL para registros activos
  • Cancelación: id_factura IS NULL para pendientes no facturados

Niveles configurables: Sigue configuración de preformul (default: EMPRESA y SUCURSAL)

Migración: migrations/tenancy/20250211134902_new_table_prefa.php

Condición de ejecución: Solo se crea si la empresa tiene empres.pedido = true OR empres.remito = true Y el módulo Ventas está habilitado.


Tabla: preformul

Nivel: EMPRESA o SUCURSAL (configurable)

Descripción: Configuración de tipos de comprobantes pendientes

CampoTipoConstraintsDescripción
idINTEGERPRIMARY KEYID único del tipo
nombreVARCHARNOT NULLNombre del tipo (ej: "Pedido")
reporteVARCHARNOT NULLCódigo del reporte asociado
nrofacINTEGERNOT NULLNumerador de comprobantes (auto-incremental)
maneja_stockBOOLEANNOT NULLIndica si genera movimientos de stock

Datos iniciales:

  • ID 1: "Pedido"
  • Otros tipos pendientes de implementación

Tabla: items_prefa

Nivel: Sigue configuración de prefa (SUCURSAL o EMPRESA)

Descripción: Items (líneas de detalle) de los comprobantes pendientes

⚠️ Nota: Esta tabla NO fue encontrada en las migraciones analizadas. Se infiere su existencia por el código del modelo y servicio ItemPendiente.

Campos inferidos:

  • id: PRIMARY KEY
  • codigo: Código de producto
  • cantidad: Cantidad del item
  • precio_unitario: Precio unitario con bonificación aplicada
  • importe: Importe total del item
  • neto_unitario: Precio sin bonificación
  • bonificacion: Porcentaje de bonificación
  • monto_bonificacion: Importe de bonificación
  • descripcion: Descripción del producto
  • costo: Costo del producto
  • id_prefa: FK a prefa(id) ON DELETE CASCADE
  • manejaStock: Flag de manejo de stock (del producto)
  • Otros campos relacionados con IVA e impuestos internos

Relaciones

preformul (1) ----< (N) prefa
prefa (1) ----< (N) items_prefa
prefa (N) >---- (1) cliente
prefa (N) >---- (1) vendedor
prefa (N) >---- (0..1) factura (cancelación)
items_prefa (N) >---- (1) producto
prefa (1) ----< (N) movimiento_stock (via id_prefa)

Capa de Datos

Modelo: Pendiente

Ubicación: models/modulo-venta/Pendiente/Pendiente.php

Responsabilidades:

  • Acceso a datos de la tabla prefa
  • Mapping de columnas de BD a DTOs
  • Consultas CRUD con Doctrine DBAL
  • Soporte de scopes para optimización de consultas

Scopes definidos:

  • min: id, codigo_cliente, numero_comprobante, fecha, total
  • max: Todos los campos

Métodos públicos:

  • hasByCliente(int $cliente): bool

    • Verifica si un cliente tiene pendientes activos
    • Filtra: deleted_at IS NULL AND id_factura IS NULL
  • getAll(array $options): PendienteDTO[]

    • Obtiene listado de pendientes
    • Filtros opcionales: cliente, id_preformul
    • Scope: min o max
    • Siempre filtra: deleted_at IS NULL AND id_factura IS NULL
  • getOne(array $options): PendienteDTO

    • Obtiene un pendiente por ID o número de comprobante
    • Filtros: comprobante, id, id_preformul
    • Scope: min o max
    • Excepciones: ServerException si no existe
  • insert(PendienteDTO $pendiente): int

    • Inserta un nuevo pendiente
    • Retorna: ID del pendiente insertado
    • Usa lastInsertId() para obtener ID
  • update(PendienteDTO $pendiente): void

    • Actualiza todos los campos de un pendiente
    • Requiere pendiente.id para identificar el registro
  • delete(int $id): void

    • Eliminación lógica (soft delete)
    • Marca deleted_at con timestamp actual
  • patch(int $id, array $datosEditar, int $id_preformul = null): void

    • Actualización parcial de campos específicos
    • Valida campos contra $fields definidos
    • Filtro opcional adicional por id_preformul

Mapping de campos:

php
private $fields = [
    'id' => "id",
    'codigo_cliente' => "zf::int as codigo_cliente",
    'fecha' => "fecha",
    'neto' => 'neto',
    'subtotal' => 'subtotal',
    'total' => "total",
    'codigo_vendedor' => "vend::int as codigo_vendedor",
    'lista' => "lista::int",
    'vencimiento' => "vencimiento",
    'fecha_entrega' => 'fecha_entrega',
    'iva' => "iva",
    'comentario' => "comen as comentario",
    'provincia' => "provincia::int",
    'sucursal' => "suc::int as sucursal",
    'numero_comprobante' => "nrocomp::bigint as numero_comprobante",
    'id_preformul' => 'id_preformul',
    'id_factura' => "id_factura",
    'modo' => 'modo',
    'stock' => 'stock::boolean'
];

Dependencias:

  • Doctrine\DBAL\Connection: Acceso a base de datos

Modelo: TipoComprobantePendiente

Ubicación: models/modulo-venta/TipoComprobantePendiente.php

Responsabilidades:

  • Acceso a datos de la tabla preformul
  • CRUD de tipos de comprobantes pendientes

Métodos públicos:

  • getAll(): TipoComprobantePendienteDTO[]

    • Obtiene todos los tipos configurados
  • getById(int $id): TipoComprobantePendienteDTO

    • Obtiene un tipo por ID
    • Excepciones: ServerException si no existe
  • update(int $id, array $data): void

    • Actualiza campos de un tipo
    • Validación dinámica de campos permitidos
    • Casteo especial para maneja_stock (boolean → int)

Mapping de campos:

php
private $fields = [
    'id' => 'id',
    'nombre' => 'nombre',
    'reporte' => 'reporte',
    'numero_comprobante' => 'nrofac',
    'maneja_stock' => 'maneja_stock',
];

DTOs (Data Transfer Objects)

Pendiente

Ubicación: Resources/Venta/Pendiente/Pendiente.php

Propiedades:

php
public ?int $id = null;
public int $codigo_cliente;
public string $fecha;
public ?float $neto = null;
public ?float $subtotal = null;
public ?float $total = null;
public ?int $codigo_vendedor = null;
public ?string $lista = null;
public ?string $vencimiento = null;
public ?string $fecha_entrega = null;
public ?float $iva = null;
public ?string $comentario = null;
public ?int $provincia = null;
public int $sucursal;
public int $numero_comprobante;
public int $id_preformul;
public ?int $id_factura = null;
public ?int $modo = null; // 0: Prueba | 1: Oficial | 2: Consolidado
public bool $stock;

Casters:

  • Casteo automático de neto, subtotal, total, iva a float
  • Casteo de codigo_cliente a int
  • Casteo de lista a string
  • Casteo de id_preformul a int

PendienteRequest

Ubicación: Resources/Venta/Pendiente/PendienteRequest.php

Propiedades:

php
#[Assert\NotNull]
public int $cliente;

#[Assert\NotNull]
public int $vendedor;

#[Assert\NotNull]
public int $sucursal;

#[Assert\NotNull]
public int $lista;

#[Assert\Length(max: 100)]
public ?string $comentario = null;

#[Assert\NotNull]
public Carbon $fecha;

#[Assert\NotNull]
public Carbon $fechaEntrega;

#[Assert\NotNull]
public int $provincia;

#[Assert\NotNull]
public ?array $items = null; // ItemPendienteRequest[]

Casters:

  • fecha y fechaEntrega: Carbon::parse()
  • lista: Cast a int
  • items: Array de ItemPendienteRequest

Validaciones: Usa Symfony Validator con atributos PHP 8.


Pedido

Ubicación: Resources/Venta/Pendiente/Pedido.php

Propiedades: Extiende Pendiente y agrega:

php
public ?object $cliente = null; // Datos del cliente (scope min)
public ?array $items = null;    // Items del pedido (scope max)

TipoComprobantePendiente

Ubicación: Resources/Venta/TipoComprobantePendiente.php

Propiedades:

php
public int $id;
public string $nombre;
public string $reporte;
public int $numero_comprobante;
public bool $maneja_stock;

ItemPendiente

Ubicación: Resources/Venta/Pendiente/ItemPendiente.php

Propiedades inferidas:

php
public int $codigo;               // ID del producto
public float $cantidad;
public float $precio_unitario;    // Con bonificación aplicada
public float $importe;            // Total del item
public float $neto_unitario;      // Precio sin bonificación
public float $bonificacion;       // Porcentaje
public float $monto_bonificacion; // Importe descontado
public string $descripcion;
public float $costo;
public int $id_prefa;             // FK a prefa
public int $manejaStock;          // 0 o 1

Validaciones Implementadas

Validaciones Estructurales (Request DTOs)

Aplicadas mediante Symfony Validator en PendienteRequest:

CampoValidación
clienteNotNull
vendedorNotNull
sucursalNotNull
listaNotNull
fechaNotNull, formato Carbon
fechaEntregaNotNull, formato Carbon
provinciaNotNull
comentarioLength(max: 100)
itemsNotNull, array

Validaciones de Negocio (Service Layer)

En PedidoService:

  • Verificación de existencia de productos (ItemPedidoService)
  • Si producto no existe: BadRequest("No se encontró el producto con código: X")

En PendienteService:

  • Verificación de módulo de Stock habilitado
  • Si no se puede obtener módulos: InsertError("Error al obtener los módulos de la empresa")

En TipoComprobantePendienteService:

  • Validación de campos permitidos en actualización
  • Si no hay campos válidos: ServerException("No hay campos válidos para actualizar.")
  • Verificación de existencia del tipo
  • Si no existe: ServerException("No se encontró el comprobante con ID: {id}")

Puntos de Integración

Integración con Módulo de Stock

Servicio: MovimientoStockService

Responsabilidades:

  • Insertar movimientos de stock (egreso) al crear pendiente
  • Eliminar movimientos al actualizar o eliminar pendiente
  • Filtro por id_prefa para identificar movimientos asociados

Métodos utilizados:

  • insert(array $movimientos): Inserta movimientos en batch
  • deleteByIdPendiente(int $id_prefa): Elimina movimientos por ID de pendiente

Integración con Módulo de Clientes

Modelo: Cliente

Responsabilidades:

  • Obtener datos del cliente para mostrar en detalle del pedido

Métodos utilizados:

  • getClienteById(int $codigo, string $scope): Obtiene cliente con scope min

Integración con Módulo de Productos

Modelo: Producto

Responsabilidades:

  • Consultar productos para validar existencia
  • Obtener costo y flag de manejo de stock

Métodos utilizados:

  • getOne(array $options): Consulta producto por ID

Integración con Módulo de Facturación

Mecanismo: Campo id_factura en tabla prefa

Flujo (inferido, NO implementado en código analizado):

  1. Al generar una factura desde un pedido pendiente
  2. La factura asigna su ID al campo id_factura del pendiente
  3. El pendiente queda "cancelado" y no aparece en consultas de pendientes

⚠️ NOTA: Este flujo se gestiona desde el módulo de Facturación, no está implementado en Pendientes.


Estrategia de Testing

Tests Unitarios (Recomendados)

PendienteService:

  • testInsertPendiente(): Verificar flujo completo de inserción con transacción
  • testInsertGeneraMovimientosStock(): Verificar que se generan movimientos si aplica
  • testInsertNoGeneraMovimientosSinModuloStock(): Verificar que NO genera movimientos sin módulo
  • testUpdatePendienteActualizaItems(): Verificar actualización con eliminación/reinserción de items
  • testUpdateActualizaStock(): Verificar actualización de movimientos de stock
  • testDeleteEliminaLogicamente(): Verificar soft delete
  • testDeleteEliminaMovimientosStock(): Verificar eliminación de movimientos

PedidoService:

  • testCalculoTotalesConBonificacion(): Verificar cálculos de neto, bonificación y total
  • testMapPendienteAsignaNumeroComprobante(): Verificar asignación de número
  • testGetOneIncluyeCliente(): Verificar inclusión de datos del cliente
  • testGetOneConScopeMaxIncluyeItems(): Verificar inclusión de items

ItemPedidoService:

  • testMapItemsCalculaBonificacion(): Verificar cálculo de bonificaciones por item
  • testMapItemsConsultaProducto(): Verificar consulta y validación de productos
  • testMapItemsLanzaExcepcionProductoInexistente(): Verificar error si producto no existe
  • testMapItemsCacheaProductos(): Verificar que cachea productos consultados

Modelo Pendiente:

  • testGetAllFiltraEliminados(): Verificar filtro deleted_at IS NULL
  • testGetAllFiltraFacturados(): Verificar filtro id_factura IS NULL
  • testGetAllConScopeMin(): Verificar campos retornados en scope min
  • testGetAllConScopeMax(): Verificar campos retornados en scope max
  • testHasByClienteRetornaTrue(): Verificar detección de pendientes por cliente
  • testInsertRetornaId(): Verificar que insert retorna el ID

TipoComprobantePendienteService:

  • testUpdateIncrementaNumerador(): Verificar incremento de numero_comprobante
  • testUpdateValidaCamposPermitidos(): Verificar validación de campos

Tests de Integración (Recomendados)

Flujo completo CREATE:

  • Crear pendiente → Verificar inserción en BD → Verificar items insertados → Verificar movimientos de stock generados → Verificar incremento de numerador

Flujo completo UPDATE:

  • Actualizar pendiente → Verificar actualización en BD → Verificar eliminación/reinserción de items → Verificar actualización de movimientos de stock

Flujo completo DELETE:

  • Eliminar pendiente → Verificar soft delete → Verificar eliminación de movimientos de stock → Verificar que NO aparece en consultas

Transacciones:

  • Provocar error en mitad del proceso → Verificar rollback completo → Verificar que NO quedaron datos inconsistentes

Tests E2E (Recomendados)

Endpoint POST /pedido:

  • Request válido → Verificar 200 OK → Verificar ID retornado → Verificar datos en BD

Endpoint GET /pedido/{id}:

  • Request con scope max → Verificar datos completos con cliente e items

Endpoint PUT /pedido/{id}:

  • Request con modificación de items → Verificar 204 → Verificar actualización en BD

Endpoint DELETE /pedido/{id}:

  • Request de eliminación → Verificar 204 → Verificar que no aparece en listado

Consideraciones de Rendimiento

Scopes de Consulta

Implementación:

  • Scope min: Solo campos esenciales (id, cliente, número, fecha, total)
  • Scope max: Todos los campos del pendiente

Recomendación: Usar scope min para listados, max solo para detalles.


Cache de Productos

Implementación en ItemPedidoService:

php
$productos = []; // Cache interno
foreach ($items as $item) {
    if (isset($productos[$itemData->id])) {
        $producto = $productos[$itemData->id];
    } else {
        $producto = $productoModel->getOne(['id' => $itemData->id]);
        $productos[$producto->id] = $producto;
    }
}

Optimización: Evita consultas redundantes al mismo producto en un pedido.


Índices Sugeridos

⚠️ NOTA: No se encontraron índices definidos en la migración más allá del PRIMARY KEY.

Índices recomendados:

sql
CREATE INDEX idx_prefa_cliente ON prefa(zf) WHERE deleted_at IS NULL;
CREATE INDEX idx_prefa_comprobante ON prefa(nrocomp, id_preformul) WHERE deleted_at IS NULL;
CREATE INDEX idx_prefa_factura ON prefa(id_factura) WHERE id_factura IS NOT NULL;
CREATE INDEX idx_prefa_tipo ON prefa(id_preformul) WHERE deleted_at IS NULL;

Justificación:

  • Filtrado frecuente por cliente
  • Búsqueda por número de comprobante
  • Filtrado de pendientes no facturados
  • Filtrado por tipo de comprobante

N+1 Queries

Problema identificado: En PedidoService.getOne() con scope max:

  1. Query para obtener el pedido
  2. Query para obtener el cliente
  3. Query para obtener items (ItemPedidoService)
  4. Dentro de items: Query por cada producto (con cache)

Optimización posible: Usar JOINs en el modelo para obtener cliente y items en una sola consulta.


Consideraciones de Seguridad

Autenticación y Autorización

Implementación:

  • JWT en header Authorization
  • Middleware AuthMiddleware valida token
  • Middleware ConnectionMiddleware configura schema multi-tenant desde JWT

Permisos:

  • Vista de tipos de comprobantes: VENTAS_BASES_COMPROBANTES-PENDIENTES
  • Operaciones sobre pedidos: Requiere autenticación (permisos no especificados explícitamente en código)

Aislamiento Multi-Tenant

Implementación:

  • Uso de ConnectionManager con configuración por schema
  • Schema se obtiene del JWT payload
  • Todas las consultas ejecutan con search_path configurado al schema del tenant

Validación: El ConnectionMiddleware valida y configura el schema antes de llegar al Controller.


Inyección SQL

Protección:

  • Uso de Doctrine DBAL Query Builder
  • Todos los parámetros se pasan via setParameter()
  • No se encontró concatenación directa de strings en queries

Ejemplo:

php
$builder->where('zf = :cliente')
    ->setParameter('cliente', (int)$options['cliente']);

Sanitización de Inputs

Validación estructural: Symfony Validator en DTOs

Validación de tipos: Cast explícito a tipos esperados

Longitud de campos: Assert\Length(max: 100) en comentario


Auditoría

⚠️ CRÍTICO: NO se encontró implementación de auditoría en los servicios de Pendientes.

Recomendación: Implementar AuditableInterface y Auditable trait en PendienteService para registrar operaciones CUD.

Ejemplo esperado:

php
$this->registrarAuditoria(
    "INSERT",
    "VENTAS",
    $this->model->getTable(),
    $pendiente->id
);

Preguntas Técnicas Pendientes

Si existen preguntas técnicas sobre esta implementación, ver: Preguntas sobre Comprobantes Pendientes


Referencias


⚠️ NOTA IMPORTANTE: Esta documentación fue generada automáticamente analizando el código implementado. Validar cambios futuros contra este baseline.