Skip to content

Módulo Stock - Documentación Técnica Backend

Módulo: Stock Feature: Refactor stock-2026-03-21 (TipoComprobanteStock + MovimientoStock) Fecha: 2026-03-21

DOCUMENTACIÓN RETROSPECTIVA — Generada a partir de código implementado en el refactor stock-refactor (2026-03-21).


Documento de Negocio Relacionado


Arquitectura Implementada

El módulo Stock sigue la arquitectura 5-layer DDD estándar de Sistema Bautista via Slim Framework 4. El endpoint legacy backend/mod-stock/tipo-comprobante.php fue eliminado y reemplazado por capas desacopladas.

Patrón de flujo

Request HTTP
  → ValidationMiddleware (Validator)
  → Controller (HTTP in/out) → Input DTO (tipado)
  → Service (transacciones, auditoría, reglas de negocio)
  → Model (SQL + mapeo DTOs de salida)
  → PostgreSQL (schema por tenant)

Ubicación de componentes

CapaArchivoDescripción
RouteRoutes/Stock/StockRoutes.phpEndpoints Slim bajo /mod-stock
ValidatorValidators/Stock/TipoComprobanteStockValidator.phpValidación estructural del request
ValidatorValidators/Stock/CreateMovimientoStockValidator.phpValidación estructural de movimiento
Controllercontroller/modulo-stock/TipoComprobanteStockController.phpHTTP in/out para tipo comprobante
Controllercontroller/modulo-stock/MovimientoStockController.phpHTTP in/out + verificación de módulo habilitado
Serviceservice/Stock/TipoComprobanteStockService.phpTransacciones, auditoría, unicidad de descripción
Serviceservice/Stock/MovimientoStockService.phpInserción de movimientos de stock
Modelmodels/modulo-stock/TipoComprobanteStock.phpCRUD sobre tabla dcomprob
Modelmodels/modulo-stock/MovimientoStock.phpINSERT sobre tabla mov_sto
DTOResources/Stock/TipoComprobanteStockDTO.php6 campos mapeados desde dcomprob
DTOResources/Stock/MovimientoStock.phpCampos de movimiento incluyendo id_compras
Input DTOResources/Stock/CreateTipoComprobanteInput.phpInput DTO para POST /tipo-comprobante (sin codigo, extiende FullDTO)
Input DTOResources/Stock/UpdateTipoComprobanteInput.phpInput DTO para PUT /tipo-comprobante/{codigo} (con codigo, extiende FullDTO)
Input DTOResources/Stock/CreateMovimientoInput.phpInput DTO para POST /movimiento (campos HTTP, extiende FullDTO)

Endpoints API

Registrados bajo el prefijo /mod-stock en index.php.

GET /mod-stock/tipo-comprobante

Controller: TipoComprobanteStockController::getAll

Retorna todos los tipos de comprobante de stock ordenados por código.

Response 200:

json
{
  "status": 200,
  "data": [
    {
      "codigo": 1,
      "descri": "Ingreso por compras",
      "tipo": "I",
      "imprimir": "S",
      "valor": "N",
      "control": "S"
    }
  ]
}

POST /mod-stock/tipo-comprobante

Controller: TipoComprobanteStockController::insertMiddleware: ValidationMiddleware(TipoComprobanteStockValidator::class)

Crea un nuevo tipo de comprobante. El código se asigna automáticamente (MAX+1).

Request Body:

CampoTipoRequeridoDescripción
descristringDescripción (máx 50 chars)
tipostring'I' (Ingreso) o 'E' (Egreso)
imprimirbool/stringtrue/false o 'S'/'N'
valorbool/stringtrue/false o 'S'/'N'
controlbool/stringtrue/false o 'S'/'N'

Códigos de respuesta:

  • 201 Created: Tipo creado con DTO retornado
  • 400 Bad Request: Validación estructural falló
  • 422 Unprocessable Entity: Descripción ya existe (duplicado)

PUT /mod-stock/tipo-comprobante/{codigo}

Controller: TipoComprobanteStockController::updateMiddleware: ValidationMiddleware(TipoComprobanteStockValidator::class)

Actualiza un tipo de comprobante existente. El codigo viene como path param.

Códigos de respuesta:

  • 200 OK: Tipo actualizado con DTO retornado
  • 400 Bad Request: Validación estructural falló
  • 404 Not Found: Código no existe
  • 422 Unprocessable Entity: Descripción colisiona con otro registro

POST /mod-stock/movimiento

Controller: MovimientoStockController::insertMiddleware: ValidationMiddleware(CreateMovimientoStockValidator::class)

Registra movimientos de stock manuales. Verifica que el módulo controlstock esté habilitado antes de procesar.

Códigos de respuesta:

  • 201 Created: Movimientos registrados
  • 400 Bad Request: Validación estructural falló
  • 403 Forbidden: Módulo controlstock deshabilitado en PermisosEmpresa
  • 422 Unprocessable Entity: Producto no maneja stock

Modelo de Dominio

TipoComprobanteStockService

Archivo: service/Stock/TipoComprobanteStockService.phpImplementa: AuditableInterfaceTraits: Conectable, Auditable

Responsabilidades:

  • Orquestar CRUD sobre TipoComprobanteStock model
  • Garantizar atomicidad mediante transacciones (beginTransaction / commit / rollback)
  • Registrar auditoría en cada operación CUD via registrarAuditoria()
  • Validar unicidad de descripción (case-insensitive) antes de INSERT/UPDATE

Métodos públicos:

MétodoDescripciónExcepción
getAll(): TipoComprobanteStockDTO[]Lista todos los registros
getById(int $codigo): ?TipoComprobanteStockDTOBusca por PK
insert(CreateTipoComprobanteInput $input): TipoComprobanteStockDTOCrea registro con auditoríaInvalidArgumentException (422 si descri duplicado)
update(UpdateTipoComprobanteInput $input): TipoComprobanteStockDTOActualiza registro con auditoríaInvalidArgumentException (404 si no existe, 422 si descri colisiona)

Dependencias inyectadas: ConnectionManager, ?AuditLogger


MovimientoStockController — verificación de módulo habilitado

Archivo: controller/modulo-stock/MovimientoStockController.php

El controller implementa un patrón de verificación de módulo habilitado antes de procesar cualquier movimiento. Lee la clave PermisosEmpresa de la tabla sistema.modulos via Config::getOneByKey('PermisosEmpresa'). Si el flag modulo_controlstock es false, retorna 403 Forbidden sin delegar al service.

Flujo de verificación:

  1. Intenta leer de data_config (clave PermisosEmpresa) — permite override en tests
  2. Si la clave modulo_controlstock está definida explícitamente en data_config, la usa
  3. Si no, cae al mecanismo estándar: Config::getOneByKey('PermisosEmpresa') que lee sistema.modulos
  4. Si modulo_controlstock === false → retorna 403

Desacoplamiento de ProductoController: Antes del refactor, ProductoController instanciaba new MovimientoStock($conn) directamente para registrar movimientos. Ahora usa MovimientoStockService inyectado vía DI container, siguiendo la regla de arquitectura: un Service solo llama a su propio Model; si necesita datos de otro dominio, delega al Service correspondiente.


Esquema de Base de Datos

Tabla: dcomprob (Tipos de Comprobante de Stock)

Nivel Multi-tenant: SUCURSAL

CampoTipoConstraintsDescripción
codigoINTEGERPRIMARY KEYPK autoincremental — calculado como MAX(codigo)+1 en INSERT
descriVARCHAR(50)NOT NULLDescripción del tipo (única, validada case-insensitive en Service)
tipoVARCHAR(1)NOT NULL'I' = Ingreso, 'E' = Egreso
imprimirVARCHAR(1)NOT NULL'S' / 'N' — legacy boolean
valorVARCHAR(1)NOT NULL'S' / 'N' — legacy boolean
controlVARCHAR(1)NOT NULL'S' / 'N' — legacy boolean

Notas:

  • No usa SERIAL — el código se calcula manualmente como CASE WHEN MAX(codigo) IS NULL THEN 1 ELSE MAX(codigo)+1 END
  • Los booleanos imprimir, valor, control se almacenan en formato legacy 'S'/'N' y se normalizan en el Model al hacer INSERT/UPDATE

Tabla: mov_sto (Movimientos de Stock)

Nivel Multi-tenant: SUCURSAL/CAJA (según configuración de tenant)

Campos relevantes al refactor:

CampoTipoConstraintsDescripción
id_ventasVARCHAR(36)NULLTrazabilidad con comprobante de ventas que generó el movimiento
marcaVARCHAR(1)NULL'O' = Oficial, 'P' = Prueba, NULL = legacy. Obligatorio para nuevos movimientos
id_comprasVARCHAR(36)NULLAgregado en refactor stock-2026-03-21. Reservado para futura trazabilidad con comprobantes de compras. Sin FK activa. Siempre NULL en esta versión

Nota sobre id_compras: El campo se insertó para alinear la estructura de mov_sto con el patrón ya existente de id_ventas. No existe lógica de negocio activa que lo complete. Se incluye en el INSERT como :id_compras con valor null desde el DTO. No tiene foreign key física (decisión arquitectural: multi-tenant con schemas separados no soporta FKs cross-schema).


Capa de Datos

TipoComprobanteStock (Model)

Archivo: models/modulo-stock/TipoComprobanteStock.phpTabla: dcomprob

Métodos:

MétodoDescripción
getAll(): TipoComprobanteStockDTO[]SELECT ordenado por codigo
getById(int $codigo): ?TipoComprobanteStockDTOSELECT por PK
getByDescri(string $descri, ?int $excludeCodigo): array|falseBúsqueda case-insensitive para validar unicidad; excludeCodigo se usa en UPDATE
insert(array $data): TipoComprobanteStockDTOINSERT con código autoincremental, RETURNING completo
update(array $data): ?TipoComprobanteStockDTOUPDATE con RETURNING completo, retorna null si no existe

Mapeo de booleanos (INSERT/UPDATE): Los campos imprimir, valor, control aceptan true/false o 'S'/'N' y se normalizan a 'S'/'N' antes del SQL.


TipoComprobanteStockDTO

Archivo: Resources/Stock/TipoComprobanteStockDTO.php

PropiedadTipoDescripción
codigointPK
descristringDescripción
tipostring'I' o 'E'
imprimirstring'S' o 'N'
valorstring'S' o 'N'
controlstring'S' o 'N'

Métodos heredados: fromArray(array $data): self, toArray(): array.


Arquitectura de DTOs

El módulo Stock usa dos tipos de DTO, ambos extendiendo FullDTO (Resources/FullDTO.php).

FullDTO — clase base

  • Usa Valinor para type-mapping: fromArray(array $data): self y toArray(): array
  • Sin validación de negocio — solo mapeo de tipos PHP
  • Tipos estrictos (enums, nullable, readonly) actúan como guardia implícita de integridad

Input DTOs — datos de entrada

Representan los datos que llegan del request HTTP o de otro service. Extienden FullDTO y solo describen la forma del dato de entrada:

DTOPropósitoObservación
CreateTipoComprobanteInputPOST /tipo-comprobanteSin campo codigo (se genera en Model)
UpdateTipoComprobanteInputPUT /tipo-comprobante/Incluye codigo como campo tipado
CreateMovimientoInputPOST /movimientoCampos del request HTTP de movimiento

El controller construye el Input DTO tipado antes de llamar al service. El service recibe DTOs tipados, no arrays.

Response DTOs — datos de salida

Representan los datos retornados al cliente (ej. TipoComprobanteStockDTO). Extienden FullDTO y contienen campos generados (como codigo).

Migración MovimientoStock

Resources/Stock/MovimientoStock.php migra del patrón DTO legacy (con validaciones Rakit en constructor) a FullDTO con tipos PHP estrictos (enums TipoMovimiento, MarcaOrigen). Las validaciones de negocio se reubican en los Validators HTTP.

Cross-service pattern

Los services que insertan movimientos desde otros módulos (FacturaService, NotaCreditoService, PedidoService) siguen usando MovimientoStock::fromArray($array). Valinor hace el type-mapping y los tipos estrictos son guardia suficiente sin pasar por el Validator HTTP.


Validaciones

Nivel 1: Validaciones HTTP (Validator Middleware)

Ejecutadas antes de llegar al Controller, retornan 400 si fallan. Son la única fuente de validación de negocio para el path HTTP. Los Input DTOs no validan internamente.

TipoComprobanteStockValidator (Validators/Stock/TipoComprobanteStockValidator.php):

CampoReglas
descrirequired, string, max:50
tiporequired, string, in:I,E
imprimirrequired
valorrequired
controlrequired

Nivel 2: Validaciones de Negocio (Service)

Ejecutadas en TipoComprobanteStockService, retornan 422 si fallan. El service recibe Input DTOs tipados (no arrays).

ReglaDescripción
Unicidad de descri en INSERTLlama a model->getByDescri($descri). Si retorna resultado → InvalidArgumentException con código 422
Unicidad de descri en UPDATELlama a model->update($data) que internamente verifica con excludeCodigo
Existencia en UPDATESi model->update() retorna nullInvalidArgumentException con código 404

Puntos de Integración

Módulo Stock → Módulo Config

MovimientoStockController lee PermisosEmpresa desde App\models\general\Config para verificar si el módulo controlstock está habilitado. La tabla sistema.modulos es schema-level EMPRESA.

Módulo Stock → Módulo Ventas (ProductoController)

ProductoController inyecta MovimientoStockService vía DI container. Antes del refactor usaba new MovimientoStock($conn) directamente (violación de arquitectura). El desacoplamiento garantiza que la lógica de movimientos esté centralizada en el Service.

Campo id_compras (futura integración con Compras)

Cuando el módulo de Compras implemente trazabilidad de movimientos de stock, el campo id_compras en mov_sto recibirá el UUID del comprobante de compra. La lógica deberá implementarse en MovimientoStockService siguiendo el patrón de id_ventas.


Consideraciones de Multi-Tenancy

Las tablas dcomprob y mov_sto residen en el schema de SUCURSAL (suc0001, suc0002, etc.). El ConnectionManager resuelve el schema correcto vía header X-Schema. No se usan foreign keys físicas entre tablas de distintos schemas (decisión arquitectural del sistema).

La conexión usada es principal (alias de la conexión activa del tenant). El parámetro prueba determina si se usa la base de datos oficial o la base _p (ver skill bautista-record-modes).


Testing

Tests unitarios implementados: Tests/Unit/Stock/TipoComprobanteStockServiceTest.php

Cubren:

  • getAll() retorna array de DTOs
  • insert() exitoso con auditoría
  • insert() rechaza descripción duplicada
  • update() exitoso con auditoría
  • update() retorna 404 si código no existe

Tests de integración: No implementados en esta versión.


Notas Adicionales

  • El endpoint legacy backend/mod-stock/tipo-comprobante.php fue eliminado en este refactor y reemplazado por las nuevas capas Slim bajo /mod-stock/tipo-comprobante
  • El campo id_compras en mov_sto está disponible para futura trazabilidad de compras pero sin lógica activa
  • El código autoincremental de dcomprob usa MAX(codigo)+1 en lugar de SERIAL para mantener compatibilidad con datos históricos preexistentes

Última actualización: 2026-03-23 Estado: En desarrollo — Fase 6: DTO Architecture (Input DTOs + FullDTO migration)