Appearance
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
- Tipo de Comprobante de Stock: tipo-comprobante-stock-resource.md
- Movimientos de Stock con Marca de Origen: movimientos-stock-marca-origen-resource.md
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
| Capa | Archivo | Descripción |
|---|---|---|
| Route | Routes/Stock/StockRoutes.php | Endpoints Slim bajo /mod-stock |
| Validator | Validators/Stock/TipoComprobanteStockValidator.php | Validación estructural del request |
| Validator | Validators/Stock/CreateMovimientoStockValidator.php | Validación estructural de movimiento |
| Controller | controller/modulo-stock/TipoComprobanteStockController.php | HTTP in/out para tipo comprobante |
| Controller | controller/modulo-stock/MovimientoStockController.php | HTTP in/out + verificación de módulo habilitado |
| Service | service/Stock/TipoComprobanteStockService.php | Transacciones, auditoría, unicidad de descripción |
| Service | service/Stock/MovimientoStockService.php | Inserción de movimientos de stock |
| Model | models/modulo-stock/TipoComprobanteStock.php | CRUD sobre tabla dcomprob |
| Model | models/modulo-stock/MovimientoStock.php | INSERT sobre tabla mov_sto |
| DTO | Resources/Stock/TipoComprobanteStockDTO.php | 6 campos mapeados desde dcomprob |
| DTO | Resources/Stock/MovimientoStock.php | Campos de movimiento incluyendo id_compras |
| Input DTO | Resources/Stock/CreateTipoComprobanteInput.php | Input DTO para POST /tipo-comprobante (sin codigo, extiende FullDTO) |
| Input DTO | Resources/Stock/UpdateTipoComprobanteInput.php | Input DTO para PUT /tipo-comprobante/{codigo} (con codigo, extiende FullDTO) |
| Input DTO | Resources/Stock/CreateMovimientoInput.php | Input 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:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
descri | string | Sí | Descripción (máx 50 chars) |
tipo | string | Sí | 'I' (Ingreso) o 'E' (Egreso) |
imprimir | bool/string | Sí | true/false o 'S'/'N' |
valor | bool/string | Sí | true/false o 'S'/'N' |
control | bool/string | Sí | true/false o 'S'/'N' |
Códigos de respuesta:
201 Created: Tipo creado con DTO retornado400 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 retornado400 Bad Request: Validación estructural falló404 Not Found: Código no existe422 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 registrados400 Bad Request: Validación estructural falló403 Forbidden: Módulocontrolstockdeshabilitado enPermisosEmpresa422 Unprocessable Entity: Producto no maneja stock
Modelo de Dominio
TipoComprobanteStockService
Archivo: service/Stock/TipoComprobanteStockService.phpImplementa: AuditableInterfaceTraits: Conectable, Auditable
Responsabilidades:
- Orquestar CRUD sobre
TipoComprobanteStockmodel - 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étodo | Descripción | Excepción |
|---|---|---|
getAll(): TipoComprobanteStockDTO[] | Lista todos los registros | — |
getById(int $codigo): ?TipoComprobanteStockDTO | Busca por PK | — |
insert(CreateTipoComprobanteInput $input): TipoComprobanteStockDTO | Crea registro con auditoría | InvalidArgumentException (422 si descri duplicado) |
update(UpdateTipoComprobanteInput $input): TipoComprobanteStockDTO | Actualiza registro con auditoría | InvalidArgumentException (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:
- Intenta leer de
data_config(clavePermisosEmpresa) — permite override en tests - Si la clave
modulo_controlstockestá definida explícitamente endata_config, la usa - Si no, cae al mecanismo estándar:
Config::getOneByKey('PermisosEmpresa')que leesistema.modulos - Si
modulo_controlstock === false→ retorna403
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
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
codigo | INTEGER | PRIMARY KEY | PK autoincremental — calculado como MAX(codigo)+1 en INSERT |
descri | VARCHAR(50) | NOT NULL | Descripción del tipo (única, validada case-insensitive en Service) |
tipo | VARCHAR(1) | NOT NULL | 'I' = Ingreso, 'E' = Egreso |
imprimir | VARCHAR(1) | NOT NULL | 'S' / 'N' — legacy boolean |
valor | VARCHAR(1) | NOT NULL | 'S' / 'N' — legacy boolean |
control | VARCHAR(1) | NOT NULL | 'S' / 'N' — legacy boolean |
Notas:
- No usa
SERIAL— el código se calcula manualmente comoCASE WHEN MAX(codigo) IS NULL THEN 1 ELSE MAX(codigo)+1 END - Los booleanos
imprimir,valor,controlse 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:
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
id_ventas | VARCHAR(36) | NULL | Trazabilidad con comprobante de ventas que generó el movimiento |
marca | VARCHAR(1) | NULL | 'O' = Oficial, 'P' = Prueba, NULL = legacy. Obligatorio para nuevos movimientos |
id_compras | VARCHAR(36) | NULL | Agregado 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étodo | Descripción |
|---|---|
getAll(): TipoComprobanteStockDTO[] | SELECT ordenado por codigo |
getById(int $codigo): ?TipoComprobanteStockDTO | SELECT por PK |
getByDescri(string $descri, ?int $excludeCodigo): array|false | Búsqueda case-insensitive para validar unicidad; excludeCodigo se usa en UPDATE |
insert(array $data): TipoComprobanteStockDTO | INSERT con código autoincremental, RETURNING completo |
update(array $data): ?TipoComprobanteStockDTO | UPDATE 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
| Propiedad | Tipo | Descripción |
|---|---|---|
codigo | int | PK |
descri | string | Descripción |
tipo | string | 'I' o 'E' |
imprimir | string | 'S' o 'N' |
valor | string | 'S' o 'N' |
control | string | '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): selfytoArray(): 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:
| DTO | Propósito | Observación |
|---|---|---|
CreateTipoComprobanteInput | POST /tipo-comprobante | Sin campo codigo (se genera en Model) |
UpdateTipoComprobanteInput | PUT /tipo-comprobante/ | Incluye codigo como campo tipado |
CreateMovimientoInput | POST /movimiento | Campos 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):
| Campo | Reglas |
|---|---|
descri | required, string, max:50 |
tipo | required, string, in:I,E |
imprimir | required |
valor | required |
control | required |
Nivel 2: Validaciones de Negocio (Service)
Ejecutadas en TipoComprobanteStockService, retornan 422 si fallan. El service recibe Input DTOs tipados (no arrays).
| Regla | Descripción |
|---|---|
Unicidad de descri en INSERT | Llama a model->getByDescri($descri). Si retorna resultado → InvalidArgumentException con código 422 |
Unicidad de descri en UPDATE | Llama a model->update($data) que internamente verifica con excludeCodigo |
| Existencia en UPDATE | Si model->update() retorna null → InvalidArgumentException 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 DTOsinsert()exitoso con auditoríainsert()rechaza descripción duplicadaupdate()exitoso con auditoríaupdate()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.phpfue eliminado en este refactor y reemplazado por las nuevas capas Slim bajo/mod-stock/tipo-comprobante - El campo
id_comprasenmov_stoestá disponible para futura trazabilidad de compras pero sin lógica activa - El código autoincremental de
dcomprobusaMAX(codigo)+1en lugar deSERIALpara mantener compatibilidad con datos históricos preexistentes
Última actualización: 2026-03-23 Estado: En desarrollo — Fase 6: DTO Architecture (Input DTOs + FullDTO migration)