From fa13a9760df4151353dfd33170567c188f10bfba Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Sun, 4 Jan 2026 05:27:54 -0600 Subject: [PATCH] chore: Configurar arquitectura de subrepositorios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios principales: - Actualizar .gitmodules: gamilit usa HTTPS (github.com) - Actualizar .gitignore: ignorar proyectos con repos en Gitea - Crear SUBREPOSITORIOS.md: documentacion de arquitectura de repos - Actualizar submodulo gamilit: sincronizado con workspace desarrollo Proyectos removidos del tracking (4050 archivos): - erp-suite, erp-core, erp-construccion, erp-clinicas - erp-retail, erp-mecanicas-diesel, erp-vidrio-templado - trading-platform, betting-analytics, inmobiliaria-analytics - platform_marketing_content Estos proyectos tienen repositorios independientes en Gitea: http://72.60.226.4:3000/rckrdmrd/ 馃 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 20 + .gitmodules | 2 +- SUBREPOSITORIOS.md | 210 + .../simco/SIMCO-PROPAGACION-MEJORAS.md | 162 +- .../propagation/propagate-module-update.sh | 16 +- .../propagation/validate-propagation-chain.sh | 6 +- .../validation/validate-project-structure.sh | 130 + projects/betting-analytics/INVENTARIO.yml | 31 - projects/betting-analytics/README.md | 22 - .../betting-analytics/apps/backend/Dockerfile | 46 - .../apps/backend/package.json | 84 - .../apps/backend/service.descriptor.yml | 54 - .../apps/backend/src/app.module.ts | 32 - .../apps/backend/src/config/index.ts | 23 - .../apps/backend/src/main.ts | 36 - .../backend/src/modules/auth/auth.module.ts | 19 - .../apps/backend/src/shared/types/index.ts | 27 - .../apps/backend/tsconfig.json | 26 - .../apps/frontend/Dockerfile | 42 - projects/betting-analytics/apps/ml/Dockerfile | 47 - projects/betting-analytics/docs/README.md | 38 - projects/betting-analytics/docs/_MAP.md | 40 - .../00-guidelines/CONTEXTO-PROYECTO.md | 136 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 110 - .../00-guidelines/HERENCIA-SIMCO.md | 125 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../orchestration/PROXIMA-ACCION.md | 11 - .../environment/PROJECT-ENV-CONFIG.yml | 47 - .../estados/REGISTRO-SUBAGENTES.json | 9 - .../inventarios/BACKEND_INVENTORY.yml | 32 - .../inventarios/DATABASE_INVENTORY.yml | 28 - .../inventarios/FRONTEND_INVENTORY.yml | 33 - .../inventarios/MASTER_INVENTORY.yml | 59 - .../trazas/TRAZA-TAREAS-BACKEND.md | 40 - .../trazas/TRAZA-TAREAS-DATABASE.md | 40 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 40 - projects/erp-clinicas/.env.example | 182 - projects/erp-clinicas/INVENTARIO.yml | 31 - projects/erp-clinicas/PROJECT-STATUS.md | 178 - .../database/HERENCIA-ERP-CORE.md | 222 - projects/erp-clinicas/database/README.md | 83 - .../database/init/00-extensions.sql | 25 - .../database/init/01-create-schemas.sql | 15 - .../database/init/02-rls-functions.sql | 37 - .../database/init/03-clinical-tables.sql | 628 - .../database/init/04-seed-data.sql | 34 - .../docs/00-vision-general/VISION-CLINICAS.md | 107 - .../CL-001-fundamentos/README.md | 20 - .../CL-002-pacientes/README.md | 20 - .../CL-003-citas/README.md | 20 - .../CL-004-consultas/README.md | 20 - .../CL-005-recetas/README.md | 20 - .../CL-006-laboratorio/README.md | 20 - .../CL-007-farmacia/README.md | 20 - .../CL-008-facturacion/README.md | 20 - .../CL-009-reportes/README.md | 20 - .../CL-010-telemedicina/README.md | 20 - .../CL-011-expediente/README.md | 20 - .../CL-012-imagenologia/README.md | 20 - .../02-definicion-modulos/INDICE-MODULOS.md | 136 - .../docs/08-epicas/EPIC-CL-001-fundamentos.md | 66 - .../docs/08-epicas/EPIC-CL-002-pacientes.md | 247 - .../docs/08-epicas/EPIC-CL-003-citas.md | 270 - .../docs/08-epicas/EPIC-CL-004-consultas.md | 277 - .../docs/08-epicas/EPIC-CL-005-recetas.md | 243 - .../docs/08-epicas/EPIC-CL-006-laboratorio.md | 242 - .../docs/08-epicas/EPIC-CL-007-farmacia.md | 239 - .../docs/08-epicas/EPIC-CL-008-facturacion.md | 268 - .../docs/08-epicas/EPIC-CL-009-reportes.md | 223 - .../08-epicas/EPIC-CL-010-telemedicina.md | 278 - .../docs/08-epicas/EPIC-CL-011-expediente.md | 241 - .../08-epicas/EPIC-CL-012-imagenologia.md | 314 - projects/erp-clinicas/docs/README.md | 38 - projects/erp-clinicas/docs/_MAP.md | 40 - .../00-guidelines/CONTEXTO-PROYECTO.md | 159 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 89 - .../00-guidelines/HERENCIA-ERP-CORE.md | 184 - .../00-guidelines/HERENCIA-SIMCO.md | 138 - .../00-guidelines/HERENCIA-SPECS-CORE.md | 199 - .../00-guidelines/HERENCIA-SPECS-ERP-CORE.md | 162 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../orchestration/PROXIMA-ACCION.md | 122 - .../DIRECTIVA-EXPEDIENTE-CLINICO.md | 242 - .../directivas/DIRECTIVA-GESTION-CITAS.md | 242 - .../environment/PROJECT-ENV-CONFIG.yml | 262 - .../inventarios/BACKEND_INVENTORY.yml | 80 - .../inventarios/DATABASE_INVENTORY.yml | 158 - .../inventarios/DEPENDENCY_GRAPH.yml | 61 - .../inventarios/FRONTEND_INVENTORY.yml | 57 - .../inventarios/MASTER_INVENTORY.yml | 213 - .../orchestration/inventarios/README.md | 103 - .../inventarios/TRACEABILITY_MATRIX.yml | 514 - .../prompts/PROMPT-CL-BACKEND-AGENT.md | 182 - .../referencias/DEPENDENCIAS-ERP-CORE.yml | 97 - .../referencias/DEPENDENCIAS-SHARED.yml | 62 - .../trazas/TRAZA-TAREAS-BACKEND.md | 38 - .../trazas/TRAZA-TAREAS-DATABASE.md | 38 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 38 - projects/erp-construccion/.env.example | 119 - .../erp-construccion/.github/workflows/ci.yml | 263 - projects/erp-construccion/CONTRIBUTING.md | 478 - projects/erp-construccion/INVENTARIO.yml | 31 - projects/erp-construccion/PROJECT-STATUS.md | 310 - projects/erp-construccion/README.md | 364 - .../erp-construccion/backend/.env.example | 71 - projects/erp-construccion/backend/Dockerfile | 84 - projects/erp-construccion/backend/README.md | 461 - .../backend/package-lock.json | 7817 --------- .../erp-construccion/backend/package.json | 73 - .../backend/scripts/sync-enums.ts | 120 - .../scripts/validate-constants-usage.ts | 385 - .../backend/service.descriptor.yml | 169 - .../admin/controllers/audit-log.controller.ts | 229 - .../admin/controllers/backup.controller.ts | 283 - .../controllers/cost-center.controller.ts | 280 - .../src/modules/admin/controllers/index.ts | 9 - .../controllers/system-setting.controller.ts | 370 - .../admin/entities/audit-log.entity.ts | 256 - .../modules/admin/entities/backup.entity.ts | 301 - .../admin/entities/cost-center.entity.ts | 208 - .../entities/custom-permission.entity.ts | 161 - .../src/modules/admin/entities/index.ts | 10 - .../admin/entities/system-setting.entity.ts | 180 - .../admin/services/audit-log.service.ts | 309 - .../modules/admin/services/backup.service.ts | 308 - .../admin/services/cost-center.service.ts | 336 - .../src/modules/admin/services/index.ts | 9 - .../admin/services/system-setting.service.ts | 336 - .../auth/controllers/auth.controller.ts | 268 - .../src/modules/auth/controllers/index.ts | 5 - .../backend/src/modules/auth/dto/auth.dto.ts | 70 - .../src/modules/auth/entities/index.ts | 8 - .../auth/entities/permission.entity.ts | 34 - .../auth/entities/refresh-token.entity.ts | 73 - .../src/modules/auth/entities/role.entity.ts | 58 - .../modules/auth/entities/user-role.entity.ts | 54 - .../backend/src/modules/auth/index.ts | 14 - .../auth/middleware/auth.middleware.ts | 178 - .../src/modules/auth/services/auth.service.ts | 370 - .../src/modules/auth/services/index.ts | 5 - .../controllers/bid-analytics.controller.ts | 175 - .../controllers/bid-budget.controller.ts | 254 - .../bidding/controllers/bid.controller.ts | 370 - .../src/modules/bidding/controllers/index.ts | 9 - .../controllers/opportunity.controller.ts | 266 - .../bidding/entities/bid-budget.entity.ts | 256 - .../bidding/entities/bid-calendar.entity.ts | 188 - .../bidding/entities/bid-competitor.entity.ts | 203 - .../bidding/entities/bid-document.entity.ts | 170 - .../bidding/entities/bid-team.entity.ts | 176 - .../modules/bidding/entities/bid.entity.ts | 311 - .../src/modules/bidding/entities/index.ts | 12 - .../bidding/entities/opportunity.entity.ts | 280 - .../bidding/services/bid-analytics.service.ts | 385 - .../bidding/services/bid-budget.service.ts | 388 - .../modules/bidding/services/bid.service.ts | 384 - .../src/modules/bidding/services/index.ts | 9 - .../bidding/services/opportunity.service.ts | 392 - .../controllers/concepto.controller.ts | 252 - .../src/modules/budgets/controllers/index.ts | 7 - .../controllers/presupuesto.controller.ts | 287 - .../budgets/entities/concepto.entity.ts | 100 - .../src/modules/budgets/entities/index.ts | 8 - .../entities/presupuesto-partida.entity.ts | 95 - .../budgets/entities/presupuesto.entity.ts | 107 - .../budgets/services/concepto.service.ts | 160 - .../src/modules/budgets/services/index.ts | 6 - .../budgets/services/presupuesto.service.ts | 262 - .../controllers/etapa.controller.ts | 181 - .../controllers/fraccionamiento.controller.ts | 157 - .../modules/construction/controllers/index.ts | 11 - .../controllers/lote.controller.ts | 273 - .../controllers/manzana.controller.ts | 180 - .../controllers/prototipo.controller.ts | 181 - .../controllers/proyecto.controller.ts | 165 - .../construction/entities/etapa.entity.ts | 83 - .../entities/fraccionamiento.entity.ts | 95 - .../modules/construction/entities/index.ts | 11 - .../construction/entities/lote.entity.ts | 92 - .../construction/entities/manzana.entity.ts | 65 - .../construction/entities/prototipo.entity.ts | 85 - .../construction/entities/proyecto.entity.ts | 88 - .../construction/services/etapa.service.ts | 163 - .../services/fraccionamiento.service.ts | 117 - .../modules/construction/services/index.ts | 11 - .../construction/services/lote.service.ts | 230 - .../construction/services/manzana.service.ts | 149 - .../services/prototipo.service.ts | 173 - .../construction/services/proyecto.service.ts | 117 - .../controllers/contract.controller.ts | 415 - .../modules/contracts/controllers/index.ts | 7 - .../controllers/subcontractor.controller.ts | 257 - .../entities/contract-addendum.entity.ts | 114 - .../contracts/entities/contract.entity.ts | 192 - .../src/modules/contracts/entities/index.ts | 10 - .../entities/subcontractor.entity.ts | 123 - .../contracts/services/contract.service.ts | 422 - .../src/modules/contracts/services/index.ts | 7 - .../services/subcontractor.service.ts | 270 - .../src/modules/core/entities/index.ts | 6 - .../modules/core/entities/tenant.entity.ts | 50 - .../src/modules/core/entities/user.entity.ts | 78 - .../controllers/anticipo.controller.ts | 324 - .../controllers/estimacion.controller.ts | 408 - .../controllers/fondo-garantia.controller.ts | 299 - .../modules/estimates/controllers/index.ts | 9 - .../controllers/retencion.controller.ts | 241 - .../estimates/entities/amortizacion.entity.ts | 86 - .../estimates/entities/anticipo.entity.ts | 134 - .../entities/estimacion-concepto.entity.ts | 122 - .../entities/estimacion-workflow.entity.ts | 87 - .../estimates/entities/estimacion.entity.ts | 173 - .../entities/fondo-garantia.entity.ts | 92 - .../estimates/entities/generador.entity.ts | 125 - .../src/modules/estimates/entities/index.ts | 13 - .../estimates/entities/retencion.entity.ts | 99 - .../estimates/services/anticipo.service.ts | 351 - .../estimates/services/estimacion.service.ts | 424 - .../services/fondo-garantia.service.ts | 275 - .../src/modules/estimates/services/index.ts | 9 - .../estimates/services/retencion.service.ts | 258 - .../controllers/accounting.controller.ts | 385 - .../finance/controllers/ap.controller.ts | 264 - .../finance/controllers/ar.controller.ts | 310 - .../bank-reconciliation.controller.ts | 434 - .../controllers/cash-flow.controller.ts | 295 - .../src/modules/finance/controllers/index.ts | 11 - .../finance/controllers/reports.controller.ts | 410 - .../entities/account-payable.entity.ts | 233 - .../entities/account-receivable.entity.ts | 224 - .../entities/accounting-entry-line.entity.ts | 131 - .../entities/accounting-entry.entity.ts | 185 - .../finance/entities/ap-payment.entity.ts | 154 - .../finance/entities/ar-payment.entity.ts | 154 - .../finance/entities/bank-account.entity.ts | 199 - .../finance/entities/bank-movement.entity.ts | 189 - .../entities/bank-reconciliation.entity.ts | 225 - .../entities/cash-flow-projection.entity.ts | 357 - .../entities/chart-of-accounts.entity.ts | 151 - .../src/modules/finance/entities/index.ts | 16 - .../backend/src/modules/finance/index.ts | 13 - .../finance/services/accounting.service.ts | 813 - .../modules/finance/services/ap.service.ts | 673 - .../modules/finance/services/ar.service.ts | 728 - .../services/bank-reconciliation.service.ts | 846 - .../finance/services/cash-flow.service.ts | 701 - .../services/erp-integration.service.ts | 699 - .../services/financial-reports.service.ts | 893 - .../src/modules/finance/services/index.ts | 12 - .../hr/controllers/employee.controller.ts | 342 - .../src/modules/hr/controllers/index.ts | 7 - .../hr/controllers/puesto.controller.ts | 193 - .../employee-fraccionamiento.entity.ts | 65 - .../modules/hr/entities/employee.entity.ts | 136 - .../backend/src/modules/hr/entities/index.ts | 8 - .../src/modules/hr/entities/puesto.entity.ts | 68 - .../modules/hr/services/employee.service.ts | 330 - .../backend/src/modules/hr/services/index.ts | 7 - .../src/modules/hr/services/puesto.service.ts | 149 - .../hse/controllers/ambiental.controller.ts | 598 - .../controllers/capacitacion.controller.ts | 223 - .../modules/hse/controllers/epp.controller.ts | 464 - .../hse/controllers/incidente.controller.ts | 398 - .../src/modules/hse/controllers/index.ts | 28 - .../hse/controllers/indicador.controller.ts | 354 - .../hse/controllers/inspeccion.controller.ts | 400 - .../controllers/permiso-trabajo.controller.ts | 323 - .../hse/controllers/stps.controller.ts | 794 - .../hse/entities/alerta-indicador.entity.ts | 76 - .../hse/entities/almacen-temporal.entity.ts | 71 - .../modules/hse/entities/auditoria.entity.ts | 82 - .../entities/capacitacion-asistente.entity.ts | 66 - .../entities/capacitacion-matriz.entity.ts | 53 - .../entities/capacitacion-sesion.entity.ts | 96 - .../hse/entities/capacitacion.entity.ts | 74 - .../hse/entities/checklist-item.entity.ts | 54 - .../entities/comision-integrante.entity.ts | 65 - .../hse/entities/comision-recorrido.entity.ts | 70 - .../hse/entities/comision-seguridad.entity.ts | 83 - .../hse/entities/constancia-dc3.entity.ts | 74 - .../hse/entities/cumplimiento-obra.entity.ts | 93 - .../hse/entities/dias-sin-accidente.entity.ts | 63 - .../hse/entities/documento-stps.entity.ts | 89 - .../hse/entities/epp-asignacion.entity.ts | 109 - .../modules/hse/entities/epp-baja.entity.ts | 64 - .../hse/entities/epp-catalogo.entity.ts | 86 - .../hse/entities/epp-inspeccion.entity.ts | 65 - .../hse/entities/epp-inventario.entity.ts | 62 - .../hse/entities/epp-matriz-puesto.entity.ts | 56 - .../hse/entities/epp-movimiento.entity.ts | 75 - .../hse/entities/hallazgo-evidencia.entity.ts | 67 - .../modules/hse/entities/hallazgo.entity.ts | 133 - .../hse/entities/horas-trabajadas.entity.ts | 70 - .../hse/entities/impacto-ambiental.entity.ts | 101 - .../hse/entities/incidente-accion.entity.ts | 71 - .../entities/incidente-evidencia.entity.ts | 62 - .../incidente-investigacion.entity.ts | 73 - .../entities/incidente-involucrado.entity.ts | 58 - .../modules/hse/entities/incidente.entity.ts | 111 - .../backend/src/modules/hse/entities/index.ts | 82 - .../hse/entities/indicador-config.entity.ts | 85 - .../entities/indicador-meta-obra.entity.ts | 54 - .../hse/entities/indicador-valor.entity.ts | 86 - .../entities/inspeccion-evaluacion.entity.ts | 58 - .../modules/hse/entities/inspeccion.entity.ts | 124 - .../modules/hse/entities/instructor.entity.ts | 69 - .../hse/entities/manifiesto-detalle.entity.ts | 60 - .../entities/manifiesto-residuos.entity.ts | 95 - .../hse/entities/norma-requisito.entity.ts | 51 - .../modules/hse/entities/norma-stps.entity.ts | 61 - .../entities/permiso-autorizacion.entity.ts | 67 - .../hse/entities/permiso-checklist.entity.ts | 64 - .../hse/entities/permiso-documento.entity.ts | 52 - .../hse/entities/permiso-evento.entity.ts | 62 - .../hse/entities/permiso-monitoreo.entity.ts | 62 - .../hse/entities/permiso-personal.entity.ts | 64 - .../hse/entities/permiso-trabajo.entity.ts | 141 - .../hse/entities/programa-actividad.entity.ts | 79 - .../entities/programa-inspeccion.entity.ts | 85 - .../hse/entities/programa-seguridad.entity.ts | 85 - .../entities/proveedor-ambiental.entity.ts | 78 - .../hse/entities/queja-ambiental.entity.ts | 89 - .../hse/entities/reporte-programado.entity.ts | 77 - .../hse/entities/residuo-catalogo.entity.ts | 59 - .../hse/entities/residuo-generacion.entity.ts | 105 - .../hse/entities/tipo-inspeccion.entity.ts | 76 - .../entities/tipo-permiso-trabajo.entity.ts | 68 - .../modules/hse/services/ambiental.service.ts | 632 - .../hse/services/capacitacion.service.ts | 159 - .../src/modules/hse/services/epp.service.ts | 442 - .../modules/hse/services/incidente.service.ts | 396 - .../backend/src/modules/hse/services/index.ts | 32 - .../modules/hse/services/indicador.service.ts | 282 - .../hse/services/inspeccion.service.ts | 354 - .../hse/services/permiso-trabajo.service.ts | 298 - .../src/modules/hse/services/stps.service.ts | 675 - .../controllers/asignacion.controller.ts | 240 - .../controllers/derechohabiente.controller.ts | 291 - .../modules/infonavit/controllers/index.ts | 7 - .../entities/acta-vivienda.entity.ts | 88 - .../modules/infonavit/entities/acta.entity.ts | 101 - .../entities/asignacion-vivienda.entity.ts | 116 - .../entities/derechohabiente.entity.ts | 119 - .../entities/historico-puntos.entity.ts | 75 - .../src/modules/infonavit/entities/index.ts | 15 - .../entities/oferta-vivienda.entity.ts | 96 - .../entities/registro-infonavit.entity.ts | 94 - .../entities/reporte-infonavit.entity.ts | 109 - .../infonavit/services/asignacion.service.ts | 290 - .../services/derechohabiente.service.ts | 328 - .../src/modules/infonavit/services/index.ts | 7 - .../controllers/consumo-obra.controller.ts | 189 - .../modules/inventory/controllers/index.ts | 7 - .../controllers/requisicion.controller.ts | 363 - .../entities/almacen-proyecto.entity.ts | 92 - .../inventory/entities/consumo-obra.entity.ts | 113 - .../src/modules/inventory/entities/index.ts | 11 - .../entities/requisicion-linea.entity.ts | 115 - .../entities/requisicion-obra.entity.ts | 117 - .../services/consumo-obra.service.ts | 200 - .../src/modules/inventory/services/index.ts | 7 - .../inventory/services/requisicion.service.ts | 339 - .../controllers/avance-obra.controller.ts | 303 - .../controllers/bitacora-obra.controller.ts | 245 - .../src/modules/progress/controllers/index.ts | 7 - .../progress/entities/avance-obra.entity.ts | 127 - .../progress/entities/bitacora-obra.entity.ts | 102 - .../progress/entities/foto-avance.entity.ts | 87 - .../src/modules/progress/entities/index.ts | 10 - .../entities/programa-actividad.entity.ts | 107 - .../progress/entities/programa-obra.entity.ts | 91 - .../progress/services/avance-obra.service.ts | 284 - .../services/bitacora-obra.service.ts | 209 - .../src/modules/progress/services/index.ts | 7 - .../controllers/comparativo.controller.ts | 308 - .../src/modules/purchase/controllers/index.ts | 6 - .../comparativo-cotizaciones.entity.ts | 101 - .../entities/comparativo-producto.entity.ts | 75 - .../entities/comparativo-proveedor.entity.ts | 87 - .../src/modules/purchase/entities/index.ts | 10 - .../purchase/services/comparativo.service.ts | 311 - .../src/modules/purchase/services/index.ts | 6 - .../src/modules/quality/controllers/index.ts | 7 - .../controllers/inspection.controller.ts | 254 - .../quality/controllers/ticket.controller.ts | 308 - .../quality/entities/checklist-item.entity.ts | 69 - .../quality/entities/checklist.entity.ts | 89 - .../entities/corrective-action.entity.ts | 100 - .../src/modules/quality/entities/index.ts | 15 - .../entities/inspection-result.entity.ts | 70 - .../quality/entities/inspection.entity.ts | 120 - .../quality/entities/non-conformity.entity.ts | 126 - .../entities/post-sale-ticket.entity.ts | 123 - .../entities/ticket-assignment.entity.ts | 92 - .../src/modules/quality/services/index.ts | 7 - .../quality/services/inspection.service.ts | 317 - .../quality/services/ticket.service.ts | 395 - .../controllers/dashboard.controller.ts | 504 - .../src/modules/reports/controllers/index.ts | 8 - .../reports/controllers/kpi.controller.ts | 349 - .../reports/controllers/report.controller.ts | 373 - .../entities/dashboard-widget.entity.ts | 222 - .../reports/entities/dashboard.entity.ts | 205 - .../src/modules/reports/entities/index.ts | 10 - .../reports/entities/kpi-snapshot.entity.ts | 220 - .../entities/report-execution.entity.ts | 191 - .../modules/reports/entities/report.entity.ts | 222 - .../reports/services/dashboard.service.ts | 471 - .../src/modules/reports/services/index.ts | 8 - .../modules/reports/services/kpi.service.ts | 425 - .../reports/services/report.service.ts | 364 - .../src/modules/users/controllers/index.ts | 1 - .../users/controllers/users.controller.ts | 270 - .../backend/src/modules/users/index.ts | 10 - .../src/modules/users/services/index.ts | 1 - .../modules/users/services/users.service.ts | 254 - .../erp-construccion/backend/src/server.ts | 364 - .../src/shared/constants/api.constants.ts | 249 - .../shared/constants/database.constants.ts | 315 - .../src/shared/constants/enums.constants.ts | 494 - .../backend/src/shared/constants/index.ts | 194 - .../src/shared/database/typeorm.config.ts | 62 - .../src/shared/interfaces/base.interface.ts | 79 - .../src/shared/services/base.service.ts | 217 - .../backend/src/shared/services/index.ts | 5 - .../erp-construccion/backend/tsconfig.json | 36 - .../database/HERENCIA-ERP-CORE.md | 362 - projects/erp-construccion/database/README.md | 185 - .../database/VALIDACION-DDL-INVENTARIOS.md | 323 - projects/erp-construccion/database/_MAP.md | 306 - .../database/drop-and-recreate-database.sh | 188 - .../init-scripts/01-init-database.sql | 317 - .../schemas/01-construction-schema-ddl.sql | 903 - .../database/schemas/02-hr-schema-ddl.sql | 156 - .../database/schemas/03-hse-schema-ddl.sql | 1268 -- .../schemas/04-estimates-schema-ddl.sql | 415 - .../schemas/05-infonavit-schema-ddl.sql | 413 - .../schemas/06-inventory-ext-schema-ddl.sql | 213 - .../schemas/07-purchase-ext-schema-ddl.sql | 227 - .../database/validate-clean-load-policy.sh | 153 - .../devops/scripts/sync-enums.ts | 120 - .../scripts/validate-constants-usage.ts | 385 - .../erp-construccion/docker-compose.prod.yml | 129 - projects/erp-construccion/docker-compose.yml | 184 - .../00-vision-general/ARQUITECTURA-SAAS.md | 1303 -- .../00-vision-general/CAMBIOS-SAAS-MVP.md | 656 - .../docs/00-vision-general/GLOSARIO.md | 589 - .../MARKETPLACE-EXTENSIONES.md | 1081 -- .../docs/00-vision-general/MVP-APP.md | 1592 -- .../00-vision-general/PORTAL-ADMIN-SAAS.md | 749 - .../MAPEO-MAI-TO-MGN.md | 477 - .../docs/01-analisis-referencias/README.md | 71 - .../erp-generico/README.md | 90 - .../01-analisis-referencias/gamilit/README.md | 93 - .../odoo/ODOO-CONSTRUCCION-MAPPING.md | 373 - .../01-analisis-referencias/odoo/README.md | 82 - .../ANALISIS-REUTILIZACION-GAMILIT.md | 477 - .../CAMBIOS-Y-ACTUALIZACIONES.md | 429 - .../MAA-017-seguridad-hse/README.md | 167 - .../requerimientos/INDICE-RF-MAA017.md | 53 - .../RF-MAA017-001-gestion-incidentes.md | 140 - .../RF-MAA017-002-control-capacitaciones.md | 512 - .../RF-MAA017-003-inspecciones-seguridad.md | 405 - .../RF-MAA017-004-control-epp.md | 378 - .../RF-MAA017-005-cumplimiento-stps.md | 470 - .../RF-MAA017-006-gestion-ambiental.md | 429 - .../RF-MAA017-007-permisos-trabajo.md | 470 - .../RF-MAA017-008-indicadores-hse.md | 453 - .../implementacion/TRACEABILITY.yml | 577 - .../implementacion/TRACEABILITY.yml | 1420 -- .../implementacion/TRACEABILITY.yml | 1060 -- .../implementacion/TRACEABILITY.yml | 833 - .../implementacion/TRACEABILITY.yml | 640 - .../implementacion/TRACEABILITY.yml | 757 - .../implementacion/TRACEABILITY.yml | 1583 -- .../MAE-014-finanzas-controlling/README.md | 83 - .../MAE-014-finanzas-controlling/_MAP.md | 698 - .../ET-FIN-001-modelo de datos financiero.md | 51 - ...T-FIN-002-servicio de flujo de efectivo.md | 41 - .../ET-FIN-003-servicio de facturaci贸n.md | 34 - .../ET-FIN-004-conciliaci贸n bancaria.md | 39 - ...FIN-005-dashboard de control de gesti贸n.md | 35 - .../US-FIN-001-proyectar flujo de efectivo.md | 16 - ...US-FIN-002-registrar movimientos reales.md | 16 - ...-FIN-003-crear factura desde estimaci贸n.md | 16 - .../US-FIN-004-gestionar cobranza.md | 16 - .../US-FIN-005-aplicar pagos de clientes.md | 16 - ...-FIN-006-registrar factura de proveedor.md | 16 - .../US-FIN-007-programar pagos.md | 16 - .../US-FIN-008-importar estado de cuenta.md | 16 - ...FIN-009-conciliar movimientos bancarios.md | 16 - .../US-FIN-010-dashboard financiero.md | 16 - .../US-FIN-011-an谩lisis de rentabilidad.md | 16 - .../RF-FIN-001-flujo de efectivo.md | 22 - .../RF-FIN-002-cuentas por cobrar.md | 22 - .../RF-FIN-003-cuentas por pagar.md | 22 - .../RF-FIN-004-conciliaci贸n bancaria.md | 22 - .../RF-FIN-005-control de gesti贸n.md | 22 - .../MAE-015-activos-maquinaria/README.md | 85 - .../MAE-015-activos-maquinaria/_MAP.md | 457 - .../ET-AST-001-modelo de datos de activos.md | 41 - ...-AST-002-servicio de gesti贸n de activos.md | 38 - .../ET-AST-003-motor de mantenimiento.md | 42 - .../ET-AST-004-control de herramientas.md | 42 - .../ET-AST-005-c谩lculo de depreciaci贸n.md | 44 - .../US-AST-001-registrar activo.md | 16 - .../US-AST-002-asignar activo a proyecto.md | 16 - .../US-AST-003-programar mantenimiento.md | 16 - ...T-004-registrar mantenimiento realizado.md | 16 - .../US-AST-005-vale de herramienta.md | 16 - .../US-AST-006-devoluci贸n de herramienta.md | 16 - .../US-AST-007-dashboard de activos.md | 16 - .../US-AST-008-reporte de depreciaci贸n.md | 16 - .../RF-AST-001-cat谩logo de activos.md | 22 - .../RF-AST-002-asignaci贸n a proyectos.md | 22 - .../RF-AST-003-mantenimiento preventivo.md | 22 - .../RF-AST-004-control de herramientas.md | 22 - .../RF-AST-005-depreciaci贸n contable.md | 22 - .../implementacion/TRACEABILITY.yml | 2000 --- .../MAE-016-gestion-documental/README.md | 90 - .../MAE-016-gestion-documental/_MAP.md | 568 - .../ET-DOC-001-modelo de datos documental.md | 51 - ...T-DOC-002-servicio de almacenamiento s3.md | 50 - .../ET-DOC-003-servicio de versionamiento.md | 53 - ...T-DOC-004-servicio de firma electr贸nica.md | 51 - .../ET-DOC-005-workflow de aprobaci贸n.md | 46 - .../US-DOC-001-subir documento.md | 16 - .../US-DOC-002-buscar y descargar.md | 16 - .../US-DOC-003-crear nueva versi贸n.md | 16 - .../US-DOC-004-firmar documento.md | 16 - .../US-DOC-005-compartir documento.md | 16 - .../US-DOC-006-aprobar documento.md | 16 - .../US-DOC-007-dashboard documental.md | 16 - ...RF-DOC-001-repositorio y almacenamiento.md | 22 - .../RF-DOC-002-versionamiento.md | 22 - .../RF-DOC-003-firma electr贸nica.md | 22 - .../RF-DOC-004-control de acceso.md | 22 - .../RF-DOC-005-workflow de aprobaci贸n.md | 22 - .../implementacion/TRACEABILITY.yml | 816 - .../MAI-001-fundamentos/README.md | 662 - .../MAI-001-fundamentos/_MAP.md | 195 - .../especificaciones/ET-AUTH-001-rbac.md | 1295 -- .../ET-AUTH-002-estados-cuenta.md | 1272 -- .../ET-AUTH-003-multi-tenancy.md | 1336 -- .../US-FUND-001-autenticacion-basica-jwt.md | 567 - ...-FUND-002-perfiles-usuario-construccion.md | 739 - .../US-FUND-003-dashboard-por-rol.md | 569 - .../US-FUND-004-infraestructura-base.md | 1612 -- .../US-FUND-005-sistema-sesiones.md | 1065 -- .../US-FUND-006-api-restful-base.md | 1187 -- .../US-FUND-007-navegacion-routing.md | 976 -- .../US-FUND-008-ui-ux-base.md | 972 -- .../implementacion/TRACEABILITY.yml | 525 - .../RF-AUTH-001-roles-construccion.md | 568 - .../RF-AUTH-002-estados-cuenta.md | 1644 -- .../RF-AUTH-003-multi-tenancy.md | 1507 -- .../MAI-002-proyectos-estructura/README.md | 481 - .../RESUMEN-EPICA-MAI-002.md | 832 - .../MAI-002-proyectos-estructura/_MAP.md | 547 - .../especificaciones/ET-PROJ-001-backend.md | 2880 ---- .../especificaciones/ET-PROJ-001-database.md | 2561 --- .../especificaciones/ET-PROJ-001-frontend.md | 2382 --- ...J-001-implementacion-catalogo-proyectos.md | 1021 -- ...02-implementacion-estructura-jerarquica.md | 3277 ---- .../ET-PROJ-003-implementacion-prototipos.md | 1961 --- ...OJ-004-implementacion-equipo-calendario.md | 1844 -- .../US-PROJ-001-catalogo-proyectos.md | 573 - .../US-PROJ-002-transiciones-estado.md | 590 - .../US-PROJ-003-estructura-fraccionamiento.md | 354 - .../US-PROJ-004-estructura-torre-vertical.md | 104 - .../US-PROJ-005-gestion-prototipos.md | 156 - ...US-PROJ-006-asignacion-prototipos-lotes.md | 167 - .../US-PROJ-007-asignacion-equipo.md | 194 - .../US-PROJ-008-calendario-hitos.md | 206 - .../US-PROJ-009-alertas-fechas-criticas.md | 220 - .../ET-PROJ-001-rls-policies.sql | 367 - .../ET-PROJ-002-rls-policies.sql | 530 - .../implementacion/TRACEABILITY.yml | 306 - .../RF-PROJ-001-catalogo-proyectos.md | 618 - .../RF-PROJ-002-estructura-jerarquica-obra.md | 694 - .../RF-PROJ-003-prototipos-vivienda.md | 601 - ...F-PROJ-004-asignacion-equipo-calendario.md | 577 - .../MAI-003-presupuestos-costos/README.md | 650 - .../RESUMEN-EPICA-MAI-003.md | 710 - .../MAI-003-presupuestos-costos/_MAP.md | 860 - .../especificaciones/ET-COST-001-backend.md | 2239 --- .../especificaciones/ET-COST-001-database.md | 1722 -- .../especificaciones/ET-COST-001-frontend.md | 2493 --- ...T-001-implementacion-catalogo-conceptos.md | 1200 -- ...ET-COST-002-implementacion-presupuestos.md | 443 - ...-COST-003-implementacion-control-costos.md | 527 - ...04-implementacion-analisis-rentabilidad.md | 508 - .../US-COST-001-catalogo-conceptos.md | 113 - .../US-COST-002-precios-compuestos.md | 98 - .../US-COST-003-actualizacion-precios.md | 94 - .../US-COST-004-presupuesto-obra.md | 107 - .../US-COST-005-presupuesto-prototipo.md | 108 - .../US-COST-006-dashboard-control-costos.md | 108 - .../US-COST-007-analisis-desviaciones.md | 139 - .../US-COST-008-analisis-rentabilidad.md | 175 - .../ET-COST-001-002-rls-policies.sql | 492 - .../implementacion/TRACEABILITY.yml | 1308 -- .../RF-COST-001-catalogo-conceptos-precios.md | 636 - .../RF-COST-002-presupuestos-maestros.md | 664 - .../RF-COST-003-control-costos-reales.md | 658 - .../RF-COST-004-analisis-rentabilidad.md | 610 - .../MAI-004-compras-inventarios/README.md | 782 - .../RESUMEN-EPICA-MAI-004.md | 405 - .../MAI-004-compras-inventarios/_MAP.md | 811 - .../especificaciones/ET-COMP-001-backend.md | 3326 ---- .../especificaciones/ET-COMP-001-database.md | 2010 --- .../especificaciones/ET-COMP-001-frontend.md | 1566 -- ...ET-PURCH-001-implementacion-proveedores.md | 266 - ...02-implementacion-requisiciones-ordenes.md | 1012 -- .../ET-PURCH-003-implementacion-almacenes.md | 958 - ...PURCH-004-implementacion-kardex-alertas.md | 964 -- .../US-PURCH-001-registro-proveedor.md | 135 - .../US-PURCH-002-solicitud-cotizaciones.md | 199 - .../US-PURCH-003-crear-requisicion-obra.md | 183 - ...-PURCH-004-aprobar-generar-orden-compra.md | 233 - .../US-PURCH-005-recibir-material-almacen.md | 221 - ...PURCH-006-control-almacenes-movimientos.md | 241 - .../US-PURCH-007-kardex-analisis-consumo.md | 236 - ...PURCH-008-dashboard-inventarios-alertas.md | 283 - .../implementacion/ET-PURCH-rls-policies.sql | 468 - .../implementacion/TRACEABILITY.yml | 610 - .../RF-PURCH-001-catalogo-proveedores.md | 511 - ...-PURCH-002-requisiciones-ordenes-compra.md | 264 - .../RF-PURCH-003-almacenes-inventarios.md | 358 - .../RF-PURCH-004-kardex-alertas.md | 365 - .../RESUMEN-EPICA-MAI-005.md | 695 - ...001-implementacion-programacion-curva-s.md | 1863 -- ...PROG-002-implementacion-captura-avances.md | 1377 -- ...03-implementacion-evidencias-checklists.md | 1169 -- ...G-004-implementacion-dashboard-reportes.md | 1146 -- .../US-PROG-001-crear-programa-obra.md | 263 - .../US-PROG-002-seguimiento-curva-s.md | 208 - .../US-PROG-003-capturar-avances-obra.md | 277 - .../US-PROG-004-aprobar-avances.md | 281 - .../US-PROG-005-evidencias-fotograficas.md | 357 - .../US-PROG-006-checklists-calidad.md | 364 - .../US-PROG-007-dashboard-ejecutivo.md | 350 - .../US-PROG-008-reportes-oficiales.md | 443 - .../implementacion/ET-WORK-rls-policies.sql | 412 - .../RF-PROG-001-programacion-curva-s.md | 402 - .../RF-PROG-002-captura-avances-fisicos.md | 421 - .../RF-PROG-003-evidencias-checklists.md | 528 - .../RF-PROG-004-dashboard-reportes-avances.md | 511 - .../MAI-005-control-obra/README.md | 791 - .../MAI-005-control-obra/_MAP.md | 578 - .../especificaciones/ET-OBRA-001-backend.md | 2987 ---- .../especificaciones/ET-OBRA-001-database.md | 1326 -- .../especificaciones/ET-OBRA-001-frontend.md | 1512 -- .../implementacion/TRACEABILITY.yml | 269 - .../MAI-006-calidad/README.md | 489 - .../MAI-006-calidad/_MAP.md | 662 - .../especificaciones/ET-CAL-001-backend.md | 2816 --- .../especificaciones/ET-CAL-001-database.md | 1341 -- .../especificaciones/ET-CAL-001-frontend.md | 1556 -- .../implementacion/TRACEABILITY.yml | 1912 -- .../RESUMEN-EPICA-MAI-006.md | 1361 -- ...-001-implementacion-reportes-ejecutivos.md | 2365 --- ...-implementacion-dashboards-interactivos.md | 2105 --- ...-003-implementacion-analisis-predictivo.md | 1839 -- ...implementacion-exportacion-distribucion.md | 1741 -- .../US-BI-001-dashboard-corporativo.md | 325 - .../US-BI-002-analisis-margenes.md | 370 - .../US-BI-003-dashboards-personalizables.md | 437 - .../US-BI-004-drill-down-filtros.md | 466 - .../US-BI-005-predicciones-ml.md | 541 - .../US-BI-006-simulacion-escenarios.md | 639 - .../US-BI-007-reportes-programados.md | 734 - .../US-BI-008-integracion-powerbi-tableau.md | 733 - ...BI-001-reportes-ejecutivos-consolidados.md | 579 - .../RF-BI-002-dashboards-interactivos.md | 1372 -- ...-BI-003-analisis-predictivo-forecasting.md | 1628 -- .../RF-BI-004-exportacion-distribucion.md | 1790 -- .../MAI-007-rrhh-asistencias/_MAP.md | 467 - .../ET-HR-001-empleados-cuadrillas.md | 1151 -- .../ET-HR-002-asistencia-biometrica.md | 948 - .../ET-HR-003-costeo-mano-obra.md | 497 - .../ET-HR-004-integracion-imss.md | 379 - .../ET-HR-005-integracion-infonavit.md | 423 - ...US-HR-001-catalogo-empleados-cuadrillas.md | 824 - ...-HR-002-app-movil-asistencia-biometrica.md | 1234 -- .../US-HR-003-costeo-mano-obra.md | 870 - .../US-HR-004-integracion-nomina-externa.md | 978 -- .../US-HR-005-exportacion-imss-infonavit.md | 823 - .../US-HR-006-reportes-asistencia.md | 712 - .../RF-HR-001-empleados-cuadrillas.md | 790 - .../RF-HR-002-asistencia-biometrica.md | 554 - .../RF-HR-003-costeo-mano-obra.md | 401 - .../RF-HR-004-integracion-imss.md | 254 - .../RF-HR-005-integracion-infonavit.md | 314 - ...F-RRHH-001-catalogo-personal-cuadrillas.md | 1382 -- .../MAI-007-seguridad-industrial/README.md | 809 - .../especificaciones/ET-SEG-001-backend.md | 3201 ---- .../especificaciones/ET-SEG-001-database.md | 1974 --- .../especificaciones/ET-SEG-001-frontend.md | 1974 --- .../implementacion/TRACEABILITY.yml | 574 - .../README.md | 281 - .../MAI-008-estimaciones-facturacion/_MAP.md | 216 - .../ET-EST-001-modelo-datos.md | 361 - .../ET-EST-002-calculo-montos.md | 262 - .../ET-EST-003-anticipos-retenciones.md | 166 - .../ET-EST-004-generacion-reportes.md | 167 - .../ET-EST-005-workflow-estados.md | 170 - .../US-EST-001-crear-estimacion-cliente.md | 67 - ...EST-002-crear-estimacion-subcontratista.md | 66 - .../US-EST-003-aplicar-anticipos.md | 56 - .../US-EST-004-aplicar-retenciones.md | 55 - .../US-EST-005-generar-pdf.md | 62 - .../US-EST-006-exportar-excel.md | 56 - .../US-EST-007-workflow-autorizacion.md | 52 - .../US-EST-008-registrar-pago.md | 62 - .../US-EST-009-dashboard-estimaciones.md | 63 - .../RF-EST-001-estimaciones-cliente.md | 380 - ...RF-EST-002-estimaciones-subcontratistas.md | 311 - .../RF-EST-003-anticipos-retenciones.md | 251 - .../RF-EST-004-reportes-documentos.md | 303 - .../RF-EST-005-workflow-autorizacion.md | 334 - .../MAI-009-calidad-postventa/README.md | 232 - .../MAI-009-calidad-postventa/_MAP.md | 567 - .../ET-QUA-001-checklists-dinamicos.md | 83 - .../ET-QUA-002-no-conformidades.md | 76 - .../ET-QUA-003-motor-tickets.md | 93 - .../ET-QUA-004-sla-alertas.md | 35 - .../ET-QUA-005-historial-vivienda.md | 52 - ...S-QUA-001-ejecutar-checklist-de-calidad.md | 22 - .../US-QUA-002-registrar-no-conformidad.md | 22 - ...US-QUA-003-crear-ticket-desde-app-m贸vil.md | 22 - .../US-QUA-004-atender-ticket-de-garant铆a.md | 22 - ...-QUA-005-consultar-historial-de-viviend.md | 22 - .../US-QUA-006-dashboard-de-calidad.md | 22 - ...-QUA-007-generar-reporte-de-incidencias.md | 22 - .../US-QUA-008-alertas-de-sla.md | 22 - .../RF-QUA-001-control-calidad.md | 61 - .../RF-QUA-002-no-conformidades.md | 62 - .../RF-QUA-003-tickets-postventa.md | 69 - .../RF-QUA-004-garantias-sla.md | 67 - .../RF-QUA-005-historial-vivienda.md | 50 - .../MAI-010-crm-derechohabientes/README.md | 47 - .../MAI-010-crm-derechohabientes/_MAP.md | 692 - .../ET-CRM-001-modelo-de-datos-de-clientes.md | 19 - ...-CRM-002-sistema-de-estados-de-vivienda.md | 19 - .../ET-CRM-003-gesti贸n-de-documentos.md | 19 - .../ET-CRM-004-integracion-whatsapp.md | 19 - .../ET-CRM-005-analytics-de-ventas.md | 19 - .../US-CRM-001-registrar-prospecto.md | 16 - .../US-CRM-002-asignar-vivienda.md | 16 - .../US-CRM-003-seguimiento-expediente.md | 16 - .../US-CRM-004-enviar-notificaciones-wha.md | 16 - .../US-CRM-005-programar-citas.md | 16 - .../US-CRM-006-dashboard-ventas.md | 16 - .../US-CRM-007-reporte-de-ventas.md | 16 - ...F-CRM-001-gesti贸n-de-prospectos-y-derec.md | 25 - ...-CRM-002-control-de-estatus-de-vivienda.md | 25 - ...-CRM-003-seguimiento-de-expediente-de-c.md | 25 - .../RF-CRM-004-comunicaci贸n-multicanal.md | 25 - ...F-CRM-005-dashboard-de-comercializaci贸n.md | 25 - .../MAI-011-infonavit-cumplimiento/README.md | 47 - .../MAI-011-infonavit-cumplimiento/_MAP.md | 597 - ...-INF-001-modelo-de-programas-y-requisit.md | 21 - ...T-INF-002-sistema-de-checklists-din谩mic.md | 21 - .../ET-INF-003-repositorio-de-evidencias.md | 21 - .../ET-INF-004-workflow-de-auditor铆as.md | 21 - .../ET-INF-005-generaci贸n-de-reportes.md | 21 - .../US-INF-001-registrar-proyecto-bajo-p.md | 16 - .../US-INF-002-configurar-checklist-requ.md | 16 - .../US-INF-003-cargar-evidencias-por-req.md | 16 - .../US-INF-004-registrar-visita-de-verif.md | 16 - .../US-INF-005-gestionar-observaciones.md | 16 - .../US-INF-006-generar-reporte-de-cumpli.md | 16 - .../US-INF-007-dashboard-de-cumplimiento.md | 16 - .../US-INF-008-alertas-de-requisitos.md | 16 - ...-INF-001-registro-de-proyecto-bajo-prog.md | 20 - ...-INF-002-checklists-de-cumplimiento-nor.md | 20 - ...F-INF-003-gesti贸n-de-evidencias-y-docum.md | 20 - .../RF-INF-004-seguimiento-de-auditor铆as.md | 20 - .../RF-INF-005-reportes-de-cumplimiento.md | 20 - .../MAI-012-contratos-subcontratos/README.md | 103 - .../MAI-012-contratos-subcontratos/_MAP.md | 543 - .../ET-CON-001-modelo de datos.md | 54 - .../ET-CON-002-motor de plantillas.md | 31 - .../ET-CON-003-servicio de contratos.md | 28 - .../ET-CON-004-workflow de aprobaci贸n.md | 40 - .../ET-CON-005-generaci贸n de documentos.md | 35 - .../US-CON-001-crear contrato cliente.md | 16 - ...S-CON-002-crear contrato subcontratista.md | 16 - ...N-003-configurar plantillas de contrato.md | 16 - .../US-CON-004-generar documento pdf.md | 16 - .../US-CON-005-aprobar contrato.md | 16 - .../US-CON-006-crear addenda.md | 16 - .../US-CON-007-evaluar subcontratista.md | 16 - .../US-CON-008-dashboard de contratos.md | 16 - .../US-CON-009-alertas de vencimiento.md | 16 - .../RF-CON-001-contratos con clientes.md | 22 - ...F-CON-002-contratos con subcontratistas.md | 22 - ...N-003-plantillas din谩micas y generaci贸n.md | 22 - .../RF-CON-004-addendas y rescisiones.md | 22 - ...N-005-workflow de aprobaci贸n multinivel.md | 22 - .../README.md | 478 - .../RESUMEN-EPICA-MAI-013.md | 483 - .../MAI-013-administracion-seguridad/_MAP.md | 323 - .../ET-ADM-001-rbac-multi-tenancy.md | 1332 -- .../ET-ADM-002-centros-costo-jerarquicos.md | 875 - .../ET-ADM-003-audit-logging.md | 380 - .../especificaciones/ET-ADM-004-backups-dr.md | 468 - .../ET-ADM-005-seguridad-datos.md | 874 - .../US-ADM-001-invitar-registrar-usuarios.md | 329 - .../US-ADM-002-cambiar-constructora.md | 284 - .../US-ADM-003-gestionar-permisos.md | 323 - .../US-ADM-004-centros-costo.md | 419 - .../US-ADM-005-bitacora-auditoria.md | 377 - .../US-ADM-006-gestionar-backups.md | 484 - .../US-ADM-007-politicas-seguridad.md | 450 - .../US-ADM-008-dashboard-admin.md | 445 - .../RF-ADM-001-usuarios-roles.md | 757 - .../RF-ADM-002-permisos-granulares.md | 702 - .../RF-ADM-003-centros-costo.md | 735 - .../requerimientos/RF-ADM-004-auditoria.md | 755 - .../requerimientos/RF-ADM-005-backups.md | 763 - .../README.md | 114 - .../_MAP.md | 410 - .../ET-PRE-001-modelo de datos.md | 70 - .../ET-PRE-002-servicio de viabilidad.md | 35 - .../ET-PRE-003-motor de licitaciones.md | 37 - .../ET-PRE-004-evaluaci贸n de propuestas.md | 52 - .../ET-PRE-005-gesti贸n de proveedores.md | 39 - .../US-PRE-001-crear estudio de viabilidad.md | 16 - ...-PRE-002-generar presupuesto preliminar.md | 16 - .../US-PRE-003-crear licitaci贸n.md | 16 - .../US-PRE-004-publicar licitaci贸n.md | 16 - .../US-PRE-005-registrar propuesta.md | 16 - .../US-PRE-006-evaluar propuestas.md | 16 - .../US-PRE-007-adjudicar licitaci贸n.md | 16 - .../US-PRE-008-gestionar proveedores.md | 16 - .../US-PRE-009-dashboard de licitaciones.md | 16 - .../RF-PRE-001-an谩lisis de viabilidad.md | 22 - .../RF-PRE-002-presupuesto preliminar.md | 22 - .../RF-PRE-003-proceso de licitaci贸n.md | 22 - .../RF-PRE-004-evaluaci贸n de propuestas.md | 22 - .../RF-PRE-005-gesti贸n de proveedores.md | 22 - .../MEJORAS-SAAS-APLICADAS.md | 296 - .../docs/02-definicion-modulos/README.md | 211 - .../REPORTE-MEJORAS-COMPLETO.md | 656 - .../RESUMEN-DOCUMENTACION-GENERADA.md | 174 - .../RESUMEN-EJECUTIVO.md | 436 - .../RESUMEN-SESION-2025-11-17.md | 414 - .../RESUMEN-SESION-COMPLETA-2025-11-17.md | 412 - .../ROADMAP-DETALLADO.md | 676 - .../docs/02-definicion-modulos/_MAP.md | 393 - .../docs/03-requerimientos/README.md | 248 - .../docs/04-modelado/README.md | 72 - .../04-modelado/database-design/README.md | 245 - .../schemas/DDL-SPEC-assets.md | 894 - .../schemas/DDL-SPEC-compliance.md | 754 - .../schemas/DDL-SPEC-construction.md | 1019 -- .../schemas/DDL-SPEC-documents.md | 842 - .../schemas/DDL-SPEC-finance.md | 1003 -- .../schemas/construction-schema-ddl.sql | 908 - .../schemas/estimates-schema-ddl.sql | 502 - .../schemas/hr-ext-schema-ddl.sql | 394 - .../schemas/infonavit-schema-ddl.sql | 462 - .../schemas/inventory-ext-schema-ddl.sql | 206 - .../schemas/purchase-ext-schema-ddl.sql | 214 - .../domain-models/ASSETS-CONTEXT.md | 651 - .../domain-models/COMPLIANCE-CONTEXT.md | 569 - .../domain-models/DOCUMENTS-CONTEXT.md | 699 - .../domain-models/FINANCE-CONTEXT.md | 627 - .../domain-models/PROJECT-CONTEXT.md | 619 - .../docs/04-modelado/domain-models/README.md | 151 - .../trazabilidad/INVENTARIO-OBJETOS-BD.yml | 613 - .../MATRIZ-TRAZABILIDAD-COMPLETA.md | 311 - .../docs/04-modelado/trazabilidad/README.md | 105 - .../modulos/TRACEABILITY-MAI-001.yaml | 209 - .../modulos/TRACEABILITY-MAI-002.yaml | 226 - .../modulos/TRACEABILITY-MAI-003.yaml | 207 - .../modulos/TRACEABILITY-MAI-004.yaml | 232 - .../modulos/TRACEABILITY-MAI-005.yaml | 223 - .../modulos/TRACEABILITY-MAI-006.yaml | 96 - .../modulos/TRACEABILITY-MAI-007.yaml | 123 - .../modulos/TRACEABILITY-MAI-008.yaml | 123 - .../modulos/TRACEABILITY-MAI-009.yaml | 107 - .../modulos/TRACEABILITY-MAI-010.yaml | 113 - .../modulos/TRACEABILITY-MAI-011.yaml | 128 - .../modulos/TRACEABILITY-MAI-012.yaml | 108 - .../modulos/TRACEABILITY-MAI-013.yaml | 111 - .../docs/05-backend-specs/README.md | 382 - .../05-backend-specs/modules/SPEC-assets.md | 1131 -- .../modules/SPEC-compliance.md | 566 - .../modules/SPEC-construction.md | 677 - .../modules/SPEC-documents.md | 1294 -- .../05-backend-specs/modules/SPEC-finance.md | 610 - .../docs/05-user-stories/README.md | 256 - .../docs/06-frontend-specs/README.md | 607 - .../components/COMP-construction.md | 523 - .../components/COMP-shared.md | 758 - .../pages/PAGES-construction.md | 400 - .../06-frontend-specs/stores/STORES-spec.md | 737 - .../docs/06-test-plans/README.md | 183 - .../erp-construccion/docs/07-devops/README.md | 316 - .../docs/07-devops/docker/docker-compose.yml | 198 - .../docs/08-epicas/EPIC-MAE-014-finanzas.md | 129 - .../08-epicas/EPIC-MAI-001-fundamentos.md | 130 - .../docs/08-epicas/EPIC-MAI-002-proyectos.md | 121 - .../08-epicas/EPIC-MAI-003-presupuestos.md | 216 - .../08-epicas/EPIC-MAI-005-control-obra.md | 111 - .../docs/08-epicas/EPIC-MAI-011-infonavit.md | 125 - .../08-epicas/EPIC-MAI-019-mobile-apps.md | 433 - .../erp-construccion/docs/08-epicas/README.md | 125 - .../REPORTE-AUDITORIA-DOCUMENTACION.md | 231 - .../docs/97-adr/ADR-001-stack-tecnologico.md | 98 - .../97-adr/ADR-002-arquitectura-modular.md | 99 - .../docs/97-adr/ADR-003-multi-tenancy.md | 83 - .../97-adr/ADR-004-sistema-constantes-ssot.md | 73 - .../docs/97-adr/ADR-005-path-aliases.md | 65 - .../97-adr/ADR-006-rbac-sistema-permisos.md | 82 - .../docs/97-adr/ADR-007-database-design.md | 82 - .../docs/97-adr/ADR-008-api-design.md | 71 - .../97-adr/ADR-009-frontend-architecture.md | 73 - .../docs/97-adr/ADR-010-testing-strategy.md | 71 - .../ADR-011-database-clean-load-strategy.md | 126 - .../ADR-012-complete-traceability-policy.md | 108 - .../erp-construccion/docs/97-adr/README.md | 111 - .../ANALISIS-IMPLEMENTACION-ARQUITECTURA.md | 485 - .../erp-construccion/docs/ARCHITECTURE.md | 647 - .../docs/ESTRUCTURA-COMPLETA.md | 285 - .../docs/GUIA-USO-REFERENCIAS-ODOO.md | 733 - ...AN-RETROALIMENTACION-DESDE-ERP-GENERICO.md | 618 - projects/erp-construccion/docs/README.md | 276 - .../docs/REPORTE-FINAL-MEJORAS-SAAS.md | 491 - .../docs/RLS-POLICIES-TODOS-LOS-MODULOS.md | 499 - projects/erp-construccion/docs/_MAP.md | 40 - .../erp-construccion/docs/api/openapi.yaml | 947 - .../docs/backend/API-REFERENCE.md | 978 -- .../erp-construccion/docs/backend/MODULES.md | 744 - .../ANALISIS-MEJORAS-SISTEMA-ORQUESTACION.md | 837 - .../REPORTE-FINAL-IMPLEMENTACION-FASE-2.md | 656 - ...RTE-IMPLEMENTACION-SISTEMA-ORQUESTACION.md | 671 - .../REPORTE-MEJORAS-SISTEMA-SUBAGENTES.md | 1084 -- .../erp-construccion/frontend/mobile/App.tsx | 37 - .../frontend/mobile/README.md | 43 - .../erp-construccion/frontend/mobile/app.json | 32 - .../frontend/mobile/package-lock.json | 14386 ---------------- .../frontend/mobile/package.json | 28 - .../frontend/mobile/tsconfig.json | 14 - .../erp-construccion/frontend/web/Dockerfile | 65 - .../erp-construccion/frontend/web/README.md | 107 - .../erp-construccion/frontend/web/index.html | 14 - .../erp-construccion/frontend/web/nginx.conf | 47 - .../frontend/web/package-lock.json | 4552 ----- .../frontend/web/package.json | 46 - .../erp-construccion/frontend/web/src/App.tsx | 60 - .../frontend/web/src/index.css | 125 - .../frontend/web/src/main.tsx | 18 - .../frontend/web/src/vite-env.d.ts | 1 - .../frontend/web/tsconfig.json | 40 - .../frontend/web/tsconfig.node.json | 11 - .../frontend/web/vite.config.ts | 45 - .../00-guidelines/CONTEXTO-PROYECTO.md | 365 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 121 - .../00-guidelines/HERENCIA-ERP-CORE.md | 271 - .../00-guidelines/HERENCIA-SIMCO.md | 280 - .../00-guidelines/HERENCIA-SPECS-CORE.md | 159 - .../00-guidelines/HERENCIA-SPECS-ERP-CORE.md | 225 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../orchestration/NAMING-CONVENTIONS.md | 276 - .../orchestration/PROXIMA-ACCION.md | 73 - .../erp-construccion/orchestration/README.md | 103 - ...NFORME-ANALISIS-CONSTRUCCION-2025-12-06.md | 623 - .../directivas/DIRECTIVA-CONTROL-OBRA.md | 528 - .../directivas/DIRECTIVA-ESTIMACIONES.md | 563 - .../DIRECTIVA-INTEGRACION-INFONAVIT.md | 331 - .../environment/PROJECT-ENV-CONFIG.yml | 127 - .../orchestration/estados/ESTADO-AGENTES.json | 57 - .../inventarios/BACKEND_INVENTORY.yml | 731 - .../inventarios/DATABASE_INVENTORY.yml | 1482 -- .../inventarios/DEPENDENCY_GRAPH.yml | 528 - .../inventarios/FRONTEND_INVENTORY.yml | 723 - .../inventarios/MASTER_INVENTORY.yml | 857 - .../orchestration/inventarios/README.md | 107 - .../inventarios/TRACEABILITY_MATRIX.yml | 588 - .../prompts/PROMPT-CON-BACKEND-AGENT.md | 208 - .../PROMPT-CONSTRUCCION-BACKEND-AGENT.md | 502 - .../PROMPT-CONSTRUCCION-DATABASE-AGENT.md | 484 - .../PROMPT-CONSTRUCCION-FRONTEND-AGENT.md | 693 - .../referencias/DEPENDENCIAS-ERP-CORE.yml | 132 - .../referencias/DEPENDENCIAS-SHARED.yml | 60 - .../trazas/TRAZA-TAREAS-BACKEND.md | 35 - .../trazas/TRAZA-TAREAS-DATABASE.md | 54 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 33 - projects/erp-core/.env.example | 22 - projects/erp-core/.gitignore | 32 - projects/erp-core/INVENTARIO.yml | 31 - projects/erp-core/PROJECT-STATUS.md | 27 - projects/erp-core/README.md | 114 - projects/erp-core/backend/.env.example | 22 - projects/erp-core/backend/.gitignore | 32 - projects/erp-core/backend/Dockerfile | 52 - .../erp-core/backend/TYPEORM_DEPENDENCIES.md | 78 - .../backend/TYPEORM_INTEGRATION_SUMMARY.md | 302 - .../backend/TYPEORM_USAGE_EXAMPLES.md | 536 - projects/erp-core/backend/package-lock.json | 8585 --------- projects/erp-core/backend/package.json | 59 - .../erp-core/backend/service.descriptor.yml | 134 - projects/erp-core/backend/src/app.ts | 112 - .../erp-core/backend/src/config/database.ts | 69 - projects/erp-core/backend/src/config/index.ts | 35 - projects/erp-core/backend/src/config/redis.ts | 178 - .../backend/src/config/swagger.config.ts | 200 - .../erp-core/backend/src/config/typeorm.ts | 215 - .../erp-core/backend/src/docs/openapi.yaml | 138 - projects/erp-core/backend/src/index.ts | 71 - .../src/modules/auth/apiKeys.controller.ts | 331 - .../src/modules/auth/apiKeys.routes.ts | 56 - .../src/modules/auth/apiKeys.service.ts | 491 - .../src/modules/auth/auth.controller.ts | 192 - .../backend/src/modules/auth/auth.routes.ts | 18 - .../backend/src/modules/auth/auth.service.ts | 234 - .../modules/auth/entities/api-key.entity.ts | 87 - .../modules/auth/entities/company.entity.ts | 93 - .../src/modules/auth/entities/group.entity.ts | 89 - .../src/modules/auth/entities/index.ts | 15 - .../auth/entities/mfa-audit-log.entity.ts | 87 - .../auth/entities/oauth-provider.entity.ts | 191 - .../auth/entities/oauth-state.entity.ts | 66 - .../auth/entities/oauth-user-link.entity.ts | 73 - .../auth/entities/password-reset.entity.ts | 45 - .../auth/entities/permission.entity.ts | 52 - .../src/modules/auth/entities/role.entity.ts | 84 - .../modules/auth/entities/session.entity.ts | 90 - .../modules/auth/entities/tenant.entity.ts | 93 - .../auth/entities/trusted-device.entity.ts | 115 - .../src/modules/auth/entities/user.entity.ts | 141 - .../auth/entities/verification-code.entity.ts | 90 - .../backend/src/modules/auth/index.ts | 8 - .../modules/auth/services/token.service.ts | 456 - .../modules/companies/companies.controller.ts | 241 - .../src/modules/companies/companies.routes.ts | 50 - .../modules/companies/companies.service.ts | 472 - .../backend/src/modules/companies/index.ts | 3 - .../src/modules/core/core.controller.ts | 257 - .../backend/src/modules/core/core.routes.ts | 51 - .../src/modules/core/countries.service.ts | 45 - .../src/modules/core/currencies.service.ts | 118 - .../modules/core/entities/country.entity.ts | 35 - .../modules/core/entities/currency.entity.ts | 43 - .../src/modules/core/entities/index.ts | 6 - .../core/entities/product-category.entity.ts | 79 - .../modules/core/entities/sequence.entity.ts | 83 - .../core/entities/uom-category.entity.ts | 30 - .../src/modules/core/entities/uom.entity.ts | 76 - .../backend/src/modules/core/index.ts | 8 - .../core/product-categories.service.ts | 223 - .../src/modules/core/sequences.service.ts | 466 - .../backend/src/modules/core/uom.service.ts | 162 - .../backend/src/modules/crm/crm.controller.ts | 682 - .../backend/src/modules/crm/crm.routes.ts | 126 - .../erp-core/backend/src/modules/crm/index.ts | 5 - .../backend/src/modules/crm/leads.service.ts | 449 - .../src/modules/crm/opportunities.service.ts | 503 - .../backend/src/modules/crm/stages.service.ts | 435 - .../src/modules/financial/MIGRATION_GUIDE.md | 612 - .../modules/financial/accounts.service.old.ts | 330 - .../src/modules/financial/accounts.service.ts | 468 - .../financial/entities/account-type.entity.ts | 38 - .../financial/entities/account.entity.ts | 93 - .../entities/fiscal-period.entity.ts | 64 - .../financial/entities/fiscal-year.entity.ts | 67 - .../src/modules/financial/entities/index.ts | 22 - .../financial/entities/invoice-line.entity.ts | 79 - .../financial/entities/invoice.entity.ts | 152 - .../entities/journal-entry-line.entity.ts | 59 - .../entities/journal-entry.entity.ts | 104 - .../financial/entities/journal.entity.ts | 94 - .../financial/entities/payment.entity.ts | 135 - .../modules/financial/entities/tax.entity.ts | 78 - .../modules/financial/financial.controller.ts | 753 - .../src/modules/financial/financial.routes.ts | 150 - .../financial/fiscalPeriods.service.ts | 369 - .../backend/src/modules/financial/index.ts | 8 - .../src/modules/financial/invoices.service.ts | 547 - .../financial/journal-entries.service.ts | 343 - .../modules/financial/journals.service.old.ts | 216 - .../src/modules/financial/journals.service.ts | 216 - .../src/modules/financial/payments.service.ts | 456 - .../modules/financial/taxes.service.old.ts | 382 - .../src/modules/financial/taxes.service.ts | 382 - .../src/modules/hr/contracts.service.ts | 346 - .../src/modules/hr/departments.service.ts | 393 - .../src/modules/hr/employees.service.ts | 402 - .../backend/src/modules/hr/hr.controller.ts | 721 - .../backend/src/modules/hr/hr.routes.ts | 152 - .../erp-core/backend/src/modules/hr/index.ts | 6 - .../backend/src/modules/hr/leaves.service.ts | 517 - .../src/modules/inventory/MIGRATION_STATUS.md | 177 - .../modules/inventory/adjustments.service.ts | 512 - .../src/modules/inventory/entities/index.ts | 11 - .../inventory-adjustment-line.entity.ts | 80 - .../entities/inventory-adjustment.entity.ts | 86 - .../inventory/entities/location.entity.ts | 96 - .../modules/inventory/entities/lot.entity.ts | 64 - .../inventory/entities/picking.entity.ts | 125 - .../inventory/entities/product.entity.ts | 154 - .../inventory/entities/stock-move.entity.ts | 104 - .../inventory/entities/stock-quant.entity.ts | 66 - .../entities/stock-valuation-layer.entity.ts | 85 - .../inventory/entities/warehouse.entity.ts | 68 - .../backend/src/modules/inventory/index.ts | 16 - .../modules/inventory/inventory.controller.ts | 875 - .../src/modules/inventory/inventory.routes.ts | 174 - .../modules/inventory/locations.service.ts | 212 - .../src/modules/inventory/lots.service.ts | 263 - .../src/modules/inventory/pickings.service.ts | 357 - .../src/modules/inventory/products.service.ts | 410 - .../modules/inventory/valuation.controller.ts | 230 - .../modules/inventory/valuation.service.ts | 522 - .../modules/inventory/warehouses.service.ts | 283 - .../src/modules/partners/entities/index.ts | 1 - .../partners/entities/partner.entity.ts | 132 - .../backend/src/modules/partners/index.ts | 6 - .../modules/partners/partners.controller.ts | 333 - .../src/modules/partners/partners.routes.ts | 90 - .../src/modules/partners/partners.service.ts | 395 - .../modules/partners/ranking.controller.ts | 368 - .../src/modules/partners/ranking.service.ts | 431 - .../backend/src/modules/projects/index.ts | 5 - .../modules/projects/projects.controller.ts | 569 - .../src/modules/projects/projects.routes.ts | 75 - .../src/modules/projects/projects.service.ts | 309 - .../src/modules/projects/tasks.service.ts | 293 - .../modules/projects/timesheets.service.ts | 302 - .../backend/src/modules/purchases/index.ts | 4 - .../modules/purchases/purchases.controller.ts | 352 - .../src/modules/purchases/purchases.routes.ts | 90 - .../modules/purchases/purchases.service.ts | 386 - .../src/modules/purchases/rfqs.service.ts | 485 - .../backend/src/modules/reports/index.ts | 3 - .../src/modules/reports/reports.controller.ts | 434 - .../src/modules/reports/reports.routes.ts | 96 - .../src/modules/reports/reports.service.ts | 580 - .../backend/src/modules/roles/index.ts | 13 - .../modules/roles/permissions.controller.ts | 218 - .../src/modules/roles/permissions.routes.ts | 55 - .../src/modules/roles/permissions.service.ts | 342 - .../src/modules/roles/roles.controller.ts | 292 - .../backend/src/modules/roles/roles.routes.ts | 57 - .../src/modules/roles/roles.service.ts | 454 - .../modules/sales/customer-groups.service.ts | 209 - .../backend/src/modules/sales/index.ts | 7 - .../src/modules/sales/orders.service.ts | 707 - .../src/modules/sales/pricelists.service.ts | 249 - .../src/modules/sales/quotations.service.ts | 588 - .../src/modules/sales/sales-teams.service.ts | 241 - .../src/modules/sales/sales.controller.ts | 889 - .../backend/src/modules/sales/sales.routes.ts | 159 - .../src/modules/system/activities.service.ts | 350 - .../backend/src/modules/system/index.ts | 5 - .../src/modules/system/messages.service.ts | 234 - .../modules/system/notifications.service.ts | 227 - .../src/modules/system/system.controller.ts | 404 - .../src/modules/system/system.routes.ts | 48 - .../backend/src/modules/tenants/index.ts | 7 - .../src/modules/tenants/tenants.controller.ts | 315 - .../src/modules/tenants/tenants.routes.ts | 69 - .../src/modules/tenants/tenants.service.ts | 449 - .../backend/src/modules/users/index.ts | 3 - .../src/modules/users/users.controller.ts | 260 - .../backend/src/modules/users/users.routes.ts | 60 - .../src/modules/users/users.service.ts | 372 - .../backend/src/shared/errors/index.ts | 18 - .../middleware/apiKeyAuth.middleware.ts | 217 - .../src/shared/middleware/auth.middleware.ts | 119 - .../middleware/fieldPermissions.middleware.ts | 343 - .../src/shared/services/base.service.ts | 429 - .../backend/src/shared/services/index.ts | 7 - .../backend/src/shared/types/index.ts | 144 - .../backend/src/shared/utils/logger.ts | 40 - projects/erp-core/backend/tsconfig.json | 30 - projects/erp-core/database/README.md | 171 - .../database/ddl/00-prerequisites.sql | 207 - .../database/ddl/01-auth-extensions.sql | 891 - projects/erp-core/database/ddl/01-auth.sql | 620 - projects/erp-core/database/ddl/02-core.sql | 755 - .../erp-core/database/ddl/03-analytics.sql | 510 - .../erp-core/database/ddl/04-financial.sql | 970 -- .../database/ddl/05-inventory-extensions.sql | 966 -- .../erp-core/database/ddl/05-inventory.sql | 772 - .../erp-core/database/ddl/06-purchase.sql | 583 - projects/erp-core/database/ddl/07-sales.sql | 705 - .../erp-core/database/ddl/08-projects.sql | 537 - projects/erp-core/database/ddl/09-system.sql | 853 - projects/erp-core/database/ddl/10-billing.sql | 638 - projects/erp-core/database/ddl/11-crm.sql | 366 - projects/erp-core/database/ddl/12-hr.sql | 379 - .../ddl/schemas/core_shared/00-schema.sql | 159 - projects/erp-core/database/docker-compose.yml | 51 - .../20251212_001_fiscal_period_validation.sql | 207 - .../20251212_002_partner_rankings.sql | 391 - .../20251212_003_financial_reports.sql | 464 - .../database/scripts/create-database.sh | 142 - .../database/scripts/drop-database.sh | 75 - .../erp-core/database/scripts/load-seeds.sh | 101 - .../database/scripts/reset-database.sh | 102 - .../database/seeds/dev/00-catalogs.sql | 81 - .../database/seeds/dev/01-tenants.sql | 49 - .../database/seeds/dev/02-companies.sql | 64 - .../erp-core/database/seeds/dev/03-roles.sql | 246 - .../erp-core/database/seeds/dev/04-users.sql | 148 - .../database/seeds/dev/05-sample-data.sql | 228 - .../docs/00-vision-general/VISION-ERP-CORE.md | 217 - .../MAPA-COMPONENTES-GENERICOS.md | 351 - .../01-analisis-referencias/RESUMEN-FASE-0.md | 817 - .../construccion/COMPONENTES-ESPECIFICOS.md | 375 - .../construccion/COMPONENTES-GENERICOS.md | 489 - .../construccion/GAP-ANALYSIS.md | 308 - .../construccion/MEJORAS-ARQUITECTONICAS.md | 684 - .../construccion/RETROALIMENTACION.md | 541 - .../gamilit/ADOPTAR-ADAPTAR-EVITAR.md | 613 - .../01-analisis-referencias/gamilit/README.md | 651 - .../gamilit/backend-patterns.md | 869 - .../gamilit/database-architecture.md | 1119 -- .../gamilit/devops-automation.md | 669 - .../gamilit/frontend-patterns.md | 759 - .../gamilit/ssot-system.md | 868 - .../odoo/MAPEO-ODOO-TO-MGN.md | 478 - .../01-analisis-referencias/odoo/README.md | 254 - .../odoo/VALIDACION-MGN-VS-ODOO.md | 391 - .../odoo/odoo-account-analysis.md | 64 - .../odoo/odoo-analytic-analysis.md | 104 - .../odoo/odoo-auth-analysis.md | 534 - .../odoo/odoo-base-analysis.md | 751 - .../odoo/odoo-crm-analysis.md | 67 - .../odoo/odoo-hr-analysis.md | 55 - .../odoo/odoo-mail-analysis.md | 273 - .../odoo/odoo-portal-analysis.md | 175 - .../odoo/odoo-project-analysis.md | 71 - .../odoo/odoo-purchase-analysis.md | 53 - .../odoo/odoo-sale-analysis.md | 56 - .../odoo/odoo-stock-analysis.md | 62 - .../01-fase-foundation/MGN-001-auth/README.md | 177 - .../01-fase-foundation/MGN-001-auth/_MAP.md | 183 - .../especificaciones/ET-AUTH-database.md | 744 - .../especificaciones/ET-auth-backend.md | 1749 -- .../especificaciones/auth-domain.md | 267 - .../historias-usuario/BACKLOG-MGN001.md | 162 - .../historias-usuario/US-MGN001-001.md | 296 - .../historias-usuario/US-MGN001-002.md | 261 - .../historias-usuario/US-MGN001-003.md | 300 - .../historias-usuario/US-MGN001-004.md | 391 - .../implementacion/TRACEABILITY.yml | 695 - .../requerimientos/INDICE-RF-AUTH.md | 188 - .../requerimientos/RF-AUTH-001.md | 234 - .../requerimientos/RF-AUTH-002.md | 264 - .../requerimientos/RF-AUTH-003.md | 261 - .../requerimientos/RF-AUTH-004.md | 288 - .../requerimientos/RF-AUTH-005.md | 345 - .../MGN-002-users/README.md | 88 - .../01-fase-foundation/MGN-002-users/_MAP.md | 110 - .../especificaciones/ET-USER-database.md | 847 - .../especificaciones/ET-users-backend.md | 1247 -- .../historias-usuario/BACKLOG-MGN002.md | 138 - .../historias-usuario/US-MGN002-001.md | 219 - .../historias-usuario/US-MGN002-002.md | 225 - .../historias-usuario/US-MGN002-003.md | 203 - .../historias-usuario/US-MGN002-004.md | 222 - .../historias-usuario/US-MGN002-005.md | 286 - .../implementacion/TRACEABILITY.yml | 513 - .../requerimientos/INDICE-RF-USER.md | 260 - .../requerimientos/RF-USER-001.md | 333 - .../requerimientos/RF-USER-002.md | 314 - .../requerimientos/RF-USER-003.md | 332 - .../requerimientos/RF-USER-004.md | 362 - .../requerimientos/RF-USER-005.md | 370 - .../MGN-003-roles/README.md | 97 - .../01-fase-foundation/MGN-003-roles/_MAP.md | 114 - .../especificaciones/ET-RBAC-database.md | 694 - .../especificaciones/ET-rbac-backend.md | 1274 -- .../historias-usuario/BACKLOG-MGN003.md | 177 - .../historias-usuario/US-MGN003-001.md | 162 - .../historias-usuario/US-MGN003-002.md | 177 - .../historias-usuario/US-MGN003-003.md | 211 - .../historias-usuario/US-MGN003-004.md | 230 - .../implementacion/TRACEABILITY.yml | 488 - .../requerimientos/INDICE-RF-ROLE.md | 221 - .../requerimientos/RF-ROLE-001.md | 364 - .../requerimientos/RF-ROLE-002.md | 338 - .../requerimientos/RF-ROLE-003.md | 350 - .../requerimientos/RF-ROLE-004.md | 530 - .../MGN-004-tenants/README.md | 102 - .../MGN-004-tenants/_MAP.md | 126 - .../especificaciones/ET-TENANT-database.md | 1117 -- .../especificaciones/ET-tenants-backend.md | 2365 --- .../historias-usuario/BACKLOG-MGN004.md | 210 - .../historias-usuario/US-MGN004-001.md | 184 - .../historias-usuario/US-MGN004-002.md | 178 - .../historias-usuario/US-MGN004-003.md | 205 - .../historias-usuario/US-MGN004-004.md | 211 - .../implementacion/TRACEABILITY.yml | 553 - .../requerimientos/INDICE-RF-TENANT.md | 271 - .../requerimientos/RF-TENANT-001.md | 396 - .../requerimientos/RF-TENANT-002.md | 370 - .../requerimientos/RF-TENANT-003.md | 424 - .../requerimientos/RF-TENANT-004.md | 460 - .../docs/01-fase-foundation/README.md | 119 - .../ALCANCE-POR-MODULO.md | 1547 -- .../DEPENDENCIAS-MODULOS.md | 939 - .../02-definicion-modulos/INDICE-MODULOS.md | 559 - .../LISTA-MODULOS-ERP-GENERICO.md | 900 - .../RETROALIMENTACION-ERP-CONSTRUCCION.md | 795 - .../gaps/GAP-ANALYSIS-MGN-001.md | 262 - .../gaps/GAP-ANALYSIS-MGN-002.md | 149 - .../gaps/GAP-ANALYSIS-MGN-003.md | 137 - .../gaps/GAP-ANALYSIS-MGN-004.md | 150 - .../gaps/GAP-ANALYSIS-MGN-005.md | 69 - .../gaps/GAP-ANALYSIS-MGN-006.md | 53 - .../gaps/GAP-ANALYSIS-MGN-007.md | 54 - .../gaps/GAP-ANALYSIS-MGN-008.md | 143 - .../gaps/GAP-ANALYSIS-MGN-009.md | 32 - .../gaps/GAP-ANALYSIS-MGN-010.md | 36 - .../gaps/GAP-ANALYSIS-MGN-011.md | 29 - .../gaps/GAP-ANALYSIS-MGN-012.md | 28 - .../gaps/GAP-ANALYSIS-MGN-013.md | 97 - .../gaps/GAP-ANALYSIS-MGN-014.md | 42 - .../gaps/GAP-ANALYSIS-MGN-015.md | 126 - .../MGN-005-catalogs/README.md | 62 - .../MGN-005-catalogs/_MAP.md | 92 - .../especificaciones/ET-CATALOG-backend.md | 1324 -- .../especificaciones/ET-CATALOG-database.md | 887 - .../especificaciones/ET-CATALOG-frontend.md | 1372 -- .../especificaciones/INDICE-ET-CATALOGS.md | 105 - .../historias-usuario/INDICE-US-CATALOGS.md | 51 - .../historias-usuario/US-MGN005-001.md | 249 - .../historias-usuario/US-MGN005-002.md | 193 - .../historias-usuario/US-MGN005-003.md | 247 - .../historias-usuario/US-MGN005-004.md | 222 - .../historias-usuario/US-MGN005-005.md | 243 - .../implementacion/TRACEABILITY.yml | 252 - .../requerimientos/INDICE-RF-CATALOG.md | 184 - .../requerimientos/RF-CATALOG-001.md | 294 - .../requerimientos/RF-CATALOG-002.md | 277 - .../requerimientos/RF-CATALOG-003.md | 320 - .../requerimientos/RF-CATALOG-004.md | 346 - .../requerimientos/RF-CATALOG-005.md | 348 - .../MGN-006-settings/README.md | 59 - .../MGN-006-settings/_MAP.md | 96 - .../especificaciones/ET-SETTINGS-backend.md | 557 - .../especificaciones/ET-SETTINGS-database.md | 406 - .../especificaciones/ET-SETTINGS-frontend.md | 1805 -- .../especificaciones/INDICE-ET-SETTINGS.md | 85 - .../historias-usuario/INDICE-US-SETTINGS.md | 68 - .../historias-usuario/US-MGN006-001.md | 234 - .../historias-usuario/US-MGN006-002.md | 223 - .../historias-usuario/US-MGN006-003.md | 228 - .../historias-usuario/US-MGN006-004.md | 276 - .../implementacion/TRACEABILITY.yml | 319 - .../requerimientos/INDICE-RF-SETTINGS.md | 77 - .../requerimientos/RF-SETTINGS-001.md | 236 - .../requerimientos/RF-SETTINGS-002.md | 208 - .../requerimientos/RF-SETTINGS-003.md | 204 - .../requerimientos/RF-SETTINGS-004.md | 259 - .../MGN-007-audit/README.md | 60 - .../MGN-007-audit/_MAP.md | 95 - .../especificaciones/ET-AUDIT-backend.md | 1151 -- .../especificaciones/ET-AUDIT-database.md | 546 - .../especificaciones/ET-AUDIT-frontend.md | 1893 -- .../especificaciones/INDICE-ET-AUDIT.md | 132 - .../historias-usuario/INDICE-US-AUDIT.md | 108 - .../historias-usuario/US-MGN007-001.md | 253 - .../historias-usuario/US-MGN007-002.md | 260 - .../historias-usuario/US-MGN007-003.md | 261 - .../historias-usuario/US-MGN007-004.md | 303 - .../implementacion/TRACEABILITY.yml | 337 - .../requerimientos/INDICE-RF-AUDIT.md | 89 - .../requerimientos/RF-AUDIT-001.md | 217 - .../requerimientos/RF-AUDIT-002.md | 210 - .../requerimientos/RF-AUDIT-003.md | 240 - .../requerimientos/RF-AUDIT-004.md | 298 - .../MGN-008-notifications/README.md | 61 - .../MGN-008-notifications/_MAP.md | 98 - .../especificaciones/ET-NOTIF-backend.md | 1049 -- .../especificaciones/ET-NOTIF-database.md | 690 - .../especificaciones/ET-NOTIF-frontend.md | 1628 -- .../especificaciones/INDICE-ET-NOTIF.md | 194 - .../historias-usuario/INDICE-US-NOTIF.md | 123 - .../historias-usuario/US-MGN008-001.md | 268 - .../historias-usuario/US-MGN008-002.md | 292 - .../historias-usuario/US-MGN008-003.md | 281 - .../historias-usuario/US-MGN008-004.md | 300 - .../implementacion/TRACEABILITY.yml | 388 - .../requerimientos/INDICE-RF-NOTIF.md | 92 - .../requerimientos/RF-NOTIF-001.md | 262 - .../requerimientos/RF-NOTIF-002.md | 286 - .../requerimientos/RF-NOTIF-003.md | 231 - .../requerimientos/RF-NOTIF-004.md | 278 - .../MGN-009-reports/README.md | 60 - .../MGN-009-reports/_MAP.md | 98 - .../especificaciones/ET-REPORT-backend.md | 1249 -- .../especificaciones/ET-REPORT-database.md | 699 - .../especificaciones/ET-REPORT-frontend.md | 1689 -- .../especificaciones/INDICE-ET-REPORT.md | 207 - .../historias-usuario/INDICE-US-REPORT.md | 125 - .../historias-usuario/US-MGN009-001.md | 292 - .../historias-usuario/US-MGN009-002.md | 284 - .../historias-usuario/US-MGN009-003.md | 304 - .../historias-usuario/US-MGN009-004.md | 310 - .../implementacion/TRACEABILITY.yml | 413 - .../requerimientos/INDICE-RF-REPORT.md | 93 - .../requerimientos/RF-REPORT-001.md | 267 - .../requerimientos/RF-REPORT-002.md | 299 - .../requerimientos/RF-REPORT-003.md | 289 - .../requerimientos/RF-REPORT-004.md | 266 - .../MGN-010-financial/README.md | 60 - .../MGN-010-financial/_MAP.md | 102 - .../especificaciones/ET-FIN-backend.md | 1092 -- .../especificaciones/ET-FIN-database.md | 872 - .../especificaciones/ET-FIN-frontend.md | 1929 --- .../especificaciones/INDICE-ET-FINANCIAL.md | 212 - .../historias-usuario/INDICE-US-FINANCIAL.md | 141 - .../historias-usuario/US-MGN010-001.md | 306 - .../historias-usuario/US-MGN010-002.md | 304 - .../historias-usuario/US-MGN010-003.md | 303 - .../historias-usuario/US-MGN010-004.md | 311 - .../implementacion/TRACEABILITY.yml | 538 - .../requerimientos/INDICE-RF-FINANCIAL.md | 113 - .../requerimientos/RF-FIN-001.md | 279 - .../requerimientos/RF-FIN-002.md | 272 - .../requerimientos/RF-FIN-003.md | 289 - .../requerimientos/RF-FIN-004.md | 314 - .../docs/02-fase-core-business/README.md | 278 - .../RF-auth/INDICE-RF-AUTH.md | 188 - .../03-requerimientos/RF-auth/RF-AUTH-001.md | 234 - .../03-requerimientos/RF-auth/RF-AUTH-002.md | 264 - .../03-requerimientos/RF-auth/RF-AUTH-003.md | 261 - .../03-requerimientos/RF-auth/RF-AUTH-004.md | 288 - .../03-requerimientos/RF-auth/RF-AUTH-005.md | 345 - .../RF-catalogs/INDICE-RF-CATALOG.md | 184 - .../RF-catalogs/RF-CATALOG-001.md | 294 - .../RF-catalogs/RF-CATALOG-002.md | 277 - .../RF-catalogs/RF-CATALOG-003.md | 320 - .../RF-catalogs/RF-CATALOG-004.md | 346 - .../RF-catalogs/RF-CATALOG-005.md | 348 - .../RF-rbac/INDICE-RF-ROLE.md | 221 - .../03-requerimientos/RF-rbac/RF-ROLE-001.md | 364 - .../03-requerimientos/RF-rbac/RF-ROLE-002.md | 338 - .../03-requerimientos/RF-rbac/RF-ROLE-003.md | 350 - .../03-requerimientos/RF-rbac/RF-ROLE-004.md | 530 - .../RF-tenants/INDICE-RF-TENANT.md | 271 - .../RF-tenants/RF-TENANT-001.md | 396 - .../RF-tenants/RF-TENANT-002.md | 370 - .../RF-tenants/RF-TENANT-003.md | 424 - .../RF-tenants/RF-TENANT-004.md | 460 - .../RF-users/INDICE-RF-USER.md | 260 - .../03-requerimientos/RF-users/RF-USER-001.md | 333 - .../03-requerimientos/RF-users/RF-USER-002.md | 314 - .../03-requerimientos/RF-users/RF-USER-003.md | 332 - .../03-requerimientos/RF-users/RF-USER-004.md | 362 - .../03-requerimientos/RF-users/RF-USER-005.md | 370 - .../04-modelado/FASE-2-INICIO-COMPLETADO.md | 260 - .../04-modelado/MAPEO-SPECS-VERTICALES.md | 213 - .../VALIDACION-DEPENDENCIAS-MGN-015-018.md | 559 - .../AUTOMATIC-TRACKING-SYSTEM.md | 432 - .../database-design/DDL-SPEC-ai_agents.md | 1058 -- .../database-design/DDL-SPEC-billing.md | 872 - .../database-design/DDL-SPEC-core_auth.md | 744 - .../database-design/DDL-SPEC-core_catalogs.md | 815 - .../database-design/DDL-SPEC-core_rbac.md | 694 - .../database-design/DDL-SPEC-core_tenants.md | 1117 -- .../database-design/DDL-SPEC-core_users.md | 847 - .../database-design/DDL-SPEC-integrations.md | 679 - .../database-design/DDL-SPEC-messaging.md | 832 - .../04-modelado/database-design/README.md | 219 - .../database-design/database-roadmap.md | 418 - .../schemas/SCHEMAS-STATISTICS.md | 504 - .../schemas/analytics-schema-ddl.sql | 510 - .../schemas/auth-schema-ddl.sql | 620 - .../schemas/core-schema-ddl.sql | 752 - .../schemas/financial-schema-ddl.sql | 948 - .../schemas/inventory-schema-ddl.sql | 750 - .../schemas/projects-schema-ddl.sql | 537 - .../schemas/purchase-schema-ddl.sql | 554 - .../schemas/sales-schema-ddl.sql | 672 - .../schemas/system-schema-ddl.sql | 853 - .../domain-models/analytics-domain.md | 337 - .../04-modelado/domain-models/auth-domain.md | 267 - .../domain-models/billing-domain.md | 308 - .../04-modelado/domain-models/crm-domain.md | 298 - .../domain-models/financial-domain.md | 348 - .../04-modelado/domain-models/hr-domain.md | 425 - .../domain-models/inventory-domain.md | 360 - .../domain-models/messaging-domain.md | 464 - .../domain-models/projects-domain.md | 357 - .../04-modelado/domain-models/sales-domain.md | 324 - .../ET-auth-backend.md | 1749 -- .../ET-rbac-backend.md | 1274 -- .../ET-tenants-backend.md | 2365 --- .../ET-users-backend.md | 1247 -- .../especificaciones-tecnicas/README.md | 641 - ...D-MGN-001-001-autenticaci贸n-de-usuarios.md | 1025 -- ...01-002-gesti贸n-de-roles-y-permisos-rbac.md | 1025 -- ...BACKEND-MGN-001-003-gesti贸n-de-usuarios.md | 1025 -- ...ulti-tenancy-con-schema-level-isolation.md | 1025 -- ...BACKEND-MGN-001-005-reset-de-contrase帽a.md | 1025 -- ...MGN-001-006-registro-de-usuarios-signup.md | 1025 -- ...BACKEND-MGN-001-007-gesti贸n-de-sesiones.md | 1025 -- ...001-008-record-rules-row-level-security.md | 1025 -- ...BACKEND-MGN-002-001-gesti贸n-de-empresas.md | 1025 -- ...ND-MGN-002-002-configuraci贸n-de-empresa.md | 1025 -- ...贸n-de-usuarios-a-empresas-multi-empresa.md | 1025 -- ...002-004-jerarqu铆as-de-empresas-holdings.md | 1025 -- ...05-plantillas-de-configuraci贸n-por-pa铆s.md | 1025 -- ...003-001-gesti贸n-de-partners-universales.md | 1025 -- ...GN-003-002-gesti贸n-de-pa铆ses-y-regiones.md | 1017 -- ...03-gesti贸n-de-monedas-y-tasas-de-cambio.md | 1017 -- ...3-004-gesti贸n-de-unidades-de-medida-uom.md | 1017 -- ...-005-gesti贸n-de-categor铆as-de-productos.md | 1017 -- ...3-006-condiciones-de-pago-payment-terms.md | 1017 -- ...-MGN-004-001-gesti贸n-de-plan-de-cuentas.md | 1017 -- ...N-004-002-gesti贸n-de-journals-contables.md | 1017 -- ...-004-003-registro-de-asientos-contables.md | 1017 -- ...ACKEND-MGN-004-004-gesti贸n-de-impuestos.md | 1017 -- ...-004-005-gesti贸n-de-facturas-de-cliente.md | 1017 -- ...04-006-gesti贸n-de-facturas-de-proveedor.md | 1017 -- ...004-007-gesti贸n-de-pagos-y-conciliaci贸n.md | 1017 -- ...-008-reportes-financieros-balance-y-p&l.md | 1017 -- ...ACKEND-MGN-005-001-gesti贸n-de-productos.md | 1017 -- ...-002-gesti贸n-de-almacenes-y-ubicaciones.md | 1017 -- ...ACKEND-MGN-005-003-movimientos-de-stock.md | 1017 -- ...04-pickings-albaranes-de-entrada-salida.md | 1017 -- ...5-trazabilidad-lotes-y-n煤meros-de-serie.md | 1017 -- ...valoraci贸n-de-inventario-fifo,-promedio.md | 1017 -- ...MGN-005-007-inventario-f铆sico-y-ajustes.md | 1017 -- ...N-006-001-solicitudes-de-cotizaci贸n-rfq.md | 1017 -- ...GN-006-002-gesti贸n-de-贸rdenes-de-compra.md | 1017 -- ...6-003-workflow-de-aprobaci贸n-de-compras.md | 1017 -- ...KEND-MGN-006-004-recepciones-de-compras.md | 1017 -- ...acturaci贸n-de-proveedores-desde-compras.md | 1017 -- ...BACKEND-MGN-006-006-reportes-de-compras.md | 1017 -- ...END-MGN-007-001-gesti贸n-de-cotizaciones.md | 1017 -- ...N-007-002-conversi贸n-a-贸rdenes-de-venta.md | 1017 -- ...MGN-007-003-gesti贸n-de-贸rdenes-de-venta.md | 1017 -- ...-BACKEND-MGN-007-004-entregas-de-ventas.md | 1017 -- ...05-facturaci贸n-de-clientes-desde-ventas.md | 1017 -- ...-BACKEND-MGN-007-006-reportes-de-ventas.md | 1017 -- ...N-008-001-gesti贸n-de-cuentas-anal铆ticas.md | 1017 -- ...N-008-002-registro-de-l铆neas-anal铆ticas.md | 1017 -- ...003-distribuci贸n-anal铆tica-multi-cuenta.md | 1017 -- .../ET-BACKEND-MGN-008-004-tags-anal铆ticos.md | 1017 -- ...05-reportes-anal铆ticos-p&l-por-proyecto.md | 1017 -- ...09-001-gesti贸n-de-leads-y-oportunidades.md | 1017 -- ...D-MGN-009-002-pipeline-de-ventas-kanban.md | 1017 -- ...D-MGN-009-003-actividades-y-seguimiento.md | 1017 -- ...MGN-009-004-lead-scoring-y-calificaci贸n.md | 1017 -- ...END-MGN-009-005-conversi贸n-a-cotizaci贸n.md | 1017 -- ...ACKEND-MGN-010-001-gesti贸n-de-empleados.md | 1017 -- ...END-MGN-010-002-departamentos-y-puestos.md | 1017 -- ...BACKEND-MGN-010-003-contratos-laborales.md | 1017 -- ...-010-004-asistencias-check-in-check-out.md | 1017 -- ...ACKEND-MGN-010-005-ausencias-y-permisos.md | 1017 -- ...ACKEND-MGN-011-001-gesti贸n-de-proyectos.md | 1017 -- ...ND-MGN-011-002-gesti贸n-de-tareas-kanban.md | 1017 -- ...ET-BACKEND-MGN-011-003-milestones-hitos.md | 1017 -- ...KEND-MGN-011-004-timesheet-de-proyectos.md | 1017 -- ...ND-MGN-011-005-vista-gantt-de-proyectos.md | 1017 -- ...ND-MGN-012-001-dashboards-configurables.md | 1017 -- ...query-builder-y-reportes-personalizados.md | 1017 -- ...03-exportaci贸n-de-datos-pdf,-excel,-csv.md | 1017 -- ...-MGN-012-004-gr谩ficos-y-visualizaciones.md | 1017 -- ...MGN-013-001-acceso-portal-para-clientes.md | 1017 -- ...N-013-002-vista-de-documentos-en-portal.md | 1017 -- ...-013-003-aprobaci贸n-y-firma-electr贸nica.md | 1017 -- ...ACKEND-MGN-013-004-mensajer铆a-en-portal.md | 1017 -- ...MGN-014-001-sistema-de-mensajes-chatter.md | 1017 -- ...N-014-002-notificaciones-in-app-y-email.md | 1017 -- ...-014-003-tracking-autom谩tico-de-cambios.md | 1017 -- ...END-MGN-014-004-actividades-programadas.md | 1017 -- ...ACKEND-MGN-014-005-followers-seguidores.md | 1017 -- ...-BACKEND-MGN-014-006-templates-de-email.md | 1017 -- .../ET-MGN-015-001-api-planes-suscripcion.md | 442 - .../ET-MGN-015-002-api-suscripciones.md | 561 - .../ET-MGN-015-003-api-metodos-pago.md | 541 - .../mgn-015/ET-MGN-015-004-api-facturacion.md | 667 - .../ET-MGN-015-005-api-uso-metricas.md | 661 - .../backend/mgn-015/README.md | 186 - ...D-MGN-001-001-autenticaci贸n-de-usuarios.md | 1116 -- ...01-002-gesti贸n-de-roles-y-permisos-rbac.md | 1116 -- ...RONTEND-MGN-001-003-gesti贸n-de-usuarios.md | 1116 -- ...ulti-tenancy-con-schema-level-isolation.md | 1116 -- ...RONTEND-MGN-001-005-reset-de-contrase帽a.md | 1116 -- ...MGN-001-006-registro-de-usuarios-signup.md | 1116 -- ...RONTEND-MGN-001-007-gesti贸n-de-sesiones.md | 1116 -- ...001-008-record-rules-row-level-security.md | 1116 -- ...RONTEND-MGN-002-001-gesti贸n-de-empresas.md | 1116 -- ...ND-MGN-002-002-configuraci贸n-de-empresa.md | 1116 -- ...贸n-de-usuarios-a-empresas-multi-empresa.md | 1116 -- ...002-004-jerarqu铆as-de-empresas-holdings.md | 1116 -- ...05-plantillas-de-configuraci贸n-por-pa铆s.md | 1116 -- ...003-001-gesti贸n-de-partners-universales.md | 1116 -- ...GN-003-002-gesti贸n-de-pa铆ses-y-regiones.md | 1116 -- ...03-gesti贸n-de-monedas-y-tasas-de-cambio.md | 1116 -- ...3-004-gesti贸n-de-unidades-de-medida-uom.md | 1116 -- ...-005-gesti贸n-de-categor铆as-de-productos.md | 1116 -- ...3-006-condiciones-de-pago-payment-terms.md | 1116 -- ...-MGN-004-001-gesti贸n-de-plan-de-cuentas.md | 1116 -- ...N-004-002-gesti贸n-de-journals-contables.md | 1116 -- ...-004-003-registro-de-asientos-contables.md | 1116 -- ...ONTEND-MGN-004-004-gesti贸n-de-impuestos.md | 1116 -- ...-004-005-gesti贸n-de-facturas-de-cliente.md | 1116 -- ...04-006-gesti贸n-de-facturas-de-proveedor.md | 1116 -- ...004-007-gesti贸n-de-pagos-y-conciliaci贸n.md | 1116 -- ...-008-reportes-financieros-balance-y-p&l.md | 1116 -- ...ONTEND-MGN-005-001-gesti贸n-de-productos.md | 1116 -- ...-002-gesti贸n-de-almacenes-y-ubicaciones.md | 1116 -- ...ONTEND-MGN-005-003-movimientos-de-stock.md | 1116 -- ...04-pickings-albaranes-de-entrada-salida.md | 1116 -- ...5-trazabilidad-lotes-y-n煤meros-de-serie.md | 1116 -- ...valoraci贸n-de-inventario-fifo,-promedio.md | 1116 -- ...MGN-005-007-inventario-f铆sico-y-ajustes.md | 1116 -- ...N-006-001-solicitudes-de-cotizaci贸n-rfq.md | 1116 -- ...GN-006-002-gesti贸n-de-贸rdenes-de-compra.md | 1116 -- ...6-003-workflow-de-aprobaci贸n-de-compras.md | 1116 -- ...TEND-MGN-006-004-recepciones-de-compras.md | 1116 -- ...acturaci贸n-de-proveedores-desde-compras.md | 1116 -- ...RONTEND-MGN-006-006-reportes-de-compras.md | 1116 -- ...END-MGN-007-001-gesti贸n-de-cotizaciones.md | 1116 -- ...N-007-002-conversi贸n-a-贸rdenes-de-venta.md | 1116 -- ...MGN-007-003-gesti贸n-de-贸rdenes-de-venta.md | 1116 -- ...FRONTEND-MGN-007-004-entregas-de-ventas.md | 1116 -- ...05-facturaci贸n-de-clientes-desde-ventas.md | 1116 -- ...FRONTEND-MGN-007-006-reportes-de-ventas.md | 1116 -- ...N-008-001-gesti贸n-de-cuentas-anal铆ticas.md | 1116 -- ...N-008-002-registro-de-l铆neas-anal铆ticas.md | 1116 -- ...003-distribuci贸n-anal铆tica-multi-cuenta.md | 1116 -- ...ET-FRONTEND-MGN-008-004-tags-anal铆ticos.md | 1116 -- ...05-reportes-anal铆ticos-p&l-por-proyecto.md | 1116 -- ...09-001-gesti贸n-de-leads-y-oportunidades.md | 1116 -- ...D-MGN-009-002-pipeline-de-ventas-kanban.md | 1116 -- ...D-MGN-009-003-actividades-y-seguimiento.md | 1116 -- ...MGN-009-004-lead-scoring-y-calificaci贸n.md | 1116 -- ...END-MGN-009-005-conversi贸n-a-cotizaci贸n.md | 1116 -- ...ONTEND-MGN-010-001-gesti贸n-de-empleados.md | 1116 -- ...END-MGN-010-002-departamentos-y-puestos.md | 1116 -- ...RONTEND-MGN-010-003-contratos-laborales.md | 1116 -- ...-010-004-asistencias-check-in-check-out.md | 1116 -- ...ONTEND-MGN-010-005-ausencias-y-permisos.md | 1116 -- ...ONTEND-MGN-011-001-gesti贸n-de-proyectos.md | 1116 -- ...ND-MGN-011-002-gesti贸n-de-tareas-kanban.md | 1116 -- ...T-FRONTEND-MGN-011-003-milestones-hitos.md | 1116 -- ...TEND-MGN-011-004-timesheet-de-proyectos.md | 1116 -- ...ND-MGN-011-005-vista-gantt-de-proyectos.md | 1116 -- ...ND-MGN-012-001-dashboards-configurables.md | 1116 -- ...query-builder-y-reportes-personalizados.md | 1116 -- ...03-exportaci贸n-de-datos-pdf,-excel,-csv.md | 1116 -- ...-MGN-012-004-gr谩ficos-y-visualizaciones.md | 1116 -- ...MGN-013-001-acceso-portal-para-clientes.md | 1116 -- ...N-013-002-vista-de-documentos-en-portal.md | 1116 -- ...-013-003-aprobaci贸n-y-firma-electr贸nica.md | 1116 -- ...ONTEND-MGN-013-004-mensajer铆a-en-portal.md | 1116 -- ...MGN-014-001-sistema-de-mensajes-chatter.md | 1116 -- ...N-014-002-notificaciones-in-app-y-email.md | 1116 -- ...-014-003-tracking-autom谩tico-de-cambios.md | 1116 -- ...END-MGN-014-004-actividades-programadas.md | 1116 -- ...ONTEND-MGN-014-005-followers-seguidores.md | 1116 -- ...FRONTEND-MGN-014-006-templates-de-email.md | 1116 -- .../especificaciones-tecnicas/generate_et.py | 2418 --- .../transversal/SPEC-ALERTAS-PRESUPUESTO.md | 1643 -- .../transversal/SPEC-BLANKET-ORDERS.md | 2258 --- .../transversal/SPEC-CONCILIACION-BANCARIA.md | 2496 --- .../SPEC-CONSOLIDACION-FINANCIERA.md | 1295 -- ...CONTABILIDAD-ANALITICA-MULTIDIMENSIONAL.md | 1210 -- .../SPEC-FIRMA-ELECTRONICA-NOM151.md | 2593 --- .../transversal/SPEC-GASTOS-EMPLEADOS.md | 1911 -- .../transversal/SPEC-IMPUESTOS-AVANZADOS.md | 1754 -- .../transversal/SPEC-INTEGRACION-CALENDAR.md | 1677 -- .../transversal/SPEC-INVENTARIOS-CICLICOS.md | 1852 -- .../transversal/SPEC-LOCALIZACION-PAISES.md | 1826 -- .../transversal/SPEC-MAIL-THREAD-TRACKING.md | 1482 -- .../transversal/SPEC-NOMINA-BASICA.md | 1809 -- .../transversal/SPEC-OAUTH2-SOCIAL-LOGIN.md | 2106 --- .../transversal/SPEC-PLANTILLAS-CUENTAS.md | 1658 -- .../transversal/SPEC-PORTAL-PROVEEDORES.md | 2024 --- .../SPEC-PRESUPUESTOS-REVISIONES.md | 1773 -- .../transversal/SPEC-PRICING-RULES.md | 1421 -- .../SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN.md | 2056 --- .../transversal/SPEC-REPORTES-FINANCIEROS.md | 1314 -- .../SPEC-RRHH-EVALUACIONES-SKILLS.md | 1384 -- .../transversal/SPEC-SCHEDULER-REPORTES.md | 1452 -- .../SPEC-SEGURIDAD-API-KEYS-PERMISOS.md | 1439 -- .../transversal/SPEC-SISTEMA-SECUENCIAS.md | 683 - .../transversal/SPEC-TAREAS-RECURRENTES.md | 1018 -- .../SPEC-TASAS-CAMBIO-AUTOMATICAS.md | 2440 --- .../SPEC-TRAZABILIDAD-LOTES-SERIES.md | 1875 -- .../SPEC-TWO-FACTOR-AUTHENTICATION.md | 1947 --- .../transversal/SPEC-VALORACION-INVENTARIO.md | 931 - .../SPEC-WIZARD-TRANSIENT-MODEL.md | 1387 -- .../requerimientos-funcionales/README.md | 355 - .../generate_rfs.py | 810 - .../RF-MGN-001-001-autenticacion-usuarios.md | 105 - .../mgn-001/RF-MGN-001-002-gestion-roles.md | 108 - .../RF-MGN-001-003-gestion-usuarios.md | 123 - .../mgn-001/RF-MGN-001-004-multi-tenancy.md | 131 - .../mgn-001/RF-MGN-001-005-reset-password.md | 136 - .../RF-MGN-001-006-registro-usuarios.md | 143 - .../RF-MGN-001-007-session-management.md | 147 - .../RF-MGN-001-008-record-rules-rls.md | 158 - .../RF-MGN-002-001-gestion-empresas.md | 130 - .../RF-MGN-002-002-configuracion-empresa.md | 147 - ...GN-002-003-asignacion-usuarios-empresas.md | 127 - .../RF-MGN-002-004-jerarquias-empresas.md | 114 - ...RF-MGN-002-005-plantillas-configuracion.md | 135 - .../RF-MGN-003-001-gestion-partners.md | 87 - ...GN-003-002-gesti贸n-de-pa铆ses-y-regiones.md | 92 - ...03-gesti贸n-de-monedas-y-tasas-de-cambio.md | 92 - ...3-004-gesti贸n-de-unidades-de-medida-uom.md | 92 - ...-005-gesti贸n-de-categor铆as-de-productos.md | 92 - ...3-006-condiciones-de-pago-payment-terms.md | 92 - ...-MGN-004-001-gesti贸n-de-plan-de-cuentas.md | 92 - ...N-004-002-gesti贸n-de-journals-contables.md | 92 - ...-004-003-registro-de-asientos-contables.md | 92 - .../RF-MGN-004-004-gesti贸n-de-impuestos.md | 92 - ...-004-005-gesti贸n-de-facturas-de-cliente.md | 92 - ...04-006-gesti贸n-de-facturas-de-proveedor.md | 92 - ...004-007-gesti贸n-de-pagos-y-conciliaci贸n.md | 92 - ...-008-reportes-financieros-balance-y-p&l.md | 92 - .../RF-MGN-005-001-gesti贸n-de-productos.md | 92 - ...-002-gesti贸n-de-almacenes-y-ubicaciones.md | 92 - .../RF-MGN-005-003-movimientos-de-stock.md | 92 - ...04-pickings-albaranes-de-entrada-salida.md | 92 - ...5-trazabilidad-lotes-y-n煤meros-de-serie.md | 92 - ...valoraci贸n-de-inventario-fifo,-promedio.md | 92 - ...MGN-005-007-inventario-f铆sico-y-ajustes.md | 92 - ...N-006-001-solicitudes-de-cotizaci贸n-rfq.md | 92 - ...GN-006-002-gesti贸n-de-贸rdenes-de-compra.md | 92 - ...6-003-workflow-de-aprobaci贸n-de-compras.md | 92 - .../RF-MGN-006-004-recepciones-de-compras.md | 92 - ...acturaci贸n-de-proveedores-desde-compras.md | 92 - .../RF-MGN-006-006-reportes-de-compras.md | 92 - .../RF-MGN-007-001-gesti贸n-de-cotizaciones.md | 92 - ...N-007-002-conversi贸n-a-贸rdenes-de-venta.md | 92 - ...MGN-007-003-gesti贸n-de-贸rdenes-de-venta.md | 92 - .../RF-MGN-007-004-entregas-de-ventas.md | 92 - ...05-facturaci贸n-de-clientes-desde-ventas.md | 92 - .../RF-MGN-007-006-reportes-de-ventas.md | 92 - ...N-008-001-gesti贸n-de-cuentas-anal铆ticas.md | 92 - ...N-008-002-registro-de-l铆neas-anal铆ticas.md | 92 - ...003-distribuci贸n-anal铆tica-multi-cuenta.md | 92 - .../mgn-008/RF-MGN-008-004-tags-anal铆ticos.md | 92 - ...05-reportes-anal铆ticos-p&l-por-proyecto.md | 92 - ...09-001-gesti贸n-de-leads-y-oportunidades.md | 92 - ...F-MGN-009-002-pipeline-de-ventas-kanban.md | 92 - ...F-MGN-009-003-actividades-y-seguimiento.md | 92 - ...MGN-009-004-lead-scoring-y-calificaci贸n.md | 92 - .../RF-MGN-009-005-conversi贸n-a-cotizaci贸n.md | 92 - .../RF-MGN-010-001-gesti贸n-de-empleados.md | 92 - .../RF-MGN-010-002-departamentos-y-puestos.md | 92 - .../RF-MGN-010-003-contratos-laborales.md | 92 - ...-010-004-asistencias-check-in-check-out.md | 92 - .../RF-MGN-010-005-ausencias-y-permisos.md | 92 - .../RF-MGN-011-001-gesti贸n-de-proyectos.md | 92 - ...RF-MGN-011-002-gesti贸n-de-tareas-kanban.md | 92 - .../RF-MGN-011-003-milestones-hitos.md | 92 - .../RF-MGN-011-004-timesheet-de-proyectos.md | 92 - ...RF-MGN-011-005-vista-gantt-de-proyectos.md | 92 - ...RF-MGN-012-001-dashboards-configurables.md | 92 - ...query-builder-y-reportes-personalizados.md | 92 - ...03-exportaci贸n-de-datos-pdf,-excel,-csv.md | 92 - ...-MGN-012-004-gr谩ficos-y-visualizaciones.md | 92 - ...MGN-013-001-acceso-portal-para-clientes.md | 92 - ...N-013-002-vista-de-documentos-en-portal.md | 92 - ...-013-003-aprobaci贸n-y-firma-electr贸nica.md | 92 - .../RF-MGN-013-004-mensajer铆a-en-portal.md | 92 - ...MGN-014-001-sistema-de-mensajes-chatter.md | 92 - ...N-014-002-notificaciones-in-app-y-email.md | 92 - ...-014-003-tracking-autom谩tico-de-cambios.md | 92 - .../RF-MGN-014-004-actividades-programadas.md | 92 - .../RF-MGN-014-005-followers-seguidores.md | 92 - .../RF-MGN-014-006-templates-de-email.md | 92 - .../mgn-015/README.md | 100 - ...-MGN-015-001-gestion-planes-suscripcion.md | 116 - ...GN-015-002-gestion-suscripciones-tenant.md | 147 - .../mgn-015/RF-MGN-015-003-metodos-pago.md | 136 - .../RF-MGN-015-004-facturacion-cobros.md | 178 - .../RF-MGN-015-005-registro-uso-metricas.md | 165 - .../RF-MGN-015-006-modo-single-tenant.md | 210 - .../RF-MGN-015-007-pricing-por-usuario.md | 257 - .../mgn-016/README.md | 142 - .../RF-MGN-016-001-integracion-mercadopago.md | 270 - .../RF-MGN-016-002-integracion-clip.md | 255 - .../mgn-017/README.md | 226 - ...-MGN-017-001-conexion-whatsapp-business.md | 356 - .../RF-MGN-017-005-chatbot-automatizado.md | 406 - .../mgn-018/README.md | 247 - .../RF-MGN-018-001-configuracion-agentes.md | 483 - .../RF-MGN-018-002-bases-conocimiento.md | 521 - .../RF-MGN-018-003-procesamiento-mensajes.md | 520 - .../RF-MGN-018-004-acciones-herramientas.md | 683 - .../RF-MGN-018-005-entrenamiento-feedback.md | 537 - .../RF-MGN-018-006-analytics-metricas.md | 578 - .../GRAFO-DEPENDENCIAS-SCHEMAS.md | 420 - .../trazabilidad/INVENTARIO-OBJETOS-BD.yml | 2169 --- .../MATRIZ-TRAZABILIDAD-RF-ET-BD.md | 315 - .../docs/04-modelado/trazabilidad/README.md | 484 - .../REPORTE-VALIDACION-DDL-DOC.md | 396 - .../REPORTE-VALIDACION-PREVIA-BD.md | 318 - .../trazabilidad/TRACEABILITY-MGN-001.yaml | 1407 -- .../trazabilidad/TRACEABILITY-MGN-002.yaml | 859 - .../trazabilidad/TRACEABILITY-MGN-003.yaml | 998 -- .../trazabilidad/TRACEABILITY-MGN-004.yaml | 1460 -- .../trazabilidad/TRACEABILITY-MGN-005.yaml | 1010 -- .../trazabilidad/TRACEABILITY-MGN-006.yaml | 447 - .../trazabilidad/TRACEABILITY-MGN-007.yaml | 1144 -- .../trazabilidad/TRACEABILITY-MGN-008.yaml | 985 -- .../trazabilidad/TRACEABILITY-MGN-009.yaml | 931 - .../trazabilidad/TRACEABILITY-MGN-010.yaml | 964 -- .../trazabilidad/TRACEABILITY-MGN-011.yaml | 946 - .../trazabilidad/TRACEABILITY-MGN-012.yaml | 765 - .../trazabilidad/TRACEABILITY-MGN-013.yaml | 747 - .../trazabilidad/TRACEABILITY-MGN-014.yaml | 1092 -- .../trazabilidad/TRACEABILITY-MGN-015.yaml | 536 - .../trazabilidad/VALIDACION-COBERTURA-ODOO.md | 563 - .../workflows/WORKFLOW-3-WAY-MATCH.md | 710 - .../WORKFLOW-CIERRE-PERIODO-CONTABLE.md | 596 - .../workflows/WORKFLOW-PAGOS-ANTICIPADOS.md | 615 - .../PLAN-EJECUCION-US-RESTANTES.md | 389 - .../erp-core/docs/05-user-stories/README.md | 360 - .../REPORTE-COMPLETACION-70-US.md | 308 - .../REPORTE-PROGRESO-FASE-3.md | 310 - .../RESUMEN-EJECUTIVO-FASE-3.md | 254 - .../_legacy_backup/MGN-001/BACKLOG-MGN001.md | 162 - .../_legacy_backup/MGN-001/US-MGN001-001.md | 296 - .../_legacy_backup/MGN-001/US-MGN001-002.md | 261 - .../_legacy_backup/MGN-001/US-MGN001-003.md | 300 - .../_legacy_backup/MGN-001/US-MGN001-004.md | 391 - .../_legacy_backup/MGN-002/BACKLOG-MGN002.md | 138 - .../_legacy_backup/MGN-002/US-MGN002-001.md | 219 - .../_legacy_backup/MGN-002/US-MGN002-002.md | 225 - .../_legacy_backup/MGN-002/US-MGN002-003.md | 203 - .../_legacy_backup/MGN-002/US-MGN002-004.md | 222 - .../_legacy_backup/MGN-003/BACKLOG-MGN003.md | 177 - .../_legacy_backup/MGN-003/US-MGN003-001.md | 162 - .../_legacy_backup/MGN-003/US-MGN003-002.md | 177 - .../_legacy_backup/MGN-003/US-MGN003-003.md | 211 - .../_legacy_backup/MGN-003/US-MGN003-004.md | 230 - .../_legacy_backup/MGN-004/BACKLOG-MGN004.md | 210 - .../_legacy_backup/MGN-004/US-MGN004-001.md | 184 - .../_legacy_backup/MGN-004/US-MGN004-002.md | 178 - .../_legacy_backup/MGN-004/US-MGN004-003.md | 205 - .../_legacy_backup/MGN-004/US-MGN004-004.md | 211 - .../mgn-001/BACKLOG-MGN-001.md | 162 - ...GN-001-001-001-login-con-email-password.md | 242 - .../US-MGN-001-001-002-renovar-token-jwt.md | 230 - ...MGN-001-002-001-crear-y-gestionar-roles.md | 259 - ...GN-001-002-002-asignar-permisos-a-roles.md | 256 - ...001-002-003-validar-permisos-en-runtime.md | 262 - .../US-MGN-001-003-001-crud-usuarios.md | 114 - ...03-002-gestion-perfil-y-cambio-password.md | 104 - .../US-MGN-001-004-001-crear-tenant.md | 66 - .../US-MGN-001-004-002-schema-isolation.md | 58 - ...GN-001-004-003-tenant-context-switching.md | 51 - .../US-MGN-001-005-001-reset-password.md | 68 - .../US-MGN-001-006-001-signup-autoregistro.md | 62 - ...GN-001-007-001-gestion-sesiones-activas.md | 60 - .../mgn-001/US-MGN-001-007-002-logout.md | 55 - .../US-MGN-001-008-001-rls-policies.md | 61 - ...US-MGN-001-008-002-field-level-security.md | 56 - .../mgn-002/BACKLOG-MGN-002.md | 138 - .../US-MGN-002-001-001-crud-empresas.md | 74 - .../US-MGN-002-001-002-logo-y-branding.md | 56 - ...002-001-configuracion-fiscal-y-contable.md | 60 - ...002-003-001-asignar-usuarios-a-empresas.md | 62 - ...-MGN-002-003-002-cambiar-empresa-activa.md | 56 - .../US-MGN-002-004-001-jerarquias-holdings.md | 62 - ...5-001-plantillas-configuracion-por-pais.md | 56 - .../mgn-003/BACKLOG-MGN-003.md | 177 - .../US-MGN-003-001-001-crud-partners.md | 366 - ...S-MGN-003-001-002-direcciones-multiples.md | 55 - .../US-MGN-003-002-001-paises-y-estados.md | 56 - .../US-MGN-003-003-001-gestion-monedas.md | 56 - .../US-MGN-003-003-002-tasas-de-cambio.md | 56 - .../US-MGN-003-004-001-unidades-de-medida.md | 56 - ...MGN-003-005-001-categorias-de-productos.md | 55 - .../US-MGN-003-006-001-condiciones-de-pago.md | 61 - .../mgn-004/BACKLOG-MGN-004.md | 210 - ...-MGN-004-001-001-crud-cuentas-contables.md | 258 - ...004-001-002-jerarquia-cuentas-contables.md | 236 - ...MGN-004-002-001-crud-journals-contables.md | 195 - ...04-003-001-crear-asiento-contable-draft.md | 232 - ...N-004-003-002-validar-y-postear-asiento.md | 252 - ...03-003-cancelar-asiento-reversing-entry.md | 244 - .../US-MGN-004-004-001-crud-impuestos.md | 185 - ...04-004-002-calculo-impuestos-automatico.md | 184 - ...004-005-001-crear-factura-cliente-draft.md | 170 - ...MGN-004-005-002-validar-factura-cliente.md | 179 - ...GN-004-005-003-cancelar-factura-cliente.md | 91 - ...4-006-001-crear-factura-proveedor-draft.md | 83 - ...N-004-006-002-validar-factura-proveedor.md | 80 - ...-004-006-003-cancelar-factura-proveedor.md | 65 - ...MGN-004-007-001-registrar-pago-recibido.md | 180 - ...GN-004-007-002-registrar-pago-realizado.md | 81 - .../US-MGN-004-007-003-cancelar-pago.md | 68 - ...008-001-reportes-financieros-balance-pl.md | 252 - .../US-MGN-005-001-001-crear-producto.md | 216 - ...05-001-002-gestionar-variantes-producto.md | 180 - ...002-001-gestionar-almacenes-ubicaciones.md | 218 - ...-MGN-005-003-001-crear-movimiento-stock.md | 198 - ...GN-005-003-002-validar-movimiento-stock.md | 197 - ...N-005-003-003-cancelar-movimiento-stock.md | 186 - ...MGN-005-004-001-visualizar-stock-quants.md | 184 - .../US-MGN-005-004-002-reservar-stock.md | 187 - ...MGN-005-005-001-crear-ajuste-inventario.md | 108 - ...N-005-005-002-validar-ajuste-inventario.md | 110 - .../US-MGN-005-006-001-valoracion-fifo.md | 114 - .../US-MGN-005-006-002-valoracion-promedio.md | 108 - ...5-007-001-reporte-inventario-valorizado.md | 116 - ...N-005-007-002-reporte-movimientos-stock.md | 111 - .../mgn-006/US-MGN-006-001-001-crear-rfq.md | 109 - .../mgn-006/US-MGN-006-001-002-enviar-rfq.md | 107 - .../US-MGN-006-002-001-crear-orden-compra.md | 113 - ...-MGN-006-002-002-confirmar-orden-compra.md | 111 - ...S-MGN-006-002-003-cancelar-orden-compra.md | 106 - ...-MGN-006-003-001-crear-recepcion-compra.md | 109 - ...GN-006-003-002-validar-recepcion-compra.md | 110 - ...006-003-003-recepcion-parcial-backorder.md | 101 - ...-006-004-001-crear-devolucion-proveedor.md | 108 - ...06-004-002-validar-devolucion-proveedor.md | 100 - .../US-MGN-006-005-001-dashboard-compras.md | 109 - ...US-MGN-006-005-002-analisis-proveedores.md | 110 - .../US-MGN-007-001-001-crear-cotizacion.md | 67 - ...MGN-007-001-002-enviar-cotizacion-email.md | 219 - ...-001-crear-sales-order-desde-cotizacion.md | 231 - ...S-MGN-007-002-002-confirmar-sales-order.md | 222 - ...US-MGN-007-002-003-cancelar-sales-order.md | 214 - ...003-001-crear-entrega-desde-sales-order.md | 214 - ...03-002-validar-entrega-actualizar-stock.md | 224 - ...N-007-003-003-entrega-parcial-backorder.md | 208 - ...GN-007-005-001-crear-devolucion-cliente.md | 221 - ...05-002-validar-devolucion-ajustar-stock.md | 182 - .../US-MGN-007-006-001-dashboard-ventas.md | 226 - ...nalisis-ventas-producto-cliente-periodo.md | 227 - ...-MGN-008-001-001-crud-planes-analiticos.md | 145 - ...-002-configurar-dimensiones-multi-nivel.md | 111 - ...02-001-crud-cuentas-analiticas-por-plan.md | 81 - ...-gestionar-jerarquia-cuentas-analiticas.md | 84 - ...3-001-asignar-distribuciones-analiticas.md | 84 - ...002-calcular-distribuciones-automaticas.md | 75 - ...porte-p-and-l-por-proyecto-departamento.md | 95 - ...04-002-drill-down-analitico-multi-nivel.md | 72 - ...08-005-001-crud-presupuestos-analiticos.md | 81 - ...5-002-alertas-desviacion-presupuestaria.md | 88 - .../mgn-009/US-MGN-009-001-001-crud-leads.md | 114 - ...-MGN-009-001-002-calificar-lead-scoring.md | 101 - .../US-MGN-009-002-001-crud-oportunidades.md | 99 - ...09-002-002-calcular-probabilidad-cierre.md | 83 - ...S-MGN-009-003-001-vista-kanban-pipeline.md | 94 - ...MGN-009-003-002-drag-drop-oportunidades.md | 82 - ...US-MGN-009-004-001-crud-actividades-crm.md | 98 - ...-009-005-001-convertir-lead-oportunidad.md | 95 - .../US-MGN-010-001-001-crud-empleados.md | 94 - ...0-001-002-gestionar-documentos-empleado.md | 70 - ...GN-010-002-001-crud-contratos-laborales.md | 65 - ...002-002-renovacion-automatica-contratos.md | 51 - ...registro-check-in-check-out-asistencias.md | 72 - ...4-001-gestionar-jerarquia-departamentos.md | 62 - .../US-MGN-010-005-001-dashboard-rrhh.md | 72 - ...11-0-001-aprobar-timesheet-de-empleados.md | 59 - ...001-asignar-miembros-y-roles-a-proyecto.md | 59 - ...S-MGN-011-0-001-crud-tareas-de-proyecto.md | 59 - ...shboard-de-proyecto-avance-budget-horas.md | 59 - ...011-0-001-diagrama-de-gantt-de-proyecto.md | 59 - ...001-gestionar-dependencias-entre-tareas.md | 59 - ...011-0-001-registrar-timesheet-por-tarea.md | 59 - ...0-001-vista-kanban-de-tareas-por-estado.md | 59 - ...011-0-002-diagrama-de-gantt-de-proyecto.md | 59 - .../US-MGN-011-001-001-crud-proyectos.md | 90 - ...2-configurar-proyecto-fases-presupuesto.md | 61 - .../mgn-011/create_mgn011_us.sh | 162 - ...001-report-builder-visual-con-drag-drop.md | 81 - ...-001-002-gestionar-widgets-de-dashboard.md | 81 - ...inancieros-estndar-balance-pl-cash-flow.md | 81 - ...01-reportes-operacionales-configurables.md | 81 - ...12-004-001-exportar-reportes-a-excelpdf.md | 81 - ...02-enviar-reportes-por-email-programado.md | 81 - ...3-001-001-login-portal-clienteproveedor.md | 77 - ...001-002-registro-self-service-en-portal.md | 77 - ...3-002-001-vista-de-documentos-en-portal.md | 77 - ...2-002-descargar-documentos-desde-portal.md | 77 - ...013-003-001-mensajera-interna-en-portal.md | 77 - ...acin-de-perfil-y-preferencias-en-portal.md | 77 - ...1-comentar-en-registros-chatter-pattern.md | 91 - ...001-002-adjuntar-archivos-a-comentarios.md | 91 - ...001-003-seguirdejar-de-seguir-registros.md | 91 - ...icaciones-push-en-tiempo-real-websocket.md | 91 - ...-notificaciones-por-email-configurables.md | 91 - ...ferencias-de-notificaciones-por-usuario.md | 91 - ...MGN-014-003-001-subir-archivos-adjuntos.md | 91 - ...03-002-gestionar-biblioteca-de-adjuntos.md | 91 - ...014-004-001-aadir-followers-a-registros.md | 91 - ...02-notificar-automticamente-a-followers.md | 91 - ...areas-llamadas-reuniones-con-calendario.md | 91 - ...mensajes-internos-vs-pblicos-en-chatter.md | 91 - .../docs/06-test-plans/MASTER-TEST-PLAN.md | 794 - .../erp-core/docs/06-test-plans/README.md | 342 - .../TEST-PLAN-MGN-001-fundamentos.md | 1012 -- .../TEST-PLAN-MGN-002-empresas.md | 500 - .../TEST-PLAN-MGN-003-catalogos.md | 548 - .../TEST-PLAN-MGN-004-financiero.md | 367 - .../TEST-PLAN-MGN-005-inventario.md | 222 - .../TEST-PLAN-MGN-006-compras.md | 242 - .../06-test-plans/TEST-PLAN-MGN-007-ventas.md | 242 - .../TEST-PLAN-MGN-008-analitica.md | 242 - .../06-test-plans/TEST-PLAN-MGN-009-crm.md | 242 - .../06-test-plans/TEST-PLAN-MGN-010-rrhh.md | 242 - .../TEST-PLAN-MGN-011-proyectos.md | 242 - .../TEST-PLAN-MGN-012-reportes.md | 242 - .../06-test-plans/TEST-PLAN-MGN-013-portal.md | 242 - .../TEST-PLAN-MGN-014-mensajeria.md | 242 - .../erp-core/docs/06-test-plans/TP-auth.md | 792 - .../erp-core/docs/06-test-plans/TP-rbac.md | 715 - .../erp-core/docs/06-test-plans/TP-tenants.md | 606 - .../erp-core/docs/06-test-plans/TP-users.md | 1124 -- .../docs/07-devops/BACKUP-RECOVERY.md | 907 - .../erp-core/docs/07-devops/CI-CD-PIPELINE.md | 831 - .../docs/07-devops/DEPLOYMENT-GUIDE.md | 1827 -- .../07-devops/MONITORING-OBSERVABILITY.md | 1660 -- projects/erp-core/docs/07-devops/README.md | 442 - .../docs/07-devops/SECURITY-HARDENING.md | 955 - .../docs/07-devops/scripts/backup-postgres.sh | 140 - .../docs/07-devops/scripts/health-check.sh | 264 - .../07-devops/scripts/restore-postgres.sh | 140 - .../docs/08-epicas/EPIC-MGN-001-auth.md | 175 - .../docs/08-epicas/EPIC-MGN-002-users.md | 172 - .../docs/08-epicas/EPIC-MGN-003-roles.md | 204 - .../docs/08-epicas/EPIC-MGN-004-tenants.md | 209 - .../docs/08-epicas/EPIC-MGN-005-catalogs.md | 163 - .../docs/08-epicas/EPIC-MGN-006-settings.md | 97 - .../docs/08-epicas/EPIC-MGN-007-audit.md | 170 - .../08-epicas/EPIC-MGN-008-notifications.md | 174 - .../docs/08-epicas/EPIC-MGN-009-reports.md | 178 - .../docs/08-epicas/EPIC-MGN-010-financial.md | 113 - .../docs/08-epicas/EPIC-MGN-011-inventory.md | 104 - .../docs/08-epicas/EPIC-MGN-012-purchasing.md | 98 - .../docs/08-epicas/EPIC-MGN-013-sales.md | 102 - .../docs/08-epicas/EPIC-MGN-014-crm.md | 180 - .../docs/08-epicas/EPIC-MGN-015-projects.md | 178 - .../docs/08-epicas/EPIC-MGN-016-billing.md | 120 - .../docs/08-epicas/EPIC-MGN-017-payments.md | 200 - .../EPIC-MGN-017-stripe-integration.md | 114 - .../docs/08-epicas/EPIC-MGN-018-whatsapp.md | 213 - .../docs/08-epicas/EPIC-MGN-019-ai-agents.md | 150 - .../08-epicas/EPIC-MGN-019-mobile-apps.md | 159 - .../docs/08-epicas/EPIC-MGN-020-onboarding.md | 163 - .../docs/08-epicas/EPIC-MGN-021-ai-tokens.md | 196 - projects/erp-core/docs/08-epicas/README.md | 132 - .../REPORTE-AUDITORIA-DOCUMENTACION.md | 227 - .../docs/97-adr/ADR-001-stack-tecnologico.md | 86 - .../97-adr/ADR-002-arquitectura-modular.md | 71 - .../docs/97-adr/ADR-003-multi-tenancy.md | 69 - .../97-adr/ADR-004-sistema-constantes-ssot.md | 20 - .../docs/97-adr/ADR-005-path-aliases.md | 20 - .../97-adr/ADR-006-rbac-sistema-permisos.md | 29 - .../docs/97-adr/ADR-007-database-design.md | 26 - .../docs/97-adr/ADR-008-api-design.md | 27 - .../97-adr/ADR-009-frontend-architecture.md | 30 - .../docs/97-adr/ADR-010-testing-strategy.md | 37 - .../ADR-011-database-clean-load-strategy.md | 177 - .../ADR-012-complete-traceability-policy.md | 291 - .../docs/CORRECCION-GAP-001-REPORTE.md | 510 - .../docs/CORRECCION-GAP-002-REPORTE.md | 493 - .../erp-core/docs/FRONTEND-PRIORITY-MATRIX.md | 281 - .../docs/INSTRUCCIONES-AGENTE-ARQUITECTURA.md | 476 - projects/erp-core/docs/LANZAR-FASE-0.md | 484 - .../erp-core/docs/PLAN-DESARROLLO-FRONTEND.md | 305 - .../docs/PLAN-DOCUMENTACION-ERP-GENERICO.md | 479 - .../erp-core/docs/PLAN-EXPANSION-BACKEND.md | 468 - .../PLAN-MAESTRO-MIGRACION-CONSOLIDACION.md | 1305 -- projects/erp-core/docs/README.md | 360 - .../docs/REPORTE-ALINEACION-DDL-SPECS.md | 366 - .../REPORTE-REVALIDACION-TECNICA-COMPLETA.md | 1950 --- .../docs/RESUMEN-EJECUTIVO-REVALIDACION.md | 210 - projects/erp-core/docs/SPRINT-PLAN-FASE-1.md | 242 - projects/erp-core/docs/_MAP.md | 40 - projects/erp-core/frontend/.eslintrc.cjs | 27 - projects/erp-core/frontend/Dockerfile | 35 - projects/erp-core/frontend/index.html | 17 - projects/erp-core/frontend/nginx.conf | 35 - projects/erp-core/frontend/package-lock.json | 7509 -------- projects/erp-core/frontend/package.json | 53 - projects/erp-core/frontend/postcss.config.js | 6 - projects/erp-core/frontend/public/vite.svg | 1 - .../frontend/src/app/layouts/AuthLayout.tsx | 75 - .../src/app/layouts/DashboardLayout.tsx | 195 - .../frontend/src/app/layouts/index.ts | 2 - .../frontend/src/app/providers/index.tsx | 15 - .../src/app/router/ProtectedRoute.tsx | 31 - .../frontend/src/app/router/index.tsx | 8 - .../frontend/src/app/router/routes.tsx | 272 - .../features/companies/api/companies.api.ts | 55 - .../src/features/companies/api/index.ts | 1 - .../components/CompanyFiltersPanel.tsx | 104 - .../companies/components/CompanyForm.tsx | 324 - .../features/companies/components/index.ts | 2 - .../src/features/companies/hooks/index.ts | 1 - .../features/companies/hooks/useCompanies.ts | 148 - .../features/companies/types/company.types.ts | 69 - .../src/features/companies/types/index.ts | 1 - .../src/features/partners/api/index.ts | 1 - .../src/features/partners/api/partners.api.ts | 74 - .../components/PartnerFiltersPanel.tsx | 165 - .../partners/components/PartnerForm.tsx | 322 - .../components/PartnerStatusBadge.tsx | 29 - .../partners/components/PartnerTypeBadge.tsx | 38 - .../src/features/partners/components/index.ts | 4 - .../src/features/partners/hooks/index.ts | 1 - .../features/partners/hooks/usePartners.ts | 141 - .../src/features/partners/types/index.ts | 1 - .../features/partners/types/partner.types.ts | 102 - .../frontend/src/features/users/api/index.ts | 1 - .../src/features/users/api/users.api.ts | 81 - .../users/components/UserFiltersPanel.tsx | 130 - .../features/users/components/UserForm.tsx | 165 - .../users/components/UserStatusBadge.tsx | 18 - .../src/features/users/components/index.ts | 3 - .../src/features/users/hooks/index.ts | 1 - .../src/features/users/hooks/useUsers.ts | 155 - .../src/features/users/types/index.ts | 1 - .../src/features/users/types/user.types.ts | 61 - projects/erp-core/frontend/src/index.css | 92 - projects/erp-core/frontend/src/main.tsx | 13 - .../frontend/src/pages/NotFoundPage.tsx | 26 - .../src/pages/auth/ForgotPasswordPage.tsx | 106 - .../frontend/src/pages/auth/LoginPage.tsx | 116 - .../frontend/src/pages/auth/RegisterPage.tsx | 150 - .../src/pages/companies/CompaniesListPage.tsx | 226 - .../src/pages/companies/CompanyCreatePage.tsx | 91 - .../src/pages/companies/CompanyDetailPage.tsx | 314 - .../src/pages/companies/CompanyEditPage.tsx | 119 - .../src/pages/dashboard/DashboardPage.tsx | 171 - .../src/pages/partners/PartnerCreatePage.tsx | 91 - .../src/pages/partners/PartnerDetailPage.tsx | 344 - .../src/pages/partners/PartnerEditPage.tsx | 119 - .../src/pages/partners/PartnersListPage.tsx | 287 - .../src/pages/users/UserCreatePage.tsx | 90 - .../src/pages/users/UserDetailPage.tsx | 346 - .../frontend/src/pages/users/UserEditPage.tsx | 118 - .../src/pages/users/UsersListPage.tsx | 287 - .../frontend/src/pages/users/index.ts | 4 - .../frontend/src/services/api/auth.api.ts | 76 - .../src/services/api/axios-instance.ts | 70 - .../frontend/src/services/api/index.ts | 3 - .../frontend/src/services/api/users.api.ts | 95 - .../erp-core/frontend/src/services/index.ts | 1 - .../shared/components/atoms/Avatar/Avatar.tsx | 144 - .../shared/components/atoms/Avatar/index.ts | 1 - .../shared/components/atoms/Badge/Badge.tsx | 42 - .../shared/components/atoms/Badge/index.ts | 1 - .../shared/components/atoms/Button/Button.tsx | 79 - .../shared/components/atoms/Button/index.ts | 1 - .../shared/components/atoms/Input/Input.tsx | 50 - .../shared/components/atoms/Input/index.ts | 1 - .../shared/components/atoms/Label/Label.tsx | 23 - .../shared/components/atoms/Label/index.ts | 1 - .../components/atoms/Spinner/Spinner.tsx | 29 - .../shared/components/atoms/Spinner/index.ts | 1 - .../components/atoms/Tooltip/Tooltip.tsx | 132 - .../shared/components/atoms/Tooltip/index.ts | 1 - .../src/shared/components/atoms/index.ts | 7 - .../frontend/src/shared/components/index.ts | 3 - .../components/molecules/Alert/Alert.tsx | 67 - .../components/molecules/Alert/index.ts | 1 - .../shared/components/molecules/Card/Card.tsx | 74 - .../shared/components/molecules/Card/index.ts | 1 - .../molecules/FormField/FormField.tsx | 59 - .../components/molecules/FormField/index.ts | 1 - .../src/shared/components/molecules/index.ts | 3 - .../organisms/Breadcrumbs/Breadcrumbs.tsx | 106 - .../components/organisms/Breadcrumbs/index.ts | 1 - .../organisms/DataTable/DataTable.tsx | 299 - .../components/organisms/DataTable/index.ts | 1 - .../organisms/DatePicker/DatePicker.tsx | 305 - .../organisms/DatePicker/DateRangePicker.tsx | 303 - .../components/organisms/DatePicker/index.ts | 2 - .../organisms/Dropdown/Dropdown.tsx | 172 - .../components/organisms/Dropdown/index.ts | 1 - .../organisms/Modal/ConfirmModal.tsx | 93 - .../components/organisms/Modal/Modal.tsx | 134 - .../components/organisms/Modal/index.ts | 2 - .../organisms/Pagination/Pagination.tsx | 146 - .../components/organisms/Pagination/index.ts | 1 - .../components/organisms/Select/Select.tsx | 283 - .../components/organisms/Select/index.ts | 1 - .../components/organisms/Sidebar/Sidebar.tsx | 286 - .../components/organisms/Sidebar/index.ts | 1 - .../shared/components/organisms/Tabs/Tabs.tsx | 138 - .../shared/components/organisms/Tabs/index.ts | 1 - .../components/organisms/Toast/Toast.tsx | 106 - .../components/organisms/Toast/index.ts | 1 - .../src/shared/components/organisms/index.ts | 10 - .../templates/EmptyState/EmptyState.tsx | 265 - .../components/templates/EmptyState/index.ts | 1 - .../src/shared/components/templates/index.ts | 1 - .../src/shared/constants/api-endpoints.ts | 94 - .../frontend/src/shared/constants/index.ts | 3 - .../frontend/src/shared/constants/roles.ts | 52 - .../frontend/src/shared/constants/status.ts | 74 - .../frontend/src/shared/hooks/index.ts | 3 - .../frontend/src/shared/hooks/useDebounce.ts | 17 - .../src/shared/hooks/useLocalStorage.ts | 40 - .../src/shared/hooks/useMediaQuery.ts | 31 - .../erp-core/frontend/src/shared/index.ts | 6 - .../frontend/src/shared/stores/index.ts | 4 - .../src/shared/stores/useAuthStore.ts | 121 - .../src/shared/stores/useCompanyStore.ts | 67 - .../src/shared/stores/useNotificationStore.ts | 86 - .../frontend/src/shared/stores/useUIStore.ts | 89 - .../frontend/src/shared/types/api.types.ts | 31 - .../src/shared/types/entities.types.ts | 82 - .../frontend/src/shared/types/index.ts | 2 - .../erp-core/frontend/src/shared/utils/cn.ts | 6 - .../frontend/src/shared/utils/formatters.ts | 46 - .../frontend/src/shared/utils/index.ts | 2 - projects/erp-core/frontend/src/vite-env.d.ts | 9 - projects/erp-core/frontend/tailwind.config.js | 82 - projects/erp-core/frontend/tsconfig.json | 40 - projects/erp-core/frontend/tsconfig.node.json | 23 - projects/erp-core/frontend/vite.config.d.ts | 2 - projects/erp-core/frontend/vite.config.ts | 31 - .../00-guidelines/CONTEXTO-PROYECTO.md | 307 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 144 - .../00-guidelines/HERENCIA-SIMCO.md | 342 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../01-analisis/ANALISIS-GAPS-CONSOLIDADO.md | 832 - .../ANALISIS-PROPAGACION-ALINEAMIENTO.md | 380 - .../erp-core/orchestration/PROXIMA-ACCION.md | 170 - projects/erp-core/orchestration/README.md | 132 - .../PLAN-CORRECCIONES-ERP-CORE.md | 1311 -- .../DIRECTIVA-DOCUMENTACION-PRE-DESARROLLO.md | 457 - .../DIRECTIVA-EXTENSION-VERTICALES.md | 290 - .../directivas/DIRECTIVA-HERENCIA-MODULOS.md | 492 - .../directivas/DIRECTIVA-MULTI-TENANT.md | 228 - .../directivas/DIRECTIVA-PATRONES-ODOO.md | 600 - .../ESTANDARES-API-REST-GENERICO.md | 686 - .../orchestration/estados/ESTADO-AGENTES.json | 32 - .../inventarios/BACKEND_INVENTORY.yml | 575 - .../inventarios/DATABASE_INVENTORY.yml | 724 - .../inventarios/DEPENDENCY_GRAPH.yml | 352 - .../inventarios/FRONTEND_INVENTORY.yml | 530 - .../inventarios/MASTER_INVENTORY.yml | 847 - .../orchestration/inventarios/README.md | 121 - .../inventarios/TRACEABILITY_MATRIX.yml | 345 - .../prompts/PROMPT-ERP-BACKEND-AGENT.md | 189 - .../prompts/PROMPT-ERP-DATABASE-AGENT.md | 517 - .../prompts/PROMPT-ERP-FRONTEND-AGENT.md | 509 - .../templates/TEMPLATE-DDL-SPECIFICATION.md | 297 - .../TEMPLATE-ESPECIFICACION-BACKEND.md | 856 - .../TEMPLATE-REQUERIMIENTO-FUNCIONAL.md | 193 - .../templates/TEMPLATE-USER-STORY.md | 279 - .../trazas/TRAZA-TAREAS-BACKEND.md | 50 - .../trazas/TRAZA-TAREAS-DATABASE.md | 197 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 34 - projects/erp-core/package-lock.json | 7429 -------- projects/erp-core/package.json | 51 - projects/erp-core/tsconfig.json | 27 - projects/erp-mecanicas-diesel/.env.example | 104 - projects/erp-mecanicas-diesel/INVENTARIO.yml | 31 - .../erp-mecanicas-diesel/PROJECT-STATUS.md | 266 - projects/erp-mecanicas-diesel/README.md | 85 - .../erp-mecanicas-diesel/backend/.env.example | 22 - .../erp-mecanicas-diesel/backend/Dockerfile | 39 - .../backend/package-lock.json | 8045 --------- .../erp-mecanicas-diesel/backend/package.json | 63 - .../backend/service.descriptor.yml | 139 - .../erp-mecanicas-diesel/backend/src/main.ts | 186 - .../src/modules/auth/auth.controller.ts | 177 - .../backend/src/modules/auth/auth.dto.ts | 39 - .../backend/src/modules/auth/auth.service.ts | 236 - .../auth/entities/refresh-token.entity.ts | 42 - .../src/modules/auth/entities/user.entity.ts | 67 - .../modules/auth/entities/workshop.entity.ts | 54 - .../backend/src/modules/auth/index.ts | 11 - .../controllers/customers.controller.ts | 280 - .../modules/customers/controllers/index.ts | 4 - .../src/modules/customers/customers.dto.ts | 46 - .../customers/entities/customer.entity.ts | 122 - .../src/modules/customers/entities/index.ts | 4 - .../backend/src/modules/customers/index.ts | 11 - .../customers/services/customers.service.ts | 224 - .../src/modules/customers/services/index.ts | 4 - .../controllers/part.controller.ts | 259 - .../controllers/supplier.controller.ts | 149 - .../parts-management/entities/index.ts | 9 - .../entities/part-category.entity.ts | 55 - .../parts-management/entities/part.entity.ts | 127 - .../entities/supplier.entity.ts | 70 - .../entities/warehouse-location.entity.ts | 59 - .../src/modules/parts-management/index.ts | 18 - .../parts-management/services/part.service.ts | 341 - .../services/supplier.service.ts | 189 - .../controllers/diagnostic.controller.ts | 151 - .../controllers/quote.controller.ts | 234 - .../controllers/service-order.controller.ts | 216 - .../entities/diagnostic.entity.ts | 93 - .../service-management/entities/index.ts | 11 - .../entities/order-item.entity.ts | 102 - .../entities/quote.entity.ts | 140 - .../entities/service-order.entity.ts | 161 - .../entities/service.entity.ts | 63 - .../entities/work-bay.entity.ts | 77 - .../src/modules/service-management/index.ts | 22 - .../services/diagnostic.service.ts | 290 - .../services/quote.service.ts | 401 - .../services/service-order.service.ts | 484 - .../backend/src/modules/users/index.ts | 8 - .../src/modules/users/users.controller.ts | 221 - .../backend/src/modules/users/users.dto.ts | 58 - .../src/modules/users/users.service.ts | 169 - .../controllers/fleet.controller.ts | 174 - .../controllers/vehicle.controller.ts | 238 - .../entities/engine-catalog.entity.ts | 62 - .../entities/fleet.entity.ts | 76 - .../vehicle-management/entities/index.ts | 10 - .../entities/maintenance-reminder.entity.ts | 103 - .../entities/vehicle-engine.entity.ts | 107 - .../entities/vehicle.entity.ts | 129 - .../src/modules/vehicle-management/index.ts | 19 - .../services/fleet.service.ts | 207 - .../services/vehicle.service.ts | 319 - .../src/shared/middleware/auth.middleware.ts | 57 - .../backend/src/shared/types/index.ts | 48 - .../backend/src/shared/utils/jwt.utils.ts | 65 - .../backend/tsconfig.json | 32 - .../database/HERENCIA-ERP-CORE.md | 324 - .../erp-mecanicas-diesel/database/README.md | 173 - .../database/init/00-extensions.sql | 14 - .../init/00.5-workshop-core-tables.sql | 158 - .../database/init/01-create-schemas.sql | 29 - .../database/init/02-rls-functions.sql | 106 - .../init/03-service-management-tables.sql | 566 - .../database/init/03.5-customers-table.sql | 91 - .../init/04-parts-management-tables.sql | 397 - .../init/05-vehicle-management-tables.sql | 365 - .../database/init/06-seed-data.sql | 81 - .../database/init/07-notifications-schema.sql | 459 - .../database/init/08-analytics-schema.sql | 387 - .../database/init/09-purchasing-schema.sql | 531 - .../database/init/10-warranty-claims.sql | 469 - .../database/init/11-quote-signature.sql | 426 - .../docker-compose.prod.yml | 65 - .../erp-mecanicas-diesel/docker-compose.yml | 137 - .../docs/00-vision-general/VISION.md | 194 - .../MMD-001-fundamentos/README.md | 100 - .../US-MMD001-001-configurar-taller.md | 171 - .../US-MMD001-002-configurar-roles.md | 163 - .../US-MMD001-003-catalogo-servicios.md | 181 - .../US-MMD001-004-datos-fiscales.md | 134 - .../US-MMD001-005-bahias-trabajo.md | 168 - .../US-MMD001-006-rls-aislamiento.md | 193 - .../US-MMD001-007-importar-catalogos.md | 176 - .../US-MMD001-008-cambiar-bahia.md | 131 - .../US-MMD001-009-dashboard-uso.md | 167 - .../MMD-002-ordenes-servicio/README.md | 69 - .../US-MMD002-001-crear-orden.md | 137 - .../US-MMD002-002-registrar-sintomas.md | 111 - .../US-MMD002-003-asignar-orden.md | 119 - .../US-MMD002-004-ver-ordenes-asignadas.md | 103 - .../US-MMD002-005-registrar-trabajos.md | 164 - .../US-MMD002-006-solicitar-refacciones.md | 111 - .../US-MMD002-007-tablero-kanban.md | 166 - .../US-MMD002-008-cerrar-orden.md | 151 - .../US-MMD002-009-notificar-cliente.md | 120 - .../US-MMD002-010-historial-vehiculo.md | 108 - .../US-MMD002-011-estados-personalizados.md | 119 - .../MMD-003-diagnosticos/README.md | 63 - ...US-MMD003-001-diagnostico-computarizado.md | 139 - .../US-MMD003-002-pruebas-inyectores.md | 147 - .../US-MMD003-003-pruebas-bomba.md | 121 - .../US-MMD003-004-comparar-referencias.md | 147 - .../US-MMD003-005-adjuntar-fotos.md | 135 - .../US-MMD003-006-recomendaciones.md | 127 - .../US-MMD003-007-historial-diagnosticos.md | 96 - .../US-MMD003-008-configurar-pruebas.md | 115 - .../MMD-004-inventario/README.md | 93 - .../US-MMD004-001-registrar-refacciones.md | 140 - .../US-MMD004-002-consultar-stock.md | 103 - .../US-MMD004-003-solicitar-refaccion.md | 158 - .../US-MMD004-004-recibir-mercancia.md | 117 - .../US-MMD004-005-ajustar-inventario.md | 123 - .../US-MMD004-006-alertas-stock.md | 117 - .../historias-usuario/US-MMD004-007-kardex.md | 125 - .../US-MMD004-008-codigos-alternos.md | 108 - .../US-MMD004-009-ubicaciones.md | 127 - .../US-MMD004-010-inventario-fisico.md | 134 - .../MMD-005-vehiculos/README.md | 84 - .../US-MMD005-001-registrar-vehiculo.md | 119 - .../US-MMD005-002-editar-vehiculo.md | 108 - .../US-MMD005-003-especificaciones-motor.md | 129 - .../US-MMD005-004-ficha-tecnica.md | 114 - .../US-MMD005-005-historial-servicios.md | 131 - .../historias-usuario/US-MMD005-006-flotas.md | 120 - .../US-MMD005-007-recordatorios.md | 124 - .../US-MMD005-008-importar-vehiculos.md | 124 - .../MMD-006-cotizaciones/README.md | 97 - .../US-MMD006-001-crear-cotizacion.md | 145 - .../US-MMD006-002-agregar-lineas.md | 118 - .../US-MMD006-003-aplicar-descuentos.md | 119 - .../US-MMD006-004-enviar-cotizacion.md | 110 - .../US-MMD006-005-generar-pdf.md | 165 - .../US-MMD006-006-convertir-orden.md | 113 - .../US-MMD006-007-historial-cotizaciones.md | 124 - .../03-modelo-datos/INTEGRACION-ERP-CORE.md | 142 - .../docs/03-modelo-datos/README.md | 130 - .../SCHEMA-PARTS-MANAGEMENT.md | 465 - .../SCHEMA-SERVICE-MANAGEMENT.md | 646 - .../SCHEMA-VEHICLE-MANAGEMENT.md | 438 - .../08-epicas/EPIC-MMD-001-fundamentos.md | 210 - .../EPIC-MMD-002-ordenes-servicio.md | 219 - .../08-epicas/EPIC-MMD-003-diagnosticos.md | 209 - .../docs/08-epicas/EPIC-MMD-004-inventario.md | 242 - .../docs/08-epicas/EPIC-MMD-005-vehiculos.md | 237 - .../08-epicas/EPIC-MMD-006-cotizaciones.md | 304 - .../docs/08-epicas/README.md | 180 - .../ANALISIS-ARQUITECTONICO-IMPLEMENTACION.md | 517 - .../ANALISIS-INDEPENDENCIA-PROYECTO.md | 311 - .../PLAN-IMPLEMENTACION-2025-12.md | 559 - .../90-transversal/PLAN-RESOLUCION-GAPS.md | 663 - .../docs/PLAN-DESARROLLO-MVP.md | 234 - projects/erp-mecanicas-diesel/docs/README.md | 100 - .../docs/REPORTE-VALIDACION-DOCUMENTACION.md | 345 - projects/erp-mecanicas-diesel/docs/_MAP.md | 40 - .../frontend/.env.example | 6 - .../erp-mecanicas-diesel/frontend/.gitignore | 24 - .../erp-mecanicas-diesel/frontend/README.md | 152 - .../frontend/eslint.config.js | 23 - .../erp-mecanicas-diesel/frontend/index.html | 13 - .../frontend/package-lock.json | 4344 ----- .../frontend/package.json | 42 - .../frontend/postcss.config.js | 6 - .../frontend/public/vite.svg | 1 - .../erp-mecanicas-diesel/frontend/src/App.tsx | 132 - .../frontend/src/assets/react.svg | 1 - .../frontend/src/components/layout/Header.tsx | 50 - .../src/components/layout/MainLayout.tsx | 23 - .../src/components/layout/Sidebar.tsx | 135 - .../frontend/src/components/layout/index.ts | 3 - .../src/components/ui/LoadingSpinner.tsx | 152 - .../frontend/src/components/ui/Modal.tsx | 275 - .../src/components/ui/StatusBadge.tsx | 71 - .../frontend/src/components/ui/Toast.tsx | 95 - .../frontend/src/components/ui/index.ts | 24 - .../frontend/src/index.css | 38 - .../frontend/src/main.tsx | 10 - .../frontend/src/pages/CustomerDetail.tsx | 688 - .../frontend/src/pages/Customers.tsx | 473 - .../frontend/src/pages/Dashboard.tsx | 307 - .../frontend/src/pages/DiagnosticDetail.tsx | 402 - .../frontend/src/pages/Diagnostics.tsx | 279 - .../frontend/src/pages/DiagnosticsNew.tsx | 526 - .../frontend/src/pages/Inventory.tsx | 564 - .../frontend/src/pages/InventoryDetail.tsx | 650 - .../frontend/src/pages/Login.tsx | 171 - .../frontend/src/pages/QuoteDetail.tsx | 486 - .../frontend/src/pages/Quotes.tsx | 291 - .../frontend/src/pages/Register.tsx | 253 - .../frontend/src/pages/ServiceOrderDetail.tsx | 406 - .../frontend/src/pages/ServiceOrderNew.tsx | 384 - .../frontend/src/pages/ServiceOrders.tsx | 290 - .../src/pages/ServiceOrdersKanban.tsx | 320 - .../frontend/src/pages/Settings.tsx | 375 - .../frontend/src/pages/Users.tsx | 579 - .../frontend/src/pages/VehicleDetail.tsx | 591 - .../frontend/src/pages/Vehicles.tsx | 564 - .../frontend/src/services/api/auth.ts | 112 - .../frontend/src/services/api/client.ts | 85 - .../frontend/src/services/api/customers.ts | 71 - .../frontend/src/services/api/diagnostics.ts | 100 - .../frontend/src/services/api/index.ts | 20 - .../frontend/src/services/api/parts.ts | 77 - .../frontend/src/services/api/quotes.ts | 93 - .../src/services/api/serviceOrders.ts | 138 - .../frontend/src/services/api/settings.ts | 143 - .../frontend/src/services/api/users.ts | 49 - .../frontend/src/services/api/vehicles.ts | 71 - .../frontend/src/store/authStore.ts | 56 - .../frontend/src/store/tallerStore.ts | 70 - .../frontend/src/store/toastStore.ts | 66 - .../frontend/src/types/index.ts | 248 - .../frontend/tailwind.config.js | 39 - .../frontend/tsconfig.app.json | 28 - .../frontend/tsconfig.json | 7 - .../frontend/tsconfig.node.json | 26 - .../frontend/vite.config.ts | 7 - .../00-guidelines/CONTEXTO-PROYECTO.md | 121 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 72 - .../00-guidelines/HERENCIA-ERP-CORE.md | 171 - .../00-guidelines/HERENCIA-SIMCO.md | 122 - .../00-guidelines/HERENCIA-SPECS-CORE.md | 169 - .../00-guidelines/HERENCIA-SPECS-ERP-CORE.md | 143 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../orchestration/PROXIMA-ACCION.md | 143 - .../DIRECTIVA-INVENTARIO-REFACCIONES.md | 181 - .../directivas/DIRECTIVA-ORDENES-TRABAJO.md | 178 - .../environment/PROJECT-ENV-CONFIG.yml | 177 - .../inventarios/BACKEND_INVENTORY.yml | 253 - .../inventarios/DATABASE_INVENTORY.yml | 228 - .../inventarios/DEPENDENCY_GRAPH.yml | 52 - .../inventarios/FRONTEND_INVENTORY.yml | 188 - .../inventarios/MASTER_INVENTORY.yml | 277 - .../orchestration/inventarios/README.md | 106 - .../inventarios/TRACEABILITY_MATRIX.yml | 104 - .../prompts/PROMPT-MMD-BACKEND-AGENT.md | 154 - .../referencias/DEPENDENCIAS-ERP-CORE.yml | 97 - .../referencias/DEPENDENCIAS-SHARED.yml | 54 - .../trazas/TRAZA-TAREAS-BACKEND.md | 38 - .../trazas/TRAZA-TAREAS-DATABASE.md | 38 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 38 - projects/erp-retail/.env.example | 129 - projects/erp-retail/INVENTARIO.yml | 31 - projects/erp-retail/PROJECT-STATUS.md | 156 - .../backend/docs/SPRINT-4-SUMMARY.md | 225 - .../backend/docs/SPRINT-5-SUMMARY.md | 232 - .../backend/docs/SPRINT-6-SUMMARY.md | 216 - projects/erp-retail/backend/package.json | 61 - projects/erp-retail/backend/src/app.ts | 120 - .../erp-retail/backend/src/config/database.ts | 102 - .../erp-retail/backend/src/config/typeorm.ts | 144 - projects/erp-retail/backend/src/index.ts | 76 - .../branches/controllers/branch.controller.ts | 236 - .../branches/entities/branch-user.entity.ts | 122 - .../branches/entities/branch.entity.ts | 148 - .../branches/entities/cash-register.entity.ts | 147 - .../src/modules/branches/entities/index.ts | 3 - .../backend/src/modules/branches/index.ts | 3 - .../modules/branches/routes/branch.routes.ts | 48 - .../branches/services/branch.service.ts | 243 - .../src/modules/branches/services/index.ts | 1 - .../branches/validation/branch.schema.ts | 82 - .../cash/controllers/cash.controller.ts | 330 - .../cash/entities/cash-closing.entity.ts | 205 - .../cash/entities/cash-count.entity.ts | 78 - .../cash/entities/cash-movement.entity.ts | 152 - .../src/modules/cash/entities/index.ts | 3 - .../backend/src/modules/cash/index.ts | 5 - .../src/modules/cash/routes/cash.routes.ts | 155 - .../cash/services/cash-closing.service.ts | 568 - .../cash/services/cash-movement.service.ts | 398 - .../src/modules/cash/services/index.ts | 2 - .../modules/cash/validation/cash.schema.ts | 160 - .../controllers/loyalty.controller.ts | 222 - .../entities/customer-membership.entity.ts | 161 - .../src/modules/customers/entities/index.ts | 4 - .../entities/loyalty-program.entity.ts | 161 - .../entities/loyalty-transaction.entity.ts | 144 - .../entities/membership-level.entity.ts | 135 - .../backend/src/modules/customers/index.ts | 5 - .../customers/routes/loyalty.routes.ts | 113 - .../src/modules/customers/services/index.ts | 1 - .../customers/services/loyalty.service.ts | 842 - .../customers/validation/customers.schema.ts | 179 - .../ecommerce/entities/cart-item.entity.ts | 156 - .../modules/ecommerce/entities/cart.entity.ts | 210 - .../entities/ecommerce-order-line.entity.ts | 198 - .../entities/ecommerce-order.entity.ts | 309 - .../src/modules/ecommerce/entities/index.ts | 5 - .../entities/shipping-rate.entity.ts | 182 - .../controllers/inventory.controller.ts | 401 - .../src/modules/inventory/entities/index.ts | 4 - .../entities/stock-adjustment-line.entity.ts | 120 - .../entities/stock-adjustment.entity.ts | 169 - .../entities/stock-transfer-line.entity.ts | 144 - .../entities/stock-transfer.entity.ts | 173 - .../backend/src/modules/inventory/index.ts | 5 - .../inventory/routes/inventory.routes.ts | 179 - .../src/modules/inventory/services/index.ts | 2 - .../services/stock-adjustment.service.ts | 630 - .../services/stock-transfer.service.ts | 591 - .../inventory/validation/inventory.schema.ts | 200 - .../invoicing/controllers/cfdi.controller.ts | 742 - .../modules/invoicing/controllers/index.ts | 1 - .../invoicing/entities/cfdi-config.entity.ts | 199 - .../modules/invoicing/entities/cfdi.entity.ts | 294 - .../src/modules/invoicing/entities/index.ts | 2 - .../backend/src/modules/invoicing/index.ts | 14 - .../modules/invoicing/routes/cfdi.routes.ts | 222 - .../src/modules/invoicing/routes/index.ts | 1 - .../services/cfdi-builder.service.ts | 507 - .../invoicing/services/cfdi.service.ts | 815 - .../src/modules/invoicing/services/index.ts | 4 - .../modules/invoicing/services/pac.service.ts | 755 - .../modules/invoicing/services/xml.service.ts | 534 - .../invoicing/validation/cfdi.schema.ts | 245 - .../src/modules/invoicing/validation/index.ts | 1 - .../modules/pos/controllers/pos.controller.ts | 605 - .../backend/src/modules/pos/entities/index.ts | 4 - .../pos/entities/pos-order-line.entity.ts | 173 - .../modules/pos/entities/pos-order.entity.ts | 223 - .../pos/entities/pos-payment.entity.ts | 173 - .../pos/entities/pos-session.entity.ts | 146 - .../backend/src/modules/pos/index.ts | 3 - .../src/modules/pos/routes/pos.routes.ts | 143 - .../backend/src/modules/pos/services/index.ts | 2 - .../modules/pos/services/pos-order.service.ts | 914 - .../pos/services/pos-session.service.ts | 360 - .../src/modules/pos/validation/pos.schema.ts | 206 - .../src/modules/pricing/controllers/index.ts | 1 - .../pricing/controllers/pricing.controller.ts | 985 -- .../entities/coupon-redemption.entity.ts | 116 - .../modules/pricing/entities/coupon.entity.ts | 195 - .../src/modules/pricing/entities/index.ts | 4 - .../entities/promotion-product.entity.ts | 91 - .../pricing/entities/promotion.entity.ts | 224 - .../backend/src/modules/pricing/index.ts | 14 - .../src/modules/pricing/routes/index.ts | 1 - .../modules/pricing/routes/pricing.routes.ts | 306 - .../pricing/services/coupon.service.ts | 643 - .../src/modules/pricing/services/index.ts | 3 - .../pricing/services/price-engine.service.ts | 725 - .../pricing/services/promotion.service.ts | 607 - .../src/modules/pricing/validation/index.ts | 1 - .../pricing/validation/pricing.schema.ts | 252 - .../modules/purchases/controllers/index.ts | 1 - .../controllers/purchases.controller.ts | 741 - .../entities/goods-receipt-line.entity.ts | 173 - .../entities/goods-receipt.entity.ts | 174 - .../src/modules/purchases/entities/index.ts | 5 - .../entities/purchase-suggestion.entity.ts | 178 - .../entities/supplier-order-line.entity.ts | 144 - .../entities/supplier-order.entity.ts | 201 - .../backend/src/modules/purchases/index.ts | 5 - .../src/modules/purchases/routes/index.ts | 1 - .../purchases/routes/purchases.routes.ts | 252 - .../services/goods-receipt.service.ts | 700 - .../src/modules/purchases/services/index.ts | 3 - .../services/purchase-suggestion.service.ts | 558 - .../services/supplier-order.service.ts | 796 - .../src/modules/purchases/validation/index.ts | 1 - .../purchases/validation/purchases.schema.ts | 385 - .../src/shared/controllers/base.controller.ts | 147 - .../erp-retail/backend/src/shared/index.ts | 3 - .../src/shared/middleware/auth.middleware.ts | 209 - .../shared/middleware/branch.middleware.ts | 208 - .../backend/src/shared/middleware/index.ts | 3 - .../shared/middleware/tenant.middleware.ts | 67 - .../src/shared/services/base.service.ts | 415 - .../backend/src/shared/types/index.ts | 130 - .../src/shared/validation/common.schema.ts | 92 - .../backend/src/shared/validation/index.ts | 2 - .../validation/validation.middleware.ts | 133 - projects/erp-retail/backend/tsconfig.json | 29 - .../erp-retail/database/HERENCIA-ERP-CORE.md | 196 - projects/erp-retail/database/README.md | 83 - .../database/init/00-extensions.sql | 22 - .../database/init/01-create-schemas.sql | 15 - .../database/init/02-rls-functions.sql | 30 - .../database/init/03-retail-tables.sql | 723 - .../docs/00-vision-general/VISION-RETAIL.md | 97 - .../02-definicion-modulos/INDICE-MODULOS.md | 116 - .../RT-001-fundamentos/README.md | 16 - .../RT-002-pos/README.md | 16 - .../RT-003-inventario/README.md | 16 - .../RT-004-compras/README.md | 16 - .../RT-005-clientes/README.md | 16 - .../RT-006-precios/README.md | 16 - .../RT-007-caja/README.md | 16 - .../RT-008-reportes/README.md | 16 - .../RT-009-ecommerce/README.md | 16 - .../RT-010-facturacion/README.md | 16 - .../docs/08-epicas/EPIC-RT-001-fundamentos.md | 58 - .../docs/08-epicas/EPIC-RT-002-pos.md | 251 - .../docs/08-epicas/EPIC-RT-003-inventario.md | 226 - .../docs/08-epicas/EPIC-RT-004-compras.md | 228 - .../docs/08-epicas/EPIC-RT-005-clientes.md | 236 - .../docs/08-epicas/EPIC-RT-006-precios.md | 241 - .../docs/08-epicas/EPIC-RT-007-caja.md | 245 - .../docs/08-epicas/EPIC-RT-008-reportes.md | 256 - .../docs/08-epicas/EPIC-RT-009-ecommerce.md | 261 - .../docs/08-epicas/EPIC-RT-010-facturacion.md | 292 - projects/erp-retail/docs/README.md | 38 - projects/erp-retail/docs/_MAP.md | 40 - .../00-guidelines/CONTEXTO-PROYECTO.md | 162 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 93 - .../00-guidelines/HERENCIA-ERP-CORE.md | 192 - .../00-guidelines/HERENCIA-SIMCO.md | 122 - .../00-guidelines/HERENCIA-SPECS-CORE.md | 184 - .../00-guidelines/HERENCIA-SPECS-ERP-CORE.md | 148 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../orchestration/PROXIMA-ACCION.md | 105 - .../DIRECTIVA-INVENTARIO-SUCURSALES.md | 213 - .../directivas/DIRECTIVA-PUNTO-VENTA.md | 256 - .../environment/PROJECT-ENV-CONFIG.yml | 206 - .../inventarios/BACKEND_INVENTORY.yml | 98 - .../inventarios/DATABASE_INVENTORY.yml | 170 - .../inventarios/DEPENDENCY_GRAPH.yml | 61 - .../inventarios/FRONTEND_INVENTORY.yml | 92 - .../inventarios/MASTER_INVENTORY.yml | 194 - .../orchestration/inventarios/README.md | 95 - .../inventarios/TRACEABILITY_MATRIX.yml | 403 - .../planes/PLAN-MAESTRO-MIGRACION-RETAIL.md | 482 - .../ANALISIS-ERP-CORE-INVENTARIO.md | 261 - .../ANALISIS-RETAIL-REQUERIMIENTOS.md | 459 - .../fase-1-analisis/GAP-ANALYSIS-RETAIL.md | 461 - .../MATRIZ-MAPEO-CORE-RETAIL.md | 302 - .../ANALISIS-RT-001-fundamentos.md | 286 - .../ANALISIS-RT-002-pos.md | 500 - .../ANALISIS-RT-003-inventario.md | 437 - .../ANALISIS-RT-004-compras.md | 423 - .../ANALISIS-RT-005-clientes.md | 498 - .../ANALISIS-RT-006-precios.md | 490 - .../ANALISIS-RT-007-caja.md | 539 - .../ANALISIS-RT-008-reportes.md | 413 - .../ANALISIS-RT-009-ecommerce.md | 567 - .../ANALISIS-RT-010-facturacion.md | 525 - .../PLAN-IMPL-BACKEND.md | 1991 --- .../PLAN-IMPL-DATABASE.md | 1371 -- .../PLAN-IMPL-FRONTEND.md | 1680 -- .../fase-3-implementacion/ROADMAP-SPRINTS.md | 577 - .../DEPENDENCY-GRAPH-VALIDADO.yml | 997 -- .../fase-4-validacion/IMPACTO-CAMBIOS.md | 405 - .../VALIDACION-COMPLETITUD.md | 432 - .../fase-5-implementacion/SPRINT-1-SUMMARY.md | 169 - .../fase-5-implementacion/SPRINT-2-SUMMARY.md | 274 - .../fase-5-implementacion/SPRINT-3-SUMMARY.md | 257 - .../prompts/PROMPT-RT-BACKEND-AGENT.md | 150 - .../referencias/DEPENDENCIAS-ERP-CORE.yml | 109 - .../referencias/DEPENDENCIAS-SHARED.yml | 66 - .../trazas/TRAZA-TAREAS-BACKEND.md | 38 - .../trazas/TRAZA-TAREAS-DATABASE.md | 38 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 38 - projects/erp-suite/DEPLOYMENT.md | 330 - projects/erp-suite/INVENTARIO.yml | 36 - projects/erp-suite/PURGE-LOG.yml | 17 - projects/erp-suite/README.md | 276 - .../apps/products/erp-basico/README.md | 191 - .../00-guidelines/CONTEXTO-PROYECTO.md | 238 - .../00-guidelines/HERENCIA-SIMCO.md | 116 - .../apps/products/pos-micro/README.md | 139 - .../products/pos-micro/backend/.env.example | 38 - .../products/pos-micro/backend/Dockerfile | 70 - .../products/pos-micro/backend/nest-cli.json | 8 - .../pos-micro/backend/package-lock.json | 10752 ------------ .../products/pos-micro/backend/package.json | 83 - .../pos-micro/backend/src/app.module.ts | 45 - .../products/pos-micro/backend/src/main.ts | 81 - .../src/modules/auth/auth.controller.ts | 110 - .../backend/src/modules/auth/auth.module.ts | 32 - .../backend/src/modules/auth/auth.service.ts | 218 - .../src/modules/auth/dto/register.dto.ts | 134 - .../modules/auth/entities/tenant.entity.ts | 94 - .../src/modules/auth/entities/user.entity.ts | 48 - .../src/modules/auth/guards/jwt-auth.guard.ts | 16 - .../modules/auth/strategies/jwt.strategy.ts | 39 - .../categories/categories.controller.ts | 94 - .../modules/categories/categories.module.ts | 17 - .../modules/categories/categories.service.ts | 115 - .../categories/entities/category.entity.ts | 43 - .../entities/payment-method.entity.ts | 36 - .../modules/payments/payments.controller.ts | 77 - .../src/modules/payments/payments.module.ts | 17 - .../src/modules/payments/payments.service.ts | 84 - .../src/modules/products/dto/product.dto.ts | 179 - .../products/entities/product.entity.ts | 75 - .../modules/products/products.controller.ts | 147 - .../src/modules/products/products.module.ts | 17 - .../src/modules/products/products.service.ts | 215 - .../backend/src/modules/sales/dto/sale.dto.ts | 161 - .../sales/entities/sale-item.entity.ts | 52 - .../src/modules/sales/entities/sale.entity.ts | 86 - .../src/modules/sales/sales.controller.ts | 101 - .../backend/src/modules/sales/sales.module.ts | 20 - .../src/modules/sales/sales.service.ts | 270 - .../products/pos-micro/backend/tsconfig.json | 27 - .../pos-micro/database/ddl/00-schema.sql | 596 - .../products/pos-micro/docker-compose.yml | 73 - .../products/pos-micro/docs/ANALISIS-GAPS.md | 192 - .../products/pos-micro/frontend/.env.example | 6 - .../products/pos-micro/frontend/Dockerfile | 58 - .../products/pos-micro/frontend/index.html | 19 - .../products/pos-micro/frontend/nginx.conf | 35 - .../pos-micro/frontend/package-lock.json | 8820 ---------- .../products/pos-micro/frontend/package.json | 40 - .../pos-micro/frontend/postcss.config.js | 6 - .../products/pos-micro/frontend/src/App.tsx | 62 - .../frontend/src/components/CartPanel.tsx | 103 - .../frontend/src/components/CategoryTabs.tsx | 58 - .../frontend/src/components/CheckoutModal.tsx | 306 - .../frontend/src/components/Header.tsx | 102 - .../frontend/src/components/ProductCard.tsx | 77 - .../frontend/src/components/SuccessModal.tsx | 114 - .../frontend/src/hooks/usePayments.ts | 14 - .../frontend/src/hooks/useProducts.ts | 70 - .../pos-micro/frontend/src/hooks/useSales.ts | 84 - .../products/pos-micro/frontend/src/main.tsx | 25 - .../frontend/src/pages/LoginPage.tsx | 183 - .../pos-micro/frontend/src/pages/POSPage.tsx | 154 - .../frontend/src/pages/ReportsPage.tsx | 115 - .../pos-micro/frontend/src/services/api.ts | 61 - .../pos-micro/frontend/src/store/auth.ts | 61 - .../pos-micro/frontend/src/store/cart.ts | 149 - .../pos-micro/frontend/src/styles/index.css | 82 - .../pos-micro/frontend/src/types/index.ts | 144 - .../pos-micro/frontend/src/vite-env.d.ts | 1 - .../pos-micro/frontend/tailwind.config.js | 26 - .../products/pos-micro/frontend/tsconfig.json | 25 - .../pos-micro/frontend/tsconfig.node.json | 10 - .../pos-micro/frontend/vite.config.ts | 70 - .../00-guidelines/CONTEXTO-PROYECTO.md | 164 - .../00-guidelines/HERENCIA-SIMCO.md | 130 - .../apps/products/pos-micro/scripts/dev.sh | 75 - projects/erp-suite/apps/saas/README.md | 198 - .../saas/billing/database/ddl/00-schema.sql | 676 - .../apps/saas/orchestration/CONTEXTO-SAAS.md | 122 - .../apps/shared-libs/core/MIGRATION_GUIDE.md | 615 - .../core/constants/database.constants.ts | 163 - .../policies/CENTRALIZATION-SUMMARY.md | 390 - .../core/database/policies/README.md | 324 - .../core/database/policies/apply-rls.ts | 564 - .../database/policies/migration-example.ts | 152 - .../core/database/policies/rls-policies.sql | 514 - .../core/database/policies/usage-example.ts | 405 - .../shared-libs/core/entities/base.entity.ts | 57 - .../core/entities/tenant.entity.ts | 43 - .../shared-libs/core/entities/user.entity.ts | 54 - .../core/errors/IMPLEMENTATION_SUMMARY.md | 277 - .../core/errors/INTEGRATION_GUIDE.md | 421 - .../core/errors/QUICK_REFERENCE.md | 243 - .../apps/shared-libs/core/errors/README.md | 574 - .../apps/shared-libs/core/errors/STRUCTURE.md | 241 - .../shared-libs/core/errors/base-error.ts | 71 - .../shared-libs/core/errors/error-filter.ts | 230 - .../core/errors/error-middleware.ts | 262 - .../errors/express-integration.example.ts | 464 - .../shared-libs/core/errors/http-errors.ts | 148 - .../apps/shared-libs/core/errors/index.ts | 44 - .../core/errors/nestjs-integration.example.ts | 272 - .../core/examples/user.repository.example.ts | 371 - .../core/factories/repository.factory.ts | 343 - .../erp-suite/apps/shared-libs/core/index.ts | 153 - .../core/interfaces/base-service.interface.ts | 83 - .../core/interfaces/repository.interface.ts | 483 - .../core/middleware/auth.middleware.ts | 131 - .../core/middleware/tenant.middleware.ts | 114 - .../shared-libs/core/services/auth.service.ts | 419 - .../core/services/base-typeorm.service.ts | 229 - .../core/types/pagination.types.ts | 54 - .../erp-suite/docker/docker-compose.prod.yml | 143 - ...LISIS-REQUERIMIENTOS-SAAS-TRANSVERSALES.md | 983 -- .../ARQUITECTURA-INFRAESTRUCTURA-SAAS.md | 875 - .../roadmap/ROADMAP-EPICAS-SAAS.md | 318 - .../stripe/SPEC-STRIPE-INTEGRATION.md | 1799 -- .../docs/ANALISIS-ARQUITECTURA-ERP-SUITE.md | 572 - .../docs/ANALISIS-ESTRUCTURA-DOCUMENTACION.md | 395 - projects/erp-suite/docs/ARCHITECTURE.md | 565 - .../docs/ESTANDAR-NOMENCLATURA-SCHEMAS.md | 345 - .../docs/ESTRUCTURA-DOCUMENTACION-ERP.md | 346 - projects/erp-suite/docs/MULTI-TENANCY.md | 674 - .../erp-suite/docs/PLAN-MIGRACION-SCHEMAS.md | 250 - .../docs/REPORTE-ALINEACION-VERTICALES.md | 281 - ...ORTE-CUMPLIMIENTO-DIRECTIVAS-VERTICALES.md | 273 - .../docs/REPORTE-VALIDACION-DDL-VERTICALES.md | 288 - projects/erp-suite/docs/VERTICAL-GUIDE.md | 763 - projects/erp-suite/docs/_MAP.md | 40 - projects/erp-suite/jenkins/Jenkinsfile | 449 - projects/erp-suite/nginx/erp-suite.conf | 402 - projects/erp-suite/nginx/erp.conf | 130 - .../00-guidelines/CONTEXTO-PROYECTO.md | 167 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 128 - .../00-guidelines/HERENCIA-ERP-CORE.md | 105 - .../00-guidelines/HERENCIA-SIMCO.md | 312 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../erp-suite/orchestration/PROXIMA-ACCION.md | 112 - .../environment/PROJECT-ENV-CONFIG.yml | 186 - .../estados/REGISTRO-SUBAGENTES.json | 92 - .../inventarios/MASTER_INVENTORY.yml | 177 - .../orchestration/inventarios/REFERENCIAS.yml | 370 - .../orchestration/inventarios/STATUS.yml | 315 - .../inventarios/SUITE_MASTER_INVENTORY.yml | 506 - .../prompts/PROMPT-BACKEND-AGENT.md | 133 - .../prompts/PROMPT-DATABASE-AGENT.md | 123 - .../prompts/PROMPT-FRONTEND-AGENT.md | 134 - .../orchestration/trazas/TRAZA-SUITE.md | 322 - projects/erp-suite/package.json | 19 - projects/erp-suite/scripts/deploy.sh | 220 - .../deploy/Jenkinsfile.backend.example | 136 - .../deploy/Jenkinsfile.frontend.example | 142 - projects/erp-suite/scripts/deploy/README.md | 193 - .../scripts/deploy/sync-to-deploy-repos.sh | 306 - projects/erp-vidrio-templado/.env.example | 118 - projects/erp-vidrio-templado/INVENTARIO.yml | 31 - .../erp-vidrio-templado/PROJECT-STATUS.md | 144 - projects/erp-vidrio-templado/README.md | 62 - .../database/HERENCIA-ERP-CORE.md | 191 - .../erp-vidrio-templado/database/README.md | 90 - .../database/init/00-extensions.sql | 23 - .../database/init/01-create-schemas.sql | 15 - .../database/init/02-rls-functions.sql | 30 - .../database/init/03-vidrio-tables.sql | 716 - .../docs/00-vision-general/VISION-VIDRIO.md | 104 - .../02-definicion-modulos/INDICE-MODULOS.md | 154 - .../VT-001-fundamentos/README.md | 21 - .../VT-002-cotizaciones/README.md | 26 - .../VT-003-produccion/README.md | 33 - .../VT-004-inventario/README.md | 27 - .../VT-005-corte/README.md | 26 - .../VT-006-templado/README.md | 28 - .../VT-007-calidad/README.md | 28 - .../VT-008-despacho/README.md | 31 - .../docs/08-epicas/EPIC-VT-001-fundamentos.md | 65 - .../08-epicas/EPIC-VT-002-cotizaciones.md | 198 - .../docs/08-epicas/EPIC-VT-003-produccion.md | 218 - .../docs/08-epicas/EPIC-VT-004-inventario.md | 188 - .../docs/08-epicas/EPIC-VT-005-corte.md | 210 - .../docs/08-epicas/EPIC-VT-006-templado.md | 249 - .../docs/08-epicas/EPIC-VT-007-calidad.md | 227 - .../docs/08-epicas/EPIC-VT-008-despacho.md | 233 - projects/erp-vidrio-templado/docs/README.md | 38 - projects/erp-vidrio-templado/docs/_MAP.md | 40 - .../00-guidelines/CONTEXTO-PROYECTO.md | 121 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 72 - .../00-guidelines/HERENCIA-ERP-CORE.md | 181 - .../00-guidelines/HERENCIA-SIMCO.md | 121 - .../00-guidelines/HERENCIA-SPECS-CORE.md | 175 - .../00-guidelines/HERENCIA-SPECS-ERP-CORE.md | 142 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../orchestration/PROXIMA-ACCION.md | 38 - .../directivas/DIRECTIVA-CONTROL-CALIDAD.md | 146 - .../directivas/DIRECTIVA-PRODUCCION-VIDRIO.md | 129 - .../environment/PROJECT-ENV-CONFIG.yml | 156 - .../inventarios/BACKEND_INVENTORY.yml | 104 - .../inventarios/DATABASE_INVENTORY.yml | 208 - .../inventarios/DEPENDENCY_GRAPH.yml | 176 - .../inventarios/FRONTEND_INVENTORY.yml | 102 - .../inventarios/MASTER_INVENTORY.yml | 168 - .../orchestration/inventarios/README.md | 94 - .../inventarios/TRACEABILITY_MATRIX.yml | 342 - .../prompts/PROMPT-VT-BACKEND-AGENT.md | 130 - .../referencias/DEPENDENCIAS-ERP-CORE.yml | 107 - .../referencias/DEPENDENCIAS-SHARED.yml | 66 - .../trazas/TRAZA-TAREAS-BACKEND.md | 38 - .../trazas/TRAZA-TAREAS-DATABASE.md | 38 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 38 - projects/gamilit | 2 +- projects/inmobiliaria-analytics/.env.ports | 37 - .../inmobiliaria-analytics/INVENTARIO.yml | 31 - projects/inmobiliaria-analytics/README.md | 22 - .../apps/backend/package.json | 84 - .../apps/backend/service.descriptor.yml | 58 - .../apps/backend/src/app.module.ts | 32 - .../apps/backend/src/config/index.ts | 23 - .../apps/backend/src/main.ts | 36 - .../backend/src/modules/auth/auth.module.ts | 19 - .../apps/backend/src/shared/types/index.ts | 27 - .../apps/backend/tsconfig.json | 26 - .../inmobiliaria-analytics/docs/README.md | 38 - projects/inmobiliaria-analytics/docs/_MAP.md | 40 - .../00-guidelines/CONTEXTO-PROYECTO.md | 139 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 110 - .../00-guidelines/HERENCIA-SIMCO.md | 105 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../orchestration/PROXIMA-ACCION.md | 11 - .../environment/PROJECT-ENV-CONFIG.yml | 47 - .../estados/REGISTRO-SUBAGENTES.json | 9 - .../inventarios/BACKEND_INVENTORY.yml | 32 - .../inventarios/DATABASE_INVENTORY.yml | 28 - .../inventarios/FRONTEND_INVENTORY.yml | 34 - .../inventarios/MASTER_INVENTORY.yml | 61 - .../trazas/TRAZA-TAREAS-BACKEND.md | 40 - .../trazas/TRAZA-TAREAS-DATABASE.md | 40 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 40 - .../platform_marketing_content/.env.ports | 49 - .../.github/CODEOWNERS | 63 - .../.husky/commit-msg | 5 - .../.husky/pre-commit | 5 - .../CONTRIBUTING.md | 702 - .../platform_marketing_content/INVENTARIO.yml | 31 - projects/platform_marketing_content/README.md | 93 - .../apps/backend/.env.example | 48 - .../apps/backend/.eslintrc.js | 26 - .../apps/backend/.gitignore | 28 - .../apps/backend/.prettierrc | 7 - .../apps/backend/Dockerfile | 37 - .../apps/backend/jest.config.ts | 32 - .../apps/backend/nest-cli.json | 8 - .../apps/backend/package-lock.json | 11341 ------------ .../apps/backend/package.json | 98 - .../apps/backend/service.descriptor.yml | 67 - .../apps/backend/src/__tests__/setup.ts | 64 - .../apps/backend/src/app.module.ts | 71 - .../decorators/current-tenant.decorator.ts | 18 - .../decorators/current-user.decorator.ts | 36 - .../src/common/decorators/public.decorator.ts | 15 - .../src/common/decorators/roles.decorator.ts | 28 - .../common/filters/http-exception.filter.ts | 70 - .../src/common/guards/jwt-auth.guard.ts | 40 - .../backend/src/common/guards/roles.guard.ts | 38 - .../src/common/guards/tenant-member.guard.ts | 34 - .../backend/src/config/database.config.ts | 19 - .../apps/backend/src/config/jwt.config.ts | 17 - .../apps/backend/src/config/redis.config.ts | 21 - .../apps/backend/src/config/storage.config.ts | 19 - .../apps/backend/src/config/swagger.config.ts | 83 - .../apps/backend/src/main.ts | 56 - .../src/modules/assets/assets.module.ts | 13 - .../assets/controllers/asset.controller.ts | 136 - .../assets/controllers/folder.controller.ts | 125 - .../src/modules/assets/controllers/index.ts | 2 - .../modules/assets/dto/create-asset.dto.ts | 95 - .../modules/assets/dto/create-folder.dto.ts | 34 - .../backend/src/modules/assets/dto/index.ts | 4 - .../modules/assets/dto/update-asset.dto.ts | 6 - .../modules/assets/dto/update-folder.dto.ts | 4 - .../assets/entities/asset-folder.entity.ts | 67 - .../modules/assets/entities/asset.entity.ts | 102 - .../src/modules/assets/entities/index.ts | 2 - .../modules/assets/services/asset.service.ts | 144 - .../modules/assets/services/folder.service.ts | 175 - .../src/modules/assets/services/index.ts | 2 - .../auth/__tests__/auth.service.spec.ts | 240 - .../backend/src/modules/auth/auth.module.ts | 30 - .../auth/controllers/auth.controller.ts | 99 - .../src/modules/auth/dto/auth-response.dto.ts | 46 - .../backend/src/modules/auth/dto/login.dto.ts | 14 - .../src/modules/auth/dto/register.dto.ts | 48 - .../modules/auth/entities/session.entity.ts | 46 - .../src/modules/auth/entities/user.entity.ts | 83 - .../src/modules/auth/services/auth.service.ts | 165 - .../modules/auth/strategies/jwt.strategy.ts | 35 - .../crm/controllers/brand.controller.ts | 108 - .../crm/controllers/client.controller.ts | 96 - .../crm/controllers/product.controller.ts | 108 - .../backend/src/modules/crm/crm.module.ts | 25 - .../src/modules/crm/dto/create-brand.dto.ts | 80 - .../src/modules/crm/dto/create-client.dto.ts | 86 - .../src/modules/crm/dto/create-product.dto.ts | 87 - .../src/modules/crm/dto/update-brand.dto.ts | 6 - .../src/modules/crm/dto/update-client.dto.ts | 4 - .../src/modules/crm/dto/update-product.dto.ts | 6 - .../src/modules/crm/entities/brand.entity.ts | 62 - .../src/modules/crm/entities/client.entity.ts | 64 - .../modules/crm/entities/product.entity.ts | 57 - .../src/modules/crm/services/brand.service.ts | 140 - .../modules/crm/services/client.service.ts | 116 - .../modules/crm/services/product.service.ts | 141 - .../controllers/content-piece.controller.ts | 175 - .../src/modules/projects/controllers/index.ts | 2 - .../controllers/project.controller.ts | 137 - .../projects/dto/create-content-piece.dto.ts | 101 - .../projects/dto/create-project.dto.ts | 82 - .../backend/src/modules/projects/dto/index.ts | 4 - .../projects/dto/update-content-piece.dto.ts | 6 - .../projects/dto/update-project.dto.ts | 6 - .../projects/entities/content-piece.entity.ts | 127 - .../src/modules/projects/entities/index.ts | 2 - .../projects/entities/project.entity.ts | 97 - .../src/modules/projects/projects.module.ts | 13 - .../services/content-piece.service.ts | 188 - .../src/modules/projects/services/index.ts | 2 - .../projects/services/project.service.ts | 145 - .../tenants/controllers/tenants.controller.ts | 93 - .../modules/tenants/dto/create-tenant.dto.ts | 38 - .../modules/tenants/dto/update-tenant.dto.ts | 4 - .../tenants/entities/tenant-plan.entity.ts | 65 - .../modules/tenants/entities/tenant.entity.ts | 67 - .../tenants/services/tenants.service.ts | 119 - .../src/modules/tenants/tenants.module.ts | 15 - .../shared/constants/database.constants.ts | 77 - .../src/shared/constants/enums.constants.ts | 109 - .../backend/src/shared/constants/index.ts | 36 - .../backend/src/shared/dto/pagination.dto.ts | 102 - .../shared/entities/tenant-aware.entity.ts | 36 - .../repositories/base.repository.interface.ts | 272 - .../backend/src/shared/repositories/index.ts | 30 - .../shared/repositories/repository.factory.ts | 343 - .../shared/services/tenant-aware.service.ts | 97 - .../apps/backend/tsconfig.json | 28 - .../apps/frontend/.env.example | 24 - .../apps/frontend/.gitignore | 24 - .../apps/frontend/Dockerfile | 21 - .../apps/frontend/index.html | 13 - .../apps/frontend/nginx.conf | 25 - .../apps/frontend/package-lock.json | 6087 ------- .../apps/frontend/package.json | 61 - .../apps/frontend/postcss.config.js | 6 - .../apps/frontend/src/App.tsx | 88 - .../components/common/Layout/AuthLayout.tsx | 11 - .../src/components/common/Layout/Header.tsx | 33 - .../components/common/Layout/MainLayout.tsx | 17 - .../src/components/common/Layout/Sidebar.tsx | 73 - .../frontend/src/components/ui/button.tsx | 55 - .../apps/frontend/src/components/ui/card.tsx | 78 - .../apps/frontend/src/components/ui/form.tsx | 176 - .../apps/frontend/src/components/ui/input.tsx | 24 - .../apps/frontend/src/components/ui/label.tsx | 24 - .../frontend/src/components/ui/select.tsx | 158 - .../frontend/src/components/ui/switch.tsx | 27 - .../frontend/src/components/ui/textarea.tsx | 24 - .../apps/frontend/src/components/ui/toast.tsx | 125 - .../frontend/src/components/ui/toaster.tsx | 33 - .../apps/frontend/src/hooks/use-toast.ts | 185 - .../apps/frontend/src/hooks/useAssets.ts | 110 - .../apps/frontend/src/hooks/useBrands.ts | 87 - .../apps/frontend/src/hooks/useClients.ts | 79 - .../frontend/src/hooks/useContentPieces.ts | 141 - .../apps/frontend/src/hooks/useFolders.ts | 101 - .../apps/frontend/src/hooks/useProducts.ts | 87 - .../apps/frontend/src/hooks/useProjects.ts | 111 - .../apps/frontend/src/index.css | 60 - .../apps/frontend/src/lib/utils.ts | 6 - .../apps/frontend/src/main.tsx | 28 - .../frontend/src/pages/assets/AssetsPage.tsx | 450 - .../frontend/src/pages/auth/LoginPage.tsx | 113 - .../frontend/src/pages/crm/BrandFormPage.tsx | 432 - .../frontend/src/pages/crm/BrandsPage.tsx | 219 - .../frontend/src/pages/crm/ClientFormPage.tsx | 398 - .../frontend/src/pages/crm/ClientsPage.tsx | 206 - .../src/pages/crm/ProductFormPage.tsx | 489 - .../frontend/src/pages/crm/ProductsPage.tsx | 206 - .../src/pages/dashboard/DashboardPage.tsx | 105 - .../src/pages/projects/ProjectsPage.tsx | 309 - .../frontend/src/services/api/assets.api.ts | 95 - .../apps/frontend/src/services/api/client.ts | 90 - .../apps/frontend/src/services/api/crm.api.ts | 139 - .../frontend/src/services/api/projects.api.ts | 128 - .../apps/frontend/src/stores/useAuthStore.ts | 60 - .../apps/frontend/tailwind.config.js | 71 - .../apps/frontend/tsconfig.json | 25 - .../apps/frontend/tsconfig.node.json | 11 - .../apps/frontend/vite.config.d.ts | 2 - .../apps/frontend/vite.config.js | 24 - .../apps/frontend/vite.config.ts | 25 - .../commitlint.config.js | 27 - .../database/schemas/001_initial_schema.sql | 472 - .../database/seeds/001_initial_data.sql | 274 - .../docker/docker-compose.prod.yml | 49 - .../00-vision-general/ARQUITECTURA-TECNICA.md | 967 -- .../docs/00-vision-general/GLOSARIO.md | 199 - ...o de Morfeo Academy y Desarrollo de una .pdf | Bin 886415 -> 0 bytes .../MVP_Plataforma_SaaS_Contenido_CRM.md | 327 - .../docs/00-vision-general/VISION-GENERAL.md | 466 - .../docs/00-vision-general/_MAP.md | 90 - .../ANALISIS-CATALOGO.md | 341 - .../ANALISIS-PROYECTOS-REFERENCIA.md | 315 - .../docs/01-analisis-referencias/_INDEX.md | 76 - .../02-definicion-modulos/PMC-001-TENANTS.md | 295 - .../docs/02-definicion-modulos/PMC-002-CRM.md | 387 - .../02-definicion-modulos/PMC-003-PROJECTS.md | 381 - .../PMC-004-GENERATION.md | 515 - .../PMC-005-AUTOMATION.md | 472 - .../02-definicion-modulos/PMC-006-ASSETS.md | 449 - .../02-definicion-modulos/PMC-007-ADMIN.md | 535 - .../PMC-008-ANALYTICS.md | 443 - .../docs/02-definicion-modulos/_INDEX.md | 148 - .../03-requerimientos/RF-PMC-001-TENANTS.md | 512 - .../docs/03-requerimientos/RF-PMC-002-CRM.md | 587 - .../03-requerimientos/RF-PMC-003-PROJECTS.md | 503 - .../RF-PMC-004-GENERATION.md | 641 - .../RF-PMC-005-AUTOMATION.md | 414 - .../03-requerimientos/RF-PMC-006-ASSETS.md | 511 - .../03-requerimientos/RF-PMC-007-ADMIN.md | 438 - .../03-requerimientos/RF-PMC-008-ANALYTICS.md | 356 - .../docs/03-requerimientos/_INDEX.md | 58 - .../docs/04-modelado/ESQUEMA-BD.md | 1020 -- .../docs/04-modelado/MODELO-DOMINIO.md | 275 - .../docs/05-user-stories/EPIC-001-SETUP.md | 172 - .../docs/05-user-stories/EPIC-002-CRM.md | 235 - .../docs/05-user-stories/EPIC-003-PROJECTS.md | 213 - .../05-user-stories/EPIC-004-GENERATION.md | 293 - .../docs/05-user-stories/EPIC-005-ASSETS.md | 209 - .../05-user-stories/EPIC-006-AUTOMATION.md | 138 - .../05-user-stories/EPIC-007-ANALYTICS.md | 124 - .../docs/05-user-stories/EPIC-008-ADMIN.md | 243 - .../docs/05-user-stories/_INDEX.md | 67 - .../docs/90-transversal/README.md | 127 - .../AUDITORIA-DOCUMENTACION-PMC.md | 414 - .../90-transversal/roadmap/ROADMAP-PMC.md | 174 - .../95-guias-desarrollo/GUIA-CONVENCIONES.md | 473 - .../docs/95-guias-desarrollo/GUIA-SETUP.md | 416 - .../docs/97-adr/ADR-001-stack-tecnologico.md | 124 - .../docs/97-adr/ADR-002-multi-tenancy.md | 142 - .../docs/97-adr/ADR-003-motor-generacion.md | 179 - .../docs/97-adr/ADR-004-cola-tareas.md | 265 - .../docs/97-adr/_INDEX.md | 63 - .../docs/ARCHITECTURE.md | 477 - .../docs/CMS-GUIDE.md | 824 - .../platform_marketing_content/docs/_MAP.md | 40 - .../jenkins/Jenkinsfile | 78 - .../lint-staged.config.js | 38 - .../platform_marketing_content/nginx/pmc.conf | 48 - .../00-guidelines/CONTEXTO-PROYECTO.md | 216 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 223 - .../00-guidelines/HERENCIA-SIMCO.md | 105 - .../00-guidelines/PROJECT-STATUS.md | 33 - .../orchestration/PROXIMA-ACCION.md | 205 - .../DIRECTIVA-ARQUITECTURA-MULTI-TENANT.md | 340 - .../directivas/DIRECTIVA-GENERACION-IA-PMC.md | 502 - .../directivas/GUIA-NOMENCLATURA-PMC.md | 400 - .../orchestration/estados/ESTADO-GENERAL.json | 29 - .../estados/REGISTRO-SUBAGENTES.json | 81 - .../inventarios/BACKEND_INVENTORY.yml | 373 - .../inventarios/DATABASE_INVENTORY.yml | 281 - .../inventarios/DEPENDENCY_GRAPH.yml | 386 - .../inventarios/FRONTEND_INVENTORY.yml | 452 - .../inventarios/MASTER_INVENTORY.yml | 570 - .../inventarios/TRACEABILITY_MATRIX.yml | 462 - .../prompts/PROMPT-BACKEND-PMC.md | 499 - .../prompts/PROMPT-DATABASE-PMC.md | 381 - .../prompts/PROMPT-FRONTEND-PMC.md | 556 - .../prompts/PROMPT-GENERATION-PMC.md | 660 - .../prompts/PROMPT-ORQUESTADOR-PMC.md | 286 - .../orchestration/prompts/_INDEX.md | 123 - ...TRAZA-2025-12-08-DOCUMENTACION-COMPLETA.md | 160 - .../trazas/TRAZA-TAREAS-BACKEND.md | 375 - .../trazas/TRAZA-TAREAS-DATABASE.md | 264 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 384 - .../platform_marketing_content/package.json | 23 - projects/trading-platform/INVENTARIO.yml | 30 - projects/trading-platform/README.md | 225 - .../apps/backend/.env.example | 159 - .../trading-platform/apps/backend/Dockerfile | 73 - .../WEBSOCKET_IMPLEMENTATION_REPORT.md | 563 - .../apps/backend/WEBSOCKET_TESTING.md | 648 - .../apps/backend/eslint.config.js | 29 - .../apps/backend/jest.config.ts | 37 - .../apps/backend/package-lock.json | 11171 ------------ .../apps/backend/package.json | 90 - .../apps/backend/service.descriptor.yml | 54 - .../src/__tests__/jest-migration.test.ts | 35 - .../src/__tests__/mocks/database.mock.ts | 101 - .../backend/src/__tests__/mocks/email.mock.ts | 79 - .../backend/src/__tests__/mocks/redis.mock.ts | 97 - .../apps/backend/src/__tests__/setup.ts | 115 - .../apps/backend/src/config/index.ts | 119 - .../apps/backend/src/config/swagger.config.ts | 175 - .../src/core/filters/http-exception.filter.ts | 172 - .../apps/backend/src/core/filters/index.ts | 5 - .../backend/src/core/guards/auth.guard.ts | 237 - .../apps/backend/src/core/guards/index.ts | 5 - .../backend/src/core/interceptors/index.ts | 5 - .../transform-response.interceptor.ts | 108 - .../src/core/middleware/auth.middleware.ts | 206 - .../src/core/middleware/error-handler.ts | 77 - .../backend/src/core/middleware/not-found.ts | 16 - .../src/core/middleware/rate-limiter.ts | 51 - .../apps/backend/src/core/websocket/index.ts | 8 - .../core/websocket/trading-stream.service.ts | 825 - .../src/core/websocket/websocket.server.ts | 418 - .../apps/backend/src/docs/openapi.yaml | 172 - .../apps/backend/src/index.ts | 196 - .../backend/src/modules/admin/admin.routes.ts | 431 - .../src/modules/agents/agents.routes.ts | 129 - .../agents/controllers/agents.controller.ts | 504 - .../modules/agents/services/agents.service.ts | 230 - .../backend/src/modules/auth/auth.routes.ts | 305 - .../auth/controllers/auth.controller.ts | 570 - .../auth/controllers/email-auth.controller.ts | 168 - .../src/modules/auth/controllers/index.ts | 57 - .../auth/controllers/oauth.controller.ts | 248 - .../auth/controllers/phone-auth.controller.ts | 71 - .../auth/controllers/token.controller.ts | 162 - .../auth/controllers/two-factor.controller.ts | 124 - .../modules/auth/dto/change-password.dto.ts | 41 - .../backend/src/modules/auth/dto/index.ts | 17 - .../backend/src/modules/auth/dto/login.dto.ts | 29 - .../backend/src/modules/auth/dto/oauth.dto.ts | 36 - .../src/modules/auth/dto/refresh-token.dto.ts | 11 - .../src/modules/auth/dto/register.dto.ts | 38 - .../services/__tests__/email.service.spec.ts | 497 - .../services/__tests__/token.service.spec.ts | 489 - .../modules/auth/services/email.service.ts | 583 - .../modules/auth/services/oauth.service.ts | 624 - .../modules/auth/services/phone.service.ts | 435 - .../modules/auth/services/token.service.ts | 211 - .../modules/auth/services/twofa.service.ts | 293 - .../__tests__/oauth-state.store.spec.ts | 409 - .../modules/auth/stores/oauth-state.store.ts | 239 - .../src/modules/auth/types/auth.types.ts | 217 - .../auth/validators/auth.validators.ts | 159 - .../controllers/education.controller.ts | 675 - .../src/modules/education/education.routes.ts | 182 - .../education/services/course.service.ts | 568 - .../education/services/enrollment.service.ts | 420 - .../education/types/education.types.ts | 401 - .../controllers/investment.controller.ts | 530 - .../modules/investment/investment.routes.ts | 117 - .../__tests__/account.service.spec.ts | 547 - .../__tests__/product.service.spec.ts | 378 - .../__tests__/transaction.service.spec.ts | 606 - .../investment/services/account.service.ts | 344 - .../investment/services/product.service.ts | 247 - .../services/transaction.service.ts | 589 - .../modules/llm/controllers/llm.controller.ts | 260 - .../backend/src/modules/llm/llm.routes.ts | 65 - .../src/modules/llm/services/llm.service.ts | 494 - .../ml/controllers/ml-overlay.controller.ts | 248 - .../modules/ml/controllers/ml.controller.ts | 301 - .../apps/backend/src/modules/ml/ml.routes.ts | 168 - .../ml/services/ml-integration.service.ts | 538 - .../modules/ml/services/ml-overlay.service.ts | 517 - .../controllers/payments.controller.ts | 489 - .../src/modules/payments/payments.routes.ts | 189 - .../payments/services/stripe.service.ts | 437 - .../payments/services/subscription.service.ts | 514 - .../payments/services/wallet.service.ts | 632 - .../modules/payments/types/payments.types.ts | 324 - .../controllers/portfolio.controller.ts | 460 - .../src/modules/portfolio/portfolio.routes.ts | 97 - .../__tests__/portfolio.service.spec.ts | 585 - .../portfolio/services/portfolio.service.ts | 501 - .../trading/controllers/alerts.controller.ts | 189 - .../controllers/indicators.controller.ts | 177 - .../controllers/paper-trading.controller.ts | 253 - .../trading/controllers/trading.controller.ts | 629 - .../controllers/watchlist.controller.ts | 396 - .../services/__tests__/alerts.service.spec.ts | 507 - .../__tests__/paper-trading.service.spec.ts | 473 - .../__tests__/watchlist.service.spec.ts | 372 - .../trading/services/alerts.service.ts | 332 - .../trading/services/binance.service.ts | 542 - .../modules/trading/services/cache.service.ts | 260 - .../trading/services/indicators.service.ts | 538 - .../trading/services/market.service.ts | 479 - .../trading/services/paper-trading.service.ts | 775 - .../trading/services/watchlist.service.ts | 428 - .../src/modules/trading/trading.routes.ts | 365 - .../src/modules/trading/types/market.types.ts | 104 - .../backend/src/modules/users/users.routes.ts | 64 - .../apps/backend/src/shared/clients/index.ts | 12 - .../src/shared/clients/llm-agent.client.ts | 414 - .../src/shared/clients/ml-engine.client.ts | 397 - .../shared/clients/trading-agents.client.ts | 363 - .../shared/constants/database.constants.ts | 63 - .../src/shared/constants/enums.constants.ts | 183 - .../backend/src/shared/constants/index.ts | 14 - .../src/shared/constants/routes.constants.ts | 159 - .../apps/backend/src/shared/database/index.ts | 110 - .../src/shared/factories/MIGRATION_GUIDE.md | 452 - .../backend/src/shared/factories/index.ts | 6 - .../src/shared/factories/service.factory.ts | 197 - .../backend/src/shared/interfaces/README.md | 242 - .../src/shared/interfaces/cache.interface.ts | 78 - .../interfaces/http-client.interface.ts | 43 - .../backend/src/shared/interfaces/index.ts | 12 - .../interfaces/services/auth.interface.ts | 209 - .../interfaces/services/trading.interface.ts | 442 - .../middleware/validate-dto.middleware.ts | 113 - .../backend/src/shared/types/common.types.ts | 113 - .../apps/backend/src/shared/types/index.ts | 5 - .../apps/backend/src/shared/utils/logger.ts | 37 - .../apps/backend/test-websocket.html | 506 - .../apps/backend/test-websocket.js | 137 - .../apps/backend/tsconfig.json | 29 - .../apps/data-service/.env.example | 46 - .../apps/data-service/ARCHITECTURE.md | 682 - .../apps/data-service/Dockerfile | 48 - .../data-service/IMPLEMENTATION_SUMMARY.md | 452 - .../apps/data-service/README.md | 151 - .../apps/data-service/README_SYNC.md | 375 - .../apps/data-service/TECH_LEADER_REPORT.md | 603 - .../apps/data-service/docker-compose.yml | 93 - .../apps/data-service/environment.yml | 35 - .../data-service/examples/api_examples.sh | 98 - .../data-service/examples/sync_example.py | 176 - .../migrations/002_sync_status.sql | 54 - .../apps/data-service/requirements.txt | 75 - .../apps/data-service/requirements_sync.txt | 25 - .../apps/data-service/src/__init__.py | 11 - .../apps/data-service/src/api/__init__.py | 9 - .../apps/data-service/src/api/dependencies.py | 103 - .../apps/data-service/src/api/mt4_routes.py | 555 - .../apps/data-service/src/api/routes.py | 607 - .../apps/data-service/src/api/sync_routes.py | 331 - .../apps/data-service/src/app.py | 200 - .../apps/data-service/src/app_updated.py | 282 - .../apps/data-service/src/config.py | 169 - .../apps/data-service/src/main.py | 366 - .../apps/data-service/src/models/market.py | 257 - .../data-service/src/providers/__init__.py | 17 - .../src/providers/binance_client.py | 562 - .../src/providers/metaapi_client.py | 831 - .../data-service/src/providers/mt4_client.py | 632 - .../src/providers/polygon_client.py | 479 - .../data-service/src/services/__init__.py | 9 - .../src/services/price_adjustment.py | 528 - .../data-service/src/services/scheduler.py | 313 - .../data-service/src/services/sync_service.py | 500 - .../data-service/src/websocket/__init__.py | 9 - .../data-service/src/websocket/handlers.py | 184 - .../data-service/src/websocket/manager.py | 439 - .../apps/data-service/tests/__init__.py | 3 - .../apps/data-service/tests/conftest.py | 19 - .../data-service/tests/test_polygon_client.py | 195 - .../data-service/tests/test_sync_service.py | 227 - .../DIRECTIVA-POLITICA-CARGA-LIMPIA.md | 259 - .../apps/database/ddl/00-extensions.sql | 26 - .../apps/database/ddl/01-schemas.sql | 37 - .../database/ddl/schemas/audit/00-enums.sql | 63 - .../schemas/audit/tables/01-audit_logs.sql | 54 - .../audit/tables/02-security_events.sql | 57 - .../schemas/audit/tables/03-system_events.sql | 47 - .../schemas/audit/tables/04-trading_audit.sql | 57 - .../audit/tables/05-api_request_logs.sql | 49 - .../audit/tables/06-data_access_logs.sql | 45 - .../audit/tables/07-compliance_logs.sql | 52 - .../ddl/schemas/auth/00-extensions.sql | 19 - .../database/ddl/schemas/auth/01-enums.sql | 80 - .../auth/functions/01-update_updated_at.sql | 48 - .../auth/functions/02-log_auth_event.sql | 75 - .../functions/03-cleanup_expired_sessions.sql | 58 - .../04-create_user_profile_trigger.sql | 46 - .../ddl/schemas/auth/tables/01-users.sql | 107 - .../schemas/auth/tables/02-user_profiles.sql | 70 - .../schemas/auth/tables/03-oauth_accounts.sql | 69 - .../ddl/schemas/auth/tables/04-sessions.sql | 87 - .../auth/tables/05-email_verifications.sql | 65 - .../auth/tables/06-phone_verifications.sql | 78 - .../auth/tables/07-password_reset_tokens.sql | 65 - .../ddl/schemas/auth/tables/08-auth_logs.sql | 74 - .../schemas/auth/tables/09-login_attempts.sql | 67 - .../auth/tables/10-rate_limiting_config.sql | 82 - .../ddl/schemas/education/00-enums.sql | 64 - .../database/ddl/schemas/education/README.md | 353 - .../ddl/schemas/education/TECHNICAL.md | 458 - .../functions/01-update_updated_at.sql | 69 - .../02-update_enrollment_progress.sql | 57 - .../functions/03-auto_complete_enrollment.sql | 29 - .../functions/04-generate_certificate.sql | 47 - .../functions/05-update_course_stats.sql | 59 - .../functions/06-update_enrollment_count.sql | 41 - .../07-update_gamification_profile.sql | 158 - .../schemas/education/functions/08-views.sql | 142 - .../database/ddl/schemas/education/install.sh | 132 - .../ddl/schemas/education/seeds-example.sql | 238 - .../education/tables/01-categories.sql | 42 - .../schemas/education/tables/02-courses.sql | 74 - .../schemas/education/tables/03-modules.sql | 43 - .../schemas/education/tables/04-lessons.sql | 66 - .../education/tables/05-enrollments.sql | 56 - .../schemas/education/tables/06-progress.sql | 52 - .../schemas/education/tables/07-quizzes.sql | 57 - .../education/tables/08-quiz_questions.sql | 56 - .../education/tables/09-quiz_attempts.sql | 53 - .../education/tables/10-certificates.sql | 54 - .../education/tables/11-user_achievements.sql | 47 - .../tables/12-user_gamification_profile.sql | 56 - .../education/tables/13-user_activity_log.sql | 43 - .../education/tables/14-course_reviews.sql | 48 - .../ddl/schemas/education/uninstall.sh | 55 - .../database/ddl/schemas/education/verify.sh | 145 - .../ddl/schemas/financial/00-enums.sql | 131 - .../functions/01-update_wallet_balance.sql | 283 - .../functions/02-process_transaction.sql | 326 - .../financial/functions/03-triggers.sql | 278 - .../schemas/financial/functions/04-views.sql | 258 - .../schemas/financial/tables/01-wallets.sql | 85 - .../tables/02-wallet_transactions.sql | 101 - .../financial/tables/03-subscriptions.sql | 107 - .../schemas/financial/tables/04-payments.sql | 86 - .../schemas/financial/tables/05-invoices.sql | 120 - .../financial/tables/06-wallet_audit_log.sql | 68 - .../tables/07-currency_exchange_rates.sql | 131 - .../financial/tables/08-wallet_limits.sql | 101 - .../schemas/financial/tables/09-customers.sql | 68 - .../financial/tables/10-payment_methods.sql | 180 - .../ddl/schemas/investment/00-enums.sql | 52 - .../schemas/investment/tables/01-products.sql | 60 - .../schemas/investment/tables/02-accounts.sql | 67 - .../investment/tables/03-transactions.sql | 69 - .../investment/tables/04-distributions.sql | 69 - .../tables/05-risk_questionnaire.sql | 63 - .../tables/06-withdrawal_requests.sql | 119 - .../tables/07-daily_performance.sql | 115 - .../database/ddl/schemas/llm/00-enums.sql | 63 - .../schemas/llm/tables/01-conversations.sql | 63 - .../ddl/schemas/llm/tables/02-messages.sql | 98 - .../llm/tables/03-user_preferences.sql | 68 - .../ddl/schemas/llm/tables/04-user_memory.sql | 82 - .../ddl/schemas/llm/tables/05-embeddings.sql | 122 - .../apps/database/ddl/schemas/ml/00-enums.sql | 68 - .../ddl/schemas/ml/tables/01-models.sql | 65 - .../schemas/ml/tables/02-model_versions.sql | 102 - .../ddl/schemas/ml/tables/03-predictions.sql | 93 - .../ml/tables/04-prediction_outcomes.sql | 68 - .../schemas/ml/tables/05-feature_store.sql | 120 - .../database/ddl/schemas/trading/00-enums.sql | 78 - .../functions/01-calculate_position_pnl.sql | 96 - .../trading/functions/02-update_bot_stats.sql | 88 - .../functions/03-initialize_paper_balance.sql | 165 - .../functions/04-create_default_watchlist.sql | 80 - .../ddl/schemas/trading/tables/01-symbols.sql | 49 - .../schemas/trading/tables/02-watchlists.sql | 40 - .../trading/tables/03-watchlist_items.sql | 38 - .../ddl/schemas/trading/tables/04-bots.sql | 64 - .../ddl/schemas/trading/tables/05-orders.sql | 67 - .../schemas/trading/tables/06-positions.sql | 70 - .../ddl/schemas/trading/tables/07-trades.sql | 49 - .../ddl/schemas/trading/tables/08-signals.sql | 68 - .../trading/tables/09-trading_metrics.sql | 67 - .../trading/tables/10-paper_balances.sql | 51 - .../apps/database/schemas/00_init_schemas.sql | 123 - .../database/schemas/01_public_schema.sql | 280 - .../database/schemas/01b_oauth_providers.sql | 220 - .../database/schemas/02_education_schema.sql | 398 - .../database/schemas/03_trading_schema.sql | 428 - .../database/schemas/04_investment_schema.sql | 426 - .../database/schemas/05_financial_schema.sql | 500 - .../apps/database/schemas/06_ml_schema.sql | 426 - .../apps/database/schemas/07_audit_schema.sql | 402 - .../apps/database/schemas/_MAP.md | 283 - .../apps/database/scripts/create-database.sh | 308 - .../scripts/drop-and-recreate-database.sh | 13 - .../database/scripts/migrate_all_tickers.sh | 96 - .../apps/database/scripts/migrate_direct.sh | 88 - .../scripts/migrate_mysql_to_postgres.py | 393 - .../apps/database/scripts/validate-ddl.sh | 305 - .../apps/frontend/.env.example | 16 - .../apps/frontend/.eslintrc.cjs | 40 - .../trading-platform/apps/frontend/Dockerfile | 64 - .../frontend/ML_DASHBOARD_IMPLEMENTATION.md | 318 - .../apps/frontend/eslint.config.js | 46 - .../trading-platform/apps/frontend/index.html | 18 - .../trading-platform/apps/frontend/nginx.conf | 54 - .../apps/frontend/package-lock.json | 7144 -------- .../apps/frontend/package.json | 61 - .../apps/frontend/postcss.config.js | 6 - .../apps/frontend/src/App.tsx | 80 - .../frontend/src/__tests__/mlService.test.ts | 90 - .../src/__tests__/tradingService.test.ts | 135 - .../src/components/chat/ChatInput.tsx | 149 - .../src/components/chat/ChatMessage.tsx | 182 - .../src/components/chat/ChatPanel.tsx | 212 - .../src/components/chat/ChatWidget.tsx | 68 - .../frontend/src/components/chat/index.ts | 8 - .../src/components/layout/AuthLayout.tsx | 41 - .../src/components/layout/MainLayout.tsx | 147 - .../apps/frontend/src/hooks/index.ts | 6 - .../apps/frontend/src/hooks/useMLAnalysis.ts | 291 - .../apps/frontend/src/main.tsx | 38 - .../admin/components/AgentStatsCard.tsx | 221 - .../modules/admin/components/MLModelCard.tsx | 162 - .../src/modules/admin/components/index.ts | 7 - .../modules/admin/pages/AdminDashboard.tsx | 323 - .../src/modules/admin/pages/AgentsPage.tsx | 286 - .../src/modules/admin/pages/MLModelsPage.tsx | 209 - .../modules/admin/pages/PredictionsPage.tsx | 366 - .../frontend/src/modules/admin/pages/index.ts | 9 - .../assistant/components/ChatInput.tsx | 135 - .../assistant/components/ChatMessage.tsx | 85 - .../assistant/components/SignalCard.tsx | 156 - .../src/modules/assistant/pages/Assistant.tsx | 272 - .../auth/components/PhoneLoginForm.tsx | 264 - .../auth/components/SocialLoginButtons.tsx | 129 - .../src/modules/auth/pages/AuthCallback.tsx | 96 - .../src/modules/auth/pages/ForgotPassword.tsx | 119 - .../frontend/src/modules/auth/pages/Login.tsx | 230 - .../src/modules/auth/pages/Register.tsx | 257 - .../src/modules/auth/pages/ResetPassword.tsx | 209 - .../src/modules/auth/pages/VerifyEmail.tsx | 98 - .../components/EquityCurveChart.tsx | 249 - .../components/PerformanceMetricsPanel.tsx | 339 - .../components/PredictionChart.tsx | 344 - .../components/StrategyComparisonChart.tsx | 284 - .../backtesting/components/TradesTable.tsx | 361 - .../modules/backtesting/components/index.ts | 10 - .../pages/BacktestingDashboard.tsx | 636 - .../src/modules/dashboard/pages/Dashboard.tsx | 77 - .../modules/education/pages/CourseDetail.tsx | 18 - .../src/modules/education/pages/Courses.tsx | 98 - .../modules/investment/pages/Investment.tsx | 100 - .../modules/investment/pages/Portfolio.tsx | 346 - .../src/modules/investment/pages/Products.tsx | 276 - .../apps/frontend/src/modules/ml/README.md | 204 - .../frontend/src/modules/ml/USAGE_EXAMPLES.md | 584 - .../src/modules/ml/VALIDATION_CHECKLIST.md | 245 - .../ml/components/AMDPhaseIndicator.tsx | 212 - .../modules/ml/components/AccuracyMetrics.tsx | 202 - .../ml/components/EnsembleSignalCard.tsx | 285 - .../modules/ml/components/ICTAnalysisCard.tsx | 293 - .../modules/ml/components/PredictionCard.tsx | 203 - .../modules/ml/components/SignalsTimeline.tsx | 216 - .../ml/components/TradeExecutionModal.tsx | 349 - .../src/modules/ml/components/index.ts | 12 - .../src/modules/ml/pages/MLDashboard.tsx | 567 - .../src/modules/settings/pages/Settings.tsx | 89 - .../trading/components/AccountSummary.tsx | 64 - .../trading/components/AddSymbolModal.tsx | 227 - .../trading/components/CandlestickChart.tsx | 243 - .../trading/components/ChartToolbar.tsx | 272 - .../trading/components/MLSignalsPanel.tsx | 418 - .../modules/trading/components/OrderForm.tsx | 259 - .../trading/components/PaperTradingPanel.tsx | 162 - .../trading/components/PositionsList.tsx | 173 - .../trading/components/TradesHistory.tsx | 132 - .../trading/components/TradingChart.tsx | 459 - .../trading/components/WatchlistItem.tsx | 149 - .../trading/components/WatchlistSidebar.tsx | 219 - .../src/modules/trading/pages/Trading.tsx | 271 - .../frontend/src/services/adminService.ts | 421 - .../frontend/src/services/backtestService.ts | 514 - .../frontend/src/services/chat.service.ts | 111 - .../apps/frontend/src/services/mlService.ts | 377 - .../frontend/src/services/trading.service.ts | 847 - .../src/services/websocket.service.ts | 353 - .../apps/frontend/src/stores/chatStore.ts | 332 - .../apps/frontend/src/stores/tradingStore.ts | 405 - .../apps/frontend/src/styles/index.css | 162 - .../apps/frontend/src/types/chat.types.ts | 40 - .../apps/frontend/src/types/trading.types.ts | 325 - .../apps/frontend/src/vite-env.d.ts | 10 - .../apps/frontend/tailwind.config.js | 44 - .../apps/frontend/tsconfig.json | 32 - .../apps/frontend/tsconfig.node.json | 10 - .../apps/frontend/vite.config.ts | 28 - .../apps/llm-agent/.env.example | 70 - .../apps/llm-agent/AUTO_TRADING.md | 369 - .../apps/llm-agent/DEPLOYMENT.md | 494 - .../apps/llm-agent/Dockerfile | 35 - .../apps/llm-agent/IMPLEMENTATION_SUMMARY.md | 525 - .../trading-platform/apps/llm-agent/README.md | 286 - .../apps/llm-agent/docker-compose.ollama.yml | 49 - .../apps/llm-agent/environment.yml | 62 - .../examples/auto_trading_example.py | 270 - .../apps/llm-agent/pyproject.toml | 79 - .../apps/llm-agent/requirements.txt | 59 - .../apps/llm-agent/src/__init__.py | 6 - .../apps/llm-agent/src/api/__init__.py | 3 - .../llm-agent/src/api/auto_trade_routes.py | 421 - .../apps/llm-agent/src/api/routes.py | 387 - .../apps/llm-agent/src/clients/__init__.py | 13 - .../apps/llm-agent/src/clients/mt4_client.py | 422 - .../apps/llm-agent/src/config.py | 83 - .../apps/llm-agent/src/core/__init__.py | 1 - .../llm-agent/src/core/context_manager.py | 198 - .../apps/llm-agent/src/core/llm_client.py | 681 - .../apps/llm-agent/src/core/prompt_manager.py | 176 - .../apps/llm-agent/src/main.py | 81 - .../apps/llm-agent/src/models/__init__.py | 4 - .../apps/llm-agent/src/models/auto_trade.py | 134 - .../apps/llm-agent/src/prompts/analysis.txt | 36 - .../apps/llm-agent/src/prompts/strategy.txt | 65 - .../apps/llm-agent/src/prompts/system.txt | 94 - .../llm-agent/src/prompts/trade_execution.txt | 52 - .../llm-agent/src/repositories/__init__.py | 4 - .../apps/llm-agent/src/services/__init__.py | 4 - .../src/services/auto_trade_service.py | 673 - .../apps/llm-agent/src/tools/__init__.py | 63 - .../apps/llm-agent/src/tools/auto_trading.py | 356 - .../apps/llm-agent/src/tools/base.py | 177 - .../apps/llm-agent/src/tools/education.py | 293 - .../apps/llm-agent/src/tools/ml_tools.py | 477 - .../apps/llm-agent/src/tools/mt4_tools.py | 559 - .../apps/llm-agent/src/tools/portfolio.py | 255 - .../apps/llm-agent/src/tools/signals.py | 268 - .../apps/llm-agent/src/tools/trading.py | 487 - .../apps/llm-agent/tests/__init__.py | 3 - .../apps/llm-agent/tests/conftest.py | 26 - .../apps/llm-agent/tests/test_auto_trading.py | 293 - .../llm-agent/tests/test_mt4_integration.py | 304 - .../apps/ml-engine/.env.example | 50 - .../apps/ml-engine/Dockerfile | 36 - .../apps/ml-engine/MIGRATION_REPORT.md | 436 - .../apps/ml-engine/config/database.yaml | 32 - .../apps/ml-engine/config/models.yaml | 144 - .../apps/ml-engine/config/phase2.yaml | 289 - .../apps/ml-engine/config/trading.yaml | 211 - .../apps/ml-engine/environment.yml | 54 - .../apps/ml-engine/pytest.ini | 9 - .../apps/ml-engine/requirements.txt | 45 - .../apps/ml-engine/src/__init__.py | 17 - .../apps/ml-engine/src/api/__init__.py | 10 - .../apps/ml-engine/src/api/main.py | 1089 -- .../ml-engine/src/backtesting/__init__.py | 19 - .../apps/ml-engine/src/backtesting/engine.py | 517 - .../apps/ml-engine/src/backtesting/metrics.py | 587 - .../src/backtesting/rr_backtester.py | 566 - .../apps/ml-engine/src/models/__init__.py | 63 - .../apps/ml-engine/src/models/amd_detector.py | 570 - .../apps/ml-engine/src/models/amd_models.py | 628 - .../ml-engine/src/models/ict_smc_detector.py | 1042 -- .../ml-engine/src/models/range_predictor.py | 572 - .../ml-engine/src/models/signal_generator.py | 529 - .../ml-engine/src/models/strategy_ensemble.py | 809 - .../ml-engine/src/models/tp_sl_classifier.py | 658 - .../apps/ml-engine/src/pipelines/__init__.py | 7 - .../src/pipelines/phase2_pipeline.py | 604 - .../apps/ml-engine/src/services/__init__.py | 6 - .../src/services/prediction_service.py | 628 - .../apps/ml-engine/src/training/__init__.py | 11 - .../ml-engine/src/training/walk_forward.py | 453 - .../apps/ml-engine/src/utils/__init__.py | 12 - .../apps/ml-engine/src/utils/audit.py | 772 - .../apps/ml-engine/src/utils/signal_logger.py | 546 - .../apps/ml-engine/tests/__init__.py | 1 - .../apps/ml-engine/tests/test_amd_detector.py | 170 - .../apps/ml-engine/tests/test_api.py | 191 - .../apps/ml-engine/tests/test_ict_detector.py | 267 - .../apps/mt4-gateway/.env.example | 57 - .../apps/mt4-gateway/config/agents.yml | 184 - .../apps/mt4-gateway/requirements.txt | 37 - .../apps/mt4-gateway/src/__init__.py | 0 .../apps/mt4-gateway/src/main.py | 546 - .../mt4-gateway/src/providers/__init__.py | 0 .../src/providers/mt4_bridge_client.py | 496 - .../apps/mt4-gateway/src/services/__init__.py | 0 .../apps/personal/.env.example | 109 - .../apps/personal/config.yaml | 169 - .../apps/personal/package.json | 20 - .../apps/personal/scripts/setup-personal.ts | 304 - .../apps/personal/scripts/validate-config.ts | 213 - .../apps/trading-agents/.env.example | 29 - .../apps/trading-agents/Dockerfile | 29 - .../trading-agents/IMPLEMENTATION_REPORT.md | 498 - .../apps/trading-agents/INTEGRATION.md | 587 - .../trading-agents/PAPER_TRADING_GUIDE.md | 321 - .../apps/trading-agents/README.md | 335 - .../apps/trading-agents/config/agents.yaml | 146 - .../apps/trading-agents/config/risk.yaml | 208 - .../trading-agents/config/strategies.yaml | 165 - .../apps/trading-agents/docker-compose.yml | 76 - .../apps/trading-agents/example_usage.py | 215 - .../apps/trading-agents/requirements.txt | 55 - .../apps/trading-agents/src/__init__.py | 5 - .../trading-agents/src/agents/__init__.py | 16 - .../apps/trading-agents/src/agents/atlas.py | 263 - .../apps/trading-agents/src/agents/base.py | 320 - .../apps/trading-agents/src/agents/nova.py | 305 - .../apps/trading-agents/src/agents/orion.py | 299 - .../apps/trading-agents/src/api/main.py | 260 - .../trading-agents/src/exchange/__init__.py | 7 - .../src/exchange/binance_client.py | 340 - .../trading-agents/src/execution/__init__.py | 8 - .../src/execution/risk_manager.py | 319 - .../trading-agents/src/signals/__init__.py | 7 - .../trading-agents/src/signals/ml_consumer.py | 227 - .../trading-agents/src/strategies/__init__.py | 17 - .../trading-agents/src/strategies/base.py | 82 - .../src/strategies/grid_trading.py | 182 - .../src/strategies/mean_reversion.py | 170 - .../trading-agents/src/strategies/momentum.py | 147 - .../src/strategies/trend_following.py | 192 - .../docker-compose.services.yml | 205 - projects/trading-platform/docker-compose.yml | 266 - .../docker/docker-compose.prod.yml | 214 - .../NOTA-DISCREPANCIA-PUERTOS-2025-12-08.md | 98 - .../00-vision-general/ARQUITECTURA-GENERAL.md | 432 - .../00-vision-general/STACK-TECNOLOGICO.md | 500 - ...ive.com para obtener datos financieros.pdf | Bin 80144 -> 0 bytes .../docs/00-vision-general/VISION-PRODUCTO.md | 231 - .../docs/00-vision-general/_MAP.md | 102 - .../ARQUITECTURA-MULTI-AGENTE-MT4.md | 593 - .../01-arquitectura/ARQUITECTURA-UNIFICADA.md | 606 - .../01-arquitectura/DIAGRAMA-INTEGRACIONES.md | 887 - .../INTEGRACION-API-MASSIVE.md | 1159 -- .../01-arquitectura/INTEGRACION-LLM-LOCAL.md | 1167 -- .../INTEGRACION-METATRADER4.md | 1157 -- .../INTEGRACION-TRADINGAGENT.md | 638 - .../OQI-001-fundamentos-auth/README.md | 307 - .../OQI-001-fundamentos-auth/_MAP.md | 201 - .../especificaciones/ET-AUTH-001-oauth.md | 596 - .../especificaciones/ET-AUTH-002-jwt.md | 659 - .../especificaciones/ET-AUTH-003-database.md | 690 - .../especificaciones/ET-AUTH-004-api.md | 812 - .../especificaciones/ET-AUTH-005-security.md | 710 - .../US-AUTH-001-registro-email.md | 188 - .../US-AUTH-002-login-email.md | 259 - .../US-AUTH-003-oauth-google.md | 218 - .../US-AUTH-004-oauth-facebook.md | 293 - .../US-AUTH-005-oauth-twitter.md | 316 - .../US-AUTH-006-oauth-apple.md | 337 - .../US-AUTH-007-oauth-github.md | 307 - .../US-AUTH-008-phone-sms.md | 442 - .../US-AUTH-009-phone-whatsapp.md | 392 - .../US-AUTH-010-2fa-setup.md | 302 - .../US-AUTH-011-password-reset.md | 529 - .../US-AUTH-012-session-management.md | 627 - .../implementacion/TRACEABILITY.yml | 375 - .../requerimientos/RF-AUTH-001-oauth.md | 290 - .../requerimientos/RF-AUTH-002-email.md | 351 - .../requerimientos/RF-AUTH-003-phone.md | 326 - .../requerimientos/RF-AUTH-004-2fa.md | 381 - .../requerimientos/RF-AUTH-005-sessions.md | 439 - .../OQI-002-education/README.md | 268 - .../OQI-002-education/_MAP.md | 235 - .../especificaciones/ET-EDU-001-database.md | 938 - .../especificaciones/ET-EDU-002-api.md | 1886 -- .../especificaciones/ET-EDU-003-frontend.md | 1455 -- .../especificaciones/ET-EDU-004-video.md | 1075 -- .../especificaciones/ET-EDU-005-quizzes.md | 1145 -- .../ET-EDU-006-gamification.md | 1231 -- .../especificaciones/README.md | 229 - .../US-EDU-001-ver-catalogo.md | 314 - .../historias-usuario/US-EDU-002-ver-curso.md | 364 - .../US-EDU-003-iniciar-leccion.md | 360 - .../historias-usuario/US-EDU-004-ver-video.md | 357 - .../US-EDU-005-completar-leccion.md | 376 - .../US-EDU-006-realizar-quiz.md | 473 - .../US-EDU-007-ver-progreso.md | 422 - .../US-EDU-008-obtener-certificado.md | 437 - .../implementacion/TRACEABILITY.yml | 464 - .../requerimientos/RF-EDU-001-catalogo.md | 285 - .../requerimientos/RF-EDU-002-lecciones.md | 321 - .../requerimientos/RF-EDU-003-progreso.md | 355 - .../requerimientos/RF-EDU-004-quizzes.md | 404 - .../requerimientos/RF-EDU-005-certificados.md | 323 - .../requerimientos/RF-EDU-006-gamificacion.md | 431 - .../OQI-003-trading-charts/README.md | 549 - .../OQI-003-trading-charts/_MAP.md | 192 - .../ET-TRD-001-market-data.md | 866 - .../especificaciones/ET-TRD-002-websocket.md | 969 -- .../especificaciones/ET-TRD-003-database.md | 792 - .../especificaciones/ET-TRD-004-api.md | 976 -- .../especificaciones/ET-TRD-005-frontend.md | 1098 -- .../ET-TRD-006-indicadores.md | 850 - .../ET-TRD-007-paper-engine.md | 1204 -- .../ET-TRD-008-performance.md | 1006 -- .../especificaciones/README.md | 251 - .../historias-usuario/US-TRD-001-ver-chart.md | 189 - .../US-TRD-002-cambiar-timeframe.md | 227 - .../US-TRD-003-agregar-indicador.md | 264 - .../US-TRD-004-crear-watchlist.md | 254 - .../US-TRD-005-agregar-simbolo.md | 287 - .../US-TRD-006-crear-orden-market.md | 270 - .../US-TRD-007-crear-orden-limit.md | 295 - .../US-TRD-008-cerrar-posicion.md | 299 - .../US-TRD-009-ver-posiciones.md | 305 - .../US-TRD-010-ver-historial.md | 336 - .../US-TRD-011-ver-estadisticas.md | 387 - .../US-TRD-012-configurar-tp-sl.md | 366 - .../US-TRD-013-alertas-precio.md | 437 - .../US-TRD-014-reset-balance.md | 383 - .../US-TRD-015-exportar-trades.md | 399 - .../US-TRD-016-modo-oscuro-chart.md | 421 - .../US-TRD-017-zoom-pan-chart.md | 550 - .../US-TRD-018-comparar-simbolos.md | 455 - .../implementacion/TRACEABILITY.yml | 604 - .../requerimientos/RF-TRD-001-charts.md | 158 - .../RF-TRD-002-indicadores-tecnicos.md | 270 - .../RF-TRD-003-gestion-watchlists.md | 272 - .../RF-TRD-004-paper-trading.md | 227 - .../RF-TRD-005-sistema-ordenes.md | 425 - .../RF-TRD-006-gestion-posiciones.md | 468 - .../RF-TRD-007-historial-trades.md | 475 - .../RF-TRD-008-metricas-estadisticas.md | 503 - .../OQI-004-investment-accounts/README.md | 428 - .../OQI-004-investment-accounts/_MAP.md | 200 - .../especificaciones/ET-INV-001-database.md | 805 - .../especificaciones/ET-INV-002-api.md | 1279 -- .../especificaciones/ET-INV-003-stripe.md | 938 - .../especificaciones/ET-INV-004-agents.md | 912 - .../especificaciones/ET-INV-005-frontend.md | 900 - .../especificaciones/ET-INV-006-cron.md | 832 - .../especificaciones/ET-INV-007-security.md | 845 - .../US-INV-001-ver-productos.md | 251 - .../US-INV-002-abrir-cuenta.md | 240 - .../historias-usuario/US-INV-003-depositar.md | 275 - .../US-INV-004-ver-portfolio.md | 302 - .../US-INV-005-ver-rendimiento.md | 306 - .../US-INV-006-solicitar-retiro.md | 313 - .../US-INV-007-ver-transacciones.md | 311 - .../US-INV-008-recibir-distribucion.md | 352 - .../US-INV-009-cerrar-cuenta.md | 337 - .../US-INV-010-comparar-productos.md | 323 - .../US-INV-011-exportar-reporte.md | 381 - .../US-INV-012-notificaciones.md | 411 - .../US-INV-013-kyc-basico.md | 381 - .../US-INV-014-ver-agente-performance.md | 358 - .../implementacion/TRACEABILITY.yml | 506 - .../requerimientos/RF-INV-001-productos.md | 205 - .../requerimientos/RF-INV-002-cuentas.md | 222 - .../requerimientos/RF-INV-003-depositos.md | 270 - .../requerimientos/RF-INV-004-retiros.md | 313 - .../requerimientos/RF-INV-005-agentes.md | 368 - .../requerimientos/RF-INV-006-reportes.md | 312 - .../OQI-005-payments-stripe/README.md | 241 - .../OQI-005-payments-stripe/_MAP.md | 209 - .../especificaciones/ET-PAY-001-database.md | 638 - .../especificaciones/ET-PAY-002-stripe-api.md | 761 - .../especificaciones/ET-PAY-003-webhooks.md | 493 - .../especificaciones/ET-PAY-004-api.md | 223 - .../especificaciones/ET-PAY-005-frontend.md | 429 - .../especificaciones/ET-PAY-006-security.md | 514 - .../US-PAY-001-ver-planes.md | 277 - .../US-PAY-002-suscribirse.md | 360 - .../US-PAY-005-comprar-curso.md | 393 - .../US-PAY-006-agregar-metodo-pago.md | 392 - .../US-PAY-007-ver-facturas.md | 415 - .../US-PAY-010-ver-historial.md | 484 - .../implementacion/TRACEABILITY.yml | 530 - .../RF-PAY-001-suscripciones.md | 301 - .../requerimientos/RF-PAY-002-checkout.md | 490 - .../requerimientos/RF-PAY-003-wallet.md | 410 - .../requerimientos/RF-PAY-004-facturacion.md | 446 - .../requerimientos/RF-PAY-005-webhooks.md | 552 - .../requerimientos/RF-PAY-006-reembolsos.md | 463 - .../OQI-006-ml-signals/README.md | 348 - .../OQI-006-ml-signals/_MAP.md | 232 - .../EPICA-OQI-006A-ESTRATEGIA-AMD-ML.md | 294 - .../OQI-006-ml-signals/epicas/README.md | 161 - .../US-ML-020-ver-fase-amd.md | 239 - .../US-ML-022-zonas-liquidez.md | 271 - .../US-ML-024-score-confluencia.md | 380 - .../US-ML-027-integracion-ict-smc.md | 474 - .../ET-ML-001-arquitectura.md | 609 - .../especificaciones/ET-ML-002-modelos.md | 624 - .../especificaciones/ET-ML-003-features.md | 680 - .../especificaciones/ET-ML-004-api.md | 770 - .../especificaciones/ET-ML-005-integracion.md | 932 - .../ALCANCES-FASE-1-PRIORIZADOS.md | 473 - .../estrategias/ARQUITECTURA-MODELOS-FLUJO.md | 973 -- .../estrategias/ESTRATEGIA-AMD-COMPLETA.md | 1417 -- .../estrategias/ESTRATEGIA-ICT-SMC.md | 1268 -- .../estrategias/FEATURES-TARGETS-COMPLETO.md | 984 -- .../estrategias/FEATURES-TARGETS-ML.md | 869 - .../estrategias/MODELOS-ML-DEFINICION.md | 1712 -- .../estrategias/PIPELINE-ORQUESTACION.md | 923 - .../OQI-006-ml-signals/estrategias/README.md | 329 - .../US-ML-001-ver-prediccion.md | 254 - .../historias-usuario/US-ML-002-ver-senal.md | 307 - .../US-ML-004-ver-accuracy.md | 330 - .../US-ML-006-senal-en-chart.md | 313 - .../US-ML-007-historial-senales.md | 363 - .../implementacion/TRACEABILITY.yml | 451 - .../requerimientos/RF-ML-001-predicciones.md | 287 - .../requerimientos/RF-ML-002-senales.md | 383 - .../requerimientos/RF-ML-003-indicadores.md | 453 - .../requerimientos/RF-ML-004-entrenamiento.md | 435 - .../RF-ML-005-notificaciones.md | 435 - .../OQI-007-llm-agent/README.md | 314 - .../OQI-007-llm-agent/_MAP.md | 253 - .../ET-LLM-001-arquitectura-chat.md | 752 - .../ET-LLM-002-agente-analisis.md | 565 - .../ET-LLM-003-motor-estrategias.md | 881 - .../ET-LLM-004-integracion-educacion.md | 751 - .../ET-LLM-005-arquitectura-tools.md | 1013 -- .../ET-LLM-006-gestion-memoria.md | 881 - .../US-LLM-001-enviar-mensaje.md | 135 - .../US-LLM-002-gestionar-conversaciones.md | 144 - .../US-LLM-003-analisis-simbolo.md | 149 - .../US-LLM-004-ver-senales-ml.md | 136 - .../US-LLM-005-estrategia-personalizada.md | 158 - .../US-LLM-006-historial-estrategias.md | 85 - .../US-LLM-007-asistencia-educativa.md | 130 - .../US-LLM-008-recomendaciones-aprendizaje.md | 105 - .../US-LLM-009-consultar-datos-chat.md | 121 - .../US-LLM-010-paper-trading-chat.md | 158 - .../implementacion/TRACEABILITY.yml | 474 - .../RF-LLM-001-chat-interface.md | 156 - .../RF-LLM-002-market-analysis.md | 193 - .../RF-LLM-003-strategy-suggestions.md | 195 - .../RF-LLM-004-educational-assistance.md | 197 - .../RF-LLM-005-tool-integration.md | 272 - .../RF-LLM-006-context-management.md | 284 - .../OQI-008-portfolio-manager/README.md | 349 - .../OQI-008-portfolio-manager/_MAP.md | 237 - .../ET-PFM-001-arquitectura-dashboard.md | 109 - .../ET-PFM-002-calculo-metricas.md | 112 - .../ET-PFM-003-stress-testing.md | 114 - .../ET-PFM-004-motor-rebalanceo.md | 112 - .../ET-PFM-005-historial-reportes.md | 99 - .../ET-PFM-006-reportes-fiscales.md | 138 - .../ET-PFM-007-motor-metas.md | 142 - .../US-PFM-001-ver-resumen-portfolio.md | 42 - .../US-PFM-002-ver-posiciones.md | 44 - .../US-PFM-003-ver-metricas-riesgo.md | 49 - .../US-PFM-004-ejecutar-stress-test.md | 43 - .../US-PFM-005-configurar-asignacion.md | 47 - .../US-PFM-006-ver-desviacion.md | 41 - .../US-PFM-007-ejecutar-rebalanceo.md | 43 - .../US-PFM-008-ver-historial.md | 46 - .../US-PFM-009-exportar-historial.md | 42 - .../US-PFM-010-comparar-benchmark.md | 41 - .../US-PFM-011-metricas-benchmark.md | 42 - .../US-PFM-012-reporte-fiscal.md | 50 - .../implementacion/TRACEABILITY.yml | 626 - .../RF-PFM-001-dashboard-portfolio.md | 168 - .../RF-PFM-002-analisis-riesgo.md | 210 - .../requerimientos/RF-PFM-003-rebalanceo.md | 218 - .../RF-PFM-004-historial-transacciones.md | 191 - .../RF-PFM-005-comparacion-benchmark.md | 212 - .../RF-PFM-006-reportes-fiscales.md | 228 - .../RF-PFM-007-metas-inversi贸n.md | 231 - .../docs/02-definicion-modulos/_MAP.md | 484 - .../VALIDACION-IMPLEMENTACION.md | 292 - .../ESTRATEGIA-PREDICCION-RANGOS.md | 758 - .../gaps/ANALISIS-GAPS-DOCUMENTACION.md | 316 - .../INT-DATA-001-data-service.md | 457 - .../INT-DATA-002-analisis-impacto.md | 396 - .../INT-MT4-001-gateway-service.md | 453 - .../docs/90-transversal/integraciones/_MAP.md | 76 - .../inventarios/BACKEND_INVENTORY.yml | 643 - .../inventarios/DATABASE_GAPS_ANALYSIS.md | 859 - .../inventarios/DATABASE_INVENTORY.yml | 1315 -- .../inventarios/FRONTEND_INVENTORY.yml | 552 - .../INVENTARIO-STC-PLATFORM-WEB.md | 493 - .../MATRIZ-DEPENDENCIAS-TRADING.yml | 520 - .../inventarios/MATRIZ-DEPENDENCIAS.yml | 879 - .../inventarios/MIGRACION-SUPABASE-EXPRESS.md | 701 - .../inventarios/ML_INVENTORY.yml | 313 - .../inventarios/MT4_GATEWAY_INVENTORY.yml | 504 - .../inventarios/STRATEGIES_INVENTORY.yml | 594 - .../docs/90-transversal/inventarios/_MAP.md | 71 - .../roadmap/PLAN-DESARROLLO-DETALLADO.md | 605 - .../90-transversal/roadmap/ROADMAP-GENERAL.md | 269 - .../90-transversal/setup/SETUP-MT4-TRADING.md | 336 - .../95-guias-desarrollo/JENKINS-DEPLOY.md | 1255 -- .../95-guias-desarrollo/PUERTOS-SERVICIOS.md | 400 - .../ml-engine/SETUP-PYTHON.md | 591 - .../docs/97-adr/ADR-001-stack-tecnologico.md | 119 - .../97-adr/ADR-002-MVP-OPERATIVO-TRADING.md | 408 - .../docs/97-adr/ADR-002-monorepo.md | 126 - .../ADR-003-autenticacion-multiproveedor.md | 209 - .../docs/97-adr/ADR-004-testing.md | 243 - .../docs/97-adr/ADR-005-devops.md | 294 - .../docs/97-adr/ADR-006-caching.md | 338 - .../docs/97-adr/ADR-007-security.md | 473 - projects/trading-platform/docs/97-adr/_MAP.md | 62 - .../99-analisis/DECISIONES-ARQUITECTONICAS.md | 243 - .../PLAN-IMPLEMENTACION-CORRECCIONES.md | 537 - .../REPORTE-ANALISIS-REQUISITOS.md | 493 - .../REPORTE-EJECUCION-CORRECCIONES.md | 255 - .../99-analisis/REPORTE-TRAZABILIDAD-DDL.md | 402 - projects/trading-platform/docs/API.md | 627 - .../trading-platform/docs/ARCHITECTURE.md | 567 - projects/trading-platform/docs/README.md | 278 - projects/trading-platform/docs/SECURITY.md | 813 - projects/trading-platform/docs/_MAP.md | 299 - .../docs/api-contracts/SERVICE-INTEGRATION.md | 634 - .../00-guidelines/CONTEXTO-PROYECTO.md | 291 - .../00-guidelines/HERENCIA-DIRECTIVAS.md | 114 - .../00-guidelines/HERENCIA-SIMCO.md | 287 - .../00-guidelines/PROJECT-STATUS.md | 33 - ...ELEGACION-TRADING-STRATEGIST-2025-12-08.md | 297 - .../orchestration/PROXIMA-ACCION.md | 263 - .../DIRECTIVA-ARQUITECTURA-HIBRIDA.md | 383 - .../directivas/DIRECTIVA-ML-SERVICES.md | 619 - .../DIRECTIVA-POLITICA-CARGA-LIMPIA.md | 259 - .../directivas/DIRECTIVA-STACK-TECNOLOGICO.md | 468 - .../environment/PROJECT-ENV-CONFIG.yml | 219 - .../estados/REGISTRO-SUBAGENTES.json | 65 - .../inventarios/BACKEND_INVENTORY.yml | 321 - .../inventarios/DATABASE_INVENTORY.yml | 384 - .../inventarios/FRONTEND_INVENTORY.yml | 311 - .../inventarios/MASTER_INVENTORY.yml | 200 - .../planes/PLAN-ML-LLM-TRADING.md | 315 - .../reportes/REPORTE-SESION-2025-12-07.md | 221 - .../trazas/TRAZA-TAREAS-BACKEND.md | 82 - .../trazas/TRAZA-TAREAS-DATABASE.md | 54 - .../trazas/TRAZA-TAREAS-FRONTEND.md | 82 - projects/trading-platform/package.json | 25 - .../trading-platform/packages/config/index.ts | 201 - .../packages/sdk-python/setup.py | 24 - .../sdk-python/src/orbiquant_sdk/__init__.py | 12 - .../sdk-python/src/orbiquant_sdk/client.py | 246 - .../sdk-python/src/orbiquant_sdk/config.py | 52 - .../packages/sdk-typescript/package.json | 30 - .../packages/sdk-typescript/src/auth/index.ts | 191 - .../packages/sdk-typescript/src/client.ts | 169 - .../packages/sdk-typescript/src/index.ts | 16 - .../packages/sdk-typescript/src/ml/index.ts | 422 - .../sdk-typescript/src/trading/index.ts | 382 - .../sdk-typescript/src/types/index.ts | 420 - .../knowledge-base/TRAZABILIDAD-PROYECTOS.yml | 96 + .../propagacion/USAGE-ORQUESTACION.md | 304 + .../templates/TEMPLATE-PROJECT-STATUS.md | 85 + .../TEMPLATE-BACKEND-INVENTORY.yml | 107 + .../TEMPLATE-DATABASE-INVENTORY.yml | 77 + .../inventories/TEMPLATE-MASTER-INVENTORY.yml | 56 + 4064 files changed, 1260 insertions(+), 1357531 deletions(-) create mode 100644 SUBREPOSITORIOS.md create mode 100755 devtools/scripts/validation/validate-project-structure.sh delete mode 100644 projects/betting-analytics/INVENTARIO.yml delete mode 100644 projects/betting-analytics/README.md delete mode 100644 projects/betting-analytics/apps/backend/Dockerfile delete mode 100644 projects/betting-analytics/apps/backend/package.json delete mode 100644 projects/betting-analytics/apps/backend/service.descriptor.yml delete mode 100644 projects/betting-analytics/apps/backend/src/app.module.ts delete mode 100644 projects/betting-analytics/apps/backend/src/config/index.ts delete mode 100644 projects/betting-analytics/apps/backend/src/main.ts delete mode 100644 projects/betting-analytics/apps/backend/src/modules/auth/auth.module.ts delete mode 100644 projects/betting-analytics/apps/backend/src/shared/types/index.ts delete mode 100644 projects/betting-analytics/apps/backend/tsconfig.json delete mode 100644 projects/betting-analytics/apps/frontend/Dockerfile delete mode 100644 projects/betting-analytics/apps/ml/Dockerfile delete mode 100644 projects/betting-analytics/docs/README.md delete mode 100644 projects/betting-analytics/docs/_MAP.md delete mode 100644 projects/betting-analytics/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/betting-analytics/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/betting-analytics/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/betting-analytics/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/betting-analytics/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/betting-analytics/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/betting-analytics/orchestration/estados/REGISTRO-SUBAGENTES.json delete mode 100644 projects/betting-analytics/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/betting-analytics/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/betting-analytics/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/betting-analytics/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/erp-clinicas/.env.example delete mode 100644 projects/erp-clinicas/INVENTARIO.yml delete mode 100644 projects/erp-clinicas/PROJECT-STATUS.md delete mode 100644 projects/erp-clinicas/database/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-clinicas/database/README.md delete mode 100644 projects/erp-clinicas/database/init/00-extensions.sql delete mode 100644 projects/erp-clinicas/database/init/01-create-schemas.sql delete mode 100644 projects/erp-clinicas/database/init/02-rls-functions.sql delete mode 100644 projects/erp-clinicas/database/init/03-clinical-tables.sql delete mode 100644 projects/erp-clinicas/database/init/04-seed-data.sql delete mode 100644 projects/erp-clinicas/docs/00-vision-general/VISION-CLINICAS.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-001-fundamentos/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-002-pacientes/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-003-citas/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-004-consultas/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-005-recetas/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-006-laboratorio/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-007-farmacia/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-008-facturacion/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-009-reportes/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-010-telemedicina/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-011-expediente/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/CL-012-imagenologia/README.md delete mode 100644 projects/erp-clinicas/docs/02-definicion-modulos/INDICE-MODULOS.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-001-fundamentos.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-002-pacientes.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-003-citas.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-004-consultas.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-005-recetas.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-006-laboratorio.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-007-farmacia.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-008-facturacion.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-009-reportes.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-010-telemedicina.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-011-expediente.md delete mode 100644 projects/erp-clinicas/docs/08-epicas/EPIC-CL-012-imagenologia.md delete mode 100644 projects/erp-clinicas/docs/README.md delete mode 100644 projects/erp-clinicas/docs/_MAP.md delete mode 100644 projects/erp-clinicas/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SPECS-CORE.md delete mode 100644 projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SPECS-ERP-CORE.md delete mode 100644 projects/erp-clinicas/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/erp-clinicas/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/erp-clinicas/orchestration/directivas/DIRECTIVA-EXPEDIENTE-CLINICO.md delete mode 100644 projects/erp-clinicas/orchestration/directivas/DIRECTIVA-GESTION-CITAS.md delete mode 100644 projects/erp-clinicas/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/erp-clinicas/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/erp-clinicas/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/erp-clinicas/orchestration/inventarios/DEPENDENCY_GRAPH.yml delete mode 100644 projects/erp-clinicas/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/erp-clinicas/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/erp-clinicas/orchestration/inventarios/README.md delete mode 100644 projects/erp-clinicas/orchestration/inventarios/TRACEABILITY_MATRIX.yml delete mode 100644 projects/erp-clinicas/orchestration/prompts/PROMPT-CL-BACKEND-AGENT.md delete mode 100644 projects/erp-clinicas/orchestration/referencias/DEPENDENCIAS-ERP-CORE.yml delete mode 100644 projects/erp-clinicas/orchestration/referencias/DEPENDENCIAS-SHARED.yml delete mode 100644 projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/erp-construccion/.env.example delete mode 100644 projects/erp-construccion/.github/workflows/ci.yml delete mode 100644 projects/erp-construccion/CONTRIBUTING.md delete mode 100644 projects/erp-construccion/INVENTARIO.yml delete mode 100644 projects/erp-construccion/PROJECT-STATUS.md delete mode 100644 projects/erp-construccion/README.md delete mode 100644 projects/erp-construccion/backend/.env.example delete mode 100644 projects/erp-construccion/backend/Dockerfile delete mode 100644 projects/erp-construccion/backend/README.md delete mode 100644 projects/erp-construccion/backend/package-lock.json delete mode 100644 projects/erp-construccion/backend/package.json delete mode 100644 projects/erp-construccion/backend/scripts/sync-enums.ts delete mode 100644 projects/erp-construccion/backend/scripts/validate-constants-usage.ts delete mode 100644 projects/erp-construccion/backend/service.descriptor.yml delete mode 100644 projects/erp-construccion/backend/src/modules/admin/controllers/audit-log.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/controllers/backup.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/controllers/cost-center.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/controllers/system-setting.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/entities/audit-log.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/entities/backup.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/entities/cost-center.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/entities/custom-permission.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/entities/system-setting.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/services/audit-log.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/services/backup.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/services/cost-center.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/admin/services/system-setting.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/controllers/auth.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/dto/auth.dto.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/entities/permission.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/entities/refresh-token.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/entities/role.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/entities/user-role.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/middleware/auth.middleware.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/services/auth.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/auth/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/controllers/bid-analytics.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/controllers/bid-budget.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/controllers/bid.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/controllers/opportunity.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/entities/bid-budget.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/entities/bid-calendar.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/entities/bid-competitor.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/entities/bid-document.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/entities/bid-team.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/entities/bid.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/entities/opportunity.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/services/bid-analytics.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/services/bid-budget.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/services/bid.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/bidding/services/opportunity.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/controllers/concepto.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/controllers/presupuesto.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/entities/concepto.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/entities/presupuesto-partida.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/entities/presupuesto.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/services/concepto.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/budgets/services/presupuesto.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/controllers/etapa.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/controllers/fraccionamiento.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/controllers/lote.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/controllers/manzana.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/controllers/prototipo.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/controllers/proyecto.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/entities/etapa.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/entities/fraccionamiento.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/entities/lote.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/entities/manzana.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/entities/prototipo.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/entities/proyecto.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/services/etapa.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/services/fraccionamiento.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/services/lote.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/services/manzana.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/services/prototipo.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/construction/services/proyecto.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/controllers/contract.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/controllers/subcontractor.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/entities/contract-addendum.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/entities/contract.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/entities/subcontractor.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/services/contract.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/contracts/services/subcontractor.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/core/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/core/entities/tenant.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/core/entities/user.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/controllers/anticipo.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/controllers/estimacion.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/controllers/fondo-garantia.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/controllers/retencion.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/amortizacion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/anticipo.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/estimacion-concepto.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/estimacion-workflow.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/estimacion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/fondo-garantia.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/generador.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/entities/retencion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/services/anticipo.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/services/estimacion.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/services/fondo-garantia.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/estimates/services/retencion.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/controllers/accounting.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/controllers/ap.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/controllers/ar.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/controllers/bank-reconciliation.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/controllers/cash-flow.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/controllers/reports.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/account-payable.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/account-receivable.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/accounting-entry-line.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/accounting-entry.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/ap-payment.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/ar-payment.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/bank-account.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/bank-movement.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/bank-reconciliation.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/cash-flow-projection.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/chart-of-accounts.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/services/accounting.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/services/ap.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/services/ar.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/services/bank-reconciliation.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/services/cash-flow.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/services/erp-integration.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/services/financial-reports.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/finance/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/controllers/employee.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/controllers/puesto.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/entities/employee-fraccionamiento.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/entities/employee.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/entities/puesto.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/services/employee.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hr/services/puesto.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/ambiental.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/capacitacion.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/epp.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/incidente.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/indicador.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/inspeccion.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/permiso-trabajo.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/controllers/stps.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/alerta-indicador.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/almacen-temporal.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/auditoria.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-asistente.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-matriz.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-sesion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/capacitacion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/checklist-item.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/comision-integrante.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/comision-recorrido.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/comision-seguridad.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/constancia-dc3.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/cumplimiento-obra.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/dias-sin-accidente.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/documento-stps.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/epp-asignacion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/epp-baja.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/epp-catalogo.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/epp-inspeccion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/epp-inventario.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/epp-matriz-puesto.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/epp-movimiento.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/hallazgo-evidencia.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/hallazgo.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/horas-trabajadas.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/impacto-ambiental.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/incidente-accion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/incidente-evidencia.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/incidente-investigacion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/incidente-involucrado.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/incidente.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/indicador-config.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/indicador-meta-obra.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/indicador-valor.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/inspeccion-evaluacion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/inspeccion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/instructor.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/manifiesto-detalle.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/manifiesto-residuos.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/norma-requisito.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/norma-stps.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/permiso-autorizacion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/permiso-checklist.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/permiso-documento.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/permiso-evento.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/permiso-monitoreo.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/permiso-personal.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/permiso-trabajo.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/programa-actividad.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/programa-inspeccion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/programa-seguridad.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/proveedor-ambiental.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/queja-ambiental.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/reporte-programado.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/residuo-catalogo.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/residuo-generacion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/tipo-inspeccion.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/entities/tipo-permiso-trabajo.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/ambiental.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/capacitacion.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/epp.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/incidente.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/indicador.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/inspeccion.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/permiso-trabajo.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/hse/services/stps.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/controllers/asignacion.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/controllers/derechohabiente.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/acta-vivienda.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/acta.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/asignacion-vivienda.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/derechohabiente.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/historico-puntos.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/oferta-vivienda.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/registro-infonavit.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/entities/reporte-infonavit.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/services/asignacion.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/services/derechohabiente.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/infonavit/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/controllers/consumo-obra.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/controllers/requisicion.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/entities/almacen-proyecto.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/entities/consumo-obra.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/entities/requisicion-linea.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/entities/requisicion-obra.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/services/consumo-obra.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/inventory/services/requisicion.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/controllers/avance-obra.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/controllers/bitacora-obra.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/entities/avance-obra.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/entities/bitacora-obra.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/entities/foto-avance.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/entities/programa-actividad.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/entities/programa-obra.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/services/avance-obra.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/services/bitacora-obra.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/progress/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/purchase/controllers/comparativo.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/purchase/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-cotizaciones.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-producto.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-proveedor.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/purchase/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/purchase/services/comparativo.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/purchase/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/controllers/inspection.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/controllers/ticket.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/checklist-item.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/checklist.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/corrective-action.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/inspection-result.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/inspection.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/non-conformity.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/post-sale-ticket.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/entities/ticket-assignment.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/services/inspection.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/quality/services/ticket.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/controllers/dashboard.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/controllers/kpi.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/controllers/report.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/entities/dashboard-widget.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/entities/dashboard.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/entities/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/entities/kpi-snapshot.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/entities/report-execution.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/entities/report.entity.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/services/dashboard.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/services/kpi.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/reports/services/report.service.ts delete mode 100644 projects/erp-construccion/backend/src/modules/users/controllers/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/users/controllers/users.controller.ts delete mode 100644 projects/erp-construccion/backend/src/modules/users/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/users/services/index.ts delete mode 100644 projects/erp-construccion/backend/src/modules/users/services/users.service.ts delete mode 100644 projects/erp-construccion/backend/src/server.ts delete mode 100644 projects/erp-construccion/backend/src/shared/constants/api.constants.ts delete mode 100644 projects/erp-construccion/backend/src/shared/constants/database.constants.ts delete mode 100644 projects/erp-construccion/backend/src/shared/constants/enums.constants.ts delete mode 100644 projects/erp-construccion/backend/src/shared/constants/index.ts delete mode 100644 projects/erp-construccion/backend/src/shared/database/typeorm.config.ts delete mode 100644 projects/erp-construccion/backend/src/shared/interfaces/base.interface.ts delete mode 100644 projects/erp-construccion/backend/src/shared/services/base.service.ts delete mode 100644 projects/erp-construccion/backend/src/shared/services/index.ts delete mode 100644 projects/erp-construccion/backend/tsconfig.json delete mode 100644 projects/erp-construccion/database/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-construccion/database/README.md delete mode 100644 projects/erp-construccion/database/VALIDACION-DDL-INVENTARIOS.md delete mode 100644 projects/erp-construccion/database/_MAP.md delete mode 100755 projects/erp-construccion/database/drop-and-recreate-database.sh delete mode 100644 projects/erp-construccion/database/init-scripts/01-init-database.sql delete mode 100644 projects/erp-construccion/database/schemas/01-construction-schema-ddl.sql delete mode 100644 projects/erp-construccion/database/schemas/02-hr-schema-ddl.sql delete mode 100644 projects/erp-construccion/database/schemas/03-hse-schema-ddl.sql delete mode 100644 projects/erp-construccion/database/schemas/04-estimates-schema-ddl.sql delete mode 100644 projects/erp-construccion/database/schemas/05-infonavit-schema-ddl.sql delete mode 100644 projects/erp-construccion/database/schemas/06-inventory-ext-schema-ddl.sql delete mode 100644 projects/erp-construccion/database/schemas/07-purchase-ext-schema-ddl.sql delete mode 100755 projects/erp-construccion/database/validate-clean-load-policy.sh delete mode 100644 projects/erp-construccion/devops/scripts/sync-enums.ts delete mode 100644 projects/erp-construccion/devops/scripts/validate-constants-usage.ts delete mode 100644 projects/erp-construccion/docker-compose.prod.yml delete mode 100644 projects/erp-construccion/docker-compose.yml delete mode 100644 projects/erp-construccion/docs/00-vision-general/ARQUITECTURA-SAAS.md delete mode 100644 projects/erp-construccion/docs/00-vision-general/CAMBIOS-SAAS-MVP.md delete mode 100644 projects/erp-construccion/docs/00-vision-general/GLOSARIO.md delete mode 100644 projects/erp-construccion/docs/00-vision-general/MARKETPLACE-EXTENSIONES.md delete mode 100644 projects/erp-construccion/docs/00-vision-general/MVP-APP.md delete mode 100644 projects/erp-construccion/docs/00-vision-general/PORTAL-ADMIN-SAAS.md delete mode 100644 projects/erp-construccion/docs/01-analisis-referencias/MAPEO-MAI-TO-MGN.md delete mode 100644 projects/erp-construccion/docs/01-analisis-referencias/README.md delete mode 100644 projects/erp-construccion/docs/01-analisis-referencias/erp-generico/README.md delete mode 100644 projects/erp-construccion/docs/01-analisis-referencias/gamilit/README.md delete mode 100644 projects/erp-construccion/docs/01-analisis-referencias/odoo/ODOO-CONSTRUCCION-MAPPING.md delete mode 100644 projects/erp-construccion/docs/01-analisis-referencias/odoo/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/ANALISIS-REUTILIZACION-GAMILIT.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/CAMBIOS-Y-ACTUALIZACIONES.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/INDICE-RF-MAA017.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-001-gestion-incidentes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-002-control-capacitaciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-003-inspecciones-seguridad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-004-control-epp.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-005-cumplimiento-stps.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-006-gestion-ambiental.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-007-permisos-trabajo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-008-indicadores-hse.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-008-contabilidad/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-009-facturacion/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-010-tesoreria/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-011-nomina/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-012-compras/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-013-inventarios/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-crm/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-001-modelo de datos financiero.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-002-servicio de flujo de efectivo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-003-servicio de facturaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-004-conciliaci贸n bancaria.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-005-dashboard de control de gesti贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-001-proyectar flujo de efectivo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-002-registrar movimientos reales.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-003-crear factura desde estimaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-004-gestionar cobranza.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-005-aplicar pagos de clientes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-006-registrar factura de proveedor.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-007-programar pagos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-008-importar estado de cuenta.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-009-conciliar movimientos bancarios.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-010-dashboard financiero.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-011-an谩lisis de rentabilidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-001-flujo de efectivo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-002-cuentas por cobrar.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-003-cuentas por pagar.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-004-conciliaci贸n bancaria.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-005-control de gesti贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-001-modelo de datos de activos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-002-servicio de gesti贸n de activos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-003-motor de mantenimiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-004-control de herramientas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-005-c谩lculo de depreciaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-001-registrar activo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-002-asignar activo a proyecto.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-003-programar mantenimiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-004-registrar mantenimiento realizado.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-005-vale de herramienta.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-006-devoluci贸n de herramienta.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-007-dashboard de activos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-008-reporte de depreciaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-001-cat谩logo de activos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-002-asignaci贸n a proyectos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-003-mantenimiento preventivo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-004-control de herramientas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-005-depreciaci贸n contable.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-001-modelo de datos documental.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-002-servicio de almacenamiento s3.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-003-servicio de versionamiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-004-servicio de firma electr贸nica.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-005-workflow de aprobaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-001-subir documento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-002-buscar y descargar.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-003-crear nueva versi贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-004-firmar documento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-005-compartir documento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-006-aprobar documento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-007-dashboard documental.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-001-repositorio y almacenamiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-002-versionamiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-003-firma electr贸nica.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-004-control de acceso.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-005-workflow de aprobaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAE-016-reportes/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-001-rbac.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-002-estados-cuenta.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-003-multi-tenancy.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-001-autenticacion-basica-jwt.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-002-perfiles-usuario-construccion.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-003-dashboard-por-rol.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-004-infraestructura-base.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-005-sistema-sesiones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-006-api-restful-base.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-007-navegacion-routing.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-008-ui-ux-base.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-001-roles-construccion.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-003-multi-tenancy.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/RESUMEN-EPICA-MAI-002.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-backend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-database.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-frontend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-implementacion-catalogo-proyectos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-002-implementacion-estructura-jerarquica.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-003-implementacion-prototipos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-004-implementacion-equipo-calendario.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-001-catalogo-proyectos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-002-transiciones-estado.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-003-estructura-fraccionamiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-004-estructura-torre-vertical.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-005-gestion-prototipos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-006-asignacion-prototipos-lotes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-007-asignacion-equipo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-008-calendario-hitos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/historias-usuario/US-PROJ-009-alertas-fechas-criticas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/implementacion/ET-PROJ-001-rls-policies.sql delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/implementacion/ET-PROJ-002-rls-policies.sql delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/requerimientos-funcionales/RF-PROJ-001-catalogo-proyectos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/requerimientos-funcionales/RF-PROJ-002-estructura-jerarquica-obra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/requerimientos-funcionales/RF-PROJ-003-prototipos-vivienda.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/requerimientos-funcionales/RF-PROJ-004-asignacion-equipo-calendario.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/RESUMEN-EPICA-MAI-003.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/especificaciones/ET-COST-001-backend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/especificaciones/ET-COST-001-database.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/especificaciones/ET-COST-001-frontend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/especificaciones/ET-COST-001-implementacion-catalogo-conceptos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/especificaciones/ET-COST-002-implementacion-presupuestos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/especificaciones/ET-COST-003-implementacion-control-costos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/especificaciones/ET-COST-004-implementacion-analisis-rentabilidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/historias-usuario/US-COST-001-catalogo-conceptos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/historias-usuario/US-COST-002-precios-compuestos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/historias-usuario/US-COST-003-actualizacion-precios.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/historias-usuario/US-COST-004-presupuesto-obra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/historias-usuario/US-COST-005-presupuesto-prototipo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/historias-usuario/US-COST-006-dashboard-control-costos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/historias-usuario/US-COST-007-analisis-desviaciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/historias-usuario/US-COST-008-analisis-rentabilidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/implementacion/ET-COST-001-002-rls-policies.sql delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/requerimientos/RF-COST-001-catalogo-conceptos-precios.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/requerimientos/RF-COST-002-presupuestos-maestros.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/requerimientos/RF-COST-003-control-costos-reales.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-003-presupuestos-costos/requerimientos/RF-COST-004-analisis-rentabilidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/RESUMEN-EPICA-MAI-004.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/especificaciones/ET-COMP-001-backend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/especificaciones/ET-COMP-001-database.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/especificaciones/ET-COMP-001-frontend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/especificaciones/ET-PURCH-001-implementacion-proveedores.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/especificaciones/ET-PURCH-002-implementacion-requisiciones-ordenes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/especificaciones/ET-PURCH-003-implementacion-almacenes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/especificaciones/ET-PURCH-004-implementacion-kardex-alertas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/historias-usuario/US-PURCH-001-registro-proveedor.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/historias-usuario/US-PURCH-002-solicitud-cotizaciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/historias-usuario/US-PURCH-003-crear-requisicion-obra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/historias-usuario/US-PURCH-004-aprobar-generar-orden-compra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/historias-usuario/US-PURCH-005-recibir-material-almacen.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/historias-usuario/US-PURCH-006-control-almacenes-movimientos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/historias-usuario/US-PURCH-007-kardex-analisis-consumo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/historias-usuario/US-PURCH-008-dashboard-inventarios-alertas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/implementacion/ET-PURCH-rls-policies.sql delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/requerimientos/RF-PURCH-001-catalogo-proveedores.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/requerimientos/RF-PURCH-002-requisiciones-ordenes-compra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/requerimientos/RF-PURCH-003-almacenes-inventarios.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-004-compras-inventarios/requerimientos/RF-PURCH-004-kardex-alertas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/RESUMEN-EPICA-MAI-005.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/especificaciones/ET-PROG-001-implementacion-programacion-curva-s.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/especificaciones/ET-PROG-002-implementacion-captura-avances.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/especificaciones/ET-PROG-003-implementacion-evidencias-checklists.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/especificaciones/ET-PROG-004-implementacion-dashboard-reportes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/historias-usuario/US-PROG-001-crear-programa-obra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/historias-usuario/US-PROG-002-seguimiento-curva-s.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/historias-usuario/US-PROG-003-capturar-avances-obra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/historias-usuario/US-PROG-004-aprobar-avances.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/historias-usuario/US-PROG-005-evidencias-fotograficas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/historias-usuario/US-PROG-006-checklists-calidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/historias-usuario/US-PROG-007-dashboard-ejecutivo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/historias-usuario/US-PROG-008-reportes-oficiales.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/implementacion/ET-WORK-rls-policies.sql delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/requerimientos/RF-PROG-001-programacion-curva-s.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/requerimientos/RF-PROG-002-captura-avances-fisicos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/requerimientos/RF-PROG-003-evidencias-checklists.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra-avances/requerimientos/RF-PROG-004-dashboard-reportes-avances.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra/especificaciones/ET-OBRA-001-backend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra/especificaciones/ET-OBRA-001-database.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra/especificaciones/ET-OBRA-001-frontend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-calidad/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-calidad/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-calidad/especificaciones/ET-CAL-001-backend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-calidad/especificaciones/ET-CAL-001-database.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-calidad/especificaciones/ET-CAL-001-frontend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-calidad/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/RESUMEN-EPICA-MAI-006.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/especificaciones/ET-BI-001-implementacion-reportes-ejecutivos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/especificaciones/ET-BI-002-implementacion-dashboards-interactivos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/especificaciones/ET-BI-003-implementacion-analisis-predictivo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/especificaciones/ET-BI-004-implementacion-exportacion-distribucion.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/historias-usuario/US-BI-001-dashboard-corporativo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/historias-usuario/US-BI-002-analisis-margenes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/historias-usuario/US-BI-003-dashboards-personalizables.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/historias-usuario/US-BI-004-drill-down-filtros.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/historias-usuario/US-BI-005-predicciones-ml.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/historias-usuario/US-BI-006-simulacion-escenarios.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/historias-usuario/US-BI-007-reportes-programados.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/historias-usuario/US-BI-008-integracion-powerbi-tableau.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/requerimientos/RF-BI-001-reportes-ejecutivos-consolidados.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/requerimientos/RF-BI-002-dashboards-interactivos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/requerimientos/RF-BI-003-analisis-predictivo-forecasting.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-006-reportes-analytics/requerimientos/RF-BI-004-exportacion-distribucion.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/especificaciones/ET-HR-001-empleados-cuadrillas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/especificaciones/ET-HR-002-asistencia-biometrica.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/especificaciones/ET-HR-003-costeo-mano-obra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/especificaciones/ET-HR-004-integracion-imss.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/especificaciones/ET-HR-005-integracion-infonavit.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-001-catalogo-empleados-cuadrillas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-002-app-movil-asistencia-biometrica.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-003-costeo-mano-obra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-004-integracion-nomina-externa.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-005-exportacion-imss-infonavit.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/historias-usuario/US-HR-006-reportes-asistencia.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/requerimientos/RF-HR-001-empleados-cuadrillas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/requerimientos/RF-HR-002-asistencia-biometrica.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/requerimientos/RF-HR-003-costeo-mano-obra.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/requerimientos/RF-HR-004-integracion-imss.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/requerimientos/RF-HR-005-integracion-infonavit.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-rrhh-asistencias/requerimientos/RF-RRHH-001-catalogo-personal-cuadrillas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-seguridad-industrial/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-seguridad-industrial/especificaciones/ET-SEG-001-backend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-seguridad-industrial/especificaciones/ET-SEG-001-database.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-seguridad-industrial/especificaciones/ET-SEG-001-frontend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-007-seguridad-industrial/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/especificaciones/ET-EST-001-modelo-datos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/especificaciones/ET-EST-002-calculo-montos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/especificaciones/ET-EST-003-anticipos-retenciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/especificaciones/ET-EST-004-generacion-reportes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/especificaciones/ET-EST-005-workflow-estados.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-001-crear-estimacion-cliente.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-002-crear-estimacion-subcontratista.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-003-aplicar-anticipos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-004-aplicar-retenciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-005-generar-pdf.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-006-exportar-excel.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-007-workflow-autorizacion.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-008-registrar-pago.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/historias-usuario/US-EST-009-dashboard-estimaciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/requerimientos/RF-EST-001-estimaciones-cliente.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/requerimientos/RF-EST-002-estimaciones-subcontratistas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/requerimientos/RF-EST-003-anticipos-retenciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/requerimientos/RF-EST-004-reportes-documentos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-008-estimaciones-facturacion/requerimientos/RF-EST-005-workflow-autorizacion.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/especificaciones/ET-QUA-001-checklists-dinamicos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/especificaciones/ET-QUA-002-no-conformidades.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/especificaciones/ET-QUA-003-motor-tickets.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/especificaciones/ET-QUA-004-sla-alertas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/especificaciones/ET-QUA-005-historial-vivienda.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/historias-usuario/US-QUA-001-ejecutar-checklist-de-calidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/historias-usuario/US-QUA-002-registrar-no-conformidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/historias-usuario/US-QUA-003-crear-ticket-desde-app-m贸vil.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/historias-usuario/US-QUA-004-atender-ticket-de-garant铆a.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/historias-usuario/US-QUA-005-consultar-historial-de-viviend.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/historias-usuario/US-QUA-006-dashboard-de-calidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/historias-usuario/US-QUA-007-generar-reporte-de-incidencias.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/historias-usuario/US-QUA-008-alertas-de-sla.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/requerimientos/RF-QUA-001-control-calidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/requerimientos/RF-QUA-002-no-conformidades.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/requerimientos/RF-QUA-003-tickets-postventa.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/requerimientos/RF-QUA-004-garantias-sla.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-009-calidad-postventa/requerimientos/RF-QUA-005-historial-vivienda.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/especificaciones/ET-CRM-001-modelo-de-datos-de-clientes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/especificaciones/ET-CRM-002-sistema-de-estados-de-vivienda.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/especificaciones/ET-CRM-003-gesti贸n-de-documentos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/especificaciones/ET-CRM-004-integracion-whatsapp.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/especificaciones/ET-CRM-005-analytics-de-ventas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/historias-usuario/US-CRM-001-registrar-prospecto.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/historias-usuario/US-CRM-002-asignar-vivienda.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/historias-usuario/US-CRM-003-seguimiento-expediente.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/historias-usuario/US-CRM-004-enviar-notificaciones-wha.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/historias-usuario/US-CRM-005-programar-citas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/historias-usuario/US-CRM-006-dashboard-ventas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/historias-usuario/US-CRM-007-reporte-de-ventas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/requerimientos/RF-CRM-001-gesti贸n-de-prospectos-y-derec.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/requerimientos/RF-CRM-002-control-de-estatus-de-vivienda.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/requerimientos/RF-CRM-003-seguimiento-de-expediente-de-c.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/requerimientos/RF-CRM-004-comunicaci贸n-multicanal.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-010-crm-derechohabientes/requerimientos/RF-CRM-005-dashboard-de-comercializaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/especificaciones/ET-INF-001-modelo-de-programas-y-requisit.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/especificaciones/ET-INF-002-sistema-de-checklists-din谩mic.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/especificaciones/ET-INF-003-repositorio-de-evidencias.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/especificaciones/ET-INF-004-workflow-de-auditor铆as.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/especificaciones/ET-INF-005-generaci贸n-de-reportes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/historias-usuario/US-INF-001-registrar-proyecto-bajo-p.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/historias-usuario/US-INF-002-configurar-checklist-requ.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/historias-usuario/US-INF-003-cargar-evidencias-por-req.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/historias-usuario/US-INF-004-registrar-visita-de-verif.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/historias-usuario/US-INF-005-gestionar-observaciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/historias-usuario/US-INF-006-generar-reporte-de-cumpli.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/historias-usuario/US-INF-007-dashboard-de-cumplimiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/historias-usuario/US-INF-008-alertas-de-requisitos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/requerimientos/RF-INF-001-registro-de-proyecto-bajo-prog.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/requerimientos/RF-INF-002-checklists-de-cumplimiento-nor.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/requerimientos/RF-INF-003-gesti贸n-de-evidencias-y-docum.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/requerimientos/RF-INF-004-seguimiento-de-auditor铆as.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-011-infonavit-cumplimiento/requerimientos/RF-INF-005-reportes-de-cumplimiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/especificaciones/ET-CON-001-modelo de datos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/especificaciones/ET-CON-002-motor de plantillas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/especificaciones/ET-CON-003-servicio de contratos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/especificaciones/ET-CON-004-workflow de aprobaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/especificaciones/ET-CON-005-generaci贸n de documentos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-001-crear contrato cliente.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-002-crear contrato subcontratista.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-003-configurar plantillas de contrato.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-004-generar documento pdf.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-005-aprobar contrato.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-006-crear addenda.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-007-evaluar subcontratista.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-008-dashboard de contratos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/historias-usuario/US-CON-009-alertas de vencimiento.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/requerimientos/RF-CON-001-contratos con clientes.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/requerimientos/RF-CON-002-contratos con subcontratistas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/requerimientos/RF-CON-003-plantillas din谩micas y generaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/requerimientos/RF-CON-004-addendas y rescisiones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-012-contratos-subcontratos/requerimientos/RF-CON-005-workflow de aprobaci贸n multinivel.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/RESUMEN-EPICA-MAI-013.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/especificaciones/ET-ADM-001-rbac-multi-tenancy.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/especificaciones/ET-ADM-002-centros-costo-jerarquicos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/especificaciones/ET-ADM-003-audit-logging.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/especificaciones/ET-ADM-004-backups-dr.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/especificaciones/ET-ADM-005-seguridad-datos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/historias-usuario/US-ADM-001-invitar-registrar-usuarios.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/historias-usuario/US-ADM-002-cambiar-constructora.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/historias-usuario/US-ADM-003-gestionar-permisos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/historias-usuario/US-ADM-004-centros-costo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/historias-usuario/US-ADM-005-bitacora-auditoria.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/historias-usuario/US-ADM-006-gestionar-backups.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/historias-usuario/US-ADM-007-politicas-seguridad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/historias-usuario/US-ADM-008-dashboard-admin.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/requerimientos/RF-ADM-001-usuarios-roles.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/requerimientos/RF-ADM-002-permisos-granulares.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/requerimientos/RF-ADM-003-centros-costo.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/requerimientos/RF-ADM-004-auditoria.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-013-administracion-seguridad/requerimientos/RF-ADM-005-backups.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/_MAP.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/especificaciones/ET-PRE-001-modelo de datos.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/especificaciones/ET-PRE-002-servicio de viabilidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/especificaciones/ET-PRE-003-motor de licitaciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/especificaciones/ET-PRE-004-evaluaci贸n de propuestas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/especificaciones/ET-PRE-005-gesti贸n de proveedores.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-001-crear estudio de viabilidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-002-generar presupuesto preliminar.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-003-crear licitaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-004-publicar licitaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-005-registrar propuesta.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-006-evaluar propuestas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-007-adjudicar licitaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-008-gestionar proveedores.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/historias-usuario/US-PRE-009-dashboard de licitaciones.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/requerimientos/RF-PRE-001-an谩lisis de viabilidad.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/requerimientos/RF-PRE-002-presupuesto preliminar.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/requerimientos/RF-PRE-003-proceso de licitaci贸n.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/requerimientos/RF-PRE-004-evaluaci贸n de propuestas.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MAI-018-preconstruccion-licitaciones/requerimientos/RF-PRE-005-gesti贸n de proveedores.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/MEJORAS-SAAS-APLICADAS.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/README.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/REPORTE-MEJORAS-COMPLETO.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/RESUMEN-DOCUMENTACION-GENERADA.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/RESUMEN-EJECUTIVO.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/RESUMEN-SESION-2025-11-17.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/RESUMEN-SESION-COMPLETA-2025-11-17.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/ROADMAP-DETALLADO.md delete mode 100644 projects/erp-construccion/docs/02-definicion-modulos/_MAP.md delete mode 100644 projects/erp-construccion/docs/03-requerimientos/README.md delete mode 100644 projects/erp-construccion/docs/04-modelado/README.md delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/README.md delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/DDL-SPEC-assets.md delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/DDL-SPEC-compliance.md delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/DDL-SPEC-construction.md delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/DDL-SPEC-documents.md delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/DDL-SPEC-finance.md delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/construction-schema-ddl.sql delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/estimates-schema-ddl.sql delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/hr-ext-schema-ddl.sql delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/infonavit-schema-ddl.sql delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/inventory-ext-schema-ddl.sql delete mode 100644 projects/erp-construccion/docs/04-modelado/database-design/schemas/purchase-ext-schema-ddl.sql delete mode 100644 projects/erp-construccion/docs/04-modelado/domain-models/ASSETS-CONTEXT.md delete mode 100644 projects/erp-construccion/docs/04-modelado/domain-models/COMPLIANCE-CONTEXT.md delete mode 100644 projects/erp-construccion/docs/04-modelado/domain-models/DOCUMENTS-CONTEXT.md delete mode 100644 projects/erp-construccion/docs/04-modelado/domain-models/FINANCE-CONTEXT.md delete mode 100644 projects/erp-construccion/docs/04-modelado/domain-models/PROJECT-CONTEXT.md delete mode 100644 projects/erp-construccion/docs/04-modelado/domain-models/README.md delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/INVENTARIO-OBJETOS-BD.yml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/MATRIZ-TRAZABILIDAD-COMPLETA.md delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/README.md delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-001.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-002.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-003.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-004.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-005.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-006.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-007.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-008.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-009.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-010.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-011.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-012.yaml delete mode 100644 projects/erp-construccion/docs/04-modelado/trazabilidad/modulos/TRACEABILITY-MAI-013.yaml delete mode 100644 projects/erp-construccion/docs/05-backend-specs/README.md delete mode 100644 projects/erp-construccion/docs/05-backend-specs/modules/SPEC-assets.md delete mode 100644 projects/erp-construccion/docs/05-backend-specs/modules/SPEC-compliance.md delete mode 100644 projects/erp-construccion/docs/05-backend-specs/modules/SPEC-construction.md delete mode 100644 projects/erp-construccion/docs/05-backend-specs/modules/SPEC-documents.md delete mode 100644 projects/erp-construccion/docs/05-backend-specs/modules/SPEC-finance.md delete mode 100644 projects/erp-construccion/docs/05-user-stories/README.md delete mode 100644 projects/erp-construccion/docs/06-frontend-specs/README.md delete mode 100644 projects/erp-construccion/docs/06-frontend-specs/components/COMP-construction.md delete mode 100644 projects/erp-construccion/docs/06-frontend-specs/components/COMP-shared.md delete mode 100644 projects/erp-construccion/docs/06-frontend-specs/pages/PAGES-construction.md delete mode 100644 projects/erp-construccion/docs/06-frontend-specs/stores/STORES-spec.md delete mode 100644 projects/erp-construccion/docs/06-test-plans/README.md delete mode 100644 projects/erp-construccion/docs/07-devops/README.md delete mode 100644 projects/erp-construccion/docs/07-devops/docker/docker-compose.yml delete mode 100644 projects/erp-construccion/docs/08-epicas/EPIC-MAE-014-finanzas.md delete mode 100644 projects/erp-construccion/docs/08-epicas/EPIC-MAI-001-fundamentos.md delete mode 100644 projects/erp-construccion/docs/08-epicas/EPIC-MAI-002-proyectos.md delete mode 100644 projects/erp-construccion/docs/08-epicas/EPIC-MAI-003-presupuestos.md delete mode 100644 projects/erp-construccion/docs/08-epicas/EPIC-MAI-005-control-obra.md delete mode 100644 projects/erp-construccion/docs/08-epicas/EPIC-MAI-011-infonavit.md delete mode 100644 projects/erp-construccion/docs/08-epicas/EPIC-MAI-019-mobile-apps.md delete mode 100644 projects/erp-construccion/docs/08-epicas/README.md delete mode 100644 projects/erp-construccion/docs/90-transversal/REPORTE-AUDITORIA-DOCUMENTACION.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-001-stack-tecnologico.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-002-arquitectura-modular.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-003-multi-tenancy.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-004-sistema-constantes-ssot.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-005-path-aliases.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-006-rbac-sistema-permisos.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-007-database-design.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-008-api-design.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-009-frontend-architecture.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-010-testing-strategy.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-011-database-clean-load-strategy.md delete mode 100644 projects/erp-construccion/docs/97-adr/ADR-012-complete-traceability-policy.md delete mode 100644 projects/erp-construccion/docs/97-adr/README.md delete mode 100644 projects/erp-construccion/docs/ANALISIS-IMPLEMENTACION-ARQUITECTURA.md delete mode 100644 projects/erp-construccion/docs/ARCHITECTURE.md delete mode 100644 projects/erp-construccion/docs/ESTRUCTURA-COMPLETA.md delete mode 100644 projects/erp-construccion/docs/GUIA-USO-REFERENCIAS-ODOO.md delete mode 100644 projects/erp-construccion/docs/PLAN-RETROALIMENTACION-DESDE-ERP-GENERICO.md delete mode 100644 projects/erp-construccion/docs/README.md delete mode 100644 projects/erp-construccion/docs/REPORTE-FINAL-MEJORAS-SAAS.md delete mode 100644 projects/erp-construccion/docs/RLS-POLICIES-TODOS-LOS-MODULOS.md delete mode 100644 projects/erp-construccion/docs/_MAP.md delete mode 100644 projects/erp-construccion/docs/api/openapi.yaml delete mode 100644 projects/erp-construccion/docs/backend/API-REFERENCE.md delete mode 100644 projects/erp-construccion/docs/backend/MODULES.md delete mode 100644 projects/erp-construccion/docs/orchestration/ANALISIS-MEJORAS-SISTEMA-ORQUESTACION.md delete mode 100644 projects/erp-construccion/docs/orchestration/REPORTE-FINAL-IMPLEMENTACION-FASE-2.md delete mode 100644 projects/erp-construccion/docs/orchestration/REPORTE-IMPLEMENTACION-SISTEMA-ORQUESTACION.md delete mode 100644 projects/erp-construccion/docs/orchestration/REPORTE-MEJORAS-SISTEMA-SUBAGENTES.md delete mode 100644 projects/erp-construccion/frontend/mobile/App.tsx delete mode 100644 projects/erp-construccion/frontend/mobile/README.md delete mode 100644 projects/erp-construccion/frontend/mobile/app.json delete mode 100644 projects/erp-construccion/frontend/mobile/package-lock.json delete mode 100644 projects/erp-construccion/frontend/mobile/package.json delete mode 100644 projects/erp-construccion/frontend/mobile/tsconfig.json delete mode 100644 projects/erp-construccion/frontend/web/Dockerfile delete mode 100644 projects/erp-construccion/frontend/web/README.md delete mode 100644 projects/erp-construccion/frontend/web/index.html delete mode 100644 projects/erp-construccion/frontend/web/nginx.conf delete mode 100644 projects/erp-construccion/frontend/web/package-lock.json delete mode 100644 projects/erp-construccion/frontend/web/package.json delete mode 100644 projects/erp-construccion/frontend/web/src/App.tsx delete mode 100644 projects/erp-construccion/frontend/web/src/index.css delete mode 100644 projects/erp-construccion/frontend/web/src/main.tsx delete mode 100644 projects/erp-construccion/frontend/web/src/vite-env.d.ts delete mode 100644 projects/erp-construccion/frontend/web/tsconfig.json delete mode 100644 projects/erp-construccion/frontend/web/tsconfig.node.json delete mode 100644 projects/erp-construccion/frontend/web/vite.config.ts delete mode 100644 projects/erp-construccion/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-construccion/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/erp-construccion/orchestration/00-guidelines/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-construccion/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/erp-construccion/orchestration/00-guidelines/HERENCIA-SPECS-CORE.md delete mode 100644 projects/erp-construccion/orchestration/00-guidelines/HERENCIA-SPECS-ERP-CORE.md delete mode 100644 projects/erp-construccion/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/erp-construccion/orchestration/NAMING-CONVENTIONS.md delete mode 100644 projects/erp-construccion/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/erp-construccion/orchestration/README.md delete mode 100644 projects/erp-construccion/orchestration/analisis/INFORME-ANALISIS-CONSTRUCCION-2025-12-06.md delete mode 100644 projects/erp-construccion/orchestration/directivas/DIRECTIVA-CONTROL-OBRA.md delete mode 100644 projects/erp-construccion/orchestration/directivas/DIRECTIVA-ESTIMACIONES.md delete mode 100644 projects/erp-construccion/orchestration/directivas/DIRECTIVA-INTEGRACION-INFONAVIT.md delete mode 100644 projects/erp-construccion/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/erp-construccion/orchestration/estados/ESTADO-AGENTES.json delete mode 100644 projects/erp-construccion/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/erp-construccion/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/erp-construccion/orchestration/inventarios/DEPENDENCY_GRAPH.yml delete mode 100644 projects/erp-construccion/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/erp-construccion/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/erp-construccion/orchestration/inventarios/README.md delete mode 100644 projects/erp-construccion/orchestration/inventarios/TRACEABILITY_MATRIX.yml delete mode 100644 projects/erp-construccion/orchestration/prompts/PROMPT-CON-BACKEND-AGENT.md delete mode 100644 projects/erp-construccion/orchestration/prompts/PROMPT-CONSTRUCCION-BACKEND-AGENT.md delete mode 100644 projects/erp-construccion/orchestration/prompts/PROMPT-CONSTRUCCION-DATABASE-AGENT.md delete mode 100644 projects/erp-construccion/orchestration/prompts/PROMPT-CONSTRUCCION-FRONTEND-AGENT.md delete mode 100644 projects/erp-construccion/orchestration/referencias/DEPENDENCIAS-ERP-CORE.yml delete mode 100644 projects/erp-construccion/orchestration/referencias/DEPENDENCIAS-SHARED.yml delete mode 100644 projects/erp-construccion/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/erp-construccion/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/erp-construccion/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/erp-core/.env.example delete mode 100644 projects/erp-core/.gitignore delete mode 100644 projects/erp-core/INVENTARIO.yml delete mode 100644 projects/erp-core/PROJECT-STATUS.md delete mode 100644 projects/erp-core/README.md delete mode 100644 projects/erp-core/backend/.env.example delete mode 100644 projects/erp-core/backend/.gitignore delete mode 100644 projects/erp-core/backend/Dockerfile delete mode 100644 projects/erp-core/backend/TYPEORM_DEPENDENCIES.md delete mode 100644 projects/erp-core/backend/TYPEORM_INTEGRATION_SUMMARY.md delete mode 100644 projects/erp-core/backend/TYPEORM_USAGE_EXAMPLES.md delete mode 100644 projects/erp-core/backend/package-lock.json delete mode 100644 projects/erp-core/backend/package.json delete mode 100644 projects/erp-core/backend/service.descriptor.yml delete mode 100644 projects/erp-core/backend/src/app.ts delete mode 100644 projects/erp-core/backend/src/config/database.ts delete mode 100644 projects/erp-core/backend/src/config/index.ts delete mode 100644 projects/erp-core/backend/src/config/redis.ts delete mode 100644 projects/erp-core/backend/src/config/swagger.config.ts delete mode 100644 projects/erp-core/backend/src/config/typeorm.ts delete mode 100644 projects/erp-core/backend/src/docs/openapi.yaml delete mode 100644 projects/erp-core/backend/src/index.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/apiKeys.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/apiKeys.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/apiKeys.service.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/auth.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/auth.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/auth.service.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/api-key.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/company.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/group.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/index.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/mfa-audit-log.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/oauth-provider.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/oauth-state.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/oauth-user-link.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/password-reset.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/permission.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/role.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/session.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/tenant.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/trusted-device.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/user.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/entities/verification-code.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/index.ts delete mode 100644 projects/erp-core/backend/src/modules/auth/services/token.service.ts delete mode 100644 projects/erp-core/backend/src/modules/companies/companies.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/companies/companies.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/companies/companies.service.ts delete mode 100644 projects/erp-core/backend/src/modules/companies/index.ts delete mode 100644 projects/erp-core/backend/src/modules/core/core.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/core/core.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/core/countries.service.ts delete mode 100644 projects/erp-core/backend/src/modules/core/currencies.service.ts delete mode 100644 projects/erp-core/backend/src/modules/core/entities/country.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/core/entities/currency.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/core/entities/index.ts delete mode 100644 projects/erp-core/backend/src/modules/core/entities/product-category.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/core/entities/sequence.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/core/entities/uom-category.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/core/entities/uom.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/core/index.ts delete mode 100644 projects/erp-core/backend/src/modules/core/product-categories.service.ts delete mode 100644 projects/erp-core/backend/src/modules/core/sequences.service.ts delete mode 100644 projects/erp-core/backend/src/modules/core/uom.service.ts delete mode 100644 projects/erp-core/backend/src/modules/crm/crm.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/crm/crm.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/crm/index.ts delete mode 100644 projects/erp-core/backend/src/modules/crm/leads.service.ts delete mode 100644 projects/erp-core/backend/src/modules/crm/opportunities.service.ts delete mode 100644 projects/erp-core/backend/src/modules/crm/stages.service.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/MIGRATION_GUIDE.md delete mode 100644 projects/erp-core/backend/src/modules/financial/accounts.service.old.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/accounts.service.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/account-type.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/account.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/fiscal-period.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/fiscal-year.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/index.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/invoice-line.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/invoice.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/journal-entry-line.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/journal-entry.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/journal.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/payment.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/entities/tax.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/financial.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/financial.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/fiscalPeriods.service.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/index.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/invoices.service.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/journal-entries.service.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/journals.service.old.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/journals.service.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/payments.service.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/taxes.service.old.ts delete mode 100644 projects/erp-core/backend/src/modules/financial/taxes.service.ts delete mode 100644 projects/erp-core/backend/src/modules/hr/contracts.service.ts delete mode 100644 projects/erp-core/backend/src/modules/hr/departments.service.ts delete mode 100644 projects/erp-core/backend/src/modules/hr/employees.service.ts delete mode 100644 projects/erp-core/backend/src/modules/hr/hr.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/hr/hr.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/hr/index.ts delete mode 100644 projects/erp-core/backend/src/modules/hr/leaves.service.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/MIGRATION_STATUS.md delete mode 100644 projects/erp-core/backend/src/modules/inventory/adjustments.service.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/index.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/inventory-adjustment-line.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/inventory-adjustment.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/location.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/lot.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/picking.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/product.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/stock-move.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/stock-quant.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/stock-valuation-layer.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/entities/warehouse.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/index.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/inventory.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/inventory.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/locations.service.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/lots.service.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/pickings.service.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/products.service.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/valuation.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/valuation.service.ts delete mode 100644 projects/erp-core/backend/src/modules/inventory/warehouses.service.ts delete mode 100644 projects/erp-core/backend/src/modules/partners/entities/index.ts delete mode 100644 projects/erp-core/backend/src/modules/partners/entities/partner.entity.ts delete mode 100644 projects/erp-core/backend/src/modules/partners/index.ts delete mode 100644 projects/erp-core/backend/src/modules/partners/partners.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/partners/partners.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/partners/partners.service.ts delete mode 100644 projects/erp-core/backend/src/modules/partners/ranking.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/partners/ranking.service.ts delete mode 100644 projects/erp-core/backend/src/modules/projects/index.ts delete mode 100644 projects/erp-core/backend/src/modules/projects/projects.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/projects/projects.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/projects/projects.service.ts delete mode 100644 projects/erp-core/backend/src/modules/projects/tasks.service.ts delete mode 100644 projects/erp-core/backend/src/modules/projects/timesheets.service.ts delete mode 100644 projects/erp-core/backend/src/modules/purchases/index.ts delete mode 100644 projects/erp-core/backend/src/modules/purchases/purchases.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/purchases/purchases.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/purchases/purchases.service.ts delete mode 100644 projects/erp-core/backend/src/modules/purchases/rfqs.service.ts delete mode 100644 projects/erp-core/backend/src/modules/reports/index.ts delete mode 100644 projects/erp-core/backend/src/modules/reports/reports.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/reports/reports.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/reports/reports.service.ts delete mode 100644 projects/erp-core/backend/src/modules/roles/index.ts delete mode 100644 projects/erp-core/backend/src/modules/roles/permissions.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/roles/permissions.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/roles/permissions.service.ts delete mode 100644 projects/erp-core/backend/src/modules/roles/roles.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/roles/roles.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/roles/roles.service.ts delete mode 100644 projects/erp-core/backend/src/modules/sales/customer-groups.service.ts delete mode 100644 projects/erp-core/backend/src/modules/sales/index.ts delete mode 100644 projects/erp-core/backend/src/modules/sales/orders.service.ts delete mode 100644 projects/erp-core/backend/src/modules/sales/pricelists.service.ts delete mode 100644 projects/erp-core/backend/src/modules/sales/quotations.service.ts delete mode 100644 projects/erp-core/backend/src/modules/sales/sales-teams.service.ts delete mode 100644 projects/erp-core/backend/src/modules/sales/sales.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/sales/sales.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/system/activities.service.ts delete mode 100644 projects/erp-core/backend/src/modules/system/index.ts delete mode 100644 projects/erp-core/backend/src/modules/system/messages.service.ts delete mode 100644 projects/erp-core/backend/src/modules/system/notifications.service.ts delete mode 100644 projects/erp-core/backend/src/modules/system/system.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/system/system.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/tenants/index.ts delete mode 100644 projects/erp-core/backend/src/modules/tenants/tenants.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/tenants/tenants.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/tenants/tenants.service.ts delete mode 100644 projects/erp-core/backend/src/modules/users/index.ts delete mode 100644 projects/erp-core/backend/src/modules/users/users.controller.ts delete mode 100644 projects/erp-core/backend/src/modules/users/users.routes.ts delete mode 100644 projects/erp-core/backend/src/modules/users/users.service.ts delete mode 100644 projects/erp-core/backend/src/shared/errors/index.ts delete mode 100644 projects/erp-core/backend/src/shared/middleware/apiKeyAuth.middleware.ts delete mode 100644 projects/erp-core/backend/src/shared/middleware/auth.middleware.ts delete mode 100644 projects/erp-core/backend/src/shared/middleware/fieldPermissions.middleware.ts delete mode 100644 projects/erp-core/backend/src/shared/services/base.service.ts delete mode 100644 projects/erp-core/backend/src/shared/services/index.ts delete mode 100644 projects/erp-core/backend/src/shared/types/index.ts delete mode 100644 projects/erp-core/backend/src/shared/utils/logger.ts delete mode 100644 projects/erp-core/backend/tsconfig.json delete mode 100644 projects/erp-core/database/README.md delete mode 100644 projects/erp-core/database/ddl/00-prerequisites.sql delete mode 100644 projects/erp-core/database/ddl/01-auth-extensions.sql delete mode 100644 projects/erp-core/database/ddl/01-auth.sql delete mode 100644 projects/erp-core/database/ddl/02-core.sql delete mode 100644 projects/erp-core/database/ddl/03-analytics.sql delete mode 100644 projects/erp-core/database/ddl/04-financial.sql delete mode 100644 projects/erp-core/database/ddl/05-inventory-extensions.sql delete mode 100644 projects/erp-core/database/ddl/05-inventory.sql delete mode 100644 projects/erp-core/database/ddl/06-purchase.sql delete mode 100644 projects/erp-core/database/ddl/07-sales.sql delete mode 100644 projects/erp-core/database/ddl/08-projects.sql delete mode 100644 projects/erp-core/database/ddl/09-system.sql delete mode 100644 projects/erp-core/database/ddl/10-billing.sql delete mode 100644 projects/erp-core/database/ddl/11-crm.sql delete mode 100644 projects/erp-core/database/ddl/12-hr.sql delete mode 100644 projects/erp-core/database/ddl/schemas/core_shared/00-schema.sql delete mode 100644 projects/erp-core/database/docker-compose.yml delete mode 100644 projects/erp-core/database/migrations/20251212_001_fiscal_period_validation.sql delete mode 100644 projects/erp-core/database/migrations/20251212_002_partner_rankings.sql delete mode 100644 projects/erp-core/database/migrations/20251212_003_financial_reports.sql delete mode 100755 projects/erp-core/database/scripts/create-database.sh delete mode 100755 projects/erp-core/database/scripts/drop-database.sh delete mode 100755 projects/erp-core/database/scripts/load-seeds.sh delete mode 100755 projects/erp-core/database/scripts/reset-database.sh delete mode 100644 projects/erp-core/database/seeds/dev/00-catalogs.sql delete mode 100644 projects/erp-core/database/seeds/dev/01-tenants.sql delete mode 100644 projects/erp-core/database/seeds/dev/02-companies.sql delete mode 100644 projects/erp-core/database/seeds/dev/03-roles.sql delete mode 100644 projects/erp-core/database/seeds/dev/04-users.sql delete mode 100644 projects/erp-core/database/seeds/dev/05-sample-data.sql delete mode 100644 projects/erp-core/docs/00-vision-general/VISION-ERP-CORE.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/MAPA-COMPONENTES-GENERICOS.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/RESUMEN-FASE-0.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/construccion/COMPONENTES-ESPECIFICOS.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/construccion/COMPONENTES-GENERICOS.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/construccion/GAP-ANALYSIS.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/construccion/MEJORAS-ARQUITECTONICAS.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/construccion/RETROALIMENTACION.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/gamilit/ADOPTAR-ADAPTAR-EVITAR.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/gamilit/README.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/gamilit/backend-patterns.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/gamilit/database-architecture.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/gamilit/devops-automation.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/gamilit/frontend-patterns.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/gamilit/ssot-system.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/MAPEO-ODOO-TO-MGN.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/README.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/VALIDACION-MGN-VS-ODOO.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-account-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-analytic-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-auth-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-base-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-crm-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-hr-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-mail-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-portal-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-project-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-purchase-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-sale-analysis.md delete mode 100644 projects/erp-core/docs/01-analisis-referencias/odoo/odoo-stock-analysis.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/README.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/_MAP.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/especificaciones/ET-AUTH-database.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/especificaciones/ET-auth-backend.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/especificaciones/auth-domain.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/historias-usuario/BACKLOG-MGN001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/requerimientos/INDICE-RF-AUTH.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/requerimientos/RF-AUTH-001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/requerimientos/RF-AUTH-002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/requerimientos/RF-AUTH-003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/requerimientos/RF-AUTH-004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-001-auth/requerimientos/RF-AUTH-005.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/README.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/_MAP.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/especificaciones/ET-USER-database.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/especificaciones/ET-users-backend.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/historias-usuario/BACKLOG-MGN002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-005.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/requerimientos/INDICE-RF-USER.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/requerimientos/RF-USER-001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/requerimientos/RF-USER-002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/requerimientos/RF-USER-003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/requerimientos/RF-USER-004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-002-users/requerimientos/RF-USER-005.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/README.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/_MAP.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/especificaciones/ET-RBAC-database.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/especificaciones/ET-rbac-backend.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/historias-usuario/BACKLOG-MGN003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/requerimientos/INDICE-RF-ROLE.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/requerimientos/RF-ROLE-001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/requerimientos/RF-ROLE-002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/requerimientos/RF-ROLE-003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-003-roles/requerimientos/RF-ROLE-004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/README.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/_MAP.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/especificaciones/ET-TENANT-database.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/especificaciones/ET-tenants-backend.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/BACKLOG-MGN004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/requerimientos/INDICE-RF-TENANT.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/requerimientos/RF-TENANT-001.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/requerimientos/RF-TENANT-002.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/requerimientos/RF-TENANT-003.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/MGN-004-tenants/requerimientos/RF-TENANT-004.md delete mode 100644 projects/erp-core/docs/01-fase-foundation/README.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/ALCANCE-POR-MODULO.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/DEPENDENCIAS-MODULOS.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/INDICE-MODULOS.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/LISTA-MODULOS-ERP-GENERICO.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/RETROALIMENTACION-ERP-CONSTRUCCION.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-001.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-002.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-003.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-004.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-005.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-006.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-007.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-008.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-009.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-010.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-011.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-012.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-013.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-014.md delete mode 100644 projects/erp-core/docs/02-definicion-modulos/gaps/GAP-ANALYSIS-MGN-015.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/README.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/_MAP.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/especificaciones/ET-CATALOG-backend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/especificaciones/ET-CATALOG-database.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/especificaciones/ET-CATALOG-frontend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/especificaciones/INDICE-ET-CATALOGS.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/INDICE-US-CATALOGS.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-005.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/requerimientos/INDICE-RF-CATALOG.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/requerimientos/RF-CATALOG-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/requerimientos/RF-CATALOG-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/requerimientos/RF-CATALOG-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/requerimientos/RF-CATALOG-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-005-catalogs/requerimientos/RF-CATALOG-005.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/README.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/_MAP.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/especificaciones/ET-SETTINGS-backend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/especificaciones/ET-SETTINGS-database.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/especificaciones/ET-SETTINGS-frontend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/especificaciones/INDICE-ET-SETTINGS.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/historias-usuario/INDICE-US-SETTINGS.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/requerimientos/INDICE-RF-SETTINGS.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/requerimientos/RF-SETTINGS-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/requerimientos/RF-SETTINGS-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/requerimientos/RF-SETTINGS-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-006-settings/requerimientos/RF-SETTINGS-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/README.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/_MAP.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/especificaciones/ET-AUDIT-backend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/especificaciones/ET-AUDIT-database.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/especificaciones/ET-AUDIT-frontend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/especificaciones/INDICE-ET-AUDIT.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/historias-usuario/INDICE-US-AUDIT.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/requerimientos/INDICE-RF-AUDIT.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/requerimientos/RF-AUDIT-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/requerimientos/RF-AUDIT-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/requerimientos/RF-AUDIT-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-007-audit/requerimientos/RF-AUDIT-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/README.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/_MAP.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/especificaciones/ET-NOTIF-backend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/especificaciones/ET-NOTIF-database.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/especificaciones/ET-NOTIF-frontend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/especificaciones/INDICE-ET-NOTIF.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/INDICE-US-NOTIF.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/requerimientos/INDICE-RF-NOTIF.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/requerimientos/RF-NOTIF-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/requerimientos/RF-NOTIF-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/requerimientos/RF-NOTIF-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-008-notifications/requerimientos/RF-NOTIF-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/README.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/_MAP.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/especificaciones/ET-REPORT-backend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/especificaciones/ET-REPORT-database.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/especificaciones/ET-REPORT-frontend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/especificaciones/INDICE-ET-REPORT.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/historias-usuario/INDICE-US-REPORT.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/requerimientos/INDICE-RF-REPORT.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/requerimientos/RF-REPORT-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/requerimientos/RF-REPORT-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/requerimientos/RF-REPORT-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-009-reports/requerimientos/RF-REPORT-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/README.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/_MAP.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/especificaciones/ET-FIN-backend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/especificaciones/ET-FIN-database.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/especificaciones/ET-FIN-frontend.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/especificaciones/INDICE-ET-FINANCIAL.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/historias-usuario/INDICE-US-FINANCIAL.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/implementacion/TRACEABILITY.yml delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/requerimientos/INDICE-RF-FINANCIAL.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/requerimientos/RF-FIN-001.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/requerimientos/RF-FIN-002.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/requerimientos/RF-FIN-003.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/MGN-010-financial/requerimientos/RF-FIN-004.md delete mode 100644 projects/erp-core/docs/02-fase-core-business/README.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-auth/INDICE-RF-AUTH.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-auth/RF-AUTH-001.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-auth/RF-AUTH-002.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-auth/RF-AUTH-003.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-auth/RF-AUTH-004.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-auth/RF-AUTH-005.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-catalogs/INDICE-RF-CATALOG.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-catalogs/RF-CATALOG-001.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-catalogs/RF-CATALOG-002.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-catalogs/RF-CATALOG-003.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-catalogs/RF-CATALOG-004.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-catalogs/RF-CATALOG-005.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-rbac/INDICE-RF-ROLE.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-rbac/RF-ROLE-001.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-rbac/RF-ROLE-002.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-rbac/RF-ROLE-003.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-rbac/RF-ROLE-004.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-tenants/INDICE-RF-TENANT.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-tenants/RF-TENANT-001.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-tenants/RF-TENANT-002.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-tenants/RF-TENANT-003.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-tenants/RF-TENANT-004.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-users/INDICE-RF-USER.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-users/RF-USER-001.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-users/RF-USER-002.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-users/RF-USER-003.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-users/RF-USER-004.md delete mode 100644 projects/erp-core/docs/03-requerimientos/RF-users/RF-USER-005.md delete mode 100644 projects/erp-core/docs/04-modelado/FASE-2-INICIO-COMPLETADO.md delete mode 100644 projects/erp-core/docs/04-modelado/MAPEO-SPECS-VERTICALES.md delete mode 100644 projects/erp-core/docs/04-modelado/VALIDACION-DEPENDENCIAS-MGN-015-018.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/AUTOMATIC-TRACKING-SYSTEM.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-ai_agents.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-billing.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-core_auth.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-core_catalogs.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-core_rbac.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-core_tenants.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-core_users.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-integrations.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/DDL-SPEC-messaging.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/README.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/database-roadmap.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/SCHEMAS-STATISTICS.md delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/analytics-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/auth-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/core-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/financial-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/inventory-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/projects-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/purchase-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/sales-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/database-design/schemas/system-schema-ddl.sql delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/analytics-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/auth-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/billing-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/crm-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/financial-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/hr-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/inventory-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/messaging-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/projects-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/domain-models/sales-domain.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/ET-auth-backend.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/ET-rbac-backend.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/ET-tenants-backend.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/ET-users-backend.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/README.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-001-autenticaci贸n-de-usuarios.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-002-gesti贸n-de-roles-y-permisos-rbac.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-003-gesti贸n-de-usuarios.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-004-multi-tenancy-con-schema-level-isolation.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-005-reset-de-contrase帽a.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-006-registro-de-usuarios-signup.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-007-gesti贸n-de-sesiones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-001/ET-BACKEND-MGN-001-008-record-rules-row-level-security.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-002/ET-BACKEND-MGN-002-001-gesti贸n-de-empresas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-002/ET-BACKEND-MGN-002-002-configuraci贸n-de-empresa.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-002/ET-BACKEND-MGN-002-003-asignaci贸n-de-usuarios-a-empresas-multi-empresa.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-002/ET-BACKEND-MGN-002-004-jerarqu铆as-de-empresas-holdings.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-002/ET-BACKEND-MGN-002-005-plantillas-de-configuraci贸n-por-pa铆s.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-003/ET-BACKEND-MGN-003-001-gesti贸n-de-partners-universales.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-003/ET-BACKEND-MGN-003-002-gesti贸n-de-pa铆ses-y-regiones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-003/ET-BACKEND-MGN-003-003-gesti贸n-de-monedas-y-tasas-de-cambio.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-003/ET-BACKEND-MGN-003-004-gesti贸n-de-unidades-de-medida-uom.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-003/ET-BACKEND-MGN-003-005-gesti贸n-de-categor铆as-de-productos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-003/ET-BACKEND-MGN-003-006-condiciones-de-pago-payment-terms.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-001-gesti贸n-de-plan-de-cuentas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-002-gesti贸n-de-journals-contables.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-003-registro-de-asientos-contables.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-004-gesti贸n-de-impuestos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-005-gesti贸n-de-facturas-de-cliente.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-006-gesti贸n-de-facturas-de-proveedor.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-007-gesti贸n-de-pagos-y-conciliaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-004/ET-BACKEND-MGN-004-008-reportes-financieros-balance-y-p&l.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-005/ET-BACKEND-MGN-005-001-gesti贸n-de-productos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-005/ET-BACKEND-MGN-005-002-gesti贸n-de-almacenes-y-ubicaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-005/ET-BACKEND-MGN-005-003-movimientos-de-stock.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-005/ET-BACKEND-MGN-005-004-pickings-albaranes-de-entrada-salida.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-005/ET-BACKEND-MGN-005-005-trazabilidad-lotes-y-n煤meros-de-serie.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-005/ET-BACKEND-MGN-005-006-valoraci贸n-de-inventario-fifo,-promedio.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-005/ET-BACKEND-MGN-005-007-inventario-f铆sico-y-ajustes.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-006/ET-BACKEND-MGN-006-001-solicitudes-de-cotizaci贸n-rfq.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-006/ET-BACKEND-MGN-006-002-gesti贸n-de-贸rdenes-de-compra.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-006/ET-BACKEND-MGN-006-003-workflow-de-aprobaci贸n-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-006/ET-BACKEND-MGN-006-004-recepciones-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-006/ET-BACKEND-MGN-006-005-facturaci贸n-de-proveedores-desde-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-006/ET-BACKEND-MGN-006-006-reportes-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-007/ET-BACKEND-MGN-007-001-gesti贸n-de-cotizaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-007/ET-BACKEND-MGN-007-002-conversi贸n-a-贸rdenes-de-venta.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-007/ET-BACKEND-MGN-007-003-gesti贸n-de-贸rdenes-de-venta.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-007/ET-BACKEND-MGN-007-004-entregas-de-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-007/ET-BACKEND-MGN-007-005-facturaci贸n-de-clientes-desde-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-007/ET-BACKEND-MGN-007-006-reportes-de-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-008/ET-BACKEND-MGN-008-001-gesti贸n-de-cuentas-anal铆ticas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-008/ET-BACKEND-MGN-008-002-registro-de-l铆neas-anal铆ticas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-008/ET-BACKEND-MGN-008-003-distribuci贸n-anal铆tica-multi-cuenta.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-008/ET-BACKEND-MGN-008-004-tags-anal铆ticos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-008/ET-BACKEND-MGN-008-005-reportes-anal铆ticos-p&l-por-proyecto.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-009/ET-BACKEND-MGN-009-001-gesti贸n-de-leads-y-oportunidades.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-009/ET-BACKEND-MGN-009-002-pipeline-de-ventas-kanban.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-009/ET-BACKEND-MGN-009-003-actividades-y-seguimiento.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-009/ET-BACKEND-MGN-009-004-lead-scoring-y-calificaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-009/ET-BACKEND-MGN-009-005-conversi贸n-a-cotizaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-010/ET-BACKEND-MGN-010-001-gesti贸n-de-empleados.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-010/ET-BACKEND-MGN-010-002-departamentos-y-puestos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-010/ET-BACKEND-MGN-010-003-contratos-laborales.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-010/ET-BACKEND-MGN-010-004-asistencias-check-in-check-out.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-010/ET-BACKEND-MGN-010-005-ausencias-y-permisos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-011/ET-BACKEND-MGN-011-001-gesti贸n-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-011/ET-BACKEND-MGN-011-002-gesti贸n-de-tareas-kanban.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-011/ET-BACKEND-MGN-011-003-milestones-hitos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-011/ET-BACKEND-MGN-011-004-timesheet-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-011/ET-BACKEND-MGN-011-005-vista-gantt-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-012/ET-BACKEND-MGN-012-001-dashboards-configurables.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-012/ET-BACKEND-MGN-012-002-query-builder-y-reportes-personalizados.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-012/ET-BACKEND-MGN-012-003-exportaci贸n-de-datos-pdf,-excel,-csv.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-012/ET-BACKEND-MGN-012-004-gr谩ficos-y-visualizaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-013/ET-BACKEND-MGN-013-001-acceso-portal-para-clientes.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-013/ET-BACKEND-MGN-013-002-vista-de-documentos-en-portal.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-013/ET-BACKEND-MGN-013-003-aprobaci贸n-y-firma-electr贸nica.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-013/ET-BACKEND-MGN-013-004-mensajer铆a-en-portal.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-014/ET-BACKEND-MGN-014-001-sistema-de-mensajes-chatter.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-014/ET-BACKEND-MGN-014-002-notificaciones-in-app-y-email.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-014/ET-BACKEND-MGN-014-003-tracking-autom谩tico-de-cambios.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-014/ET-BACKEND-MGN-014-004-actividades-programadas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-014/ET-BACKEND-MGN-014-005-followers-seguidores.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-014/ET-BACKEND-MGN-014-006-templates-de-email.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-015/ET-MGN-015-001-api-planes-suscripcion.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-015/ET-MGN-015-002-api-suscripciones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-015/ET-MGN-015-003-api-metodos-pago.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-015/ET-MGN-015-004-api-facturacion.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-015/ET-MGN-015-005-api-uso-metricas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/backend/mgn-015/README.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-001-autenticaci贸n-de-usuarios.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-002-gesti贸n-de-roles-y-permisos-rbac.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-003-gesti贸n-de-usuarios.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-004-multi-tenancy-con-schema-level-isolation.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-005-reset-de-contrase帽a.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-006-registro-de-usuarios-signup.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-007-gesti贸n-de-sesiones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-001/ET-FRONTEND-MGN-001-008-record-rules-row-level-security.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-002/ET-FRONTEND-MGN-002-001-gesti贸n-de-empresas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-002/ET-FRONTEND-MGN-002-002-configuraci贸n-de-empresa.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-002/ET-FRONTEND-MGN-002-003-asignaci贸n-de-usuarios-a-empresas-multi-empresa.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-002/ET-FRONTEND-MGN-002-004-jerarqu铆as-de-empresas-holdings.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-002/ET-FRONTEND-MGN-002-005-plantillas-de-configuraci贸n-por-pa铆s.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-003/ET-FRONTEND-MGN-003-001-gesti贸n-de-partners-universales.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-003/ET-FRONTEND-MGN-003-002-gesti贸n-de-pa铆ses-y-regiones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-003/ET-FRONTEND-MGN-003-003-gesti贸n-de-monedas-y-tasas-de-cambio.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-003/ET-FRONTEND-MGN-003-004-gesti贸n-de-unidades-de-medida-uom.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-003/ET-FRONTEND-MGN-003-005-gesti贸n-de-categor铆as-de-productos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-003/ET-FRONTEND-MGN-003-006-condiciones-de-pago-payment-terms.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-001-gesti贸n-de-plan-de-cuentas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-002-gesti贸n-de-journals-contables.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-003-registro-de-asientos-contables.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-004-gesti贸n-de-impuestos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-005-gesti贸n-de-facturas-de-cliente.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-006-gesti贸n-de-facturas-de-proveedor.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-007-gesti贸n-de-pagos-y-conciliaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-004/ET-FRONTEND-MGN-004-008-reportes-financieros-balance-y-p&l.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-005/ET-FRONTEND-MGN-005-001-gesti贸n-de-productos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-005/ET-FRONTEND-MGN-005-002-gesti贸n-de-almacenes-y-ubicaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-005/ET-FRONTEND-MGN-005-003-movimientos-de-stock.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-005/ET-FRONTEND-MGN-005-004-pickings-albaranes-de-entrada-salida.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-005/ET-FRONTEND-MGN-005-005-trazabilidad-lotes-y-n煤meros-de-serie.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-005/ET-FRONTEND-MGN-005-006-valoraci贸n-de-inventario-fifo,-promedio.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-005/ET-FRONTEND-MGN-005-007-inventario-f铆sico-y-ajustes.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-006/ET-FRONTEND-MGN-006-001-solicitudes-de-cotizaci贸n-rfq.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-006/ET-FRONTEND-MGN-006-002-gesti贸n-de-贸rdenes-de-compra.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-006/ET-FRONTEND-MGN-006-003-workflow-de-aprobaci贸n-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-006/ET-FRONTEND-MGN-006-004-recepciones-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-006/ET-FRONTEND-MGN-006-005-facturaci贸n-de-proveedores-desde-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-006/ET-FRONTEND-MGN-006-006-reportes-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-007/ET-FRONTEND-MGN-007-001-gesti贸n-de-cotizaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-007/ET-FRONTEND-MGN-007-002-conversi贸n-a-贸rdenes-de-venta.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-007/ET-FRONTEND-MGN-007-003-gesti贸n-de-贸rdenes-de-venta.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-007/ET-FRONTEND-MGN-007-004-entregas-de-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-007/ET-FRONTEND-MGN-007-005-facturaci贸n-de-clientes-desde-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-007/ET-FRONTEND-MGN-007-006-reportes-de-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-008/ET-FRONTEND-MGN-008-001-gesti贸n-de-cuentas-anal铆ticas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-008/ET-FRONTEND-MGN-008-002-registro-de-l铆neas-anal铆ticas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-008/ET-FRONTEND-MGN-008-003-distribuci贸n-anal铆tica-multi-cuenta.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-008/ET-FRONTEND-MGN-008-004-tags-anal铆ticos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-008/ET-FRONTEND-MGN-008-005-reportes-anal铆ticos-p&l-por-proyecto.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-009/ET-FRONTEND-MGN-009-001-gesti贸n-de-leads-y-oportunidades.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-009/ET-FRONTEND-MGN-009-002-pipeline-de-ventas-kanban.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-009/ET-FRONTEND-MGN-009-003-actividades-y-seguimiento.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-009/ET-FRONTEND-MGN-009-004-lead-scoring-y-calificaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-009/ET-FRONTEND-MGN-009-005-conversi贸n-a-cotizaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-010/ET-FRONTEND-MGN-010-001-gesti贸n-de-empleados.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-010/ET-FRONTEND-MGN-010-002-departamentos-y-puestos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-010/ET-FRONTEND-MGN-010-003-contratos-laborales.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-010/ET-FRONTEND-MGN-010-004-asistencias-check-in-check-out.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-010/ET-FRONTEND-MGN-010-005-ausencias-y-permisos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-011/ET-FRONTEND-MGN-011-001-gesti贸n-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-011/ET-FRONTEND-MGN-011-002-gesti贸n-de-tareas-kanban.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-011/ET-FRONTEND-MGN-011-003-milestones-hitos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-011/ET-FRONTEND-MGN-011-004-timesheet-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-011/ET-FRONTEND-MGN-011-005-vista-gantt-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-012/ET-FRONTEND-MGN-012-001-dashboards-configurables.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-012/ET-FRONTEND-MGN-012-002-query-builder-y-reportes-personalizados.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-012/ET-FRONTEND-MGN-012-003-exportaci贸n-de-datos-pdf,-excel,-csv.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-012/ET-FRONTEND-MGN-012-004-gr谩ficos-y-visualizaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-013/ET-FRONTEND-MGN-013-001-acceso-portal-para-clientes.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-013/ET-FRONTEND-MGN-013-002-vista-de-documentos-en-portal.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-013/ET-FRONTEND-MGN-013-003-aprobaci贸n-y-firma-electr贸nica.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-013/ET-FRONTEND-MGN-013-004-mensajer铆a-en-portal.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-014/ET-FRONTEND-MGN-014-001-sistema-de-mensajes-chatter.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-014/ET-FRONTEND-MGN-014-002-notificaciones-in-app-y-email.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-014/ET-FRONTEND-MGN-014-003-tracking-autom谩tico-de-cambios.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-014/ET-FRONTEND-MGN-014-004-actividades-programadas.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-014/ET-FRONTEND-MGN-014-005-followers-seguidores.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/frontend/mgn-014/ET-FRONTEND-MGN-014-006-templates-de-email.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/generate_et.py delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-ALERTAS-PRESUPUESTO.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-BLANKET-ORDERS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-CONCILIACION-BANCARIA.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-CONSOLIDACION-FINANCIERA.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-CONTABILIDAD-ANALITICA-MULTIDIMENSIONAL.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-FIRMA-ELECTRONICA-NOM151.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-GASTOS-EMPLEADOS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-IMPUESTOS-AVANZADOS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-INTEGRACION-CALENDAR.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-INVENTARIOS-CICLICOS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-LOCALIZACION-PAISES.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-MAIL-THREAD-TRACKING.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-NOMINA-BASICA.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-OAUTH2-SOCIAL-LOGIN.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-PLANTILLAS-CUENTAS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-PORTAL-PROVEEDORES.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-PRESUPUESTOS-REVISIONES.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-PRICING-RULES.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-REPORTES-FINANCIEROS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-RRHH-EVALUACIONES-SKILLS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-SCHEDULER-REPORTES.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-SEGURIDAD-API-KEYS-PERMISOS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-SISTEMA-SECUENCIAS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-TAREAS-RECURRENTES.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-TASAS-CAMBIO-AUTOMATICAS.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-TRAZABILIDAD-LOTES-SERIES.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-TWO-FACTOR-AUTHENTICATION.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-VALORACION-INVENTARIO.md delete mode 100644 projects/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-WIZARD-TRANSIENT-MODEL.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/README.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/generate_rfs.py delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-001-autenticacion-usuarios.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-002-gestion-roles.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-003-gestion-usuarios.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-004-multi-tenancy.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-005-reset-password.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-006-registro-usuarios.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-007-session-management.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-001/RF-MGN-001-008-record-rules-rls.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-002/RF-MGN-002-001-gestion-empresas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-002/RF-MGN-002-002-configuracion-empresa.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-002/RF-MGN-002-003-asignacion-usuarios-empresas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-002/RF-MGN-002-004-jerarquias-empresas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-002/RF-MGN-002-005-plantillas-configuracion.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-003/RF-MGN-003-001-gestion-partners.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-003/RF-MGN-003-002-gesti贸n-de-pa铆ses-y-regiones.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-003/RF-MGN-003-003-gesti贸n-de-monedas-y-tasas-de-cambio.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-003/RF-MGN-003-004-gesti贸n-de-unidades-de-medida-uom.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-003/RF-MGN-003-005-gesti贸n-de-categor铆as-de-productos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-003/RF-MGN-003-006-condiciones-de-pago-payment-terms.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-001-gesti贸n-de-plan-de-cuentas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-002-gesti贸n-de-journals-contables.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-003-registro-de-asientos-contables.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-004-gesti贸n-de-impuestos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-005-gesti贸n-de-facturas-de-cliente.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-006-gesti贸n-de-facturas-de-proveedor.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-007-gesti贸n-de-pagos-y-conciliaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-004/RF-MGN-004-008-reportes-financieros-balance-y-p&l.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-005/RF-MGN-005-001-gesti贸n-de-productos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-005/RF-MGN-005-002-gesti贸n-de-almacenes-y-ubicaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-005/RF-MGN-005-003-movimientos-de-stock.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-005/RF-MGN-005-004-pickings-albaranes-de-entrada-salida.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-005/RF-MGN-005-005-trazabilidad-lotes-y-n煤meros-de-serie.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-005/RF-MGN-005-006-valoraci贸n-de-inventario-fifo,-promedio.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-005/RF-MGN-005-007-inventario-f铆sico-y-ajustes.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-006/RF-MGN-006-001-solicitudes-de-cotizaci贸n-rfq.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-006/RF-MGN-006-002-gesti贸n-de-贸rdenes-de-compra.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-006/RF-MGN-006-003-workflow-de-aprobaci贸n-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-006/RF-MGN-006-004-recepciones-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-006/RF-MGN-006-005-facturaci贸n-de-proveedores-desde-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-006/RF-MGN-006-006-reportes-de-compras.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-007/RF-MGN-007-001-gesti贸n-de-cotizaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-007/RF-MGN-007-002-conversi贸n-a-贸rdenes-de-venta.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-007/RF-MGN-007-003-gesti贸n-de-贸rdenes-de-venta.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-007/RF-MGN-007-004-entregas-de-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-007/RF-MGN-007-005-facturaci贸n-de-clientes-desde-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-007/RF-MGN-007-006-reportes-de-ventas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-008/RF-MGN-008-001-gesti贸n-de-cuentas-anal铆ticas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-008/RF-MGN-008-002-registro-de-l铆neas-anal铆ticas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-008/RF-MGN-008-003-distribuci贸n-anal铆tica-multi-cuenta.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-008/RF-MGN-008-004-tags-anal铆ticos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-008/RF-MGN-008-005-reportes-anal铆ticos-p&l-por-proyecto.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-009/RF-MGN-009-001-gesti贸n-de-leads-y-oportunidades.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-009/RF-MGN-009-002-pipeline-de-ventas-kanban.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-009/RF-MGN-009-003-actividades-y-seguimiento.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-009/RF-MGN-009-004-lead-scoring-y-calificaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-009/RF-MGN-009-005-conversi贸n-a-cotizaci贸n.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-010/RF-MGN-010-001-gesti贸n-de-empleados.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-010/RF-MGN-010-002-departamentos-y-puestos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-010/RF-MGN-010-003-contratos-laborales.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-010/RF-MGN-010-004-asistencias-check-in-check-out.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-010/RF-MGN-010-005-ausencias-y-permisos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-011/RF-MGN-011-001-gesti贸n-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-011/RF-MGN-011-002-gesti贸n-de-tareas-kanban.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-011/RF-MGN-011-003-milestones-hitos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-011/RF-MGN-011-004-timesheet-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-011/RF-MGN-011-005-vista-gantt-de-proyectos.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-012/RF-MGN-012-001-dashboards-configurables.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-012/RF-MGN-012-002-query-builder-y-reportes-personalizados.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-012/RF-MGN-012-003-exportaci贸n-de-datos-pdf,-excel,-csv.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-012/RF-MGN-012-004-gr谩ficos-y-visualizaciones.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-013/RF-MGN-013-001-acceso-portal-para-clientes.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-013/RF-MGN-013-002-vista-de-documentos-en-portal.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-013/RF-MGN-013-003-aprobaci贸n-y-firma-electr贸nica.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-013/RF-MGN-013-004-mensajer铆a-en-portal.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-014/RF-MGN-014-001-sistema-de-mensajes-chatter.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-014/RF-MGN-014-002-notificaciones-in-app-y-email.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-014/RF-MGN-014-003-tracking-autom谩tico-de-cambios.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-014/RF-MGN-014-004-actividades-programadas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-014/RF-MGN-014-005-followers-seguidores.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-014/RF-MGN-014-006-templates-de-email.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-015/README.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-015/RF-MGN-015-001-gestion-planes-suscripcion.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-015/RF-MGN-015-002-gestion-suscripciones-tenant.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-015/RF-MGN-015-003-metodos-pago.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-015/RF-MGN-015-004-facturacion-cobros.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-015/RF-MGN-015-005-registro-uso-metricas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-015/RF-MGN-015-006-modo-single-tenant.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-015/RF-MGN-015-007-pricing-por-usuario.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-016/README.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-016/RF-MGN-016-001-integracion-mercadopago.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-016/RF-MGN-016-002-integracion-clip.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-017/README.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-017/RF-MGN-017-001-conexion-whatsapp-business.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-017/RF-MGN-017-005-chatbot-automatizado.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-018/README.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-018/RF-MGN-018-001-configuracion-agentes.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-018/RF-MGN-018-002-bases-conocimiento.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-018/RF-MGN-018-003-procesamiento-mensajes.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-018/RF-MGN-018-004-acciones-herramientas.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-018/RF-MGN-018-005-entrenamiento-feedback.md delete mode 100644 projects/erp-core/docs/04-modelado/requerimientos-funcionales/mgn-018/RF-MGN-018-006-analytics-metricas.md delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/GRAFO-DEPENDENCIAS-SCHEMAS.md delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/INVENTARIO-OBJETOS-BD.yml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/MATRIZ-TRAZABILIDAD-RF-ET-BD.md delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/README.md delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/REPORTE-VALIDACION-DDL-DOC.md delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/REPORTE-VALIDACION-PREVIA-BD.md delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-001.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-002.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-003.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-004.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-005.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-006.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-007.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-008.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-009.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-010.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-011.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-012.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-013.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-014.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/TRACEABILITY-MGN-015.yaml delete mode 100644 projects/erp-core/docs/04-modelado/trazabilidad/VALIDACION-COBERTURA-ODOO.md delete mode 100644 projects/erp-core/docs/04-modelado/workflows/WORKFLOW-3-WAY-MATCH.md delete mode 100644 projects/erp-core/docs/04-modelado/workflows/WORKFLOW-CIERRE-PERIODO-CONTABLE.md delete mode 100644 projects/erp-core/docs/04-modelado/workflows/WORKFLOW-PAGOS-ANTICIPADOS.md delete mode 100644 projects/erp-core/docs/05-user-stories/PLAN-EJECUCION-US-RESTANTES.md delete mode 100644 projects/erp-core/docs/05-user-stories/README.md delete mode 100644 projects/erp-core/docs/05-user-stories/REPORTE-COMPLETACION-70-US.md delete mode 100644 projects/erp-core/docs/05-user-stories/REPORTE-PROGRESO-FASE-3.md delete mode 100644 projects/erp-core/docs/05-user-stories/RESUMEN-EJECUTIVO-FASE-3.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-001/BACKLOG-MGN001.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-001.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-002.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-003.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-004.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-002/BACKLOG-MGN002.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-001.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-002.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-003.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-004.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-003/BACKLOG-MGN003.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-001.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-002.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-003.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-004.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-004/BACKLOG-MGN004.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-001.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-002.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-003.md delete mode 100644 projects/erp-core/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-004.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/BACKLOG-MGN-001.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-001-001-login-con-email-password.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-001-002-renovar-token-jwt.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-002-001-crear-y-gestionar-roles.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-002-002-asignar-permisos-a-roles.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-002-003-validar-permisos-en-runtime.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-003-001-crud-usuarios.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-003-002-gestion-perfil-y-cambio-password.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-004-001-crear-tenant.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-004-002-schema-isolation.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-004-003-tenant-context-switching.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-005-001-reset-password.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-006-001-signup-autoregistro.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-007-001-gestion-sesiones-activas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-007-002-logout.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-008-001-rls-policies.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-001/US-MGN-001-008-002-field-level-security.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-002/BACKLOG-MGN-002.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-002/US-MGN-002-001-001-crud-empresas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-002/US-MGN-002-001-002-logo-y-branding.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-002/US-MGN-002-002-001-configuracion-fiscal-y-contable.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-002/US-MGN-002-003-001-asignar-usuarios-a-empresas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-002/US-MGN-002-003-002-cambiar-empresa-activa.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-002/US-MGN-002-004-001-jerarquias-holdings.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-002/US-MGN-002-005-001-plantillas-configuracion-por-pais.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/BACKLOG-MGN-003.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/US-MGN-003-001-001-crud-partners.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/US-MGN-003-001-002-direcciones-multiples.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/US-MGN-003-002-001-paises-y-estados.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/US-MGN-003-003-001-gestion-monedas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/US-MGN-003-003-002-tasas-de-cambio.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/US-MGN-003-004-001-unidades-de-medida.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/US-MGN-003-005-001-categorias-de-productos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-003/US-MGN-003-006-001-condiciones-de-pago.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/BACKLOG-MGN-004.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-001-001-crud-cuentas-contables.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-001-002-jerarquia-cuentas-contables.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-002-001-crud-journals-contables.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-003-001-crear-asiento-contable-draft.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-003-002-validar-y-postear-asiento.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-003-003-cancelar-asiento-reversing-entry.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-004-001-crud-impuestos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-004-002-calculo-impuestos-automatico.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-005-001-crear-factura-cliente-draft.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-005-002-validar-factura-cliente.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-005-003-cancelar-factura-cliente.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-006-001-crear-factura-proveedor-draft.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-006-002-validar-factura-proveedor.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-006-003-cancelar-factura-proveedor.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-007-001-registrar-pago-recibido.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-007-002-registrar-pago-realizado.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-007-003-cancelar-pago.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-004/US-MGN-004-008-001-reportes-financieros-balance-pl.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-001-001-crear-producto.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-001-002-gestionar-variantes-producto.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-002-001-gestionar-almacenes-ubicaciones.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-003-001-crear-movimiento-stock.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-003-002-validar-movimiento-stock.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-003-003-cancelar-movimiento-stock.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-004-001-visualizar-stock-quants.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-004-002-reservar-stock.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-005-001-crear-ajuste-inventario.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-005-002-validar-ajuste-inventario.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-006-001-valoracion-fifo.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-006-002-valoracion-promedio.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-007-001-reporte-inventario-valorizado.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-005/US-MGN-005-007-002-reporte-movimientos-stock.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-001-001-crear-rfq.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-001-002-enviar-rfq.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-002-001-crear-orden-compra.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-002-002-confirmar-orden-compra.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-002-003-cancelar-orden-compra.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-003-001-crear-recepcion-compra.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-003-002-validar-recepcion-compra.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-003-003-recepcion-parcial-backorder.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-004-001-crear-devolucion-proveedor.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-004-002-validar-devolucion-proveedor.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-005-001-dashboard-compras.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-006/US-MGN-006-005-002-analisis-proveedores.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-001-001-crear-cotizacion.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-001-002-enviar-cotizacion-email.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-002-001-crear-sales-order-desde-cotizacion.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-002-002-confirmar-sales-order.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-002-003-cancelar-sales-order.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-003-001-crear-entrega-desde-sales-order.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-003-002-validar-entrega-actualizar-stock.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-003-003-entrega-parcial-backorder.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-005-001-crear-devolucion-cliente.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-005-002-validar-devolucion-ajustar-stock.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-006-001-dashboard-ventas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-007/US-MGN-007-006-002-analisis-ventas-producto-cliente-periodo.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-001-001-crud-planes-analiticos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-001-002-configurar-dimensiones-multi-nivel.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-002-001-crud-cuentas-analiticas-por-plan.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-002-002-gestionar-jerarquia-cuentas-analiticas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-003-001-asignar-distribuciones-analiticas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-003-002-calcular-distribuciones-automaticas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-004-001-reporte-p-and-l-por-proyecto-departamento.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-004-002-drill-down-analitico-multi-nivel.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-005-001-crud-presupuestos-analiticos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-008/US-MGN-008-005-002-alertas-desviacion-presupuestaria.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-009/US-MGN-009-001-001-crud-leads.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-009/US-MGN-009-001-002-calificar-lead-scoring.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-009/US-MGN-009-002-001-crud-oportunidades.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-009/US-MGN-009-002-002-calcular-probabilidad-cierre.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-009/US-MGN-009-003-001-vista-kanban-pipeline.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-009/US-MGN-009-003-002-drag-drop-oportunidades.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-009/US-MGN-009-004-001-crud-actividades-crm.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-009/US-MGN-009-005-001-convertir-lead-oportunidad.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-010/US-MGN-010-001-001-crud-empleados.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-010/US-MGN-010-001-002-gestionar-documentos-empleado.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-010/US-MGN-010-002-001-crud-contratos-laborales.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-010/US-MGN-010-002-002-renovacion-automatica-contratos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-010/US-MGN-010-003-001-registro-check-in-check-out-asistencias.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-010/US-MGN-010-004-001-gestionar-jerarquia-departamentos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-010/US-MGN-010-005-001-dashboard-rrhh.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-001-aprobar-timesheet-de-empleados.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-001-asignar-miembros-y-roles-a-proyecto.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-001-crud-tareas-de-proyecto.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-001-dashboard-de-proyecto-avance-budget-horas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-001-diagrama-de-gantt-de-proyecto.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-001-gestionar-dependencias-entre-tareas.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-001-registrar-timesheet-por-tarea.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-001-vista-kanban-de-tareas-por-estado.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-0-002-diagrama-de-gantt-de-proyecto.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-001-001-crud-proyectos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-011/US-MGN-011-001-002-configurar-proyecto-fases-presupuesto.md delete mode 100755 projects/erp-core/docs/05-user-stories/mgn-011/create_mgn011_us.sh delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-012/US-MGN-012-001-001-report-builder-visual-con-drag-drop.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-012/US-MGN-012-001-002-gestionar-widgets-de-dashboard.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-012/US-MGN-012-002-001-reportes-financieros-estndar-balance-pl-cash-flow.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-012/US-MGN-012-003-001-reportes-operacionales-configurables.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-012/US-MGN-012-004-001-exportar-reportes-a-excelpdf.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-012/US-MGN-012-004-002-enviar-reportes-por-email-programado.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-013/US-MGN-013-001-001-login-portal-clienteproveedor.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-013/US-MGN-013-001-002-registro-self-service-en-portal.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-013/US-MGN-013-002-001-vista-de-documentos-en-portal.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-013/US-MGN-013-002-002-descargar-documentos-desde-portal.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-013/US-MGN-013-003-001-mensajera-interna-en-portal.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-013/US-MGN-013-004-001-configuracin-de-perfil-y-preferencias-en-portal.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-001-001-comentar-en-registros-chatter-pattern.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-001-002-adjuntar-archivos-a-comentarios.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-001-003-seguirdejar-de-seguir-registros.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-002-001-notificaciones-push-en-tiempo-real-websocket.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-002-002-notificaciones-por-email-configurables.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-002-003-configurar-preferencias-de-notificaciones-por-usuario.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-003-001-subir-archivos-adjuntos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-003-002-gestionar-biblioteca-de-adjuntos.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-004-001-aadir-followers-a-registros.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-004-002-notificar-automticamente-a-followers.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-005-001-crud-actividades-tareas-llamadas-reuniones-con-calendario.md delete mode 100644 projects/erp-core/docs/05-user-stories/mgn-014/US-MGN-014-006-001-mensajes-internos-vs-pblicos-en-chatter.md delete mode 100644 projects/erp-core/docs/06-test-plans/MASTER-TEST-PLAN.md delete mode 100644 projects/erp-core/docs/06-test-plans/README.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-001-fundamentos.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-002-empresas.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-003-catalogos.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-004-financiero.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-005-inventario.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-006-compras.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-007-ventas.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-008-analitica.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-009-crm.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-010-rrhh.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-011-proyectos.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-012-reportes.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-013-portal.md delete mode 100644 projects/erp-core/docs/06-test-plans/TEST-PLAN-MGN-014-mensajeria.md delete mode 100644 projects/erp-core/docs/06-test-plans/TP-auth.md delete mode 100644 projects/erp-core/docs/06-test-plans/TP-rbac.md delete mode 100644 projects/erp-core/docs/06-test-plans/TP-tenants.md delete mode 100644 projects/erp-core/docs/06-test-plans/TP-users.md delete mode 100644 projects/erp-core/docs/07-devops/BACKUP-RECOVERY.md delete mode 100644 projects/erp-core/docs/07-devops/CI-CD-PIPELINE.md delete mode 100644 projects/erp-core/docs/07-devops/DEPLOYMENT-GUIDE.md delete mode 100644 projects/erp-core/docs/07-devops/MONITORING-OBSERVABILITY.md delete mode 100644 projects/erp-core/docs/07-devops/README.md delete mode 100644 projects/erp-core/docs/07-devops/SECURITY-HARDENING.md delete mode 100755 projects/erp-core/docs/07-devops/scripts/backup-postgres.sh delete mode 100755 projects/erp-core/docs/07-devops/scripts/health-check.sh delete mode 100755 projects/erp-core/docs/07-devops/scripts/restore-postgres.sh delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-001-auth.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-002-users.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-003-roles.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-004-tenants.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-005-catalogs.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-006-settings.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-007-audit.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-008-notifications.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-009-reports.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-010-financial.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-011-inventory.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-012-purchasing.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-013-sales.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-014-crm.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-015-projects.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-016-billing.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-017-payments.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-017-stripe-integration.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-018-whatsapp.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-019-ai-agents.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-019-mobile-apps.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-020-onboarding.md delete mode 100644 projects/erp-core/docs/08-epicas/EPIC-MGN-021-ai-tokens.md delete mode 100644 projects/erp-core/docs/08-epicas/README.md delete mode 100644 projects/erp-core/docs/90-transversal/REPORTE-AUDITORIA-DOCUMENTACION.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-001-stack-tecnologico.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-002-arquitectura-modular.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-003-multi-tenancy.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-004-sistema-constantes-ssot.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-005-path-aliases.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-006-rbac-sistema-permisos.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-007-database-design.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-008-api-design.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-009-frontend-architecture.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-010-testing-strategy.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-011-database-clean-load-strategy.md delete mode 100644 projects/erp-core/docs/97-adr/ADR-012-complete-traceability-policy.md delete mode 100644 projects/erp-core/docs/CORRECCION-GAP-001-REPORTE.md delete mode 100644 projects/erp-core/docs/CORRECCION-GAP-002-REPORTE.md delete mode 100644 projects/erp-core/docs/FRONTEND-PRIORITY-MATRIX.md delete mode 100644 projects/erp-core/docs/INSTRUCCIONES-AGENTE-ARQUITECTURA.md delete mode 100644 projects/erp-core/docs/LANZAR-FASE-0.md delete mode 100644 projects/erp-core/docs/PLAN-DESARROLLO-FRONTEND.md delete mode 100644 projects/erp-core/docs/PLAN-DOCUMENTACION-ERP-GENERICO.md delete mode 100644 projects/erp-core/docs/PLAN-EXPANSION-BACKEND.md delete mode 100644 projects/erp-core/docs/PLAN-MAESTRO-MIGRACION-CONSOLIDACION.md delete mode 100644 projects/erp-core/docs/README.md delete mode 100644 projects/erp-core/docs/REPORTE-ALINEACION-DDL-SPECS.md delete mode 100644 projects/erp-core/docs/REPORTE-REVALIDACION-TECNICA-COMPLETA.md delete mode 100644 projects/erp-core/docs/RESUMEN-EJECUTIVO-REVALIDACION.md delete mode 100644 projects/erp-core/docs/SPRINT-PLAN-FASE-1.md delete mode 100644 projects/erp-core/docs/_MAP.md delete mode 100644 projects/erp-core/frontend/.eslintrc.cjs delete mode 100644 projects/erp-core/frontend/Dockerfile delete mode 100644 projects/erp-core/frontend/index.html delete mode 100644 projects/erp-core/frontend/nginx.conf delete mode 100644 projects/erp-core/frontend/package-lock.json delete mode 100644 projects/erp-core/frontend/package.json delete mode 100644 projects/erp-core/frontend/postcss.config.js delete mode 100644 projects/erp-core/frontend/public/vite.svg delete mode 100644 projects/erp-core/frontend/src/app/layouts/AuthLayout.tsx delete mode 100644 projects/erp-core/frontend/src/app/layouts/DashboardLayout.tsx delete mode 100644 projects/erp-core/frontend/src/app/layouts/index.ts delete mode 100644 projects/erp-core/frontend/src/app/providers/index.tsx delete mode 100644 projects/erp-core/frontend/src/app/router/ProtectedRoute.tsx delete mode 100644 projects/erp-core/frontend/src/app/router/index.tsx delete mode 100644 projects/erp-core/frontend/src/app/router/routes.tsx delete mode 100644 projects/erp-core/frontend/src/features/companies/api/companies.api.ts delete mode 100644 projects/erp-core/frontend/src/features/companies/api/index.ts delete mode 100644 projects/erp-core/frontend/src/features/companies/components/CompanyFiltersPanel.tsx delete mode 100644 projects/erp-core/frontend/src/features/companies/components/CompanyForm.tsx delete mode 100644 projects/erp-core/frontend/src/features/companies/components/index.ts delete mode 100644 projects/erp-core/frontend/src/features/companies/hooks/index.ts delete mode 100644 projects/erp-core/frontend/src/features/companies/hooks/useCompanies.ts delete mode 100644 projects/erp-core/frontend/src/features/companies/types/company.types.ts delete mode 100644 projects/erp-core/frontend/src/features/companies/types/index.ts delete mode 100644 projects/erp-core/frontend/src/features/partners/api/index.ts delete mode 100644 projects/erp-core/frontend/src/features/partners/api/partners.api.ts delete mode 100644 projects/erp-core/frontend/src/features/partners/components/PartnerFiltersPanel.tsx delete mode 100644 projects/erp-core/frontend/src/features/partners/components/PartnerForm.tsx delete mode 100644 projects/erp-core/frontend/src/features/partners/components/PartnerStatusBadge.tsx delete mode 100644 projects/erp-core/frontend/src/features/partners/components/PartnerTypeBadge.tsx delete mode 100644 projects/erp-core/frontend/src/features/partners/components/index.ts delete mode 100644 projects/erp-core/frontend/src/features/partners/hooks/index.ts delete mode 100644 projects/erp-core/frontend/src/features/partners/hooks/usePartners.ts delete mode 100644 projects/erp-core/frontend/src/features/partners/types/index.ts delete mode 100644 projects/erp-core/frontend/src/features/partners/types/partner.types.ts delete mode 100644 projects/erp-core/frontend/src/features/users/api/index.ts delete mode 100644 projects/erp-core/frontend/src/features/users/api/users.api.ts delete mode 100644 projects/erp-core/frontend/src/features/users/components/UserFiltersPanel.tsx delete mode 100644 projects/erp-core/frontend/src/features/users/components/UserForm.tsx delete mode 100644 projects/erp-core/frontend/src/features/users/components/UserStatusBadge.tsx delete mode 100644 projects/erp-core/frontend/src/features/users/components/index.ts delete mode 100644 projects/erp-core/frontend/src/features/users/hooks/index.ts delete mode 100644 projects/erp-core/frontend/src/features/users/hooks/useUsers.ts delete mode 100644 projects/erp-core/frontend/src/features/users/types/index.ts delete mode 100644 projects/erp-core/frontend/src/features/users/types/user.types.ts delete mode 100644 projects/erp-core/frontend/src/index.css delete mode 100644 projects/erp-core/frontend/src/main.tsx delete mode 100644 projects/erp-core/frontend/src/pages/NotFoundPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/auth/ForgotPasswordPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/auth/LoginPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/auth/RegisterPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/companies/CompaniesListPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/companies/CompanyCreatePage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/companies/CompanyDetailPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/companies/CompanyEditPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/dashboard/DashboardPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/partners/PartnerCreatePage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/partners/PartnerDetailPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/partners/PartnerEditPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/partners/PartnersListPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/users/UserCreatePage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/users/UserDetailPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/users/UserEditPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/users/UsersListPage.tsx delete mode 100644 projects/erp-core/frontend/src/pages/users/index.ts delete mode 100644 projects/erp-core/frontend/src/services/api/auth.api.ts delete mode 100644 projects/erp-core/frontend/src/services/api/axios-instance.ts delete mode 100644 projects/erp-core/frontend/src/services/api/index.ts delete mode 100644 projects/erp-core/frontend/src/services/api/users.api.ts delete mode 100644 projects/erp-core/frontend/src/services/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Avatar/Avatar.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Avatar/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Badge/Badge.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Badge/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Button/Button.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Button/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Input/Input.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Input/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Label/Label.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Label/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Spinner/Spinner.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Spinner/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Tooltip/Tooltip.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/Tooltip/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/atoms/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/molecules/Alert/Alert.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/molecules/Alert/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/molecules/Card/Card.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/molecules/Card/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/molecules/FormField/FormField.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/molecules/FormField/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/molecules/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Breadcrumbs/Breadcrumbs.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Breadcrumbs/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/DataTable/DataTable.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/DataTable/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/DatePicker/DatePicker.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/DatePicker/DateRangePicker.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/DatePicker/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Dropdown/Dropdown.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Dropdown/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Modal/ConfirmModal.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Modal/Modal.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Modal/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Pagination/Pagination.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Pagination/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Select/Select.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Select/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Sidebar/Sidebar.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Sidebar/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Tabs/Tabs.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Tabs/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Toast/Toast.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/Toast/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/organisms/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/templates/EmptyState/EmptyState.tsx delete mode 100644 projects/erp-core/frontend/src/shared/components/templates/EmptyState/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/components/templates/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/constants/api-endpoints.ts delete mode 100644 projects/erp-core/frontend/src/shared/constants/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/constants/roles.ts delete mode 100644 projects/erp-core/frontend/src/shared/constants/status.ts delete mode 100644 projects/erp-core/frontend/src/shared/hooks/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/hooks/useDebounce.ts delete mode 100644 projects/erp-core/frontend/src/shared/hooks/useLocalStorage.ts delete mode 100644 projects/erp-core/frontend/src/shared/hooks/useMediaQuery.ts delete mode 100644 projects/erp-core/frontend/src/shared/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/stores/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/stores/useAuthStore.ts delete mode 100644 projects/erp-core/frontend/src/shared/stores/useCompanyStore.ts delete mode 100644 projects/erp-core/frontend/src/shared/stores/useNotificationStore.ts delete mode 100644 projects/erp-core/frontend/src/shared/stores/useUIStore.ts delete mode 100644 projects/erp-core/frontend/src/shared/types/api.types.ts delete mode 100644 projects/erp-core/frontend/src/shared/types/entities.types.ts delete mode 100644 projects/erp-core/frontend/src/shared/types/index.ts delete mode 100644 projects/erp-core/frontend/src/shared/utils/cn.ts delete mode 100644 projects/erp-core/frontend/src/shared/utils/formatters.ts delete mode 100644 projects/erp-core/frontend/src/shared/utils/index.ts delete mode 100644 projects/erp-core/frontend/src/vite-env.d.ts delete mode 100644 projects/erp-core/frontend/tailwind.config.js delete mode 100644 projects/erp-core/frontend/tsconfig.json delete mode 100644 projects/erp-core/frontend/tsconfig.node.json delete mode 100644 projects/erp-core/frontend/vite.config.d.ts delete mode 100644 projects/erp-core/frontend/vite.config.ts delete mode 100644 projects/erp-core/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-core/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/erp-core/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/erp-core/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/erp-core/orchestration/01-analisis/ANALISIS-GAPS-CONSOLIDADO.md delete mode 100644 projects/erp-core/orchestration/01-analisis/ANALISIS-PROPAGACION-ALINEAMIENTO.md delete mode 100644 projects/erp-core/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/erp-core/orchestration/README.md delete mode 100644 projects/erp-core/orchestration/agentes/requirements-analyst/PLAN-CORRECCIONES-ERP-CORE.md delete mode 100644 projects/erp-core/orchestration/directivas/DIRECTIVA-DOCUMENTACION-PRE-DESARROLLO.md delete mode 100644 projects/erp-core/orchestration/directivas/DIRECTIVA-EXTENSION-VERTICALES.md delete mode 100644 projects/erp-core/orchestration/directivas/DIRECTIVA-HERENCIA-MODULOS.md delete mode 100644 projects/erp-core/orchestration/directivas/DIRECTIVA-MULTI-TENANT.md delete mode 100644 projects/erp-core/orchestration/directivas/DIRECTIVA-PATRONES-ODOO.md delete mode 100644 projects/erp-core/orchestration/directivas/ESTANDARES-API-REST-GENERICO.md delete mode 100644 projects/erp-core/orchestration/estados/ESTADO-AGENTES.json delete mode 100644 projects/erp-core/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/erp-core/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/erp-core/orchestration/inventarios/DEPENDENCY_GRAPH.yml delete mode 100644 projects/erp-core/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/erp-core/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/erp-core/orchestration/inventarios/README.md delete mode 100644 projects/erp-core/orchestration/inventarios/TRACEABILITY_MATRIX.yml delete mode 100644 projects/erp-core/orchestration/prompts/PROMPT-ERP-BACKEND-AGENT.md delete mode 100644 projects/erp-core/orchestration/prompts/PROMPT-ERP-DATABASE-AGENT.md delete mode 100644 projects/erp-core/orchestration/prompts/PROMPT-ERP-FRONTEND-AGENT.md delete mode 100644 projects/erp-core/orchestration/templates/TEMPLATE-DDL-SPECIFICATION.md delete mode 100644 projects/erp-core/orchestration/templates/TEMPLATE-ESPECIFICACION-BACKEND.md delete mode 100644 projects/erp-core/orchestration/templates/TEMPLATE-REQUERIMIENTO-FUNCIONAL.md delete mode 100644 projects/erp-core/orchestration/templates/TEMPLATE-USER-STORY.md delete mode 100644 projects/erp-core/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/erp-core/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/erp-core/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/erp-core/package-lock.json delete mode 100644 projects/erp-core/package.json delete mode 100644 projects/erp-core/tsconfig.json delete mode 100644 projects/erp-mecanicas-diesel/.env.example delete mode 100644 projects/erp-mecanicas-diesel/INVENTARIO.yml delete mode 100644 projects/erp-mecanicas-diesel/PROJECT-STATUS.md delete mode 100644 projects/erp-mecanicas-diesel/README.md delete mode 100644 projects/erp-mecanicas-diesel/backend/.env.example delete mode 100644 projects/erp-mecanicas-diesel/backend/Dockerfile delete mode 100644 projects/erp-mecanicas-diesel/backend/package-lock.json delete mode 100644 projects/erp-mecanicas-diesel/backend/package.json delete mode 100644 projects/erp-mecanicas-diesel/backend/service.descriptor.yml delete mode 100644 projects/erp-mecanicas-diesel/backend/src/main.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/auth/auth.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/auth/auth.dto.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/auth/auth.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/auth/entities/refresh-token.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/auth/entities/user.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/auth/entities/workshop.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/auth/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/customers/controllers/customers.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/customers/controllers/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/customers/customers.dto.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/customers/entities/customer.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/customers/entities/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/customers/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/customers/services/customers.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/customers/services/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/controllers/part.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/controllers/supplier.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/entities/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/entities/part-category.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/entities/part.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/entities/supplier.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/entities/warehouse-location.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/services/part.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/parts-management/services/supplier.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/controllers/diagnostic.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/controllers/quote.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/controllers/service-order.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/entities/diagnostic.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/entities/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/entities/order-item.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/entities/quote.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/entities/service-order.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/entities/service.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/entities/work-bay.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/services/diagnostic.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/services/quote.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/service-management/services/service-order.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/users/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/users/users.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/users/users.dto.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/users/users.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/controllers/fleet.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/controllers/vehicle.controller.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/entities/engine-catalog.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/entities/fleet.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/entities/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/entities/maintenance-reminder.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/entities/vehicle-engine.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/entities/vehicle.entity.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/services/fleet.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/modules/vehicle-management/services/vehicle.service.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/shared/middleware/auth.middleware.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/shared/types/index.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/src/shared/utils/jwt.utils.ts delete mode 100644 projects/erp-mecanicas-diesel/backend/tsconfig.json delete mode 100644 projects/erp-mecanicas-diesel/database/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-mecanicas-diesel/database/README.md delete mode 100644 projects/erp-mecanicas-diesel/database/init/00-extensions.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/00.5-workshop-core-tables.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/01-create-schemas.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/02-rls-functions.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/03-service-management-tables.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/03.5-customers-table.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/04-parts-management-tables.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/05-vehicle-management-tables.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/06-seed-data.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/07-notifications-schema.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/08-analytics-schema.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/09-purchasing-schema.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/10-warranty-claims.sql delete mode 100644 projects/erp-mecanicas-diesel/database/init/11-quote-signature.sql delete mode 100644 projects/erp-mecanicas-diesel/docker-compose.prod.yml delete mode 100644 projects/erp-mecanicas-diesel/docker-compose.yml delete mode 100644 projects/erp-mecanicas-diesel/docs/00-vision-general/VISION.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-001-configurar-taller.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-002-configurar-roles.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-003-catalogo-servicios.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-004-datos-fiscales.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-005-bahias-trabajo.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-006-rls-aislamiento.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-007-importar-catalogos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-008-cambiar-bahia.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-001-fundamentos/historias-usuario/US-MMD001-009-dashboard-uso.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-001-crear-orden.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-002-registrar-sintomas.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-003-asignar-orden.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-004-ver-ordenes-asignadas.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-005-registrar-trabajos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-006-solicitar-refacciones.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-007-tablero-kanban.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-008-cerrar-orden.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-009-notificar-cliente.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-010-historial-vehiculo.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-002-ordenes-servicio/historias-usuario/US-MMD002-011-estados-personalizados.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/historias-usuario/US-MMD003-001-diagnostico-computarizado.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/historias-usuario/US-MMD003-002-pruebas-inyectores.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/historias-usuario/US-MMD003-003-pruebas-bomba.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/historias-usuario/US-MMD003-004-comparar-referencias.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/historias-usuario/US-MMD003-005-adjuntar-fotos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/historias-usuario/US-MMD003-006-recomendaciones.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/historias-usuario/US-MMD003-007-historial-diagnosticos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-003-diagnosticos/historias-usuario/US-MMD003-008-configurar-pruebas.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-001-registrar-refacciones.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-002-consultar-stock.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-003-solicitar-refaccion.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-004-recibir-mercancia.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-005-ajustar-inventario.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-006-alertas-stock.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-007-kardex.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-008-codigos-alternos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-009-ubicaciones.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-004-inventario/historias-usuario/US-MMD004-010-inventario-fisico.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/historias-usuario/US-MMD005-001-registrar-vehiculo.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/historias-usuario/US-MMD005-002-editar-vehiculo.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/historias-usuario/US-MMD005-003-especificaciones-motor.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/historias-usuario/US-MMD005-004-ficha-tecnica.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/historias-usuario/US-MMD005-005-historial-servicios.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/historias-usuario/US-MMD005-006-flotas.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/historias-usuario/US-MMD005-007-recordatorios.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-005-vehiculos/historias-usuario/US-MMD005-008-importar-vehiculos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-006-cotizaciones/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-006-cotizaciones/historias-usuario/US-MMD006-001-crear-cotizacion.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-006-cotizaciones/historias-usuario/US-MMD006-002-agregar-lineas.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-006-cotizaciones/historias-usuario/US-MMD006-003-aplicar-descuentos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-006-cotizaciones/historias-usuario/US-MMD006-004-enviar-cotizacion.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-006-cotizaciones/historias-usuario/US-MMD006-005-generar-pdf.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-006-cotizaciones/historias-usuario/US-MMD006-006-convertir-orden.md delete mode 100644 projects/erp-mecanicas-diesel/docs/02-definicion-modulos/MMD-006-cotizaciones/historias-usuario/US-MMD006-007-historial-cotizaciones.md delete mode 100644 projects/erp-mecanicas-diesel/docs/03-modelo-datos/INTEGRACION-ERP-CORE.md delete mode 100644 projects/erp-mecanicas-diesel/docs/03-modelo-datos/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/03-modelo-datos/SCHEMA-PARTS-MANAGEMENT.md delete mode 100644 projects/erp-mecanicas-diesel/docs/03-modelo-datos/SCHEMA-SERVICE-MANAGEMENT.md delete mode 100644 projects/erp-mecanicas-diesel/docs/03-modelo-datos/SCHEMA-VEHICLE-MANAGEMENT.md delete mode 100644 projects/erp-mecanicas-diesel/docs/08-epicas/EPIC-MMD-001-fundamentos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/08-epicas/EPIC-MMD-002-ordenes-servicio.md delete mode 100644 projects/erp-mecanicas-diesel/docs/08-epicas/EPIC-MMD-003-diagnosticos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/08-epicas/EPIC-MMD-004-inventario.md delete mode 100644 projects/erp-mecanicas-diesel/docs/08-epicas/EPIC-MMD-005-vehiculos.md delete mode 100644 projects/erp-mecanicas-diesel/docs/08-epicas/EPIC-MMD-006-cotizaciones.md delete mode 100644 projects/erp-mecanicas-diesel/docs/08-epicas/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/90-transversal/ANALISIS-ARQUITECTONICO-IMPLEMENTACION.md delete mode 100644 projects/erp-mecanicas-diesel/docs/90-transversal/ANALISIS-INDEPENDENCIA-PROYECTO.md delete mode 100644 projects/erp-mecanicas-diesel/docs/90-transversal/PLAN-IMPLEMENTACION-2025-12.md delete mode 100644 projects/erp-mecanicas-diesel/docs/90-transversal/PLAN-RESOLUCION-GAPS.md delete mode 100644 projects/erp-mecanicas-diesel/docs/PLAN-DESARROLLO-MVP.md delete mode 100644 projects/erp-mecanicas-diesel/docs/README.md delete mode 100644 projects/erp-mecanicas-diesel/docs/REPORTE-VALIDACION-DOCUMENTACION.md delete mode 100644 projects/erp-mecanicas-diesel/docs/_MAP.md delete mode 100644 projects/erp-mecanicas-diesel/frontend/.env.example delete mode 100644 projects/erp-mecanicas-diesel/frontend/.gitignore delete mode 100644 projects/erp-mecanicas-diesel/frontend/README.md delete mode 100644 projects/erp-mecanicas-diesel/frontend/eslint.config.js delete mode 100644 projects/erp-mecanicas-diesel/frontend/index.html delete mode 100644 projects/erp-mecanicas-diesel/frontend/package-lock.json delete mode 100644 projects/erp-mecanicas-diesel/frontend/package.json delete mode 100644 projects/erp-mecanicas-diesel/frontend/postcss.config.js delete mode 100644 projects/erp-mecanicas-diesel/frontend/public/vite.svg delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/App.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/assets/react.svg delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/layout/Header.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/layout/MainLayout.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/layout/Sidebar.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/layout/index.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/ui/LoadingSpinner.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/ui/Modal.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/ui/StatusBadge.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/ui/Toast.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/components/ui/index.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/index.css delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/main.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/CustomerDetail.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Customers.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Dashboard.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/DiagnosticDetail.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Diagnostics.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/DiagnosticsNew.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Inventory.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/InventoryDetail.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Login.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/QuoteDetail.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Quotes.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Register.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/ServiceOrderDetail.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/ServiceOrderNew.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/ServiceOrders.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/ServiceOrdersKanban.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Settings.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Users.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/VehicleDetail.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/pages/Vehicles.tsx delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/auth.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/client.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/customers.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/diagnostics.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/index.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/parts.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/quotes.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/serviceOrders.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/settings.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/users.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/services/api/vehicles.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/store/authStore.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/store/tallerStore.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/store/toastStore.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/src/types/index.ts delete mode 100644 projects/erp-mecanicas-diesel/frontend/tailwind.config.js delete mode 100644 projects/erp-mecanicas-diesel/frontend/tsconfig.app.json delete mode 100644 projects/erp-mecanicas-diesel/frontend/tsconfig.json delete mode 100644 projects/erp-mecanicas-diesel/frontend/tsconfig.node.json delete mode 100644 projects/erp-mecanicas-diesel/frontend/vite.config.ts delete mode 100644 projects/erp-mecanicas-diesel/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/00-guidelines/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/00-guidelines/HERENCIA-SPECS-CORE.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/00-guidelines/HERENCIA-SPECS-ERP-CORE.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/directivas/DIRECTIVA-INVENTARIO-REFACCIONES.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/directivas/DIRECTIVA-ORDENES-TRABAJO.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/inventarios/DEPENDENCY_GRAPH.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/inventarios/README.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/inventarios/TRACEABILITY_MATRIX.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/prompts/PROMPT-MMD-BACKEND-AGENT.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/referencias/DEPENDENCIAS-ERP-CORE.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/referencias/DEPENDENCIAS-SHARED.yml delete mode 100644 projects/erp-mecanicas-diesel/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/erp-mecanicas-diesel/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/erp-retail/.env.example delete mode 100644 projects/erp-retail/INVENTARIO.yml delete mode 100644 projects/erp-retail/PROJECT-STATUS.md delete mode 100644 projects/erp-retail/backend/docs/SPRINT-4-SUMMARY.md delete mode 100644 projects/erp-retail/backend/docs/SPRINT-5-SUMMARY.md delete mode 100644 projects/erp-retail/backend/docs/SPRINT-6-SUMMARY.md delete mode 100644 projects/erp-retail/backend/package.json delete mode 100644 projects/erp-retail/backend/src/app.ts delete mode 100644 projects/erp-retail/backend/src/config/database.ts delete mode 100644 projects/erp-retail/backend/src/config/typeorm.ts delete mode 100644 projects/erp-retail/backend/src/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/controllers/branch.controller.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/entities/branch-user.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/entities/branch.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/entities/cash-register.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/routes/branch.routes.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/services/branch.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/services/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/branches/validation/branch.schema.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/controllers/cash.controller.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/entities/cash-closing.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/entities/cash-count.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/entities/cash-movement.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/routes/cash.routes.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/services/cash-closing.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/services/cash-movement.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/services/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/cash/validation/cash.schema.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/controllers/loyalty.controller.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/entities/customer-membership.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/entities/loyalty-program.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/entities/loyalty-transaction.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/entities/membership-level.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/routes/loyalty.routes.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/services/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/services/loyalty.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/customers/validation/customers.schema.ts delete mode 100644 projects/erp-retail/backend/src/modules/ecommerce/entities/cart-item.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/ecommerce/entities/cart.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/ecommerce/entities/ecommerce-order-line.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/ecommerce/entities/ecommerce-order.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/ecommerce/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/ecommerce/entities/shipping-rate.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/controllers/inventory.controller.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/entities/stock-adjustment-line.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/entities/stock-adjustment.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/entities/stock-transfer-line.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/entities/stock-transfer.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/routes/inventory.routes.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/services/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/services/stock-adjustment.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/services/stock-transfer.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/inventory/validation/inventory.schema.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/controllers/cfdi.controller.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/controllers/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/entities/cfdi-config.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/entities/cfdi.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/routes/cfdi.routes.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/routes/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/services/cfdi-builder.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/services/cfdi.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/services/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/services/pac.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/services/xml.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/validation/cfdi.schema.ts delete mode 100644 projects/erp-retail/backend/src/modules/invoicing/validation/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/controllers/pos.controller.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/entities/pos-order-line.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/entities/pos-order.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/entities/pos-payment.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/entities/pos-session.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/routes/pos.routes.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/services/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/services/pos-order.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/services/pos-session.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/pos/validation/pos.schema.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/controllers/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/controllers/pricing.controller.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/entities/coupon-redemption.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/entities/coupon.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/entities/promotion-product.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/entities/promotion.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/routes/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/routes/pricing.routes.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/services/coupon.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/services/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/services/price-engine.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/services/promotion.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/validation/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/pricing/validation/pricing.schema.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/controllers/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/controllers/purchases.controller.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/entities/goods-receipt-line.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/entities/goods-receipt.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/entities/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/entities/purchase-suggestion.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/entities/supplier-order-line.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/entities/supplier-order.entity.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/routes/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/routes/purchases.routes.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/services/goods-receipt.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/services/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/services/purchase-suggestion.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/services/supplier-order.service.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/validation/index.ts delete mode 100644 projects/erp-retail/backend/src/modules/purchases/validation/purchases.schema.ts delete mode 100644 projects/erp-retail/backend/src/shared/controllers/base.controller.ts delete mode 100644 projects/erp-retail/backend/src/shared/index.ts delete mode 100644 projects/erp-retail/backend/src/shared/middleware/auth.middleware.ts delete mode 100644 projects/erp-retail/backend/src/shared/middleware/branch.middleware.ts delete mode 100644 projects/erp-retail/backend/src/shared/middleware/index.ts delete mode 100644 projects/erp-retail/backend/src/shared/middleware/tenant.middleware.ts delete mode 100644 projects/erp-retail/backend/src/shared/services/base.service.ts delete mode 100644 projects/erp-retail/backend/src/shared/types/index.ts delete mode 100644 projects/erp-retail/backend/src/shared/validation/common.schema.ts delete mode 100644 projects/erp-retail/backend/src/shared/validation/index.ts delete mode 100644 projects/erp-retail/backend/src/shared/validation/validation.middleware.ts delete mode 100644 projects/erp-retail/backend/tsconfig.json delete mode 100644 projects/erp-retail/database/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-retail/database/README.md delete mode 100644 projects/erp-retail/database/init/00-extensions.sql delete mode 100644 projects/erp-retail/database/init/01-create-schemas.sql delete mode 100644 projects/erp-retail/database/init/02-rls-functions.sql delete mode 100644 projects/erp-retail/database/init/03-retail-tables.sql delete mode 100644 projects/erp-retail/docs/00-vision-general/VISION-RETAIL.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/INDICE-MODULOS.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-001-fundamentos/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-002-pos/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-003-inventario/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-004-compras/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-005-clientes/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-006-precios/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-007-caja/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-008-reportes/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-009-ecommerce/README.md delete mode 100644 projects/erp-retail/docs/02-definicion-modulos/RT-010-facturacion/README.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-001-fundamentos.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-002-pos.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-003-inventario.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-004-compras.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-005-clientes.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-006-precios.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-007-caja.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-008-reportes.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-009-ecommerce.md delete mode 100644 projects/erp-retail/docs/08-epicas/EPIC-RT-010-facturacion.md delete mode 100644 projects/erp-retail/docs/README.md delete mode 100644 projects/erp-retail/docs/_MAP.md delete mode 100644 projects/erp-retail/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-retail/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/erp-retail/orchestration/00-guidelines/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-retail/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/erp-retail/orchestration/00-guidelines/HERENCIA-SPECS-CORE.md delete mode 100644 projects/erp-retail/orchestration/00-guidelines/HERENCIA-SPECS-ERP-CORE.md delete mode 100644 projects/erp-retail/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/erp-retail/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/erp-retail/orchestration/directivas/DIRECTIVA-INVENTARIO-SUCURSALES.md delete mode 100644 projects/erp-retail/orchestration/directivas/DIRECTIVA-PUNTO-VENTA.md delete mode 100644 projects/erp-retail/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/erp-retail/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/erp-retail/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/erp-retail/orchestration/inventarios/DEPENDENCY_GRAPH.yml delete mode 100644 projects/erp-retail/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/erp-retail/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/erp-retail/orchestration/inventarios/README.md delete mode 100644 projects/erp-retail/orchestration/inventarios/TRACEABILITY_MATRIX.yml delete mode 100644 projects/erp-retail/orchestration/planes/PLAN-MAESTRO-MIGRACION-RETAIL.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-1-analisis/ANALISIS-ERP-CORE-INVENTARIO.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-1-analisis/ANALISIS-RETAIL-REQUERIMIENTOS.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-1-analisis/GAP-ANALYSIS-RETAIL.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-1-analisis/MATRIZ-MAPEO-CORE-RETAIL.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-001-fundamentos.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-002-pos.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-003-inventario.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-004-compras.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-005-clientes.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-006-precios.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-007-caja.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-008-reportes.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-009-ecommerce.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-2-analisis-modulos/ANALISIS-RT-010-facturacion.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-3-implementacion/PLAN-IMPL-BACKEND.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-3-implementacion/PLAN-IMPL-DATABASE.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-3-implementacion/PLAN-IMPL-FRONTEND.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-3-implementacion/ROADMAP-SPRINTS.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-4-validacion/DEPENDENCY-GRAPH-VALIDADO.yml delete mode 100644 projects/erp-retail/orchestration/planes/fase-4-validacion/IMPACTO-CAMBIOS.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-4-validacion/VALIDACION-COMPLETITUD.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-5-implementacion/SPRINT-1-SUMMARY.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-5-implementacion/SPRINT-2-SUMMARY.md delete mode 100644 projects/erp-retail/orchestration/planes/fase-5-implementacion/SPRINT-3-SUMMARY.md delete mode 100644 projects/erp-retail/orchestration/prompts/PROMPT-RT-BACKEND-AGENT.md delete mode 100644 projects/erp-retail/orchestration/referencias/DEPENDENCIAS-ERP-CORE.yml delete mode 100644 projects/erp-retail/orchestration/referencias/DEPENDENCIAS-SHARED.yml delete mode 100644 projects/erp-retail/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/erp-retail/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/erp-retail/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/erp-suite/DEPLOYMENT.md delete mode 100644 projects/erp-suite/INVENTARIO.yml delete mode 100644 projects/erp-suite/PURGE-LOG.yml delete mode 100644 projects/erp-suite/README.md delete mode 100644 projects/erp-suite/apps/products/erp-basico/README.md delete mode 100644 projects/erp-suite/apps/products/erp-basico/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-suite/apps/products/erp-basico/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/erp-suite/apps/products/pos-micro/README.md delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/.env.example delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/Dockerfile delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/nest-cli.json delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/package-lock.json delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/package.json delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/app.module.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/main.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/auth/auth.controller.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/auth/auth.module.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/auth/auth.service.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/auth/dto/register.dto.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/auth/entities/tenant.entity.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/auth/entities/user.entity.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/auth/guards/jwt-auth.guard.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/auth/strategies/jwt.strategy.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/categories/categories.controller.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/categories/categories.module.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/categories/categories.service.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/categories/entities/category.entity.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/payments/entities/payment-method.entity.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/payments/payments.controller.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/payments/payments.module.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/payments/payments.service.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/products/dto/product.dto.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/products/entities/product.entity.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/products/products.controller.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/products/products.module.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/products/products.service.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/sales/dto/sale.dto.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/sales/entities/sale-item.entity.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/sales/entities/sale.entity.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/sales/sales.controller.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/sales/sales.module.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/src/modules/sales/sales.service.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/backend/tsconfig.json delete mode 100644 projects/erp-suite/apps/products/pos-micro/database/ddl/00-schema.sql delete mode 100644 projects/erp-suite/apps/products/pos-micro/docker-compose.yml delete mode 100644 projects/erp-suite/apps/products/pos-micro/docs/ANALISIS-GAPS.md delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/.env.example delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/Dockerfile delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/index.html delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/nginx.conf delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/package-lock.json delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/package.json delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/postcss.config.js delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/App.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/components/CartPanel.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/components/CategoryTabs.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/components/CheckoutModal.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/components/Header.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/components/ProductCard.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/components/SuccessModal.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/usePayments.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useProducts.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useSales.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/main.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/pages/LoginPage.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/pages/POSPage.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/pages/ReportsPage.tsx delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/services/api.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/store/auth.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/store/cart.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/styles/index.css delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/types/index.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/src/vite-env.d.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/tailwind.config.js delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/tsconfig.json delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/tsconfig.node.json delete mode 100644 projects/erp-suite/apps/products/pos-micro/frontend/vite.config.ts delete mode 100644 projects/erp-suite/apps/products/pos-micro/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-suite/apps/products/pos-micro/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100755 projects/erp-suite/apps/products/pos-micro/scripts/dev.sh delete mode 100644 projects/erp-suite/apps/saas/README.md delete mode 100644 projects/erp-suite/apps/saas/billing/database/ddl/00-schema.sql delete mode 100644 projects/erp-suite/apps/saas/orchestration/CONTEXTO-SAAS.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/MIGRATION_GUIDE.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/constants/database.constants.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/database/policies/CENTRALIZATION-SUMMARY.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/database/policies/README.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/database/policies/apply-rls.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/database/policies/migration-example.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/database/policies/rls-policies.sql delete mode 100644 projects/erp-suite/apps/shared-libs/core/database/policies/usage-example.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/entities/base.entity.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/entities/tenant.entity.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/entities/user.entity.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/IMPLEMENTATION_SUMMARY.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/INTEGRATION_GUIDE.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/QUICK_REFERENCE.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/README.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/STRUCTURE.md delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/base-error.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/error-filter.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/error-middleware.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/express-integration.example.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/http-errors.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/index.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/errors/nestjs-integration.example.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/examples/user.repository.example.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/factories/repository.factory.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/index.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/interfaces/base-service.interface.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/interfaces/repository.interface.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/middleware/auth.middleware.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/middleware/tenant.middleware.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/services/auth.service.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/services/base-typeorm.service.ts delete mode 100644 projects/erp-suite/apps/shared-libs/core/types/pagination.types.ts delete mode 100644 projects/erp-suite/docker/docker-compose.prod.yml delete mode 100644 projects/erp-suite/docs/02-especificaciones-tecnicas/saas-platform/ANALISIS-REQUERIMIENTOS-SAAS-TRANSVERSALES.md delete mode 100644 projects/erp-suite/docs/02-especificaciones-tecnicas/saas-platform/arquitectura/ARQUITECTURA-INFRAESTRUCTURA-SAAS.md delete mode 100644 projects/erp-suite/docs/02-especificaciones-tecnicas/saas-platform/roadmap/ROADMAP-EPICAS-SAAS.md delete mode 100644 projects/erp-suite/docs/02-especificaciones-tecnicas/saas-platform/stripe/SPEC-STRIPE-INTEGRATION.md delete mode 100644 projects/erp-suite/docs/ANALISIS-ARQUITECTURA-ERP-SUITE.md delete mode 100644 projects/erp-suite/docs/ANALISIS-ESTRUCTURA-DOCUMENTACION.md delete mode 100644 projects/erp-suite/docs/ARCHITECTURE.md delete mode 100644 projects/erp-suite/docs/ESTANDAR-NOMENCLATURA-SCHEMAS.md delete mode 100644 projects/erp-suite/docs/ESTRUCTURA-DOCUMENTACION-ERP.md delete mode 100644 projects/erp-suite/docs/MULTI-TENANCY.md delete mode 100644 projects/erp-suite/docs/PLAN-MIGRACION-SCHEMAS.md delete mode 100644 projects/erp-suite/docs/REPORTE-ALINEACION-VERTICALES.md delete mode 100644 projects/erp-suite/docs/REPORTE-CUMPLIMIENTO-DIRECTIVAS-VERTICALES.md delete mode 100644 projects/erp-suite/docs/REPORTE-VALIDACION-DDL-VERTICALES.md delete mode 100644 projects/erp-suite/docs/VERTICAL-GUIDE.md delete mode 100644 projects/erp-suite/docs/_MAP.md delete mode 100644 projects/erp-suite/jenkins/Jenkinsfile delete mode 100644 projects/erp-suite/nginx/erp-suite.conf delete mode 100644 projects/erp-suite/nginx/erp.conf delete mode 100644 projects/erp-suite/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-suite/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/erp-suite/orchestration/00-guidelines/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-suite/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/erp-suite/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/erp-suite/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/erp-suite/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/erp-suite/orchestration/estados/REGISTRO-SUBAGENTES.json delete mode 100644 projects/erp-suite/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/erp-suite/orchestration/inventarios/REFERENCIAS.yml delete mode 100644 projects/erp-suite/orchestration/inventarios/STATUS.yml delete mode 100644 projects/erp-suite/orchestration/inventarios/SUITE_MASTER_INVENTORY.yml delete mode 100644 projects/erp-suite/orchestration/prompts/PROMPT-BACKEND-AGENT.md delete mode 100644 projects/erp-suite/orchestration/prompts/PROMPT-DATABASE-AGENT.md delete mode 100644 projects/erp-suite/orchestration/prompts/PROMPT-FRONTEND-AGENT.md delete mode 100644 projects/erp-suite/orchestration/trazas/TRAZA-SUITE.md delete mode 100644 projects/erp-suite/package.json delete mode 100755 projects/erp-suite/scripts/deploy.sh delete mode 100644 projects/erp-suite/scripts/deploy/Jenkinsfile.backend.example delete mode 100644 projects/erp-suite/scripts/deploy/Jenkinsfile.frontend.example delete mode 100644 projects/erp-suite/scripts/deploy/README.md delete mode 100755 projects/erp-suite/scripts/deploy/sync-to-deploy-repos.sh delete mode 100644 projects/erp-vidrio-templado/.env.example delete mode 100644 projects/erp-vidrio-templado/INVENTARIO.yml delete mode 100644 projects/erp-vidrio-templado/PROJECT-STATUS.md delete mode 100644 projects/erp-vidrio-templado/README.md delete mode 100644 projects/erp-vidrio-templado/database/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-vidrio-templado/database/README.md delete mode 100644 projects/erp-vidrio-templado/database/init/00-extensions.sql delete mode 100644 projects/erp-vidrio-templado/database/init/01-create-schemas.sql delete mode 100644 projects/erp-vidrio-templado/database/init/02-rls-functions.sql delete mode 100644 projects/erp-vidrio-templado/database/init/03-vidrio-tables.sql delete mode 100644 projects/erp-vidrio-templado/docs/00-vision-general/VISION-VIDRIO.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/INDICE-MODULOS.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/VT-001-fundamentos/README.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/VT-002-cotizaciones/README.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/VT-003-produccion/README.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/VT-004-inventario/README.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/VT-005-corte/README.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/VT-006-templado/README.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/VT-007-calidad/README.md delete mode 100644 projects/erp-vidrio-templado/docs/02-definicion-modulos/VT-008-despacho/README.md delete mode 100644 projects/erp-vidrio-templado/docs/08-epicas/EPIC-VT-001-fundamentos.md delete mode 100644 projects/erp-vidrio-templado/docs/08-epicas/EPIC-VT-002-cotizaciones.md delete mode 100644 projects/erp-vidrio-templado/docs/08-epicas/EPIC-VT-003-produccion.md delete mode 100644 projects/erp-vidrio-templado/docs/08-epicas/EPIC-VT-004-inventario.md delete mode 100644 projects/erp-vidrio-templado/docs/08-epicas/EPIC-VT-005-corte.md delete mode 100644 projects/erp-vidrio-templado/docs/08-epicas/EPIC-VT-006-templado.md delete mode 100644 projects/erp-vidrio-templado/docs/08-epicas/EPIC-VT-007-calidad.md delete mode 100644 projects/erp-vidrio-templado/docs/08-epicas/EPIC-VT-008-despacho.md delete mode 100644 projects/erp-vidrio-templado/docs/README.md delete mode 100644 projects/erp-vidrio-templado/docs/_MAP.md delete mode 100644 projects/erp-vidrio-templado/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/erp-vidrio-templado/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/erp-vidrio-templado/orchestration/00-guidelines/HERENCIA-ERP-CORE.md delete mode 100644 projects/erp-vidrio-templado/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/erp-vidrio-templado/orchestration/00-guidelines/HERENCIA-SPECS-CORE.md delete mode 100644 projects/erp-vidrio-templado/orchestration/00-guidelines/HERENCIA-SPECS-ERP-CORE.md delete mode 100644 projects/erp-vidrio-templado/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/erp-vidrio-templado/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/erp-vidrio-templado/orchestration/directivas/DIRECTIVA-CONTROL-CALIDAD.md delete mode 100644 projects/erp-vidrio-templado/orchestration/directivas/DIRECTIVA-PRODUCCION-VIDRIO.md delete mode 100644 projects/erp-vidrio-templado/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/inventarios/DEPENDENCY_GRAPH.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/inventarios/README.md delete mode 100644 projects/erp-vidrio-templado/orchestration/inventarios/TRACEABILITY_MATRIX.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/prompts/PROMPT-VT-BACKEND-AGENT.md delete mode 100644 projects/erp-vidrio-templado/orchestration/referencias/DEPENDENCIAS-ERP-CORE.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/referencias/DEPENDENCIAS-SHARED.yml delete mode 100644 projects/erp-vidrio-templado/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/erp-vidrio-templado/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/erp-vidrio-templado/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/inmobiliaria-analytics/.env.ports delete mode 100644 projects/inmobiliaria-analytics/INVENTARIO.yml delete mode 100644 projects/inmobiliaria-analytics/README.md delete mode 100644 projects/inmobiliaria-analytics/apps/backend/package.json delete mode 100644 projects/inmobiliaria-analytics/apps/backend/service.descriptor.yml delete mode 100644 projects/inmobiliaria-analytics/apps/backend/src/app.module.ts delete mode 100644 projects/inmobiliaria-analytics/apps/backend/src/config/index.ts delete mode 100644 projects/inmobiliaria-analytics/apps/backend/src/main.ts delete mode 100644 projects/inmobiliaria-analytics/apps/backend/src/modules/auth/auth.module.ts delete mode 100644 projects/inmobiliaria-analytics/apps/backend/src/shared/types/index.ts delete mode 100644 projects/inmobiliaria-analytics/apps/backend/tsconfig.json delete mode 100644 projects/inmobiliaria-analytics/docs/README.md delete mode 100644 projects/inmobiliaria-analytics/docs/_MAP.md delete mode 100644 projects/inmobiliaria-analytics/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/inmobiliaria-analytics/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/inmobiliaria-analytics/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/inmobiliaria-analytics/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/inmobiliaria-analytics/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/inmobiliaria-analytics/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/inmobiliaria-analytics/orchestration/estados/REGISTRO-SUBAGENTES.json delete mode 100644 projects/inmobiliaria-analytics/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/inmobiliaria-analytics/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/inmobiliaria-analytics/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/inmobiliaria-analytics/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/inmobiliaria-analytics/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/inmobiliaria-analytics/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/inmobiliaria-analytics/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/platform_marketing_content/.env.ports delete mode 100644 projects/platform_marketing_content/.github/CODEOWNERS delete mode 100755 projects/platform_marketing_content/.husky/commit-msg delete mode 100755 projects/platform_marketing_content/.husky/pre-commit delete mode 100644 projects/platform_marketing_content/CONTRIBUTING.md delete mode 100644 projects/platform_marketing_content/INVENTARIO.yml delete mode 100644 projects/platform_marketing_content/README.md delete mode 100644 projects/platform_marketing_content/apps/backend/.env.example delete mode 100644 projects/platform_marketing_content/apps/backend/.eslintrc.js delete mode 100644 projects/platform_marketing_content/apps/backend/.gitignore delete mode 100644 projects/platform_marketing_content/apps/backend/.prettierrc delete mode 100644 projects/platform_marketing_content/apps/backend/Dockerfile delete mode 100644 projects/platform_marketing_content/apps/backend/jest.config.ts delete mode 100644 projects/platform_marketing_content/apps/backend/nest-cli.json delete mode 100644 projects/platform_marketing_content/apps/backend/package-lock.json delete mode 100644 projects/platform_marketing_content/apps/backend/package.json delete mode 100644 projects/platform_marketing_content/apps/backend/service.descriptor.yml delete mode 100644 projects/platform_marketing_content/apps/backend/src/__tests__/setup.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/app.module.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/common/decorators/current-tenant.decorator.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/common/decorators/current-user.decorator.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/common/decorators/public.decorator.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/common/decorators/roles.decorator.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/common/filters/http-exception.filter.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/common/guards/jwt-auth.guard.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/common/guards/roles.guard.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/common/guards/tenant-member.guard.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/config/database.config.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/config/jwt.config.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/config/redis.config.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/config/storage.config.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/config/swagger.config.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/main.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/assets.module.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/controllers/asset.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/controllers/folder.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/controllers/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/dto/create-asset.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/dto/create-folder.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/dto/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/dto/update-asset.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/dto/update-folder.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/entities/asset-folder.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/entities/asset.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/entities/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/services/asset.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/services/folder.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/assets/services/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/__tests__/auth.service.spec.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/auth.module.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/controllers/auth.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/dto/auth-response.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/dto/login.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/dto/register.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/entities/session.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/entities/user.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/services/auth.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/auth/strategies/jwt.strategy.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/controllers/brand.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/controllers/client.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/controllers/product.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/crm.module.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/dto/create-brand.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/dto/create-client.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/dto/create-product.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/dto/update-brand.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/dto/update-client.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/dto/update-product.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/entities/brand.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/entities/client.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/entities/product.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/services/brand.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/services/client.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/crm/services/product.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/controllers/content-piece.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/controllers/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/controllers/project.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/dto/create-content-piece.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/dto/create-project.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/dto/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/dto/update-content-piece.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/dto/update-project.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/entities/content-piece.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/entities/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/entities/project.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/projects.module.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/services/content-piece.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/services/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/projects/services/project.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/tenants/controllers/tenants.controller.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/tenants/dto/create-tenant.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/tenants/dto/update-tenant.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/tenants/entities/tenant-plan.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/tenants/entities/tenant.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/tenants/services/tenants.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/modules/tenants/tenants.module.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/constants/database.constants.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/constants/enums.constants.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/constants/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/dto/pagination.dto.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/entities/tenant-aware.entity.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/repositories/base.repository.interface.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/repositories/index.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/repositories/repository.factory.ts delete mode 100644 projects/platform_marketing_content/apps/backend/src/shared/services/tenant-aware.service.ts delete mode 100644 projects/platform_marketing_content/apps/backend/tsconfig.json delete mode 100644 projects/platform_marketing_content/apps/frontend/.env.example delete mode 100644 projects/platform_marketing_content/apps/frontend/.gitignore delete mode 100644 projects/platform_marketing_content/apps/frontend/Dockerfile delete mode 100644 projects/platform_marketing_content/apps/frontend/index.html delete mode 100644 projects/platform_marketing_content/apps/frontend/nginx.conf delete mode 100644 projects/platform_marketing_content/apps/frontend/package-lock.json delete mode 100644 projects/platform_marketing_content/apps/frontend/package.json delete mode 100644 projects/platform_marketing_content/apps/frontend/postcss.config.js delete mode 100644 projects/platform_marketing_content/apps/frontend/src/App.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/common/Layout/AuthLayout.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/common/Layout/Header.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/common/Layout/MainLayout.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/common/Layout/Sidebar.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/button.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/card.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/form.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/input.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/label.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/select.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/switch.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/textarea.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/toast.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/components/ui/toaster.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/hooks/use-toast.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/hooks/useAssets.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/hooks/useBrands.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/hooks/useClients.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/hooks/useContentPieces.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/hooks/useFolders.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/hooks/useProducts.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/hooks/useProjects.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/index.css delete mode 100644 projects/platform_marketing_content/apps/frontend/src/lib/utils.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/main.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/assets/AssetsPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/auth/LoginPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/crm/BrandFormPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/crm/BrandsPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/crm/ClientFormPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/crm/ClientsPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/crm/ProductFormPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/crm/ProductsPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/dashboard/DashboardPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/pages/projects/ProjectsPage.tsx delete mode 100644 projects/platform_marketing_content/apps/frontend/src/services/api/assets.api.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/services/api/client.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/services/api/crm.api.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/services/api/projects.api.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/src/stores/useAuthStore.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/tailwind.config.js delete mode 100644 projects/platform_marketing_content/apps/frontend/tsconfig.json delete mode 100644 projects/platform_marketing_content/apps/frontend/tsconfig.node.json delete mode 100644 projects/platform_marketing_content/apps/frontend/vite.config.d.ts delete mode 100644 projects/platform_marketing_content/apps/frontend/vite.config.js delete mode 100644 projects/platform_marketing_content/apps/frontend/vite.config.ts delete mode 100644 projects/platform_marketing_content/commitlint.config.js delete mode 100644 projects/platform_marketing_content/database/schemas/001_initial_schema.sql delete mode 100644 projects/platform_marketing_content/database/seeds/001_initial_data.sql delete mode 100644 projects/platform_marketing_content/docker/docker-compose.prod.yml delete mode 100644 projects/platform_marketing_content/docs/00-vision-general/ARQUITECTURA-TECNICA.md delete mode 100644 projects/platform_marketing_content/docs/00-vision-general/GLOSARIO.md delete mode 100644 projects/platform_marketing_content/docs/00-vision-general/Investigaci贸n Profunda_ Plataforma de Generaci贸n de Contenido de Morfeo Academy y Desarrollo de una .pdf delete mode 100644 projects/platform_marketing_content/docs/00-vision-general/MVP_Plataforma_SaaS_Contenido_CRM.md delete mode 100644 projects/platform_marketing_content/docs/00-vision-general/VISION-GENERAL.md delete mode 100644 projects/platform_marketing_content/docs/00-vision-general/_MAP.md delete mode 100644 projects/platform_marketing_content/docs/01-analisis-referencias/ANALISIS-CATALOGO.md delete mode 100644 projects/platform_marketing_content/docs/01-analisis-referencias/ANALISIS-PROYECTOS-REFERENCIA.md delete mode 100644 projects/platform_marketing_content/docs/01-analisis-referencias/_INDEX.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/PMC-001-TENANTS.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/PMC-002-CRM.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/PMC-003-PROJECTS.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/PMC-004-GENERATION.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/PMC-005-AUTOMATION.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/PMC-006-ASSETS.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/PMC-007-ADMIN.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/PMC-008-ANALYTICS.md delete mode 100644 projects/platform_marketing_content/docs/02-definicion-modulos/_INDEX.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/RF-PMC-001-TENANTS.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/RF-PMC-002-CRM.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/RF-PMC-003-PROJECTS.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/RF-PMC-004-GENERATION.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/RF-PMC-005-AUTOMATION.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/RF-PMC-006-ASSETS.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/RF-PMC-007-ADMIN.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/RF-PMC-008-ANALYTICS.md delete mode 100644 projects/platform_marketing_content/docs/03-requerimientos/_INDEX.md delete mode 100644 projects/platform_marketing_content/docs/04-modelado/ESQUEMA-BD.md delete mode 100644 projects/platform_marketing_content/docs/04-modelado/MODELO-DOMINIO.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/EPIC-001-SETUP.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/EPIC-002-CRM.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/EPIC-003-PROJECTS.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/EPIC-004-GENERATION.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/EPIC-005-ASSETS.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/EPIC-006-AUTOMATION.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/EPIC-007-ANALYTICS.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/EPIC-008-ADMIN.md delete mode 100644 projects/platform_marketing_content/docs/05-user-stories/_INDEX.md delete mode 100644 projects/platform_marketing_content/docs/90-transversal/README.md delete mode 100644 projects/platform_marketing_content/docs/90-transversal/reportes-implementacion/AUDITORIA-DOCUMENTACION-PMC.md delete mode 100644 projects/platform_marketing_content/docs/90-transversal/roadmap/ROADMAP-PMC.md delete mode 100644 projects/platform_marketing_content/docs/95-guias-desarrollo/GUIA-CONVENCIONES.md delete mode 100644 projects/platform_marketing_content/docs/95-guias-desarrollo/GUIA-SETUP.md delete mode 100644 projects/platform_marketing_content/docs/97-adr/ADR-001-stack-tecnologico.md delete mode 100644 projects/platform_marketing_content/docs/97-adr/ADR-002-multi-tenancy.md delete mode 100644 projects/platform_marketing_content/docs/97-adr/ADR-003-motor-generacion.md delete mode 100644 projects/platform_marketing_content/docs/97-adr/ADR-004-cola-tareas.md delete mode 100644 projects/platform_marketing_content/docs/97-adr/_INDEX.md delete mode 100644 projects/platform_marketing_content/docs/ARCHITECTURE.md delete mode 100644 projects/platform_marketing_content/docs/CMS-GUIDE.md delete mode 100644 projects/platform_marketing_content/docs/_MAP.md delete mode 100644 projects/platform_marketing_content/jenkins/Jenkinsfile delete mode 100644 projects/platform_marketing_content/lint-staged.config.js delete mode 100644 projects/platform_marketing_content/nginx/pmc.conf delete mode 100644 projects/platform_marketing_content/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/platform_marketing_content/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/platform_marketing_content/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/platform_marketing_content/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/platform_marketing_content/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/platform_marketing_content/orchestration/directivas/DIRECTIVA-ARQUITECTURA-MULTI-TENANT.md delete mode 100644 projects/platform_marketing_content/orchestration/directivas/DIRECTIVA-GENERACION-IA-PMC.md delete mode 100644 projects/platform_marketing_content/orchestration/directivas/GUIA-NOMENCLATURA-PMC.md delete mode 100644 projects/platform_marketing_content/orchestration/estados/ESTADO-GENERAL.json delete mode 100644 projects/platform_marketing_content/orchestration/estados/REGISTRO-SUBAGENTES.json delete mode 100644 projects/platform_marketing_content/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/platform_marketing_content/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/platform_marketing_content/orchestration/inventarios/DEPENDENCY_GRAPH.yml delete mode 100644 projects/platform_marketing_content/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/platform_marketing_content/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/platform_marketing_content/orchestration/inventarios/TRACEABILITY_MATRIX.yml delete mode 100644 projects/platform_marketing_content/orchestration/prompts/PROMPT-BACKEND-PMC.md delete mode 100644 projects/platform_marketing_content/orchestration/prompts/PROMPT-DATABASE-PMC.md delete mode 100644 projects/platform_marketing_content/orchestration/prompts/PROMPT-FRONTEND-PMC.md delete mode 100644 projects/platform_marketing_content/orchestration/prompts/PROMPT-GENERATION-PMC.md delete mode 100644 projects/platform_marketing_content/orchestration/prompts/PROMPT-ORQUESTADOR-PMC.md delete mode 100644 projects/platform_marketing_content/orchestration/prompts/_INDEX.md delete mode 100644 projects/platform_marketing_content/orchestration/trazas/TRAZA-2025-12-08-DOCUMENTACION-COMPLETA.md delete mode 100644 projects/platform_marketing_content/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/platform_marketing_content/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/platform_marketing_content/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/platform_marketing_content/package.json delete mode 100644 projects/trading-platform/INVENTARIO.yml delete mode 100644 projects/trading-platform/README.md delete mode 100644 projects/trading-platform/apps/backend/.env.example delete mode 100644 projects/trading-platform/apps/backend/Dockerfile delete mode 100644 projects/trading-platform/apps/backend/WEBSOCKET_IMPLEMENTATION_REPORT.md delete mode 100644 projects/trading-platform/apps/backend/WEBSOCKET_TESTING.md delete mode 100644 projects/trading-platform/apps/backend/eslint.config.js delete mode 100644 projects/trading-platform/apps/backend/jest.config.ts delete mode 100644 projects/trading-platform/apps/backend/package-lock.json delete mode 100644 projects/trading-platform/apps/backend/package.json delete mode 100644 projects/trading-platform/apps/backend/service.descriptor.yml delete mode 100644 projects/trading-platform/apps/backend/src/__tests__/jest-migration.test.ts delete mode 100644 projects/trading-platform/apps/backend/src/__tests__/mocks/database.mock.ts delete mode 100644 projects/trading-platform/apps/backend/src/__tests__/mocks/email.mock.ts delete mode 100644 projects/trading-platform/apps/backend/src/__tests__/mocks/redis.mock.ts delete mode 100644 projects/trading-platform/apps/backend/src/__tests__/setup.ts delete mode 100644 projects/trading-platform/apps/backend/src/config/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/config/swagger.config.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/filters/http-exception.filter.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/filters/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/guards/auth.guard.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/guards/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/interceptors/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/interceptors/transform-response.interceptor.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/middleware/auth.middleware.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/middleware/error-handler.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/middleware/not-found.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/middleware/rate-limiter.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/websocket/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/websocket/trading-stream.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/core/websocket/websocket.server.ts delete mode 100644 projects/trading-platform/apps/backend/src/docs/openapi.yaml delete mode 100644 projects/trading-platform/apps/backend/src/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/admin/admin.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/agents/agents.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/agents/controllers/agents.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/agents/services/agents.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/auth.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/controllers/auth.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/controllers/email-auth.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/controllers/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/controllers/oauth.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/controllers/phone-auth.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/controllers/token.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/controllers/two-factor.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/dto/change-password.dto.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/dto/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/dto/login.dto.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/dto/oauth.dto.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/dto/refresh-token.dto.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/dto/register.dto.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/services/__tests__/email.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/services/__tests__/token.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/services/email.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/services/oauth.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/services/phone.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/services/token.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/services/twofa.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/stores/__tests__/oauth-state.store.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/stores/oauth-state.store.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/types/auth.types.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/auth/validators/auth.validators.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/education/controllers/education.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/education/education.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/education/services/course.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/education/services/enrollment.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/education/types/education.types.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/investment/controllers/investment.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/investment/investment.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/investment/services/__tests__/account.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/investment/services/__tests__/product.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/investment/services/__tests__/transaction.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/investment/services/account.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/investment/services/product.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/investment/services/transaction.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/llm/controllers/llm.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/llm/llm.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/llm/services/llm.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/ml/controllers/ml-overlay.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/ml/controllers/ml.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/ml/ml.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/ml/services/ml-integration.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/ml/services/ml-overlay.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/payments/controllers/payments.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/payments/payments.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/payments/services/stripe.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/payments/services/subscription.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/payments/services/wallet.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/payments/types/payments.types.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/portfolio/controllers/portfolio.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/portfolio/portfolio.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/portfolio/services/__tests__/portfolio.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/portfolio/services/portfolio.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/controllers/alerts.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/controllers/indicators.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/controllers/paper-trading.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/controllers/trading.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/controllers/watchlist.controller.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/__tests__/alerts.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/__tests__/paper-trading.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/__tests__/watchlist.service.spec.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/alerts.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/binance.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/cache.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/indicators.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/market.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/paper-trading.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/services/watchlist.service.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/trading.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/trading/types/market.types.ts delete mode 100644 projects/trading-platform/apps/backend/src/modules/users/users.routes.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/clients/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/clients/llm-agent.client.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/clients/ml-engine.client.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/clients/trading-agents.client.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/constants/database.constants.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/constants/enums.constants.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/constants/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/constants/routes.constants.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/database/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/factories/MIGRATION_GUIDE.md delete mode 100644 projects/trading-platform/apps/backend/src/shared/factories/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/factories/service.factory.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/interfaces/README.md delete mode 100644 projects/trading-platform/apps/backend/src/shared/interfaces/cache.interface.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/interfaces/http-client.interface.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/interfaces/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/interfaces/services/auth.interface.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/interfaces/services/trading.interface.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/middleware/validate-dto.middleware.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/types/common.types.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/types/index.ts delete mode 100644 projects/trading-platform/apps/backend/src/shared/utils/logger.ts delete mode 100644 projects/trading-platform/apps/backend/test-websocket.html delete mode 100644 projects/trading-platform/apps/backend/test-websocket.js delete mode 100644 projects/trading-platform/apps/backend/tsconfig.json delete mode 100644 projects/trading-platform/apps/data-service/.env.example delete mode 100644 projects/trading-platform/apps/data-service/ARCHITECTURE.md delete mode 100644 projects/trading-platform/apps/data-service/Dockerfile delete mode 100644 projects/trading-platform/apps/data-service/IMPLEMENTATION_SUMMARY.md delete mode 100644 projects/trading-platform/apps/data-service/README.md delete mode 100644 projects/trading-platform/apps/data-service/README_SYNC.md delete mode 100644 projects/trading-platform/apps/data-service/TECH_LEADER_REPORT.md delete mode 100644 projects/trading-platform/apps/data-service/docker-compose.yml delete mode 100644 projects/trading-platform/apps/data-service/environment.yml delete mode 100755 projects/trading-platform/apps/data-service/examples/api_examples.sh delete mode 100644 projects/trading-platform/apps/data-service/examples/sync_example.py delete mode 100644 projects/trading-platform/apps/data-service/migrations/002_sync_status.sql delete mode 100644 projects/trading-platform/apps/data-service/requirements.txt delete mode 100644 projects/trading-platform/apps/data-service/requirements_sync.txt delete mode 100644 projects/trading-platform/apps/data-service/src/__init__.py delete mode 100644 projects/trading-platform/apps/data-service/src/api/__init__.py delete mode 100644 projects/trading-platform/apps/data-service/src/api/dependencies.py delete mode 100644 projects/trading-platform/apps/data-service/src/api/mt4_routes.py delete mode 100644 projects/trading-platform/apps/data-service/src/api/routes.py delete mode 100644 projects/trading-platform/apps/data-service/src/api/sync_routes.py delete mode 100644 projects/trading-platform/apps/data-service/src/app.py delete mode 100644 projects/trading-platform/apps/data-service/src/app_updated.py delete mode 100644 projects/trading-platform/apps/data-service/src/config.py delete mode 100644 projects/trading-platform/apps/data-service/src/main.py delete mode 100644 projects/trading-platform/apps/data-service/src/models/market.py delete mode 100644 projects/trading-platform/apps/data-service/src/providers/__init__.py delete mode 100644 projects/trading-platform/apps/data-service/src/providers/binance_client.py delete mode 100644 projects/trading-platform/apps/data-service/src/providers/metaapi_client.py delete mode 100644 projects/trading-platform/apps/data-service/src/providers/mt4_client.py delete mode 100644 projects/trading-platform/apps/data-service/src/providers/polygon_client.py delete mode 100644 projects/trading-platform/apps/data-service/src/services/__init__.py delete mode 100644 projects/trading-platform/apps/data-service/src/services/price_adjustment.py delete mode 100644 projects/trading-platform/apps/data-service/src/services/scheduler.py delete mode 100644 projects/trading-platform/apps/data-service/src/services/sync_service.py delete mode 100644 projects/trading-platform/apps/data-service/src/websocket/__init__.py delete mode 100644 projects/trading-platform/apps/data-service/src/websocket/handlers.py delete mode 100644 projects/trading-platform/apps/data-service/src/websocket/manager.py delete mode 100644 projects/trading-platform/apps/data-service/tests/__init__.py delete mode 100644 projects/trading-platform/apps/data-service/tests/conftest.py delete mode 100644 projects/trading-platform/apps/data-service/tests/test_polygon_client.py delete mode 100644 projects/trading-platform/apps/data-service/tests/test_sync_service.py delete mode 100644 projects/trading-platform/apps/database/DIRECTIVA-POLITICA-CARGA-LIMPIA.md delete mode 100644 projects/trading-platform/apps/database/ddl/00-extensions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/01-schemas.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/audit/00-enums.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/audit/tables/01-audit_logs.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/audit/tables/02-security_events.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/audit/tables/03-system_events.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/audit/tables/04-trading_audit.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/audit/tables/05-api_request_logs.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/audit/tables/06-data_access_logs.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/audit/tables/07-compliance_logs.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/00-extensions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/01-enums.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/functions/01-update_updated_at.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/functions/02-log_auth_event.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/functions/03-cleanup_expired_sessions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/functions/04-create_user_profile_trigger.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/01-users.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/02-user_profiles.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/03-oauth_accounts.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/04-sessions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/05-email_verifications.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/06-phone_verifications.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/07-password_reset_tokens.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/08-auth_logs.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/09-login_attempts.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/auth/tables/10-rate_limiting_config.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/00-enums.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/README.md delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/TECHNICAL.md delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/functions/01-update_updated_at.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/functions/02-update_enrollment_progress.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/functions/03-auto_complete_enrollment.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/functions/04-generate_certificate.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/functions/05-update_course_stats.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/functions/06-update_enrollment_count.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/functions/07-update_gamification_profile.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/functions/08-views.sql delete mode 100755 projects/trading-platform/apps/database/ddl/schemas/education/install.sh delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/seeds-example.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/01-categories.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/02-courses.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/03-modules.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/04-lessons.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/05-enrollments.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/06-progress.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/07-quizzes.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/08-quiz_questions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/09-quiz_attempts.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/10-certificates.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/11-user_achievements.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/12-user_gamification_profile.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/13-user_activity_log.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/education/tables/14-course_reviews.sql delete mode 100755 projects/trading-platform/apps/database/ddl/schemas/education/uninstall.sh delete mode 100755 projects/trading-platform/apps/database/ddl/schemas/education/verify.sh delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/00-enums.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/functions/01-update_wallet_balance.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/functions/02-process_transaction.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/functions/03-triggers.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/functions/04-views.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/01-wallets.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/02-wallet_transactions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/03-subscriptions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/04-payments.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/05-invoices.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/06-wallet_audit_log.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/07-currency_exchange_rates.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/08-wallet_limits.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/09-customers.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/financial/tables/10-payment_methods.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/investment/00-enums.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/investment/tables/01-products.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/investment/tables/02-accounts.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/investment/tables/03-transactions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/investment/tables/04-distributions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/investment/tables/05-risk_questionnaire.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/investment/tables/06-withdrawal_requests.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/investment/tables/07-daily_performance.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/llm/00-enums.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/llm/tables/01-conversations.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/llm/tables/02-messages.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/llm/tables/03-user_preferences.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/llm/tables/04-user_memory.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/llm/tables/05-embeddings.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/ml/00-enums.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/ml/tables/01-models.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/ml/tables/02-model_versions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/ml/tables/03-predictions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/ml/tables/04-prediction_outcomes.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/ml/tables/05-feature_store.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/00-enums.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/functions/01-calculate_position_pnl.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/functions/02-update_bot_stats.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/functions/03-initialize_paper_balance.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/functions/04-create_default_watchlist.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/01-symbols.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/02-watchlists.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/03-watchlist_items.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/04-bots.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/05-orders.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/06-positions.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/07-trades.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/08-signals.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/09-trading_metrics.sql delete mode 100644 projects/trading-platform/apps/database/ddl/schemas/trading/tables/10-paper_balances.sql delete mode 100644 projects/trading-platform/apps/database/schemas/00_init_schemas.sql delete mode 100644 projects/trading-platform/apps/database/schemas/01_public_schema.sql delete mode 100644 projects/trading-platform/apps/database/schemas/01b_oauth_providers.sql delete mode 100644 projects/trading-platform/apps/database/schemas/02_education_schema.sql delete mode 100644 projects/trading-platform/apps/database/schemas/03_trading_schema.sql delete mode 100644 projects/trading-platform/apps/database/schemas/04_investment_schema.sql delete mode 100644 projects/trading-platform/apps/database/schemas/05_financial_schema.sql delete mode 100644 projects/trading-platform/apps/database/schemas/06_ml_schema.sql delete mode 100644 projects/trading-platform/apps/database/schemas/07_audit_schema.sql delete mode 100644 projects/trading-platform/apps/database/schemas/_MAP.md delete mode 100755 projects/trading-platform/apps/database/scripts/create-database.sh delete mode 100755 projects/trading-platform/apps/database/scripts/drop-and-recreate-database.sh delete mode 100755 projects/trading-platform/apps/database/scripts/migrate_all_tickers.sh delete mode 100755 projects/trading-platform/apps/database/scripts/migrate_direct.sh delete mode 100644 projects/trading-platform/apps/database/scripts/migrate_mysql_to_postgres.py delete mode 100644 projects/trading-platform/apps/database/scripts/validate-ddl.sh delete mode 100644 projects/trading-platform/apps/frontend/.env.example delete mode 100644 projects/trading-platform/apps/frontend/.eslintrc.cjs delete mode 100644 projects/trading-platform/apps/frontend/Dockerfile delete mode 100644 projects/trading-platform/apps/frontend/ML_DASHBOARD_IMPLEMENTATION.md delete mode 100644 projects/trading-platform/apps/frontend/eslint.config.js delete mode 100644 projects/trading-platform/apps/frontend/index.html delete mode 100644 projects/trading-platform/apps/frontend/nginx.conf delete mode 100644 projects/trading-platform/apps/frontend/package-lock.json delete mode 100644 projects/trading-platform/apps/frontend/package.json delete mode 100644 projects/trading-platform/apps/frontend/postcss.config.js delete mode 100644 projects/trading-platform/apps/frontend/src/App.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/__tests__/mlService.test.ts delete mode 100644 projects/trading-platform/apps/frontend/src/__tests__/tradingService.test.ts delete mode 100644 projects/trading-platform/apps/frontend/src/components/chat/ChatInput.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/components/chat/ChatMessage.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/components/chat/ChatPanel.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/components/chat/ChatWidget.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/components/chat/index.ts delete mode 100644 projects/trading-platform/apps/frontend/src/components/layout/AuthLayout.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/components/layout/MainLayout.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/hooks/index.ts delete mode 100644 projects/trading-platform/apps/frontend/src/hooks/useMLAnalysis.ts delete mode 100644 projects/trading-platform/apps/frontend/src/main.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/admin/components/AgentStatsCard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/admin/components/MLModelCard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/admin/components/index.ts delete mode 100644 projects/trading-platform/apps/frontend/src/modules/admin/pages/AdminDashboard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/admin/pages/AgentsPage.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/admin/pages/MLModelsPage.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/admin/pages/PredictionsPage.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/admin/pages/index.ts delete mode 100644 projects/trading-platform/apps/frontend/src/modules/assistant/components/ChatInput.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/assistant/components/ChatMessage.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/assistant/components/SignalCard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/assistant/pages/Assistant.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/auth/components/PhoneLoginForm.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/auth/components/SocialLoginButtons.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/auth/pages/AuthCallback.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/auth/pages/ForgotPassword.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/auth/pages/Login.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/auth/pages/Register.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/auth/pages/ResetPassword.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/auth/pages/VerifyEmail.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/backtesting/components/EquityCurveChart.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/backtesting/components/PerformanceMetricsPanel.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/backtesting/components/PredictionChart.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/backtesting/components/StrategyComparisonChart.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/backtesting/components/TradesTable.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/backtesting/components/index.ts delete mode 100644 projects/trading-platform/apps/frontend/src/modules/backtesting/pages/BacktestingDashboard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/dashboard/pages/Dashboard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/education/pages/CourseDetail.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/education/pages/Courses.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/investment/pages/Investment.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/investment/pages/Portfolio.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/investment/pages/Products.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/README.md delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/USAGE_EXAMPLES.md delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/VALIDATION_CHECKLIST.md delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/components/AMDPhaseIndicator.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/components/AccuracyMetrics.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/components/EnsembleSignalCard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/components/ICTAnalysisCard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/components/PredictionCard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/components/SignalsTimeline.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/components/TradeExecutionModal.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/components/index.ts delete mode 100644 projects/trading-platform/apps/frontend/src/modules/ml/pages/MLDashboard.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/settings/pages/Settings.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/AccountSummary.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/AddSymbolModal.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/CandlestickChart.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/ChartToolbar.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/MLSignalsPanel.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/OrderForm.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/PaperTradingPanel.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/PositionsList.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/TradesHistory.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/TradingChart.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/WatchlistItem.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/components/WatchlistSidebar.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/modules/trading/pages/Trading.tsx delete mode 100644 projects/trading-platform/apps/frontend/src/services/adminService.ts delete mode 100644 projects/trading-platform/apps/frontend/src/services/backtestService.ts delete mode 100644 projects/trading-platform/apps/frontend/src/services/chat.service.ts delete mode 100644 projects/trading-platform/apps/frontend/src/services/mlService.ts delete mode 100644 projects/trading-platform/apps/frontend/src/services/trading.service.ts delete mode 100644 projects/trading-platform/apps/frontend/src/services/websocket.service.ts delete mode 100644 projects/trading-platform/apps/frontend/src/stores/chatStore.ts delete mode 100644 projects/trading-platform/apps/frontend/src/stores/tradingStore.ts delete mode 100644 projects/trading-platform/apps/frontend/src/styles/index.css delete mode 100644 projects/trading-platform/apps/frontend/src/types/chat.types.ts delete mode 100644 projects/trading-platform/apps/frontend/src/types/trading.types.ts delete mode 100644 projects/trading-platform/apps/frontend/src/vite-env.d.ts delete mode 100644 projects/trading-platform/apps/frontend/tailwind.config.js delete mode 100644 projects/trading-platform/apps/frontend/tsconfig.json delete mode 100644 projects/trading-platform/apps/frontend/tsconfig.node.json delete mode 100644 projects/trading-platform/apps/frontend/vite.config.ts delete mode 100644 projects/trading-platform/apps/llm-agent/.env.example delete mode 100644 projects/trading-platform/apps/llm-agent/AUTO_TRADING.md delete mode 100644 projects/trading-platform/apps/llm-agent/DEPLOYMENT.md delete mode 100644 projects/trading-platform/apps/llm-agent/Dockerfile delete mode 100644 projects/trading-platform/apps/llm-agent/IMPLEMENTATION_SUMMARY.md delete mode 100644 projects/trading-platform/apps/llm-agent/README.md delete mode 100644 projects/trading-platform/apps/llm-agent/docker-compose.ollama.yml delete mode 100644 projects/trading-platform/apps/llm-agent/environment.yml delete mode 100755 projects/trading-platform/apps/llm-agent/examples/auto_trading_example.py delete mode 100644 projects/trading-platform/apps/llm-agent/pyproject.toml delete mode 100644 projects/trading-platform/apps/llm-agent/requirements.txt delete mode 100644 projects/trading-platform/apps/llm-agent/src/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/api/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/api/auto_trade_routes.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/api/routes.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/clients/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/clients/mt4_client.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/config.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/core/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/core/context_manager.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/core/llm_client.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/core/prompt_manager.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/main.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/models/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/models/auto_trade.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/prompts/analysis.txt delete mode 100644 projects/trading-platform/apps/llm-agent/src/prompts/strategy.txt delete mode 100644 projects/trading-platform/apps/llm-agent/src/prompts/system.txt delete mode 100644 projects/trading-platform/apps/llm-agent/src/prompts/trade_execution.txt delete mode 100644 projects/trading-platform/apps/llm-agent/src/repositories/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/services/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/services/auto_trade_service.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/auto_trading.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/base.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/education.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/ml_tools.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/mt4_tools.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/portfolio.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/signals.py delete mode 100644 projects/trading-platform/apps/llm-agent/src/tools/trading.py delete mode 100644 projects/trading-platform/apps/llm-agent/tests/__init__.py delete mode 100644 projects/trading-platform/apps/llm-agent/tests/conftest.py delete mode 100644 projects/trading-platform/apps/llm-agent/tests/test_auto_trading.py delete mode 100644 projects/trading-platform/apps/llm-agent/tests/test_mt4_integration.py delete mode 100644 projects/trading-platform/apps/ml-engine/.env.example delete mode 100644 projects/trading-platform/apps/ml-engine/Dockerfile delete mode 100644 projects/trading-platform/apps/ml-engine/MIGRATION_REPORT.md delete mode 100644 projects/trading-platform/apps/ml-engine/config/database.yaml delete mode 100644 projects/trading-platform/apps/ml-engine/config/models.yaml delete mode 100644 projects/trading-platform/apps/ml-engine/config/phase2.yaml delete mode 100644 projects/trading-platform/apps/ml-engine/config/trading.yaml delete mode 100644 projects/trading-platform/apps/ml-engine/environment.yml delete mode 100644 projects/trading-platform/apps/ml-engine/pytest.ini delete mode 100644 projects/trading-platform/apps/ml-engine/requirements.txt delete mode 100644 projects/trading-platform/apps/ml-engine/src/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/api/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/api/main.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/backtesting/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/backtesting/engine.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/backtesting/metrics.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/backtesting/rr_backtester.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/models/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/models/amd_detector.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/models/amd_models.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/models/ict_smc_detector.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/models/range_predictor.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/models/signal_generator.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/models/strategy_ensemble.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/models/tp_sl_classifier.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/pipelines/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/pipelines/phase2_pipeline.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/services/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/services/prediction_service.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/training/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/training/walk_forward.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/utils/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/utils/audit.py delete mode 100644 projects/trading-platform/apps/ml-engine/src/utils/signal_logger.py delete mode 100644 projects/trading-platform/apps/ml-engine/tests/__init__.py delete mode 100644 projects/trading-platform/apps/ml-engine/tests/test_amd_detector.py delete mode 100644 projects/trading-platform/apps/ml-engine/tests/test_api.py delete mode 100644 projects/trading-platform/apps/ml-engine/tests/test_ict_detector.py delete mode 100644 projects/trading-platform/apps/mt4-gateway/.env.example delete mode 100644 projects/trading-platform/apps/mt4-gateway/config/agents.yml delete mode 100644 projects/trading-platform/apps/mt4-gateway/requirements.txt delete mode 100644 projects/trading-platform/apps/mt4-gateway/src/__init__.py delete mode 100644 projects/trading-platform/apps/mt4-gateway/src/main.py delete mode 100644 projects/trading-platform/apps/mt4-gateway/src/providers/__init__.py delete mode 100644 projects/trading-platform/apps/mt4-gateway/src/providers/mt4_bridge_client.py delete mode 100644 projects/trading-platform/apps/mt4-gateway/src/services/__init__.py delete mode 100644 projects/trading-platform/apps/personal/.env.example delete mode 100644 projects/trading-platform/apps/personal/config.yaml delete mode 100644 projects/trading-platform/apps/personal/package.json delete mode 100644 projects/trading-platform/apps/personal/scripts/setup-personal.ts delete mode 100644 projects/trading-platform/apps/personal/scripts/validate-config.ts delete mode 100644 projects/trading-platform/apps/trading-agents/.env.example delete mode 100644 projects/trading-platform/apps/trading-agents/Dockerfile delete mode 100644 projects/trading-platform/apps/trading-agents/IMPLEMENTATION_REPORT.md delete mode 100644 projects/trading-platform/apps/trading-agents/INTEGRATION.md delete mode 100644 projects/trading-platform/apps/trading-agents/PAPER_TRADING_GUIDE.md delete mode 100644 projects/trading-platform/apps/trading-agents/README.md delete mode 100644 projects/trading-platform/apps/trading-agents/config/agents.yaml delete mode 100644 projects/trading-platform/apps/trading-agents/config/risk.yaml delete mode 100644 projects/trading-platform/apps/trading-agents/config/strategies.yaml delete mode 100644 projects/trading-platform/apps/trading-agents/docker-compose.yml delete mode 100644 projects/trading-platform/apps/trading-agents/example_usage.py delete mode 100644 projects/trading-platform/apps/trading-agents/requirements.txt delete mode 100644 projects/trading-platform/apps/trading-agents/src/__init__.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/agents/__init__.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/agents/atlas.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/agents/base.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/agents/nova.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/agents/orion.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/api/main.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/exchange/__init__.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/exchange/binance_client.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/execution/__init__.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/execution/risk_manager.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/signals/__init__.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/signals/ml_consumer.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/strategies/__init__.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/strategies/base.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/strategies/grid_trading.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/strategies/mean_reversion.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/strategies/momentum.py delete mode 100644 projects/trading-platform/apps/trading-agents/src/strategies/trend_following.py delete mode 100644 projects/trading-platform/docker-compose.services.yml delete mode 100644 projects/trading-platform/docker-compose.yml delete mode 100644 projects/trading-platform/docker/docker-compose.prod.yml delete mode 100644 projects/trading-platform/docs/00-notas/NOTA-DISCREPANCIA-PUERTOS-2025-12-08.md delete mode 100644 projects/trading-platform/docs/00-vision-general/ARQUITECTURA-GENERAL.md delete mode 100644 projects/trading-platform/docs/00-vision-general/STACK-TECNOLOGICO.md delete mode 100644 projects/trading-platform/docs/00-vision-general/Uso de la API de Massive.com para obtener datos financieros.pdf delete mode 100644 projects/trading-platform/docs/00-vision-general/VISION-PRODUCTO.md delete mode 100644 projects/trading-platform/docs/00-vision-general/_MAP.md delete mode 100644 projects/trading-platform/docs/01-arquitectura/ARQUITECTURA-MULTI-AGENTE-MT4.md delete mode 100644 projects/trading-platform/docs/01-arquitectura/ARQUITECTURA-UNIFICADA.md delete mode 100644 projects/trading-platform/docs/01-arquitectura/DIAGRAMA-INTEGRACIONES.md delete mode 100644 projects/trading-platform/docs/01-arquitectura/INTEGRACION-API-MASSIVE.md delete mode 100644 projects/trading-platform/docs/01-arquitectura/INTEGRACION-LLM-LOCAL.md delete mode 100644 projects/trading-platform/docs/01-arquitectura/INTEGRACION-METATRADER4.md delete mode 100644 projects/trading-platform/docs/01-arquitectura/INTEGRACION-TRADINGAGENT.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/_MAP.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/especificaciones/ET-AUTH-001-oauth.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/especificaciones/ET-AUTH-002-jwt.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/especificaciones/ET-AUTH-003-database.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/especificaciones/ET-AUTH-004-api.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/especificaciones/ET-AUTH-005-security.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-001-registro-email.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-002-login-email.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-003-oauth-google.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-004-oauth-facebook.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-005-oauth-twitter.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-006-oauth-apple.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-007-oauth-github.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-008-phone-sms.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-009-phone-whatsapp.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-010-2fa-setup.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-011-password-reset.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/historias-usuario/US-AUTH-012-session-management.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/implementacion/TRACEABILITY.yml delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/requerimientos/RF-AUTH-001-oauth.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/requerimientos/RF-AUTH-002-email.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/requerimientos/RF-AUTH-003-phone.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/requerimientos/RF-AUTH-004-2fa.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/requerimientos/RF-AUTH-005-sessions.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/_MAP.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/especificaciones/ET-EDU-001-database.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/especificaciones/ET-EDU-002-api.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/especificaciones/ET-EDU-003-frontend.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/especificaciones/ET-EDU-004-video.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/especificaciones/ET-EDU-005-quizzes.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/especificaciones/ET-EDU-006-gamification.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/especificaciones/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/historias-usuario/US-EDU-001-ver-catalogo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/historias-usuario/US-EDU-002-ver-curso.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/historias-usuario/US-EDU-003-iniciar-leccion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/historias-usuario/US-EDU-004-ver-video.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/historias-usuario/US-EDU-005-completar-leccion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/historias-usuario/US-EDU-006-realizar-quiz.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/historias-usuario/US-EDU-007-ver-progreso.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/historias-usuario/US-EDU-008-obtener-certificado.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/implementacion/TRACEABILITY.yml delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/requerimientos/RF-EDU-001-catalogo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/requerimientos/RF-EDU-002-lecciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/requerimientos/RF-EDU-003-progreso.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/requerimientos/RF-EDU-004-quizzes.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/requerimientos/RF-EDU-005-certificados.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-002-education/requerimientos/RF-EDU-006-gamificacion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/_MAP.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-001-market-data.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-002-websocket.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-003-database.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-004-api.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-005-frontend.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-006-indicadores.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-007-paper-engine.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/ET-TRD-008-performance.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/especificaciones/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-001-ver-chart.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-002-cambiar-timeframe.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-003-agregar-indicador.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-004-crear-watchlist.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-005-agregar-simbolo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-006-crear-orden-market.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-007-crear-orden-limit.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-008-cerrar-posicion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-009-ver-posiciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-010-ver-historial.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-011-ver-estadisticas.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-012-configurar-tp-sl.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-013-alertas-precio.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-014-reset-balance.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-015-exportar-trades.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-016-modo-oscuro-chart.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-017-zoom-pan-chart.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-018-comparar-simbolos.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/implementacion/TRACEABILITY.yml delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/requerimientos/RF-TRD-001-charts.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/requerimientos/RF-TRD-002-indicadores-tecnicos.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/requerimientos/RF-TRD-003-gestion-watchlists.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/requerimientos/RF-TRD-004-paper-trading.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/requerimientos/RF-TRD-005-sistema-ordenes.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/requerimientos/RF-TRD-006-gestion-posiciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/requerimientos/RF-TRD-007-historial-trades.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/requerimientos/RF-TRD-008-metricas-estadisticas.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/_MAP.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-001-database.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-002-api.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-003-stripe.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-004-agents.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-005-frontend.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-006-cron.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-007-security.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-001-ver-productos.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-002-abrir-cuenta.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-003-depositar.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-004-ver-portfolio.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-005-ver-rendimiento.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-006-solicitar-retiro.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-007-ver-transacciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-008-recibir-distribucion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-009-cerrar-cuenta.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-010-comparar-productos.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-011-exportar-reporte.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-012-notificaciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-013-kyc-basico.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/historias-usuario/US-INV-014-ver-agente-performance.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/implementacion/TRACEABILITY.yml delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/requerimientos/RF-INV-001-productos.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/requerimientos/RF-INV-002-cuentas.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/requerimientos/RF-INV-003-depositos.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/requerimientos/RF-INV-004-retiros.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/requerimientos/RF-INV-005-agentes.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/requerimientos/RF-INV-006-reportes.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/_MAP.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/especificaciones/ET-PAY-001-database.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/especificaciones/ET-PAY-002-stripe-api.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/especificaciones/ET-PAY-003-webhooks.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/especificaciones/ET-PAY-004-api.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/especificaciones/ET-PAY-005-frontend.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/especificaciones/ET-PAY-006-security.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/historias-usuario/US-PAY-001-ver-planes.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/historias-usuario/US-PAY-002-suscribirse.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/historias-usuario/US-PAY-005-comprar-curso.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/historias-usuario/US-PAY-006-agregar-metodo-pago.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/historias-usuario/US-PAY-007-ver-facturas.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/historias-usuario/US-PAY-010-ver-historial.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/implementacion/TRACEABILITY.yml delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-001-suscripciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-002-checkout.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-003-wallet.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-004-facturacion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-005-webhooks.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-006-reembolsos.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/_MAP.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/epicas/EPICA-OQI-006A-ESTRATEGIA-AMD-ML.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/epicas/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/epicas/historias-usuario/US-ML-020-ver-fase-amd.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/epicas/historias-usuario/US-ML-022-zonas-liquidez.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/epicas/historias-usuario/US-ML-024-score-confluencia.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/epicas/historias-usuario/US-ML-027-integracion-ict-smc.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/especificaciones/ET-ML-001-arquitectura.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/especificaciones/ET-ML-002-modelos.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/especificaciones/ET-ML-003-features.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/especificaciones/ET-ML-004-api.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/especificaciones/ET-ML-005-integracion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/ALCANCES-FASE-1-PRIORIZADOS.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/ARQUITECTURA-MODELOS-FLUJO.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/ESTRATEGIA-AMD-COMPLETA.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/ESTRATEGIA-ICT-SMC.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/FEATURES-TARGETS-COMPLETO.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/FEATURES-TARGETS-ML.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/MODELOS-ML-DEFINICION.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/PIPELINE-ORQUESTACION.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/estrategias/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/historias-usuario/US-ML-001-ver-prediccion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/historias-usuario/US-ML-002-ver-senal.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/historias-usuario/US-ML-004-ver-accuracy.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/historias-usuario/US-ML-006-senal-en-chart.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/historias-usuario/US-ML-007-historial-senales.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/implementacion/TRACEABILITY.yml delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/requerimientos/RF-ML-001-predicciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/requerimientos/RF-ML-002-senales.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/requerimientos/RF-ML-003-indicadores.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/requerimientos/RF-ML-004-entrenamiento.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-006-ml-signals/requerimientos/RF-ML-005-notificaciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/_MAP.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/especificaciones/ET-LLM-001-arquitectura-chat.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/especificaciones/ET-LLM-002-agente-analisis.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/especificaciones/ET-LLM-003-motor-estrategias.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/especificaciones/ET-LLM-004-integracion-educacion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/especificaciones/ET-LLM-005-arquitectura-tools.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/especificaciones/ET-LLM-006-gestion-memoria.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-001-enviar-mensaje.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-002-gestionar-conversaciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-003-analisis-simbolo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-004-ver-senales-ml.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-005-estrategia-personalizada.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-006-historial-estrategias.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-007-asistencia-educativa.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-008-recomendaciones-aprendizaje.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-009-consultar-datos-chat.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/historias-usuario/US-LLM-010-paper-trading-chat.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/implementacion/TRACEABILITY.yml delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/requerimientos/RF-LLM-001-chat-interface.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/requerimientos/RF-LLM-002-market-analysis.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/requerimientos/RF-LLM-003-strategy-suggestions.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/requerimientos/RF-LLM-004-educational-assistance.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/requerimientos/RF-LLM-005-tool-integration.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-007-llm-agent/requerimientos/RF-LLM-006-context-management.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/README.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/_MAP.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/especificaciones/ET-PFM-001-arquitectura-dashboard.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/especificaciones/ET-PFM-002-calculo-metricas.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/especificaciones/ET-PFM-003-stress-testing.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/especificaciones/ET-PFM-004-motor-rebalanceo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/especificaciones/ET-PFM-005-historial-reportes.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/especificaciones/ET-PFM-006-reportes-fiscales.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/especificaciones/ET-PFM-007-motor-metas.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-001-ver-resumen-portfolio.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-002-ver-posiciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-003-ver-metricas-riesgo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-004-ejecutar-stress-test.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-005-configurar-asignacion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-006-ver-desviacion.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-007-ejecutar-rebalanceo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-008-ver-historial.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-009-exportar-historial.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-010-comparar-benchmark.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-011-metricas-benchmark.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/historias-usuario/US-PFM-012-reporte-fiscal.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/implementacion/TRACEABILITY.yml delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/requerimientos/RF-PFM-001-dashboard-portfolio.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/requerimientos/RF-PFM-002-analisis-riesgo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/requerimientos/RF-PFM-003-rebalanceo.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/requerimientos/RF-PFM-004-historial-transacciones.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/requerimientos/RF-PFM-005-comparacion-benchmark.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/requerimientos/RF-PFM-006-reportes-fiscales.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/requerimientos/RF-PFM-007-metas-inversi贸n.md delete mode 100644 projects/trading-platform/docs/02-definicion-modulos/_MAP.md delete mode 100644 projects/trading-platform/docs/90-transversal/VALIDACION-IMPLEMENTACION.md delete mode 100644 projects/trading-platform/docs/90-transversal/estrategias/ESTRATEGIA-PREDICCION-RANGOS.md delete mode 100644 projects/trading-platform/docs/90-transversal/gaps/ANALISIS-GAPS-DOCUMENTACION.md delete mode 100644 projects/trading-platform/docs/90-transversal/integraciones/INT-DATA-001-data-service.md delete mode 100644 projects/trading-platform/docs/90-transversal/integraciones/INT-DATA-002-analisis-impacto.md delete mode 100644 projects/trading-platform/docs/90-transversal/integraciones/INT-MT4-001-gateway-service.md delete mode 100644 projects/trading-platform/docs/90-transversal/integraciones/_MAP.md delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/DATABASE_GAPS_ANALYSIS.md delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/INVENTARIO-STC-PLATFORM-WEB.md delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/MATRIZ-DEPENDENCIAS-TRADING.yml delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/MATRIZ-DEPENDENCIAS.yml delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/MIGRACION-SUPABASE-EXPRESS.md delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/ML_INVENTORY.yml delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/MT4_GATEWAY_INVENTORY.yml delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/STRATEGIES_INVENTORY.yml delete mode 100644 projects/trading-platform/docs/90-transversal/inventarios/_MAP.md delete mode 100644 projects/trading-platform/docs/90-transversal/roadmap/PLAN-DESARROLLO-DETALLADO.md delete mode 100644 projects/trading-platform/docs/90-transversal/roadmap/ROADMAP-GENERAL.md delete mode 100644 projects/trading-platform/docs/90-transversal/setup/SETUP-MT4-TRADING.md delete mode 100644 projects/trading-platform/docs/95-guias-desarrollo/JENKINS-DEPLOY.md delete mode 100644 projects/trading-platform/docs/95-guias-desarrollo/PUERTOS-SERVICIOS.md delete mode 100644 projects/trading-platform/docs/95-guias-desarrollo/ml-engine/SETUP-PYTHON.md delete mode 100644 projects/trading-platform/docs/97-adr/ADR-001-stack-tecnologico.md delete mode 100644 projects/trading-platform/docs/97-adr/ADR-002-MVP-OPERATIVO-TRADING.md delete mode 100644 projects/trading-platform/docs/97-adr/ADR-002-monorepo.md delete mode 100644 projects/trading-platform/docs/97-adr/ADR-003-autenticacion-multiproveedor.md delete mode 100644 projects/trading-platform/docs/97-adr/ADR-004-testing.md delete mode 100644 projects/trading-platform/docs/97-adr/ADR-005-devops.md delete mode 100644 projects/trading-platform/docs/97-adr/ADR-006-caching.md delete mode 100644 projects/trading-platform/docs/97-adr/ADR-007-security.md delete mode 100644 projects/trading-platform/docs/97-adr/_MAP.md delete mode 100644 projects/trading-platform/docs/99-analisis/DECISIONES-ARQUITECTONICAS.md delete mode 100644 projects/trading-platform/docs/99-analisis/PLAN-IMPLEMENTACION-CORRECCIONES.md delete mode 100644 projects/trading-platform/docs/99-analisis/REPORTE-ANALISIS-REQUISITOS.md delete mode 100644 projects/trading-platform/docs/99-analisis/REPORTE-EJECUCION-CORRECCIONES.md delete mode 100644 projects/trading-platform/docs/99-analisis/REPORTE-TRAZABILIDAD-DDL.md delete mode 100644 projects/trading-platform/docs/API.md delete mode 100644 projects/trading-platform/docs/ARCHITECTURE.md delete mode 100644 projects/trading-platform/docs/README.md delete mode 100644 projects/trading-platform/docs/SECURITY.md delete mode 100644 projects/trading-platform/docs/_MAP.md delete mode 100644 projects/trading-platform/docs/api-contracts/SERVICE-INTEGRATION.md delete mode 100644 projects/trading-platform/orchestration/00-guidelines/CONTEXTO-PROYECTO.md delete mode 100644 projects/trading-platform/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md delete mode 100644 projects/trading-platform/orchestration/00-guidelines/HERENCIA-SIMCO.md delete mode 100644 projects/trading-platform/orchestration/00-guidelines/PROJECT-STATUS.md delete mode 100644 projects/trading-platform/orchestration/06-subagentes/DELEGACION-TRADING-STRATEGIST-2025-12-08.md delete mode 100644 projects/trading-platform/orchestration/PROXIMA-ACCION.md delete mode 100644 projects/trading-platform/orchestration/directivas/DIRECTIVA-ARQUITECTURA-HIBRIDA.md delete mode 100644 projects/trading-platform/orchestration/directivas/DIRECTIVA-ML-SERVICES.md delete mode 100644 projects/trading-platform/orchestration/directivas/DIRECTIVA-POLITICA-CARGA-LIMPIA.md delete mode 100644 projects/trading-platform/orchestration/directivas/DIRECTIVA-STACK-TECNOLOGICO.md delete mode 100644 projects/trading-platform/orchestration/environment/PROJECT-ENV-CONFIG.yml delete mode 100644 projects/trading-platform/orchestration/estados/REGISTRO-SUBAGENTES.json delete mode 100644 projects/trading-platform/orchestration/inventarios/BACKEND_INVENTORY.yml delete mode 100644 projects/trading-platform/orchestration/inventarios/DATABASE_INVENTORY.yml delete mode 100644 projects/trading-platform/orchestration/inventarios/FRONTEND_INVENTORY.yml delete mode 100644 projects/trading-platform/orchestration/inventarios/MASTER_INVENTORY.yml delete mode 100644 projects/trading-platform/orchestration/planes/PLAN-ML-LLM-TRADING.md delete mode 100644 projects/trading-platform/orchestration/reportes/REPORTE-SESION-2025-12-07.md delete mode 100644 projects/trading-platform/orchestration/trazas/TRAZA-TAREAS-BACKEND.md delete mode 100644 projects/trading-platform/orchestration/trazas/TRAZA-TAREAS-DATABASE.md delete mode 100644 projects/trading-platform/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md delete mode 100644 projects/trading-platform/package.json delete mode 100644 projects/trading-platform/packages/config/index.ts delete mode 100644 projects/trading-platform/packages/sdk-python/setup.py delete mode 100644 projects/trading-platform/packages/sdk-python/src/orbiquant_sdk/__init__.py delete mode 100644 projects/trading-platform/packages/sdk-python/src/orbiquant_sdk/client.py delete mode 100644 projects/trading-platform/packages/sdk-python/src/orbiquant_sdk/config.py delete mode 100644 projects/trading-platform/packages/sdk-typescript/package.json delete mode 100644 projects/trading-platform/packages/sdk-typescript/src/auth/index.ts delete mode 100644 projects/trading-platform/packages/sdk-typescript/src/client.ts delete mode 100644 projects/trading-platform/packages/sdk-typescript/src/index.ts delete mode 100644 projects/trading-platform/packages/sdk-typescript/src/ml/index.ts delete mode 100644 projects/trading-platform/packages/sdk-typescript/src/trading/index.ts delete mode 100644 projects/trading-platform/packages/sdk-typescript/src/types/index.ts create mode 100644 shared/knowledge-base/propagacion/USAGE-ORQUESTACION.md create mode 100644 shared/knowledge-base/templates/TEMPLATE-PROJECT-STATUS.md create mode 100644 shared/knowledge-base/templates/inventories/TEMPLATE-BACKEND-INVENTORY.yml create mode 100644 shared/knowledge-base/templates/inventories/TEMPLATE-DATABASE-INVENTORY.yml create mode 100644 shared/knowledge-base/templates/inventories/TEMPLATE-MASTER-INVENTORY.yml diff --git a/.gitignore b/.gitignore index 61d692c05..643aef56b 100644 --- a/.gitignore +++ b/.gitignore @@ -180,3 +180,23 @@ backups/ # Backup de gamilit (submodule migration) projects/gamilit.bak.*/ + +# ----------------------------------------------------------------------------- +# SUBREPOSITORIOS - Proyectos con repositorios independientes en Gitea +# ----------------------------------------------------------------------------- +# Estos proyectos tienen sus propios repositorios en http://72.60.226.4:3000/rckrdmrd +# Ver archivo SUBREPOSITORIOS.md para referencias completas +# +# NOTA: gamilit NO se ignora porque es un submodulo Git (ver .gitmodules) +# ----------------------------------------------------------------------------- +projects/erp-suite/ +projects/erp-core/ +projects/erp-construccion/ +projects/erp-clinicas/ +projects/erp-retail/ +projects/erp-mecanicas-diesel/ +projects/erp-vidrio-templado/ +projects/trading-platform/ +projects/betting-analytics/ +projects/inmobiliaria-analytics/ +projects/platform_marketing_content/ diff --git a/.gitmodules b/.gitmodules index 3db94a6a1..d980dae6b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "projects/gamilit"] path = projects/gamilit - url = git@github.com:rckrdmrd/gamilit-workspace.git + url = https://github.com/rckrdmrd/gamilit-workspace.git diff --git a/SUBREPOSITORIOS.md b/SUBREPOSITORIOS.md new file mode 100644 index 000000000..1fd3c0c95 --- /dev/null +++ b/SUBREPOSITORIOS.md @@ -0,0 +1,210 @@ +# Subrepositorios del Workspace + +**Fecha:** 2025-01-04 +**Version:** 1.1 + +--- + +## Arquitectura de Repositorios + +Este workspace utiliza una arquitectura de repositorios donde cada proyecto puede tener su propio repositorio independiente. + +### Repositorio Principal + +| Campo | Valor | +|-------|-------| +| **Nombre** | workspace-v1 | +| **Path Local** | `/home/isem/workspace-v1` | +| **Remote SSH** | `git@gitea-server:rckrdmrd/workspace-v1.git` | +| **Remote HTTP** | `http://72.60.226.4:3000/rckrdmrd/workspace-v1` | + +--- + +## GAMILIT - Workspace Independiente + +### Configuracion + +| Campo | Valor | +|-------|-------| +| **Path Local** | `projects/gamilit` | +| **Tipo** | Submodulo Git | +| **Remote HTTPS** | `https://github.com/rckrdmrd/gamilit-workspace.git` | +| **Remote SSH** | `git@github.com:rckrdmrd/gamilit-workspace.git` | +| **Referencia** | `/home/isem/workspace-old/wsl-ubuntu/workspace/workspace-gamilit/gamilit/projects/gamilit` | + +### Estructura + +Gamilit es un **workspace completo** SIN subrepositorios. Contiene: + +``` +projects/gamilit/ +鈹溾攢鈹 apps/ +鈹 鈹溾攢鈹 backend/ # NestJS API (NO es subrepositorio) +鈹 鈹溾攢鈹 frontend/ # React App (NO es subrepositorio) +鈹 鈹溾攢鈹 database/ # DDL y Scripts (NO es subrepositorio) +鈹 鈹斺攢鈹 devops/ # Scripts DevOps (NO es subrepositorio) +鈹溾攢鈹 docs/ # Documentacion (incluye contenido de niveles superiores) +鈹溾攢鈹 orchestration/ # Sistema NEXUS (incluye contenido de niveles superiores) +鈹溾攢鈹 scripts/ # Scripts de produccion +鈹溾攢鈹 k8s/ # Configuracion Kubernetes +鈹斺攢鈹 ... +``` + +### Reglas Especiales + +1. **Sin subrepositorios**: Todo el contenido de `apps/` es parte del mismo repositorio +2. **Solo ignora node_modules**: Los archivos de codigo van al repo +3. **Workspace autocontenido**: Contiene su propia documentacion y orchestration +4. **Deployment directo**: Se clona directamente en produccion + +### Servidor de Produccion + +| Campo | Valor | +|-------|-------| +| **IP** | 74.208.126.102 | +| **Path** | `/home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit` | +| **Backend** | Puerto 3006 (PM2 cluster x2) | +| **Frontend** | Puerto 3005 (PM2 fork) | +| **Database** | PostgreSQL :5432, `gamilit_platform` | + +--- + +## Proyectos con Repositorios en Gitea + +Los siguientes proyectos tienen repositorios independientes en `http://72.60.226.4:3000/rckrdmrd/`. +Estan ignorados en el `.gitignore` del workspace principal. + +Estos proyectos SI pueden tener subrepositorios para sus apps (backend, frontend, database). + +### Familia ERP + +| Proyecto | Path Local | Repositorio | +|----------|------------|-------------| +| **erp-suite** | `projects/erp-suite` | `http://72.60.226.4:3000/rckrdmrd/erp-suite.git` | +| **erp-core** | `projects/erp-core` | `http://72.60.226.4:3000/rckrdmrd/erp-core.git` | +| **erp-construccion** | `projects/erp-construccion` | `http://72.60.226.4:3000/rckrdmrd/erp-construccion.git` | +| **erp-clinicas** | `projects/erp-clinicas` | `http://72.60.226.4:3000/rckrdmrd/erp-clinicas.git` | +| **erp-retail** | `projects/erp-retail` | `http://72.60.226.4:3000/rckrdmrd/erp-retail.git` | +| **erp-mecanicas-diesel** | `projects/erp-mecanicas-diesel` | `http://72.60.226.4:3000/rckrdmrd/erp-mecanicas-diesel.git` | +| **erp-vidrio-templado** | `projects/erp-vidrio-templado` | `http://72.60.226.4:3000/rckrdmrd/erp-vidrio-templado.git` | + +### Otros Proyectos + +| Proyecto | Path Local | Repositorio | +|----------|------------|-------------| +| **trading-platform** | `projects/trading-platform` | `http://72.60.226.4:3000/rckrdmrd/trading-platform.git` | +| **betting-analytics** | `projects/betting-analytics` | `http://72.60.226.4:3000/rckrdmrd/betting-analytics.git` | +| **inmobiliaria-analytics** | `projects/inmobiliaria-analytics` | `http://72.60.226.4:3000/rckrdmrd/inmobiliaria-analytics.git` | +| **platform_marketing_content** | `projects/platform_marketing_content` | `http://72.60.226.4:3000/rckrdmrd/platform_marketing_content.git` | + +### Estructura con Subrepositorios (para proyectos Gitea) + +Los proyectos en Gitea pueden usar esta estructura de subrepositorios: + +``` +projects/[proyecto]/ +鈹溾攢鈹 .gitmodules # Define subrepositorios +鈹溾攢鈹 apps/ +鈹 鈹溾攢鈹 backend/ # Subrepositorio -> [proyecto]-backend.git +鈹 鈹溾攢鈹 frontend/ # Subrepositorio -> [proyecto]-frontend.git +鈹 鈹斺攢鈹 database/ # Subrepositorio -> [proyecto]-database.git +鈹溾攢鈹 docs/ +鈹斺攢鈹 orchestration/ +``` + +--- + +## Configuracion SSH + +### Para Gitea (72.60.226.4) + +``` +# ~/.ssh/config +Host gitea-server + HostName 72.60.226.4 + Port 22 + User git + IdentityFile ~/.ssh/id_ed25519 + IdentitiesOnly yes +``` + +### Para GitHub + +``` +# ~/.ssh/config +Host github.com + HostName github.com + User git + IdentityFile ~/.ssh/id_ed25519 + IdentitiesOnly yes +``` + +--- + +## Comandos Utiles + +### Gamilit (GitHub) + +```bash +# Actualizar submodulo +cd /home/isem/workspace-v1 +git submodule update --remote projects/gamilit + +# Trabajar dentro de gamilit +cd /home/isem/workspace-v1/projects/gamilit +git pull origin master +git add -A +git commit -m "mensaje" +git push origin master + +# Actualizar referencia en workspace-v1 +cd /home/isem/workspace-v1 +git add projects/gamilit +git commit -m "chore: actualizar submodulo gamilit" +``` + +### Proyectos Gitea + +```bash +# Clonar un proyecto +cd /home/isem/workspace-v1/projects +git clone http://72.60.226.4:3000/rckrdmrd/[PROYECTO].git + +# Inicializar subrepositorios (si aplica) +cd [PROYECTO] +git submodule update --init --recursive +``` + +### Ver estado de todos los repositorios + +```bash +# Workspace principal +git -C /home/isem/workspace-v1 status + +# Gamilit +git -C /home/isem/workspace-v1/projects/gamilit status +``` + +--- + +## Notas Importantes + +1. **gamilit** es especial: + - Es un workspace independiente sin subrepositorios + - Se despliega directamente en produccion + - Contiene docs y orchestration propios (redundantes con workspace) + - Usa GitHub (no Gitea) + +2. **Otros proyectos** (ERP, trading, etc.): + - Usan Gitea como servidor Git + - Pueden tener subrepositorios para sus apps + - No se incluyen en el repositorio workspace-v1 + +3. **Sincronizacion**: + - Desarrollo activo de gamilit: `/home/isem/workspace/projects/gamilit` + - Referencia de produccion: `/home/isem/workspace-old/.../gamilit` + - Submodulo en workspace-v1: `/home/isem/workspace-v1/projects/gamilit` + +--- + +*Generado por NEXUS v3.4 - Sistema de Orquestacion* diff --git a/core/orchestration/directivas/simco/SIMCO-PROPAGACION-MEJORAS.md b/core/orchestration/directivas/simco/SIMCO-PROPAGACION-MEJORAS.md index caf201a4c..70b607c53 100644 --- a/core/orchestration/directivas/simco/SIMCO-PROPAGACION-MEJORAS.md +++ b/core/orchestration/directivas/simco/SIMCO-PROPAGACION-MEJORAS.md @@ -1,10 +1,11 @@ # SIMCO: PROPAGACION DE MEJORAS -**Version:** 1.0.0 +**Version:** 2.0.0 **Prioridad:** OBLIGATORIA para mejoras en modulos compartidos **Sistema:** NEXUS v3.4 **Alias:** @PROPAGACION **Fecha:** 2026-01-04 +**Actualizado:** 2026-01-04 (EPIC-012) --- @@ -352,15 +353,164 @@ Este proyecto participa en el sistema de propagacion de NEXUS. --- -## 9. REFERENCIAS +## 9. PROPAGACION POR NIVELES (EPIC-012) -- **Catalogo de modulos:** `@CATALOG` 鈫 `shared/knowledge-base/CATALOGO-MODULOS.yml` -- **Trazabilidad:** `shared/knowledge-base/TRAZABILIDAD-PROYECTOS.yml` -- **Guia de uso:** `shared/knowledge-base/propagacion/USAGE.md` -- **Registro:** `shared/knowledge-base/propagacion/REGISTRO-PROPAGACIONES.yml` +Ver: @NIVELES_PROP (`shared/knowledge-base/propagacion/NIVELES-PROPAGACION.yml`) + +### Jerarquia + +| Nivel | Nombre | Contenido | +|-------|--------|-----------| +| 0 | Core Catalog | core/catalog/ | +| 1 | Knowledge-Base | shared/knowledge-base/ | +| 2 | Proyectos Base | erp-core, gamilit, trading-platform | +| 3 | Proyectos Hoja | Verticales ERP, otros | + +### Reglas de Cascada + +1. Siempre propagar de nivel N a nivel N+1 (nunca saltar) +2. Validar en cada nivel antes de continuar +3. Si nivel N falla, detener propagacion +4. Resolver dependencias internas en nivel 1 primero + +### Script + +```bash +./devtools/scripts/propagation/cascade-propagation.sh [--start-level N] [--stop-level N] +``` + +--- + +## 10. ROL DE @KB_MANAGER (EPIC-012) + +Ver: @KB_MANAGER (`core/orchestration/agents/perfiles/PERFIL-KB-MANAGER.md`) + +### Responsabilidades + +1. Recibir notificaciones de mejoras propagables +2. Analizar impacto usando CAPVED +3. Coordinar actualizacion en core y KB +4. Generar tareas SCRUM para proyectos +5. Validar integracion final + +### Cuando Activar @KB_MANAGER + +- Mejora en modulo de knowledge-base +- Cambio que afecta multiples proyectos +- Security-fix o bug-fix critico + +--- + +## 11. GENERACION DE TAREAS SCRUM (EPIC-012) + +### Script + +```bash +./devtools/scripts/propagation/generate-scrum-tasks.sh [--epic] [--output-dir DIR] +``` + +### Tipos de Cambio + +| Tipo | Descripcion | Prioridad | +|------|-------------|-----------| +| security-fix | Vulnerabilidad corregida | Critica | +| bug-fix | Error corregido | Alta | +| feature | Nueva funcionalidad | Media | +| refactor | Mejora interna | Baja | + +### Tipos de Salida + +| Tipo | Archivo | Cuando Usar | +|------|---------|-------------| +| Task | TASK-PROP-XXX-{proyecto}.md | Cambio simple | +| Epic + Tasks | EPIC-PROP-XXX.md + TASKs | Cambio complejo o multi-proyecto | + +### Formato de Tarea SCRUM + +Ver: `shared/knowledge-base/propagacion/templates/TAREA-PROPAGACION.md` + +--- + +## 12. PROTOCOLO DE COORDINACION (EPIC-012) + +Ver: @PROTOCOLO_PROP (`shared/knowledge-base/propagacion/PROTOCOLO-COORDINACION.yml`) + +### Flujo Resumido + +``` +1. Desarrollador detecta mejora + | + v +2. Notifica a @KB_MANAGER + | + v +3. @KB_MANAGER analiza con CAPVED + | + v +4. Si propagar: Actualizar KB -> Generar tareas SCRUM + | + v +5. @PERFIL_PROJECT_AGENT ejecuta en cada proyecto + | + v +6. @KB_MANAGER valida integracion final +``` + +### Handoff + +| De | A | Canal | +|----|---|-------| +| @KB_MANAGER | @PROJECT_AGENT | Tarea SCRUM formal | +| @PROJECT_AGENT | @KB_MANAGER | Reporte de ejecucion | + +### Validacion Final + +```bash +./devtools/scripts/propagation/validate-propagation-chain.sh +``` + +--- + +## 13. COMANDOS ACTUALIZADOS (EPIC-012) + +### Propagacion Basica +```bash +./devtools/scripts/propagation/propagate-module-update.sh +``` + +### Propagacion con Tareas SCRUM +```bash +./devtools/scripts/propagation/propagate-module-update.sh --scrum +./devtools/scripts/propagation/propagate-module-update.sh --scrum --scrum-epic +``` + +### Propagacion en Cascada +```bash +./devtools/scripts/propagation/propagate-module-update.sh --cascade +``` + +### Combinado +```bash +./devtools/scripts/propagation/propagate-module-update.sh --scrum --cascade --tipo security-fix +``` + +--- + +## 14. REFERENCIAS ACTUALIZADAS + +| Alias | Archivo | +|-------|---------| +| @PROPAGACION | Esta directiva | +| @KB_MANAGER | core/orchestration/agents/perfiles/PERFIL-KB-MANAGER.md | +| @NIVELES_PROP | shared/knowledge-base/propagacion/NIVELES-PROPAGACION.yml | +| @PROTOCOLO_PROP | shared/knowledge-base/propagacion/PROTOCOLO-COORDINACION.yml | +| @USAGE_PROP | shared/knowledge-base/propagacion/USAGE.md | +| @USAGE_ORQUESTACION | shared/knowledge-base/propagacion/USAGE-ORQUESTACION.md | +| @CATALOG | shared/knowledge-base/CATALOGO-MODULOS.yml | --- **Directiva creada:** EPIC-007 +**Actualizada:** EPIC-012 (Orquestacion Avanzada) **Sistema:** NEXUS v3.4 **Autor:** Claude Opus 4.5 diff --git a/devtools/scripts/propagation/propagate-module-update.sh b/devtools/scripts/propagation/propagate-module-update.sh index a36c3074b..b190caf3b 100755 --- a/devtools/scripts/propagation/propagate-module-update.sh +++ b/devtools/scripts/propagation/propagate-module-update.sh @@ -372,15 +372,27 @@ main() { generate_tasks update_registry + # Ejecutar generacion SCRUM si se solicito + run_scrum_generation + + # Ejecutar cascada si se solicito + run_cascade_propagation + echo "" echo "============================================" echo " Proximos pasos:" echo "============================================" echo "" - echo " 1. Revisar tareas generadas" - echo " 2. Priorizar segun tipo de cambio" + if $GENERATE_SCRUM; then + echo " 1. Revisar tareas SCRUM generadas" + echo " 2. Asignar a @PERFIL_PROJECT_AGENT" + else + echo " 1. Revisar tareas generadas" + echo " 2. Priorizar segun tipo de cambio" + fi echo " 3. Ejecutar propagacion en cada proyecto" echo " 4. Actualizar TRAZABILIDAD-PROYECTOS.yml" + echo " 5. Validar: ./validate-propagation-chain.sh " echo "" echo " Ver: @PROPAGACION para proceso completo" echo "" diff --git a/devtools/scripts/propagation/validate-propagation-chain.sh b/devtools/scripts/propagation/validate-propagation-chain.sh index 3aa7c1105..6324a8be2 100755 --- a/devtools/scripts/propagation/validate-propagation-chain.sh +++ b/devtools/scripts/propagation/validate-propagation-chain.sh @@ -51,9 +51,9 @@ FAIL=0 WARN=0 # Funciones de logging -check_pass() { ((PASS++)); echo -e " ${GREEN}[PASS]${NC} $1"; } -check_fail() { ((FAIL++)); echo -e " ${RED}[FAIL]${NC} $1"; } -check_warn() { ((WARN++)); echo -e " ${YELLOW}[WARN]${NC} $1"; } +check_pass() { PASS=$((PASS + 1)); echo -e " ${GREEN}[PASS]${NC} $1"; } +check_fail() { FAIL=$((FAIL + 1)); echo -e " ${RED}[FAIL]${NC} $1"; } +check_warn() { WARN=$((WARN + 1)); echo -e " ${YELLOW}[WARN]${NC} $1"; } log_info() { echo -e " ${BLUE}[INFO]${NC} $1"; } # Funcion de ayuda diff --git a/devtools/scripts/validation/validate-project-structure.sh b/devtools/scripts/validation/validate-project-structure.sh new file mode 100755 index 000000000..64208799c --- /dev/null +++ b/devtools/scripts/validation/validate-project-structure.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# ============================================================================= +# validate-project-structure.sh +# Validar estructura de proyecto segun estandar EPIC-011 +# Sistema: NEXUS v3.4 + SIMCO +# ============================================================================= + +set -e + +PROJECT_PATH="${1:-.}" +PROJECT_NAME=$(basename "$PROJECT_PATH") + +# Colores +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +ERRORS=0 +WARNINGS=0 + +echo "==============================================" +echo "Validando: $PROJECT_NAME" +echo "Ruta: $PROJECT_PATH" +echo "==============================================" +echo "" + +# ----------------------------------------------------------------------------- +# Funcion para verificar existencia +# ----------------------------------------------------------------------------- +check_exists() { + local path="$1" + local type="$2" # "dir" o "file" + local required="$3" # "required" o "recommended" + local name="$4" + + if [ "$type" = "dir" ]; then + if [ -d "$path" ]; then + echo -e "${GREEN}[OK]${NC} $name" + return 0 + fi + else + if [ -f "$path" ]; then + echo -e "${GREEN}[OK]${NC} $name" + return 0 + fi + fi + + if [ "$required" = "required" ]; then + echo -e "${RED}[ERROR]${NC} Falta: $name" + ((ERRORS++)) || true + return 1 + else + echo -e "${YELLOW}[WARN]${NC} Recomendado: $name" + ((WARNINGS++)) || true + return 0 + fi +} + +# ----------------------------------------------------------------------------- +# Validacion de orchestration/ +# ----------------------------------------------------------------------------- +echo "--- Verificando orchestration/ ---" +check_exists "$PROJECT_PATH/orchestration" "dir" "required" "orchestration/" +check_exists "$PROJECT_PATH/orchestration/00-guidelines" "dir" "required" "orchestration/00-guidelines/" + +# Archivos obligatorios en 00-guidelines +echo "" +echo "--- Archivos obligatorios ---" +check_exists "$PROJECT_PATH/orchestration/00-guidelines/CONTEXTO-PROYECTO.md" "file" "required" "CONTEXTO-PROYECTO.md" +check_exists "$PROJECT_PATH/orchestration/00-guidelines/HERENCIA-SIMCO.md" "file" "required" "HERENCIA-SIMCO.md" +check_exists "$PROJECT_PATH/orchestration/00-guidelines/PROJECT-STATUS.md" "file" "required" "PROJECT-STATUS.md" + +# Inventarios +echo "" +echo "--- Inventarios ---" +check_exists "$PROJECT_PATH/orchestration/inventarios" "dir" "required" "orchestration/inventarios/" +check_exists "$PROJECT_PATH/orchestration/inventarios/MASTER_INVENTORY.yml" "file" "recommended" "MASTER_INVENTORY.yml" + +# Trazas (recomendado) +echo "" +echo "--- Trazas ---" +check_exists "$PROJECT_PATH/orchestration/trazas" "dir" "recommended" "orchestration/trazas/" + +# README +echo "" +echo "--- Documentacion ---" +check_exists "$PROJECT_PATH/orchestration/README.md" "file" "recommended" "orchestration/README.md" +check_exists "$PROJECT_PATH/docs/_MAP.md" "file" "recommended" "docs/_MAP.md" + +# ----------------------------------------------------------------------------- +# Validar contenido de HERENCIA-SIMCO.md +# ----------------------------------------------------------------------------- +echo "" +echo "--- Validando rutas en HERENCIA-SIMCO.md ---" +HERENCIA_FILE="$PROJECT_PATH/orchestration/00-guidelines/HERENCIA-SIMCO.md" +if [ -f "$HERENCIA_FILE" ]; then + # Verificar que no tenga rutas obsoletas (workspace sin -v1) + if grep -q "/home/isem/workspace/" "$HERENCIA_FILE" 2>/dev/null; then + if ! grep -q "workspace-v1" "$HERENCIA_FILE" 2>/dev/null; then + echo -e "${RED}[ERROR]${NC} Rutas obsoletas encontradas en HERENCIA-SIMCO.md" + ((ERRORS++)) || true + else + echo -e "${GREEN}[OK]${NC} Rutas correctas en HERENCIA-SIMCO.md" + fi + else + echo -e "${GREEN}[OK]${NC} Sin rutas obsoletas en HERENCIA-SIMCO.md" + fi +else + echo -e "${YELLOW}[SKIP]${NC} HERENCIA-SIMCO.md no existe" +fi + +# ----------------------------------------------------------------------------- +# Resumen +# ----------------------------------------------------------------------------- +echo "" +echo "==============================================" +echo "RESUMEN: $PROJECT_NAME" +echo "==============================================" +echo -e "Errores: ${RED}$ERRORS${NC}" +echo -e "Warnings: ${YELLOW}$WARNINGS${NC}" +echo "" + +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}Estado: PASS${NC}" + exit 0 +else + echo -e "${RED}Estado: FAIL${NC}" + exit 1 +fi diff --git a/projects/betting-analytics/INVENTARIO.yml b/projects/betting-analytics/INVENTARIO.yml deleted file mode 100644 index bd7f64c24..000000000 --- a/projects/betting-analytics/INVENTARIO.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Inventario generado por EPIC-008 -proyecto: betting-analytics -fecha: "2026-01-04" -generado_por: "inventory-project.sh v1.0.0" - -inventario: - docs: - total: 1 - por_tipo: - markdown: 1 - yaml: 0 - json: 0 - orchestration: - total: 13 - por_tipo: - markdown: 7 - yaml: 5 - json: 1 - -problemas: - archivos_obsoletos: 0 - referencias_antiguas: 0 - simco_faltantes: - - _MAP.md en docs/ - - PROJECT-STATUS.md - -estado_simco: - herencia_simco: true - contexto_proyecto: true - map_docs: false - project_status: false diff --git a/projects/betting-analytics/README.md b/projects/betting-analytics/README.md deleted file mode 100644 index b460d74c4..000000000 --- a/projects/betting-analytics/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# betting-analytics - -## Descripci贸n - -[Descripci贸n del proyecto pendiente de definir] - -## Estado - -- **Estado:** Planificaci贸n -- **Creado:** 2025-12-05 - -## Estructura - -``` -betting-analytics/ -鈹溾攢鈹 apps/ -鈹溾攢鈹 docs/ -鈹斺攢鈹 orchestration/ -``` - ---- -*Proyecto parte del workspace de F谩brica de Software con Agentes IA* diff --git a/projects/betting-analytics/apps/backend/Dockerfile b/projects/betting-analytics/apps/backend/Dockerfile deleted file mode 100644 index 3237dc7e1..000000000 --- a/projects/betting-analytics/apps/backend/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -# ============================================================================== -# BETTING-ANALYTICS BACKEND - Express.js Dockerfile -# ============================================================================== -# Multi-stage build for production-ready Express application -# ============================================================================== - -# Stage 1: Dependencies -FROM node:20-alpine AS deps -WORKDIR /app - -COPY package*.json ./ -RUN npm ci --only=production && npm cache clean --force - -# Stage 2: Builder -FROM node:20-alpine AS builder -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -COPY . . -RUN npm run build - -# Stage 3: Production -FROM node:20-alpine AS runner -WORKDIR /app - -ENV NODE_ENV=production - -# Create non-root user -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 express - -# Copy built application -COPY --from=builder /app/dist ./dist -COPY --from=deps /app/node_modules ./node_modules -COPY --from=builder /app/package.json ./package.json - -USER express - -EXPOSE 3090 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3090/health || exit 1 - -CMD ["node", "dist/main.js"] diff --git a/projects/betting-analytics/apps/backend/package.json b/projects/betting-analytics/apps/backend/package.json deleted file mode 100644 index b182f1fa8..000000000 --- a/projects/betting-analytics/apps/backend/package.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "name": "@betting-analytics/backend", - "version": "0.1.0", - "description": "Betting Analytics - Backend API", - "author": "Betting Analytics Team", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@nestjs/common": "^10.3.0", - "@nestjs/config": "^3.1.1", - "@nestjs/core": "^10.3.0", - "@nestjs/jwt": "^10.2.0", - "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.3.0", - "@nestjs/typeorm": "^10.0.1", - "bcrypt": "^5.1.1", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", - "passport": "^0.7.0", - "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", - "pg": "^8.11.3", - "reflect-metadata": "^0.2.1", - "rxjs": "^7.8.1", - "typeorm": "^0.3.19" - }, - "devDependencies": { - "@nestjs/cli": "^10.3.0", - "@nestjs/schematics": "^10.1.0", - "@nestjs/testing": "^10.3.0", - "@types/bcrypt": "^5.0.2", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.11", - "@types/node": "^20.10.6", - "@types/passport-jwt": "^4.0.0", - "@types/passport-local": "^1.0.38", - "@typescript-eslint/eslint-plugin": "^6.18.0", - "@typescript-eslint/parser": "^6.18.0", - "eslint": "^8.56.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.2", - "jest": "^29.7.0", - "prettier": "^3.1.1", - "source-map-support": "^0.5.21", - "supertest": "^6.3.4", - "ts-jest": "^29.1.1", - "ts-loader": "^9.5.1", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.3.3" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" - } -} diff --git a/projects/betting-analytics/apps/backend/service.descriptor.yml b/projects/betting-analytics/apps/backend/service.descriptor.yml deleted file mode 100644 index 27ddb7be2..000000000 --- a/projects/betting-analytics/apps/backend/service.descriptor.yml +++ /dev/null @@ -1,54 +0,0 @@ -# ============================================================================== -# SERVICE DESCRIPTOR - BETTING ANALYTICS API -# ============================================================================== -version: "1.0.0" - -service: - name: "betting-api" - display_name: "Betting Analytics API" - description: "API para analisis de apuestas deportivas" - type: "backend" - runtime: "python" - framework: "fastapi" - owner_agent: "NEXUS-BACKEND" - -ports: - internal: 3050 - registry_ref: "projects.betting.services.api" - protocol: "http" - -database: - registry_ref: "betting" - role: "runtime" - -modules: - data_ingestion: - description: "Recoleccion de datos" - status: "planned" - analytics: - description: "Analisis estadistico" - status: "planned" - predictions: - description: "Modelos ML" - status: "planned" - -docker: - networks: - - "betting_${ENV:-local}" - - "infra_shared" - labels: - traefik: - enable: true - rule: "Host(`api.betting.localhost`)" - -healthcheck: - endpoint: "/health" - -status: - phase: "planned" - version: "0.0.1" - completeness: 5 - -metadata: - created_at: "2025-12-18" - project: "betting-analytics" diff --git a/projects/betting-analytics/apps/backend/src/app.module.ts b/projects/betting-analytics/apps/backend/src/app.module.ts deleted file mode 100644 index 8d1305254..000000000 --- a/projects/betting-analytics/apps/backend/src/app.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { appConfig, databaseConfig, jwtConfig } from './config'; -import { AuthModule } from './modules/auth/auth.module'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [appConfig, databaseConfig, jwtConfig], - }), - TypeOrmModule.forRootAsync({ - useFactory: (configService) => ({ - type: 'postgres', - host: configService.get('database.host'), - port: configService.get('database.port'), - username: configService.get('database.username'), - password: configService.get('database.password'), - database: configService.get('database.database'), - entities: [__dirname + '/**/*.entity{.ts,.js}'], - synchronize: configService.get('database.synchronize'), - logging: configService.get('database.logging'), - }), - inject: [ConfigModule], - }), - AuthModule, - ], - controllers: [], - providers: [], -}) -export class AppModule {} diff --git a/projects/betting-analytics/apps/backend/src/config/index.ts b/projects/betting-analytics/apps/backend/src/config/index.ts deleted file mode 100644 index a508fa919..000000000 --- a/projects/betting-analytics/apps/backend/src/config/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -export const databaseConfig = registerAs('database', () => ({ - type: 'postgres', - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT, 10) || 5432, - username: process.env.DB_USERNAME || 'postgres', - password: process.env.DB_PASSWORD || 'postgres', - database: process.env.DB_NAME || 'betting_analytics', - synchronize: process.env.NODE_ENV !== 'production', - logging: process.env.NODE_ENV === 'development', -})); - -export const jwtConfig = registerAs('jwt', () => ({ - secret: process.env.JWT_SECRET || 'change-me-in-production', - expiresIn: process.env.JWT_EXPIRES_IN || '1d', -})); - -export const appConfig = registerAs('app', () => ({ - port: parseInt(process.env.PORT, 10) || 3000, - environment: process.env.NODE_ENV || 'development', - apiPrefix: process.env.API_PREFIX || 'api', -})); diff --git a/projects/betting-analytics/apps/backend/src/main.ts b/projects/betting-analytics/apps/backend/src/main.ts deleted file mode 100644 index 871564ef2..000000000 --- a/projects/betting-analytics/apps/backend/src/main.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { ValidationPipe } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const app = await NestFactory.create(AppModule); - const configService = app.get(ConfigService); - - // Global validation pipe - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - }), - ); - - // CORS configuration - app.enableCors({ - origin: process.env.CORS_ORIGIN || '*', - credentials: true, - }); - - // API prefix - const apiPrefix = configService.get('app.apiPrefix', 'api'); - app.setGlobalPrefix(apiPrefix); - - // Start server - const port = configService.get('app.port', 3000); - await app.listen(port); - - console.log(`Betting Analytics API running on: http://localhost:${port}/${apiPrefix}`); -} - -bootstrap(); diff --git a/projects/betting-analytics/apps/backend/src/modules/auth/auth.module.ts b/projects/betting-analytics/apps/backend/src/modules/auth/auth.module.ts deleted file mode 100644 index fe44ab6ca..000000000 --- a/projects/betting-analytics/apps/backend/src/modules/auth/auth.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common'; - -/** - * Authentication module placeholder - * - * TODO: Implement authentication logic including: - * - User authentication service - * - JWT strategy - * - Local strategy - * - Auth controller - * - Auth guards - */ -@Module({ - imports: [], - controllers: [], - providers: [], - exports: [], -}) -export class AuthModule {} diff --git a/projects/betting-analytics/apps/backend/src/shared/types/index.ts b/projects/betting-analytics/apps/backend/src/shared/types/index.ts deleted file mode 100644 index f0da0fff4..000000000 --- a/projects/betting-analytics/apps/backend/src/shared/types/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Shared type definitions for Betting Analytics - */ - -export interface ApiResponse { - success: boolean; - data?: T; - error?: string; - message?: string; -} - -export interface PaginatedResponse { - data: T[]; - total: number; - page: number; - limit: number; - totalPages: number; -} - -export interface JwtPayload { - sub: string; - email: string; - iat?: number; - exp?: number; -} - -export type Environment = 'development' | 'production' | 'test'; diff --git a/projects/betting-analytics/apps/backend/tsconfig.json b/projects/betting-analytics/apps/backend/tsconfig.json deleted file mode 100644 index c86586bf4..000000000 --- a/projects/betting-analytics/apps/backend/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] -} diff --git a/projects/betting-analytics/apps/frontend/Dockerfile b/projects/betting-analytics/apps/frontend/Dockerfile deleted file mode 100644 index cbcfb551a..000000000 --- a/projects/betting-analytics/apps/frontend/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -# ============================================================================== -# BETTING-ANALYTICS FRONTEND - React Dockerfile -# ============================================================================== -# Multi-stage build for production-ready React application with Nginx -# ============================================================================== - -# Stage 1: Dependencies -FROM node:20-alpine AS deps -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -# Stage 2: Builder -FROM node:20-alpine AS builder -WORKDIR /app - -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -ARG VITE_API_URL -ARG VITE_WS_URL - -ENV VITE_API_URL=${VITE_API_URL} -ENV VITE_WS_URL=${VITE_WS_URL} - -RUN npm run build - -# Stage 3: Production with Nginx -FROM nginx:alpine AS runner - -COPY nginx.conf /etc/nginx/nginx.conf -COPY --from=builder /app/dist /usr/share/nginx/html - -RUN chown -R nginx:nginx /usr/share/nginx/html - -EXPOSE 80 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:80/health || exit 1 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/projects/betting-analytics/apps/ml/Dockerfile b/projects/betting-analytics/apps/ml/Dockerfile deleted file mode 100644 index 1f6b48c7f..000000000 --- a/projects/betting-analytics/apps/ml/Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -# ============================================================================== -# BETTING-ANALYTICS ML ENGINE - Python FastAPI Dockerfile -# ============================================================================== -# Multi-stage build for ML service with optional GPU support -# ============================================================================== - -# Stage 1: Builder -FROM python:3.11-slim AS builder - -WORKDIR /app - -# Install build dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - && rm -rf /var/lib/apt/lists/* - -# Install Python dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir --user -r requirements.txt - -# Stage 2: Production -FROM python:3.11-slim AS runner - -WORKDIR /app - -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 -ENV PATH="/home/appuser/.local/bin:$PATH" - -# Create non-root user -RUN addgroup --system --gid 1001 appgroup -RUN adduser --system --uid 1001 --gid 1001 appuser - -# Copy Python packages from builder -COPY --from=builder /root/.local /home/appuser/.local - -# Copy application code -COPY --chown=appuser:appgroup . . - -USER appuser - -EXPOSE 3093 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:3093/health')" || exit 1 - -CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "3093"] diff --git a/projects/betting-analytics/docs/README.md b/projects/betting-analytics/docs/README.md deleted file mode 100644 index faa8f6ace..000000000 --- a/projects/betting-analytics/docs/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# DOCUMENTACI脫N - Betting Analytics - -**Proyecto:** Betting Analytics -**Versi贸n:** 1.0.0 -**Fecha:** 2025-12-05 -**Estado:** Por iniciar - ---- - -## Estructura de Documentaci贸n - -``` -docs/ -鈹溾攢鈹 00-vision-general/ # Visi贸n, objetivos y alcance -鈹溾攢鈹 01-analisis-referencias/ # An谩lisis de sistemas de referencia -鈹溾攢鈹 02-definicion-modulos/ # Lista, 铆ndice y dependencias de m贸dulos -鈹溾攢鈹 03-requerimientos/ # Requerimientos funcionales por m贸dulo -鈹溾攢鈹 04-modelado/ # Dise帽o t茅cnico -鈹 鈹溾攢鈹 database-design/ # DDL specs, schemas -鈹 鈹溾攢鈹 domain-models/ # Modelos de dominio -鈹 鈹斺攢鈹 especificaciones-tecnicas/ # ET backend/frontend -鈹溾攢鈹 05-user-stories/ # Historias de usuario -鈹溾攢鈹 06-test-plans/ # Planes de prueba -鈹溾攢鈹 07-devops/ # CI/CD, infraestructura -鈹溾攢鈹 90-transversal/ # Documentos transversales -鈹溾攢鈹 95-guias-desarrollo/ # Gu铆as para desarrolladores -鈹斺攢鈹 97-adr/ # Architecture Decision Records -``` - ---- - -## Directiva Aplicable - -Ver: `/workspace/core/orchestration/directivas/DIRECTIVA-ESTRUCTURA-DOCUMENTACION-PROYECTOS.md` - ---- - -**脷ltima actualizaci贸n:** 2025-12-05 diff --git a/projects/betting-analytics/docs/_MAP.md b/projects/betting-analytics/docs/_MAP.md deleted file mode 100644 index 720fc4143..000000000 --- a/projects/betting-analytics/docs/_MAP.md +++ /dev/null @@ -1,40 +0,0 @@ -# Mapa de Documentacion: betting-analytics - -**Proyecto:** betting-analytics -**Actualizado:** 2026-01-04 -**Generado por:** EPIC-008 adapt-simco.sh - ---- - -## Estructura de Documentacion - -``` -docs/ -鈹溾攢鈹 _MAP.md # Este archivo (indice de navegacion) -鈹溾攢鈹 00-overview/ # Vision general del proyecto -鈹溾攢鈹 01-architecture/ # Arquitectura y decisiones (ADRs) -鈹溾攢鈹 02-specs/ # Especificaciones tecnicas -鈹溾攢鈹 03-api/ # Documentacion de APIs -鈹溾攢鈹 04-guides/ # Guias de desarrollo -鈹斺攢鈹 99-finiquito/ # Entregables cliente (si aplica) -``` - -## Navegacion Rapida - -| Seccion | Descripcion | Estado | -|---------|-------------|--------| -| Overview | Vision general | - | -| Architecture | Decisiones arquitectonicas | - | -| Specs | Especificaciones tecnicas | - | -| API | Documentacion de endpoints | - | -| Guides | Guias de desarrollo | - | - -## Estadisticas - -- Total archivos en docs/: 1 -- Fecha de adaptacion: 2026-01-04 - ---- - -**Nota:** Este archivo fue generado automaticamente por EPIC-008. -Actualizar manualmente con la estructura real del proyecto. diff --git a/projects/betting-analytics/orchestration/00-guidelines/CONTEXTO-PROYECTO.md b/projects/betting-analytics/orchestration/00-guidelines/CONTEXTO-PROYECTO.md deleted file mode 100644 index 701777a0e..000000000 --- a/projects/betting-analytics/orchestration/00-guidelines/CONTEXTO-PROYECTO.md +++ /dev/null @@ -1,136 +0,0 @@ -# Contexto del Proyecto - Betting Analytics - -## Identificaci贸n - -| Campo | Valor | -|-------|-------| -| **Nombre** | Betting Analytics Platform | -| **C贸digo** | betting-analytics | -| **Nivel** | 2A (Standalone) | -| **Estado** | Planificaci贸n | -| **Progreso** | 0% | -| **Versi贸n** | 0.0.1 | -| **Creado** | 2025-12-05 | -| **Path** | `/home/isem/workspace-v1/projects/betting-analytics/` | - ---- - -## Descripci贸n - -Plataforma de an谩lisis y estad铆sticas para apuestas deportivas. El proyecto est谩 actualmente en fase de planificaci贸n. - -**Funcionalidades previstas:** -- An谩lisis estad铆stico de eventos deportivos -- Tracking de resultados y ROI -- Predicciones basadas en ML -- Dashboard de rendimiento -- Gesti贸n de bankroll - ---- - -## Stack Tecnol贸gico (Propuesto) - -| Capa | Tecnolog铆a | -|------|------------| -| **Frontend** | React 18 + TypeScript + Vite + Tailwind CSS | -| **Backend** | Express.js / NestJS + TypeScript | -| **Database** | PostgreSQL 15+ | -| **ML** | Python + FastAPI (predicciones) | - ---- - -## Estructura del Proyecto - -``` -betting-analytics/ -鈹溾攢鈹 apps/ -鈹 鈹溾攢鈹 backend/ # API REST (vac铆o) -鈹 鈹溾攢鈹 database/ # DDL y migraciones (vac铆o) -鈹 鈹斺攢鈹 frontend/ # SPA React (vac铆o) -鈹 -鈹溾攢鈹 docs/ # Documentaci贸n -鈹 鈹溾攢鈹 00-vision-general/ -鈹 鈹溾攢鈹 01-analisis-referencias/ -鈹 鈹溾攢鈹 02-definicion-modulos/ -鈹 鈹溾攢鈹 03-requerimientos/ -鈹 鈹溾攢鈹 04-modelado/ -鈹 鈹溾攢鈹 05-user-stories/ -鈹 鈹溾攢鈹 06-test-plans/ -鈹 鈹溾攢鈹 07-devops/ -鈹 鈹溾攢鈹 90-transversal/ -鈹 鈹溾攢鈹 95-guias-desarrollo/ -鈹 鈹斺攢鈹 97-adr/ -鈹 -鈹斺攢鈹 orchestration/ # Sistema NEXUS - 鈹溾攢鈹 00-guidelines/ - 鈹溾攢鈹 inventarios/ - 鈹溾攢鈹 trazas/ - 鈹斺攢鈹 ... -``` - ---- - -## Variables del Proyecto - -```yaml -# Para uso en delegaciones SIMCO -PROJECT_NAME: betting-analytics -PROJECT_PATH: /home/isem/workspace-v1/projects/betting-analytics - -# Database (propuesto) -DB_NAME: betting_analytics -DB_DDL_PATH: apps/database/ddl -DB_SEEDS_PATH: apps/database/seeds - -# Backend (propuesto) -BACKEND_ROOT: apps/backend -BACKEND_SRC: apps/backend/src - -# Frontend (propuesto) -FRONTEND_ROOT: apps/frontend -FRONTEND_SRC: apps/frontend/src -``` - ---- - -## Estado Actual - -| Aspecto | Estado | -|---------|--------| -| Documentaci贸n | Estructura creada, sin contenido | -| Base de datos | No iniciado | -| Backend | No iniciado | -| Frontend | No iniciado | -| Tests | No iniciado | - ---- - -## Pr贸ximos Pasos - -1. [ ] Definir alcance y requerimientos -2. [ ] Documentar visi贸n general en `docs/00-vision-general/` -3. [ ] Dise帽ar modelo de datos -4. [ ] Crear 茅picas y user stories -5. [ ] Iniciar desarrollo del MVP - ---- - -## Herencia de Directivas - -Este proyecto hereda directivas de: -1. **Core Global:** `core/orchestration/directivas/` -2. **Principios:** `core/orchestration/directivas/principios/` -3. **SIMCO:** `core/orchestration/directivas/simco/` - ---- - -## Referencias - -| Recurso | Path | -|---------|------| -| Core orchestration | `/home/isem/workspace-v1/core/orchestration/` | -| Cat谩logo global | `/home/isem/workspace-v1/core/catalog/` | - ---- -*Contexto del proyecto - Sistema NEXUS + SIMCO v2.2.0* -*脷ltima actualizaci贸n: 2025-12-08* diff --git a/projects/betting-analytics/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md b/projects/betting-analytics/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md deleted file mode 100644 index 378fac189..000000000 --- a/projects/betting-analytics/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md +++ /dev/null @@ -1,110 +0,0 @@ -# Herencia de Directivas - Betting Analytics - -## Arquitectura de Directivas - -Este proyecto hereda directivas del workspace (core) y define directivas espec铆ficas. - -## Directivas Globales (heredadas de core) - -**Path:** `~/workspace/core/orchestration/directivas/` - -Estas directivas aplican a TODOS los proyectos del workspace: - -| Directiva | Prop贸sito | -|-----------|-----------| -| `DIRECTIVA-FLUJO-5-FASES.md` | Workflow obligatorio de 5 fases | -| `DIRECTIVA-VALIDACION-SUBAGENTES.md` | Validaci贸n de entregables | -| `POLITICAS-USO-AGENTES.md` | Reglas de delegaci贸n | -| `DIRECTIVA-DOCUMENTACION-OBLIGATORIA.md` | Documentaci贸n requerida | -| `DIRECTIVA-CALIDAD-CODIGO.md` | Est谩ndares de c贸digo | -| `DIRECTIVA-CONTROL-VERSIONES.md` | Git y versionado | -| `DIRECTIVA-GESTION-BACKUPS-GITIGNORE.md` | Backups y gitignore | -| `PROTOCOLO-ESCALAMIENTO-PO.md` | Escalamiento al PO | -| `ESTANDARES-NOMENCLATURA-BASE.md` | Nomenclatura base | -| `SISTEMA-RETROALIMENTACION.md` | Mejora continua | - -## Directivas Espec铆ficas de Betting Analytics - -**Path:** `~/workspace/projects/betting-analytics/orchestration/directivas/` - -| Directiva | Prop贸sito | -|-----------|-----------| -| *(Por definir seg煤n necesidades del proyecto)* | - | - -## Prompts Base (heredados de core) - -**Path:** `~/workspace/core/orchestration/prompts/base/` - -| Prompt | Uso | -|--------|-----| -| `PROMPT-SUBAGENTES-BASE.md` | Instrucciones generales para subagentes | -| `PROMPT-BUG-FIXER.md` | Correcci贸n de bugs gen茅rico | -| `PROMPT-CODE-REVIEWER.md` | Revisi贸n de c贸digo | -| `PROMPT-FEATURE-DEVELOPER.md` | Desarrollo de features | -| `PROMPT-DOCUMENTATION-VALIDATOR.md` | Validaci贸n de documentaci贸n | -| `PROMPT-POLICY-AUDITOR.md` | Auditor铆a de pol铆ticas | -| `PROMPT-REQUIREMENTS-ANALYST.md` | An谩lisis de requerimientos | -| `PROMPT-WORKSPACE-MANAGER.md` | Gesti贸n del workspace | - -## Prompts Espec铆ficos de Betting Analytics - -**Path:** `~/workspace/projects/betting-analytics/orchestration/prompts/` - -| Prompt | Uso | -|--------|-----| -| *(Por definir seg煤n necesidades del proyecto)* | - | - -## Templates y Checklists (core) - -**Path:** `~/workspace/core/orchestration/templates/` -- `TEMPLATE-ANALISIS.md` -- `TEMPLATE-PLAN.md` -- `TEMPLATE-VALIDACION.md` -- `TEMPLATE-CONTEXTO-SUBAGENTE.md` - -**Path:** `~/workspace/core/orchestration/checklists/` -- `CHECKLIST-CODE-REVIEW-API.md` -- `CHECKLIST-REFACTORIZACION.md` - -## Orden de Precedencia - -Cuando hay conflicto entre directivas: - -1. **Directivas espec铆ficas del proyecto** (mayor prioridad) -2. **Directivas globales del workspace** -3. **Prompts espec铆ficos del proyecto** -4. **Prompts base del workspace** - -## Cat谩logo de Funcionalidades Usado - -Este proyecto utiliza las siguientes funcionalidades del cat谩logo core: - -| Funcionalidad | Uso en el proyecto | -|---------------|-------------------| -| *(Por definir seg煤n necesidades del proyecto)* | - | - -**Path cat谩logo:** `~/workspace/core/catalog/` - -## Uso para Subagentes - -Al invocar un subagente, incluir en el contexto: - -```yaml -DIRECTIVAS_A_LEER: - globales: - - ~/workspace/core/orchestration/directivas/DIRECTIVA-FLUJO-5-FASES.md - - ~/workspace/core/orchestration/directivas/POLITICAS-USO-AGENTES.md - especificas: - - ~/workspace/projects/betting-analytics/orchestration/directivas/[DIRECTIVA-RELEVANTE].md - prompt_base: - - ~/workspace/core/orchestration/prompts/base/PROMPT-SUBAGENTES-BASE.md - prompt_especifico: - - ~/workspace/projects/betting-analytics/orchestration/prompts/[PROMPT-AGENTE].md - contexto_proyecto: - - ~/workspace/projects/betting-analytics/orchestration/00-guidelines/CONTEXTO-PROYECTO.md -``` - ---- -*Sistema NEXUS + SIMCO v2.2.0 - Betting Analytics* -*Nivel: 2A (Standalone)* -*Actualizado: 2025-12-08* diff --git a/projects/betting-analytics/orchestration/00-guidelines/HERENCIA-SIMCO.md b/projects/betting-analytics/orchestration/00-guidelines/HERENCIA-SIMCO.md deleted file mode 100644 index 52a85c2bd..000000000 --- a/projects/betting-analytics/orchestration/00-guidelines/HERENCIA-SIMCO.md +++ /dev/null @@ -1,125 +0,0 @@ -# Herencia SIMCO - Betting Analytics - -**Sistema:** SIMCO v2.2.0 + CAPVED + CCA Protocol -**Fecha:** 2025-12-08 - ---- - -## Configuraci贸n del Proyecto - -| Propiedad | Valor | -|-----------|-------| -| **Proyecto** | Betting Analytics | -| **Nivel** | STANDALONE | -| **Padre** | core/orchestration | -| **SIMCO Version** | 2.2.0 | -| **CAPVED** | Habilitado | -| **CCA Protocol** | Habilitado | - -## Jerarqu铆a de Herencia - -``` -Nivel 0: core/orchestration/ 鈫 FUENTE PRINCIPAL - 鈹 - 鈹斺攢鈹 STANDALONE: betting-analytics/orchestration/ 鈫 ESTE PROYECTO -``` - ---- - -## Directivas Heredadas de CORE (OBLIGATORIAS) - -### Ciclo de Vida -| Alias | Prop贸sito | -|-------|-----------| -| `@TAREA` | Punto de entrada para toda HU | -| `@CAPVED` | Ciclo de 6 fases | -| `@INICIALIZACION` | Bootstrap de agentes | - -### Operaciones Universales -| Alias | Prop贸sito | -|-------|-----------| -| `@CREAR` | Crear archivos nuevos | -| `@MODIFICAR` | Modificar existentes | -| `@VALIDAR` | Validar c贸digo | -| `@DOCUMENTAR` | Documentar trabajo | -| `@BUSCAR` | Buscar informaci贸n | -| `@DELEGAR` | Delegar a subagentes | - -### Principios Fundamentales -| Alias | Resumen | -|-------|---------| -| `@CAPVED` | Toda tarea pasa por 6 fases | -| `@DOC_PRIMERO` | Consultar docs/ antes de implementar | -| `@ANTI_DUP` | Verificar que no existe | -| `@VALIDACION` | Build y lint DEBEN pasar | -| `@TOKENS` | Desglosar tareas grandes | - ---- - -## Directivas por Dominio T茅cnico - -| Alias | Aplica | Notas | -|-------|--------|-------| -| `@OP_DDL` | **S脥** | Schemas de apuestas | -| `@OP_BACKEND` | **S脥** | APIs de an谩lisis | -| `@OP_FRONTEND` | **S脥** | Dashboard de estad铆sticas | -| `@OP_MOBILE` | Por definir | - | -| `@OP_ML` | **S脥** | Predicci贸n de resultados | - ---- - -## Patrones Heredados (OBLIGATORIOS) - -Todos los patrones de `core/orchestration/patrones/` aplican. - ---- - -## Variables de Contexto CCA - -```yaml -PROJECT_NAME: "betting-analytics" -PROJECT_LEVEL: "STANDALONE" -PROJECT_ROOT: "/home/isem/workspace-v1/projects/betting-analytics" - -DB_DDL_PATH: "database/ddl" -BACKEND_ROOT: "backend/src" -FRONTEND_ROOT: "frontend/src" - -MASTER_INVENTORY: "orchestration/inventarios/MASTER_INVENTORY.yml" -``` - ---- - -## Mapeo: Directivas Antiguas 鈫 SIMCO - -| Directiva Antigua | Reemplazada Por | Alias | -|-------------------|-----------------|-------| -| `DIRECTIVA-FLUJO-5-FASES.md` | `SIMCO-TAREA.md` + `PRINCIPIO-CAPVED.md` | @TAREA, @CAPVED | -| `DIRECTIVA-VALIDACION-SUBAGENTES.md` | `SIMCO-VALIDAR.md` | @VALIDAR | -| `POLITICAS-USO-AGENTES.md` | `SIMCO-DELEGACION.md` | @DELEGAR | - ---- - -## Propagacion de Mejoras - -Este proyecto participa en el sistema de propagacion de mejoras de NEXUS. - -### Modulos Base Usados - -| Modulo | Version | Estado | -|--------|---------|--------| -| auth-jwt-nestjs | 2.1.0 | Al dia | -| api-rest-crud | 2.0.0 | Al dia | -| api-pagination | 1.5.0 | Al dia | - -Ver estado completo: `shared/knowledge-base/TRAZABILIDAD-PROYECTOS.yml` - -### Recibir Propagaciones - -Ver directiva completa: @PROPAGACION - ---- - -**Sistema:** SIMCO v2.2.0 + CAPVED + CCA Protocol -**Nivel:** STANDALONE -**脷ltima actualizaci贸n:** 2026-01-04 diff --git a/projects/betting-analytics/orchestration/00-guidelines/PROJECT-STATUS.md b/projects/betting-analytics/orchestration/00-guidelines/PROJECT-STATUS.md deleted file mode 100644 index 536486720..000000000 --- a/projects/betting-analytics/orchestration/00-guidelines/PROJECT-STATUS.md +++ /dev/null @@ -1,33 +0,0 @@ -# PROJECT STATUS: betting-analytics - -**Ultima actualizacion:** 2026-01-04 -**Estado general:** Activo - ---- - -## Metricas Rapidas - -| Metrica | Valor | -|---------|-------| -| Archivos docs/ | 1 | -| Archivos orchestration/ | 13 | -| Estado SIMCO | Adaptado | - -## Migracion EPIC-008 - -- [x] Migracion desde workspace-v1-bckp (EPIC-004/005) -- [x] Adaptacion SIMCO (EPIC-008) -- [x] docs/_MAP.md creado -- [x] PROJECT-STATUS.md creado -- [x] HERENCIA-SIMCO.md verificado -- [x] CONTEXTO-PROYECTO.md verificado - -## Historial de Cambios - -| Fecha | Cambio | EPIC | -|-------|--------|------| -| 2026-01-04 | Adaptacion SIMCO completada | EPIC-008 | - ---- - -**Generado por:** EPIC-008 adapt-simco.sh diff --git a/projects/betting-analytics/orchestration/PROXIMA-ACCION.md b/projects/betting-analytics/orchestration/PROXIMA-ACCION.md deleted file mode 100644 index 226525e52..000000000 --- a/projects/betting-analytics/orchestration/PROXIMA-ACCION.md +++ /dev/null @@ -1,11 +0,0 @@ -# Pr贸xima Acci贸n - betting-analytics - -## Estado Actual -- **Fecha:** 2025-12-05 -- **Estado:** Pendiente de planificaci贸n - -## Pr贸xima Tarea -Definir alcance y requerimientos iniciales del proyecto. - ---- -*Actualizado: 2025-12-05* diff --git a/projects/betting-analytics/orchestration/environment/PROJECT-ENV-CONFIG.yml b/projects/betting-analytics/orchestration/environment/PROJECT-ENV-CONFIG.yml deleted file mode 100644 index 4251fe5cd..000000000 --- a/projects/betting-analytics/orchestration/environment/PROJECT-ENV-CONFIG.yml +++ /dev/null @@ -1,47 +0,0 @@ -# ============================================================================= -# PROJECT-ENV-CONFIG.yml -# Configuraci贸n de Ambiente espec铆fica del proyecto BETTING-ANALYTICS -# ============================================================================= -# Proyecto: BETTING-ANALYTICS - An谩lisis de Apuestas Deportivas -# Actualizado: 2025-12-05 -# Referencia: ~/workspace/core/devtools/environment/DEV-ENVIRONMENT-REGISTRY.yml -# ============================================================================= - -project: - name: "BETTING_ANALYTICS" - slug: "betting-analytics" - description: "Plataforma de An谩lisis de Apuestas Deportivas" - status: "development" - port_block: 3090 - -ports: - frontend: 3090 - backend: 3091 - ml_service: 8003 - -database: - host: "localhost" - port: 5438 # Puerto asignado para betting-analytics (ver DEVENV-PORTS-INVENTORY.yml) - name: "betting_analytics" - user: "betting_user" - # password: Ver archivo .env local - -urls: - frontend: "http://localhost:3090" - backend_api: "http://localhost:3091/api" - swagger: "http://localhost:3091/api/docs" - -env_files: - backend: "apps/backend/.env" - frontend: "apps/frontend/.env" - -stack: - backend: "NestJS + TypeScript" - frontend: "React + TypeScript + Vite" - database: "PostgreSQL 15" - ml_service: "FastAPI + Python" - -notes: | - - Proyecto en desarrollo inicial - - Servicio ML para an谩lisis predictivo de apuestas - - Base de datos: betting_analytics (en instancia PostgreSQL compartida) diff --git a/projects/betting-analytics/orchestration/estados/REGISTRO-SUBAGENTES.json b/projects/betting-analytics/orchestration/estados/REGISTRO-SUBAGENTES.json deleted file mode 100644 index 311e0b938..000000000 --- a/projects/betting-analytics/orchestration/estados/REGISTRO-SUBAGENTES.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "proyecto": "betting-analytics", - "limite_maximo": 15, - "slots_disponibles": 15, - "ultima_actualizacion": "2025-12-05T02:19:51-06:00", - "activos": [], - "completados": [], - "fallidos": [] -} diff --git a/projects/betting-analytics/orchestration/inventarios/BACKEND_INVENTORY.yml b/projects/betting-analytics/orchestration/inventarios/BACKEND_INVENTORY.yml deleted file mode 100644 index 224d1e681..000000000 --- a/projects/betting-analytics/orchestration/inventarios/BACKEND_INVENTORY.yml +++ /dev/null @@ -1,32 +0,0 @@ -# BACKEND INVENTORY - Betting Analytics Platform -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto no iniciado - -backend: - framework: Express.js / NestJS + TypeScript (propuesto) - path: apps/backend/ - src: apps/backend/src/ - estado: No iniciado - -estructura: {} -# Se definir谩 cuando inicie el desarrollo - -modulos: [] -# M贸dulos propuestos: -# - auth: Autenticaci贸n -# - users: Gesti贸n de usuarios -# - events: Eventos deportivos -# - bets: Apuestas -# - analytics: An谩lisis -# - predictions: Predicciones ML - -resumen: - total_modulos: 0 - total_controllers: 0 - total_services: 0 - total_endpoints: 0 - -ultima_actualizacion: 2025-12-08 -actualizado_por: NEXUS-System -nota: Este es un template inicial. Se actualizar谩 cuando inicie el desarrollo. diff --git a/projects/betting-analytics/orchestration/inventarios/DATABASE_INVENTORY.yml b/projects/betting-analytics/orchestration/inventarios/DATABASE_INVENTORY.yml deleted file mode 100644 index 4528d4d5e..000000000 --- a/projects/betting-analytics/orchestration/inventarios/DATABASE_INVENTORY.yml +++ /dev/null @@ -1,28 +0,0 @@ -# DATABASE INVENTORY - Betting Analytics Platform -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto no iniciado - -database: - nombre: betting_analytics - motor: PostgreSQL 15+ (propuesto) - path_ddl: apps/database/ddl/ - path_seeds: apps/database/seeds/ - estado: No iniciado - -schemas: [] -# Schemas se agregar谩n cuando se inicie el desarrollo -# Ejemplos propuestos: -# - auth: Autenticaci贸n y usuarios -# - betting: Apuestas y eventos -# - analytics: An谩lisis y estad铆sticas -# - ml: Predicciones ML - -resumen: - total_schemas: 0 - total_tablas: 0 - total_funciones: 0 - -ultima_actualizacion: 2025-12-08 -actualizado_por: NEXUS-System -nota: Este es un template inicial. Se actualizar谩 cuando inicie el desarrollo. diff --git a/projects/betting-analytics/orchestration/inventarios/FRONTEND_INVENTORY.yml b/projects/betting-analytics/orchestration/inventarios/FRONTEND_INVENTORY.yml deleted file mode 100644 index 487513fac..000000000 --- a/projects/betting-analytics/orchestration/inventarios/FRONTEND_INVENTORY.yml +++ /dev/null @@ -1,33 +0,0 @@ -# FRONTEND INVENTORY - Betting Analytics Platform -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto no iniciado - -frontend: - framework: React 18 + TypeScript + Vite (propuesto) - path: apps/frontend/ - src: apps/frontend/src/ - styling: Tailwind CSS (propuesto) - estado: No iniciado - -estructura: {} -# Se definir谩 cuando inicie el desarrollo - -modulos: [] -# M贸dulos propuestos: -# - auth: Login, register, etc. -# - dashboard: Dashboard principal -# - events: Listado de eventos -# - bets: Gesti贸n de apuestas -# - analytics: Reportes y an谩lisis -# - settings: Configuraci贸n - -resumen: - total_paginas: 0 - total_componentes: 0 - total_hooks: 0 - total_stores: 0 - -ultima_actualizacion: 2025-12-08 -actualizado_por: NEXUS-System -nota: Este es un template inicial. Se actualizar谩 cuando inicie el desarrollo. diff --git a/projects/betting-analytics/orchestration/inventarios/MASTER_INVENTORY.yml b/projects/betting-analytics/orchestration/inventarios/MASTER_INVENTORY.yml deleted file mode 100644 index 2b176687b..000000000 --- a/projects/betting-analytics/orchestration/inventarios/MASTER_INVENTORY.yml +++ /dev/null @@ -1,59 +0,0 @@ -# MASTER INVENTORY - Betting Analytics Platform -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 - -proyecto: - nombre: Betting Analytics Platform - codigo: betting-analytics - nivel: 2A (Standalone) - estado: Planificacion - version: 0.0.1 - path: /home/isem/workspace/projects/betting-analytics - -resumen_general: - total_schemas: 0 - total_tablas: 0 - total_servicios_backend: 0 - total_componentes_frontend: 0 - test_coverage: N/A - ultima_actualizacion: 2025-12-08 - -epicas: - # Pendiente definir epicas - - codigo: TBD - nombre: Por definir - sp: TBD - estado: No iniciado - -capas: - database: - inventario: DATABASE_INVENTORY.yml - total_objetos: 0 - estado: No iniciado - - backend: - inventario: BACKEND_INVENTORY.yml - total_objetos: 0 - estado: No iniciado - - frontend: - inventario: FRONTEND_INVENTORY.yml - total_objetos: 0 - estado: No iniciado - -stack_propuesto: - frontend: React 18 + TypeScript + Vite + Tailwind CSS - backend: Express.js / NestJS + TypeScript - database: PostgreSQL 15+ - ml: Python + FastAPI - -proximos_pasos: - - Definir alcance y requerimientos - - Documentar vision general - - Disenar modelo de datos - - Crear epicas y user stories - -referencias: - docs: docs/ - orchestration: orchestration/ - contexto: orchestration/00-guidelines/CONTEXTO-PROYECTO.md diff --git a/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-BACKEND.md b/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-BACKEND.md deleted file mode 100644 index f60cc5018..000000000 --- a/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-BACKEND.md +++ /dev/null @@ -1,40 +0,0 @@ -# TRAZA DE TAREAS - BACKEND LAYER -# Proyecto: Betting Analytics Platform -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto en planificaci贸n - ---- - -## Formato de Registro - -```yaml -[FECHA] - [ID_TAREA] - [OPERACION] -Descripcion: {descripcion} -Archivos: - - {archivo_1} - - {archivo_2} -Estado: {COMPLETADO | EN_PROGRESO | BLOQUEADO} -Ejecutado_por: {AGENTE | USUARIO} -Notas: {observaciones} -``` - ---- - -## Historial de Tareas - -*Sin tareas registradas - Proyecto en planificaci贸n* - ---- - -## Resumen - -| M茅trica | Valor | -|---------|-------| -| Total tareas | 0 | -| Completadas | 0 | -| En progreso | 0 | -| Bloqueadas | 0 | -| 脷ltima actualizaci贸n | 2025-12-08 | - ---- -*Traza de tareas - Sistema NEXUS* diff --git a/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-DATABASE.md b/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-DATABASE.md deleted file mode 100644 index 6f2800400..000000000 --- a/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-DATABASE.md +++ /dev/null @@ -1,40 +0,0 @@ -# TRAZA DE TAREAS - DATABASE LAYER -# Proyecto: Betting Analytics Platform -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto en planificaci贸n - ---- - -## Formato de Registro - -```yaml -[FECHA] - [ID_TAREA] - [OPERACION] -Descripcion: {descripcion} -Archivos: - - {archivo_1} - - {archivo_2} -Estado: {COMPLETADO | EN_PROGRESO | BLOQUEADO} -Ejecutado_por: {AGENTE | USUARIO} -Notas: {observaciones} -``` - ---- - -## Historial de Tareas - -*Sin tareas registradas - Proyecto en planificaci贸n* - ---- - -## Resumen - -| M茅trica | Valor | -|---------|-------| -| Total tareas | 0 | -| Completadas | 0 | -| En progreso | 0 | -| Bloqueadas | 0 | -| 脷ltima actualizaci贸n | 2025-12-08 | - ---- -*Traza de tareas - Sistema NEXUS* diff --git a/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md b/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md deleted file mode 100644 index 614676db4..000000000 --- a/projects/betting-analytics/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md +++ /dev/null @@ -1,40 +0,0 @@ -# TRAZA DE TAREAS - FRONTEND LAYER -# Proyecto: Betting Analytics Platform -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto en planificaci贸n - ---- - -## Formato de Registro - -```yaml -[FECHA] - [ID_TAREA] - [OPERACION] -Descripcion: {descripcion} -Archivos: - - {archivo_1} - - {archivo_2} -Estado: {COMPLETADO | EN_PROGRESO | BLOQUEADO} -Ejecutado_por: {AGENTE | USUARIO} -Notas: {observaciones} -``` - ---- - -## Historial de Tareas - -*Sin tareas registradas - Proyecto en planificaci贸n* - ---- - -## Resumen - -| M茅trica | Valor | -|---------|-------| -| Total tareas | 0 | -| Completadas | 0 | -| En progreso | 0 | -| Bloqueadas | 0 | -| 脷ltima actualizaci贸n | 2025-12-08 | - ---- -*Traza de tareas - Sistema NEXUS* diff --git a/projects/erp-clinicas/.env.example b/projects/erp-clinicas/.env.example deleted file mode 100644 index 820e8661a..000000000 --- a/projects/erp-clinicas/.env.example +++ /dev/null @@ -1,182 +0,0 @@ -# =========================================== -# CLINICAS - Variables de Entorno -# =========================================== -# Copiar este archivo a .env y configurar valores -# Puertos seg煤n DEVENV-PORTS.md -# NOTA: Este sistema requiere cumplimiento NOM-024 y LFPDPPP - -# ------------------------------------------- -# BASE DE DATOS POSTGRESQL -# ------------------------------------------- -DB_HOST=localhost -DB_PORT=5437 -DB_NAME=clinicas_db -DB_USER=clinicas_user -DB_PASSWORD=clinicas_secret_2025 - -# URL de conexion completa -DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} - -# ------------------------------------------- -# SCHEMAS DE BASE DE DATOS -# ------------------------------------------- -# Schemas heredados de erp-core -DB_SCHEMA_AUTH=auth -DB_SCHEMA_CORE=core -DB_SCHEMA_INVENTORY=inventory - -# Schemas propios de cl铆nicas -DB_SCHEMA_CLINICAL=clinical -DB_SCHEMA_PHARMACY=pharmacy -DB_SCHEMA_LABORATORY=laboratory -DB_SCHEMA_IMAGING=imaging -DB_SCHEMA_TELEMEDICINE=telemedicine - -# ------------------------------------------- -# APLICACION -# ------------------------------------------- -APP_NAME=clinicas -APP_ENV=development -APP_PORT=3061 -APP_URL=http://localhost:3061 - -# ------------------------------------------- -# FRONTEND -# ------------------------------------------- -FRONTEND_PORT=3060 -FRONTEND_URL=http://localhost:3060 - -# ------------------------------------------- -# AUTENTICACION JWT -# ------------------------------------------- -JWT_SECRET=your_jwt_secret_here_change_in_production -JWT_EXPIRES_IN=8h -JWT_REFRESH_EXPIRES_IN=24h - -# ------------------------------------------- -# TWO-FACTOR AUTHENTICATION (OBLIGATORIO) -# ------------------------------------------- -# Requerido para personal m茅dico seg煤n LFPDPPP -TWO_FACTOR_ENABLED=true -TWO_FACTOR_METHOD=totp -TOTP_ISSUER=ERP-Clinicas -TOTP_WINDOW=1 - -# SMS 2FA (Twilio) -TWILIO_ACCOUNT_SID= -TWILIO_AUTH_TOKEN= -TWILIO_PHONE_FROM= - -# ------------------------------------------- -# ENCRIPTACION DE DATOS SENSIBLES (LFPDPPP) -# ------------------------------------------- -# CRITICO: Cambiar en producci贸n -ENCRYPTION_KEY=your_32_byte_encryption_key_here -ENCRYPTION_ALGORITHM=aes-256-gcm -ENCRYPTION_IV_LENGTH=16 - -# Campos encriptados autom谩ticamente: -# - antecedentes_medicos -# - alergias -# - diagnosticos -# - notas_clinicas - -# ------------------------------------------- -# MULTI-TENANT -# ------------------------------------------- -TENANT_ID_HEADER=X-Tenant-ID -TENANT_ID_PARAM=tenant_id - -# ------------------------------------------- -# ALMACENAMIENTO DE ARCHIVOS -# ------------------------------------------- -STORAGE_TYPE=local -STORAGE_PATH=./uploads -# Para producci贸n usar S3 con encriptaci贸n: -# STORAGE_TYPE=s3 -# AWS_ACCESS_KEY_ID= -# AWS_SECRET_ACCESS_KEY= -# AWS_REGION=us-east-1 -# AWS_S3_BUCKET=clinicas-files -# AWS_S3_ENCRYPTION=AES256 - -# ------------------------------------------- -# NOTIFICACIONES -# ------------------------------------------- -# Email (SMTP) -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USER= -SMTP_PASSWORD= -SMTP_FROM=noreply@clinicas-erp.com - -# SMS para recordatorios de citas -SMS_PROVIDER=twilio -SMS_REMINDER_HOURS_BEFORE=24 - -# ------------------------------------------- -# FACTURACION ELECTRONICA (SAT) -# ------------------------------------------- -SAT_ENVIRONMENT=sandbox -SAT_RFC= -SAT_CER_PATH=./certs/csd.cer -SAT_KEY_PATH=./certs/csd.key -SAT_KEY_PASSWORD= - -# ------------------------------------------- -# LOGGING Y AUDITORIA -# ------------------------------------------- -LOG_LEVEL=debug -LOG_FORMAT=json - -# Auditor铆a de accesos (NOM-024) -AUDIT_ENABLED=true -AUDIT_RETENTION_YEARS=10 -AUDIT_LOG_MEDICAL_RECORD_ACCESS=true -AUDIT_LOG_PRESCRIPTION_CREATED=true -AUDIT_LOG_PATIENT_DATA_MODIFIED=true - -# ------------------------------------------- -# REDIS (Cache y Colas) -# ------------------------------------------- -REDIS_HOST=localhost -REDIS_PORT=6384 -REDIS_PASSWORD= - -# ------------------------------------------- -# CORS -# ------------------------------------------- -CORS_ORIGIN=http://localhost:3060,http://localhost:3061 - -# ------------------------------------------- -# EXPEDIENTE CLINICO (NOM-024-SSA3-2012) -# ------------------------------------------- -# Estructura SOAP obligatoria -NOM024_SOAP_REQUIRED=true -NOM024_CIE10_VALIDATION=true -NOM024_PRESCRIPTION_SIGNATURE_REQUIRED=true -NOM024_CONSENT_REQUIRED=true - -# ------------------------------------------- -# TELEMEDICINA (Opcional) -# ------------------------------------------- -TELEMEDICINE_ENABLED=false -TELEMEDICINE_PROVIDER=jitsi -TELEMEDICINE_SERVER_URL= -TELEMEDICINE_RECORDING_ENABLED=false - -# ------------------------------------------- -# IMAGENOLOGIA DICOM (Opcional) -# ------------------------------------------- -DICOM_ENABLED=false -DICOM_SERVER_HOST= -DICOM_SERVER_PORT=4242 -DICOM_AE_TITLE=CLINICAS_ERP - -# ------------------------------------------- -# INTEROPERABILIDAD (HL7/FHIR) -# ------------------------------------------- -HL7_ENABLED=false -HL7_ENDPOINT= -FHIR_ENABLED=false -FHIR_SERVER_URL= diff --git a/projects/erp-clinicas/INVENTARIO.yml b/projects/erp-clinicas/INVENTARIO.yml deleted file mode 100644 index 31a14d518..000000000 --- a/projects/erp-clinicas/INVENTARIO.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Inventario generado por EPIC-008 -proyecto: erp-clinicas -fecha: "2026-01-04" -generado_por: "inventory-project.sh v1.0.0" - -inventario: - docs: - total: 27 - por_tipo: - markdown: 27 - yaml: 0 - json: 0 - orchestration: - total: 23 - por_tipo: - markdown: 14 - yaml: 9 - json: 0 - -problemas: - archivos_obsoletos: 0 - referencias_antiguas: 0 - simco_faltantes: - - _MAP.md en docs/ - - PROJECT-STATUS.md - -estado_simco: - herencia_simco: true - contexto_proyecto: true - map_docs: false - project_status: false diff --git a/projects/erp-clinicas/PROJECT-STATUS.md b/projects/erp-clinicas/PROJECT-STATUS.md deleted file mode 100644 index 6836b5747..000000000 --- a/projects/erp-clinicas/PROJECT-STATUS.md +++ /dev/null @@ -1,178 +0,0 @@ -# ESTADO DEL PROYECTO - ERP Cl铆nicas - -**Proyecto:** ERP Cl铆nicas (Proyecto Independiente) -**Estado:** 馃搵 En planificaci贸n -**Progreso:** 25% -**脷ltima actualizaci贸n:** 2025-12-08 - ---- - -## 馃搳 RESUMEN EJECUTIVO - -| 脕rea | Estado | Descripci贸n | -|------|--------|-------------| -| **Documentaci贸n** | 馃煛 Inicial | 12 m贸dulos definidos, estructura base | -| **DDL/Schemas** | 鉂 No iniciado | Pendiente dise帽o de BD | -| **Backend** | 鉂 No iniciado | Pendiente desarrollo | -| **Frontend** | 鉂 No iniciado | Pendiente desarrollo | - ---- - -## 馃搵 M脫DULOS DEFINIDOS (12) - -| C贸digo | Nombre | Descripci贸n | Reutilizaci贸n | Estado | -|--------|--------|-------------|---------------|--------| -| CL-001 | Fundamentos | Auth, Users, Tenants | 100% core | PLANIFICADO | -| CL-002 | Pacientes | Registro y expediente | 20% core | PLANIFICADO | -| CL-003 | Citas | Agenda m茅dica | 30% core | PLANIFICADO | -| CL-004 | Consultas | Notas m茅dicas | 0% (nuevo) | PLANIFICADO | -| CL-005 | Recetas | Prescripciones | 10% core | PLANIFICADO | -| CL-006 | Laboratorio | Estudios y resultados | 10% core | PLANIFICADO | -| CL-007 | Farmacia | Inventario medicamentos | 60% core | PLANIFICADO | -| CL-008 | Facturaci贸n | Cobros y seguros | 50% core | PLANIFICADO | -| CL-009 | Reportes | Estad铆sticas cl铆nicas | 60% core | PLANIFICADO | -| CL-010 | Telemedicina | Consultas remotas | 0% (nuevo) | PLANIFICADO | -| CL-011 | Expediente | Historia cl铆nica completa | 10% core | PLANIFICADO | -| CL-012 | Imagenolog铆a | Estudios DICOM | 5% core | PLANIFICADO | - -**Story Points Estimados:** 451 SP (detallado en 茅picas) - ---- - -## 馃彞 DOMINIO DE NEGOCIO - -### Modelo de Negocio -- Cl铆nicas y consultorios m茅dicos -- Consultas presenciales y telemedicina -- Expediente cl铆nico electr贸nico -- Integraci贸n con laboratorios e imagen - -### Proceso Principal -``` -Cita 鈫 Check-in 鈫 Consulta 鈫 Diagn贸stico 鈫 Receta 鈫 Facturaci贸n - 鈫 - Estudios (Lab/Imagen) - 鈫 - Expediente Cl铆nico -``` - -### Cumplimiento Normativo -- NOM-024-SSA3-2012 (Expediente cl铆nico) -- NOM-004-SSA3-2012 (Sistemas de informaci贸n) -- NOM-151-SCFI-2016 (Firma electr贸nica) -- CIE-10 (Codificaci贸n de diagn贸sticos) - ---- - -## 馃搧 ESTRUCTURA DE DOCUMENTACI脫N - -``` -docs/ -鈹溾攢鈹 00-vision-general/ -鈹 鈹斺攢鈹 VISION-CLINICAS.md 鉁 -鈹溾攢鈹 02-definicion-modulos/ -鈹 鈹溾攢鈹 INDICE-MODULOS.md 鉁 -鈹 鈹溾攢鈹 CL-001-fundamentos/README.md 鉁 -鈹 鈹溾攢鈹 CL-002-pacientes/README.md 鉁 -鈹 鈹溾攢鈹 CL-003-citas/README.md 鉁 -鈹 鈹溾攢鈹 CL-004-consultas/README.md 鉁 -鈹 鈹溾攢鈹 CL-005-recetas/README.md 鉁 -鈹 鈹溾攢鈹 CL-006-laboratorio/README.md 鉁 -鈹 鈹溾攢鈹 CL-007-farmacia/README.md 鉁 -鈹 鈹溾攢鈹 CL-008-facturacion/README.md 鉁 -鈹 鈹溾攢鈹 CL-009-reportes/README.md 鉁 -鈹 鈹溾攢鈹 CL-010-telemedicina/README.md 鉁 -鈹 鈹溾攢鈹 CL-011-expediente/README.md 鉁 -鈹 鈹斺攢鈹 CL-012-imagenologia/README.md 鉁 -鈹斺攢鈹 08-epicas/ - 鈹斺攢鈹 EPIC-CL-001-fundamentos.md 鉁 -``` - ---- - -## 馃幆 PR脫XIMOS PASOS - -### Fase 1: Documentaci贸n Detallada -1. [ ] Crear 茅picas completas (EPIC-CL-002 a 012) -2. [ ] Documentar User Stories por m贸dulo -3. [ ] Definir requerimientos funcionales (RF) -4. [ ] Crear especificaciones t茅cnicas (ET) -5. [ ] Documentar cumplimiento normativo - -### Fase 2: Dise帽o de Base de Datos -6. [ ] Dise帽ar schemas de BD -7. [ ] Implementar DDL -8. [ ] Documentar modelo de datos - -### Fase 3: Desarrollo -9. [ ] Implementar backend (TypeScript/Express) -10. [ ] Implementar frontend (React) -11. [ ] Integraci贸n telemedicina (WebRTC) -12. [ ] Integraci贸n DICOM -13. [ ] Testing - ---- - -## 馃搱 M脡TRICAS - -| M茅trica | Valor | -|---------|-------| -| M贸dulos definidos | 12 | -| 脡picas creadas | 12/12 鉁 | -| User Stories | 0 (pendiente) | -| Story Points | 451 | -| Archivos MD | 31 | -| Archivos SQL | 0 | -| Archivos TS | 0 | - ---- - -## 馃彈锔 ARQUITECTURA - -**Tipo:** Proyecto Independiente (fork conceptual del ERP-Core) - -**Patrones a reutilizar del ERP-Core:** -- Multi-tenancy con RLS (para cadenas de cl铆nicas) -- Estructura de autenticaci贸n con 2FA obligatorio -- Patrones de inventario (farmacia) -- Sistema de reportes - -**M贸dulos 100% nuevos:** -- CL-004: Consultas (notas m茅dicas estructuradas) -- CL-010: Telemedicina (videoconsultas WebRTC) -- CL-012: Imagenolog铆a (visor DICOM) - -**Integraciones externas:** -- Proveedores de videollamadas (Twilio, Zoom API) -- Servidores PACS para imagen m茅dica -- Servicios de timbrado CFDI - -**Opera de forma aut贸noma:** No requiere ERP-Core instalado - ---- - -## 鈿狅笍 CONSIDERACIONES ESPECIALES - -### Seguridad de Datos M茅dicos -- Encriptaci贸n de datos sensibles (expedientes) -- Auditor铆a completa de accesos -- Consentimiento informado digital -- Respaldo autom谩tico de expedientes - -### Cumplimiento Normativo -- Implementar estructura de NOM-024-SSA3-2012 -- Firma electr贸nica para recetas (NOM-151) -- Codificaci贸n CIE-10 para diagn贸sticos - ---- - -## 馃敆 REFERENCIAS - -- 脥ndice de m贸dulos: `docs/02-definicion-modulos/INDICE-MODULOS.md` -- Visi贸n: `docs/00-vision-general/VISION-CLINICAS.md` -- SPECS heredadas: `orchestration/00-guidelines/HERENCIA-SPECS-CORE.md` -- Directivas: `orchestration/directivas/` - ---- - -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/database/HERENCIA-ERP-CORE.md b/projects/erp-clinicas/database/HERENCIA-ERP-CORE.md deleted file mode 100644 index 505bb9aba..000000000 --- a/projects/erp-clinicas/database/HERENCIA-ERP-CORE.md +++ /dev/null @@ -1,222 +0,0 @@ -# Herencia de Base de Datos - ERP Core -> Cl铆nicas - -**Fecha:** 2025-12-08 -**Versi贸n:** 1.0 -**Vertical:** Cl铆nicas -**Nivel:** 2B.2 - ---- - -## RESUMEN - -La vertical de Cl铆nicas hereda los schemas base del ERP Core y extiende con schemas espec铆ficos del dominio de gesti贸n m茅dica y expediente cl铆nico. - -**Ubicaci贸n DDL Core:** `apps/erp-core/database/ddl/` - ---- - -## ARQUITECTURA DE HERENCIA - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ERP CORE (Base) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 auth 鈹 鈹 core 鈹 鈹俧inancial鈹 鈹俰nventory鈹 鈹 hr 鈹 鈹 -鈹 鈹 26 tbl 鈹 鈹 12 tbl 鈹 鈹 15 tbl 鈹 鈹 15 tbl 鈹 鈹 6 tbl 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 sales 鈹 鈹俛nalytics鈹 鈹 system 鈹 鈹 crm 鈹 鈹 -鈹 鈹 6 tbl 鈹 鈹 5 tbl 鈹 鈹 10 tbl 鈹 鈹 5 tbl 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 TOTAL: ~100 tablas heredadas 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹 HEREDA - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CL脥NICAS (Extensiones) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 medical 鈹 鈹 appointments 鈹 鈹 patients 鈹 鈹 -鈹 鈹 (expediente) 鈹 鈹 (citas) 鈹 鈹 (pacientes) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 EXTENSIONES: ~35 tablas (planificadas) 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## SCHEMAS HEREDADOS DEL CORE - -| Schema | Tablas | Uso en Cl铆nicas | -|--------|--------|-----------------| -| `auth` | 26 | Autenticaci贸n, usuarios m茅dicos | -| `core` | 12 | Partners (pacientes), cat谩logos | -| `financial` | 15 | Facturas de servicios m茅dicos | -| `inventory` | 15 | Medicamentos, insumos | -| `hr` | 6 | Personal m茅dico | -| `sales` | 6 | Servicios m茅dicos | -| `crm` | 5 | Seguimiento de pacientes | -| `analytics` | 5 | Estad铆sticas m茅dicas | -| `system` | 10 | Recordatorios, notificaciones | - -**Total heredado:** ~100 tablas - ---- - -## SCHEMAS ESPEC脥FICOS DE CL脥NICAS (Planificados) - -### 1. Schema `patients` (estimado 10+ tablas) - -**Prop贸sito:** Gesti贸n de pacientes - -```sql --- Tablas principales planificadas: -patients.patients -- Pacientes (extiende core.partners) -patients.patient_contacts -- Contactos de emergencia -patients.insurance_policies -- P贸lizas de seguro -patients.medical_history -- Antecedentes m茅dicos -patients.allergies -- Alergias -patients.family_history -- Antecedentes familiares -``` - -### 2. Schema `medical` (estimado 15+ tablas) - -**Prop贸sito:** Expediente cl铆nico electr贸nico - -```sql --- Tablas principales planificadas: -medical.consultations -- Consultas m茅dicas -medical.diagnoses -- Diagn贸sticos (CIE-10) -medical.prescriptions -- Recetas m茅dicas -medical.prescription_lines -- Medicamentos recetados -medical.vital_signs -- Signos vitales -medical.lab_results -- Resultados de laboratorio -medical.imaging_studies -- Estudios de imagen -medical.clinical_notes -- Notas cl铆nicas -medical.treatments -- Tratamientos -``` - -### 3. Schema `appointments` (estimado 10+ tablas) - -**Prop贸sito:** Gesti贸n de citas - -```sql --- Tablas principales planificadas: -appointments.doctors -- M茅dicos -appointments.specialties -- Especialidades -appointments.doctor_schedules -- Horarios de m茅dicos -appointments.consulting_rooms -- Consultorios -appointments.appointments -- Citas -appointments.appointment_types -- Tipos de cita -appointments.reminders -- Recordatorios -``` - ---- - -## SPECS DEL CORE APLICABLES - -**Documento detallado:** `orchestration/00-guidelines/HERENCIA-SPECS-CORE.md` - -### Correcciones de DDL Core (2025-12-08) - -El DDL del ERP-Core fue corregido para resolver FK inv谩lidas: - -1. **stock_valuation_layers**: Campos `journal_entry_id` y `journal_entry_line_id` (antes `account_move_*`) -2. **stock_move_consume_rel**: Nueva tabla de trazabilidad (antes `move_line_consume_rel`) -3. **category_stock_accounts**: FK corregida a `core.product_categories` -4. **product_categories**: ALTERs ahora apuntan a schema `core` - -### SPECS Obligatorias - -| Spec Core | Aplicaci贸n en Cl铆nicas | SP | Estado | -|-----------|----------------------|----:|--------| -| SPEC-SISTEMA-SECUENCIAS | Foliado de expedientes y citas | 8 | 鉁 DDL LISTO | -| SPEC-SEGURIDAD-API-KEYS-PERMISOS | Control de acceso a expedientes | 31 | 鉁 DDL LISTO | -| SPEC-INTEGRACION-CALENDAR | Agenda de citas m茅dicas | 8 | PENDIENTE | -| SPEC-RRHH-EVALUACIONES-SKILLS | Credenciales m茅dicas | 26 | 鉁 DDL LISTO | -| SPEC-MAIL-THREAD-TRACKING | Historial de comunicaci贸n | 13 | 鉁 DDL LISTO | -| SPEC-WIZARD-TRANSIENT-MODEL | Wizards de receta y referencia | 8 | PENDIENTE | -| SPEC-FIRMA-ELECTRONICA-NOM151 | Firma de expedientes cl铆nicos | 13 | PENDIENTE | -| SPEC-TWO-FACTOR-AUTHENTICATION | Seguridad de acceso | 13 | 鉁 DDL LISTO | -| SPEC-OAUTH2-SOCIAL-LOGIN | Portal de pacientes | 8 | 鉁 DDL LISTO | - -### SPECS Opcionales - -| Spec Core | Decisi贸n | Raz贸n | -|-----------|----------|-------| -| SPEC-VALORACION-INVENTARIO | EVALUAR | Solo si hay farmacia interna | -| SPEC-PRICING-RULES | EVALUAR | Para paquetes de servicios | -| SPEC-TAREAS-RECURRENTES | EVALUAR | Para citas peri贸dicas | - -### SPECS No Aplican - -| Spec Core | Raz贸n | -|-----------|-------| -| SPEC-PORTAL-PROVEEDORES | No hay compras complejas | -| SPEC-BLANKET-ORDERS | No aplica en servicios m茅dicos | -| SPEC-INVENTARIOS-CICLICOS | Solo si hay farmacia grande | -| SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN | No hay proyectos de este tipo | - -### Cumplimiento Normativo - -| Norma | Descripci贸n | SPECS Relacionadas | -|-------|-------------|-------------------| -| NOM-024-SSA3-2012 | Expediente cl铆nico electr贸nico | SPEC-SEGURIDAD, SPEC-MAIL-THREAD | -| LFPDPPP | Protecci贸n de datos personales | SPEC-SEGURIDAD, SPEC-2FA | -| NOM-004-SSA3-2012 | Expediente cl铆nico | SPEC-FIRMA-ELECTRONICA | - ---- - -## CUMPLIMIENTO NORMATIVO - -Este sistema debe cumplir con: - -| Norma | Descripci贸n | Impacto | -|-------|-------------|---------| -| NOM-024-SSA3-2012 | Expediente cl铆nico electr贸nico | Estructura de datos | -| LFPDPPP | Protecci贸n de datos personales | Seguridad y acceso | -| NOM-004-SSA3-2012 | Expediente cl铆nico | Contenido m铆nimo | - ---- - -## ORDEN DE EJECUCI脫N DDL (Futuro) - -```bash -# PASO 1: Cargar ERP Core (base) -cd apps/erp-core/database -./scripts/reset-database.sh --force - -# PASO 2: Cargar extensiones de Cl铆nicas -cd apps/verticales/clinicas/database -psql $DATABASE_URL -f init/00-extensions.sql -psql $DATABASE_URL -f init/01-create-schemas.sql -psql $DATABASE_URL -f init/02-patients-tables.sql -psql $DATABASE_URL -f init/03-medical-tables.sql -psql $DATABASE_URL -f init/04-appointments-tables.sql -``` - ---- - -## MAPEO DE NOMENCLATURA - -| Core | Cl铆nicas | -|------|----------| -| `core.partners` | Pacientes base | -| `hr.employees` | Personal m茅dico | -| `inventory.products` | Medicamentos, insumos | -| `sales.sale_orders` | Servicios m茅dicos | -| `financial.invoices` | Facturas de consultas | - ---- - -## REFERENCIAS - -- ERP Core DDL: `apps/erp-core/database/ddl/` -- ERP Core README: `apps/erp-core/database/README.md` -- Directivas: `orchestration/directivas/` -- Inventarios: `orchestration/inventarios/` - ---- - -**Documento de herencia oficial** -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/database/README.md b/projects/erp-clinicas/database/README.md deleted file mode 100644 index d42da1fb9..000000000 --- a/projects/erp-clinicas/database/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Base de Datos - ERP Cl铆nicas - -## Resumen - -| Aspecto | Valor | -|---------|-------| -| **Schema principal** | `clinica` | -| **Tablas espec铆ficas** | 13 | -| **ENUMs** | 4 | -| **Hereda de ERP-Core** | 144 tablas (12 schemas) | - -## Prerequisitos - -1. **ERP-Core instalado** con todos sus schemas: - - auth, core, financial, inventory, purchase, sales, projects, analytics, system, billing, crm, hr - -2. **Extensiones PostgreSQL**: - - pgcrypto (encriptaci贸n) - - pg_trgm (b煤squeda de texto) - -## Orden de Ejecuci贸n DDL - -```bash -# 1. Instalar ERP-Core primero -cd apps/erp-core/database -./scripts/reset-database.sh - -# 2. Instalar extensi贸n Cl铆nicas -cd apps/verticales/clinicas/database -psql $DATABASE_URL -f init/00-extensions.sql -psql $DATABASE_URL -f init/01-create-schemas.sql -psql $DATABASE_URL -f init/02-rls-functions.sql -psql $DATABASE_URL -f init/03-clinical-tables.sql -psql $DATABASE_URL -f init/04-seed-data.sql -``` - -## Tablas Implementadas - -### Schema: clinica (13 tablas) - -| Tabla | M贸dulo | Descripci贸n | -|-------|--------|-------------| -| specialties | CL-002 | Cat谩logo de especialidades m茅dicas | -| doctors | CL-002 | M茅dicos (extiende hr.employees) | -| patients | CL-001 | Pacientes (extiende core.partners) | -| patient_contacts | CL-001 | Contactos de emergencia | -| patient_insurance | CL-001 | Informaci贸n de seguros | -| appointment_slots | CL-002 | Horarios disponibles | -| appointments | CL-002 | Citas m茅dicas | -| medical_records | CL-003 | Expediente cl铆nico electr贸nico | -| consultations | CL-003 | Consultas realizadas | -| vital_signs | CL-003 | Signos vitales | -| diagnoses | CL-003 | Diagn贸sticos (CIE-10) | -| prescriptions | CL-003 | Recetas m茅dicas | -| prescription_items | CL-003 | Medicamentos en receta | - -## ENUMs - -| Enum | Valores | -|------|---------| -| appointment_status | scheduled, confirmed, in_progress, completed, cancelled, no_show | -| patient_gender | male, female, other, prefer_not_to_say | -| blood_type | A+, A-, B+, B-, AB+, AB-, O+, O-, unknown | -| consultation_status | draft, in_progress, completed, cancelled | - -## Row Level Security - -Todas las tablas tienen RLS habilitado con aislamiento por tenant: - -```sql -tenant_id = current_setting('app.current_tenant_id', true)::UUID -``` - -## Consideraciones de Seguridad - -- **NOM-024-SSA3-2012**: Expediente cl铆nico electr贸nico -- **Datos sensibles**: medical_records, consultations requieren encriptaci贸n -- **Auditor铆a completa**: Todas las tablas tienen campos de auditor铆a - -## Referencias - -- [HERENCIA-ERP-CORE.md](./HERENCIA-ERP-CORE.md) -- [DATABASE_INVENTORY.yml](../orchestration/inventarios/DATABASE_INVENTORY.yml) diff --git a/projects/erp-clinicas/database/init/00-extensions.sql b/projects/erp-clinicas/database/init/00-extensions.sql deleted file mode 100644 index bed7df79c..000000000 --- a/projects/erp-clinicas/database/init/00-extensions.sql +++ /dev/null @@ -1,25 +0,0 @@ --- ============================================================================ --- EXTENSIONES PostgreSQL - ERP Cl铆nicas --- ============================================================================ --- Versi贸n: 1.0.0 --- Fecha: 2025-12-09 --- Prerequisito: ERP-Core debe estar instalado --- ============================================================================ - --- Verificar que ERP-Core est茅 instalado -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN - RAISE EXCEPTION 'ERP-Core no instalado. Ejecutar primero DDL de erp-core.'; - END IF; -END $$; - --- Extensi贸n para encriptaci贸n de datos sensibles (expedientes m茅dicos) -CREATE EXTENSION IF NOT EXISTS pgcrypto; - --- Extensi贸n para b煤squeda de texto (diagn贸sticos CIE-10) -CREATE EXTENSION IF NOT EXISTS pg_trgm; - --- ============================================================================ --- FIN EXTENSIONES --- ============================================================================ diff --git a/projects/erp-clinicas/database/init/01-create-schemas.sql b/projects/erp-clinicas/database/init/01-create-schemas.sql deleted file mode 100644 index fd058a9af..000000000 --- a/projects/erp-clinicas/database/init/01-create-schemas.sql +++ /dev/null @@ -1,15 +0,0 @@ --- ============================================================================ --- SCHEMAS - ERP Cl铆nicas --- ============================================================================ --- Versi贸n: 1.0.0 --- Fecha: 2025-12-09 --- ============================================================================ - --- Schema principal para operaciones cl铆nicas -CREATE SCHEMA IF NOT EXISTS clinica; - -COMMENT ON SCHEMA clinica IS 'Schema para operaciones de cl铆nica/consultorio m茅dico'; - --- ============================================================================ --- FIN SCHEMAS --- ============================================================================ diff --git a/projects/erp-clinicas/database/init/02-rls-functions.sql b/projects/erp-clinicas/database/init/02-rls-functions.sql deleted file mode 100644 index 2a31da823..000000000 --- a/projects/erp-clinicas/database/init/02-rls-functions.sql +++ /dev/null @@ -1,37 +0,0 @@ --- ============================================================================ --- FUNCIONES RLS - ERP Cl铆nicas --- ============================================================================ --- Versi贸n: 1.0.0 --- Fecha: 2025-12-09 --- Nota: Usa las funciones de contexto de ERP-Core (auth schema) --- ============================================================================ - --- Las funciones principales est谩n en ERP-Core: --- auth.get_current_tenant_id() --- auth.get_current_user_id() --- auth.get_current_company_id() - --- Funci贸n auxiliar para verificar acceso a expediente m茅dico -CREATE OR REPLACE FUNCTION clinica.can_access_medical_record( - p_patient_id UUID, - p_user_id UUID DEFAULT NULL -) -RETURNS BOOLEAN AS $$ -DECLARE - v_user_id UUID; - v_has_access BOOLEAN := FALSE; -BEGIN - v_user_id := COALESCE(p_user_id, current_setting('app.current_user_id', true)::UUID); - - -- TODO: Implementar l贸gica de permisos espec铆ficos - -- Por ahora, cualquier usuario del tenant puede acceder - RETURN TRUE; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -COMMENT ON FUNCTION clinica.can_access_medical_record IS -'Verifica si el usuario tiene permiso para acceder al expediente m茅dico del paciente'; - --- ============================================================================ --- FIN FUNCIONES RLS --- ============================================================================ diff --git a/projects/erp-clinicas/database/init/03-clinical-tables.sql b/projects/erp-clinicas/database/init/03-clinical-tables.sql deleted file mode 100644 index 5a972450d..000000000 --- a/projects/erp-clinicas/database/init/03-clinical-tables.sql +++ /dev/null @@ -1,628 +0,0 @@ --- ============================================================================ --- TABLAS CL脥NICAS - ERP Cl铆nicas --- ============================================================================ --- M贸dulos: CL-001 (Pacientes), CL-002 (Citas), CL-003 (Expediente) --- Versi贸n: 1.0.0 --- Fecha: 2025-12-09 --- ============================================================================ --- PREREQUISITOS: --- 1. ERP-Core instalado (auth.tenants, auth.users, core.partners) --- 2. Schema clinica creado --- ============================================================================ - --- ============================================================================ --- TYPES (ENUMs) --- ============================================================================ - -DO $$ BEGIN - CREATE TYPE clinica.appointment_status AS ENUM ( - 'scheduled', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE clinica.patient_gender AS ENUM ( - 'male', 'female', 'other', 'prefer_not_to_say' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE clinica.blood_type AS ENUM ( - 'A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', 'unknown' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE clinica.consultation_status AS ENUM ( - 'draft', 'in_progress', 'completed', 'cancelled' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - --- ============================================================================ --- CAT脕LOGOS BASE --- ============================================================================ - --- Tabla: specialties (Especialidades m茅dicas) -CREATE TABLE IF NOT EXISTS clinica.specialties ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - code VARCHAR(20) NOT NULL, - name VARCHAR(100) NOT NULL, - description TEXT, - consultation_duration INTEGER DEFAULT 30, -- minutos - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_specialties_code UNIQUE (tenant_id, code) -); - --- Tabla: doctors (M茅dicos - extiende hr.employees) -CREATE TABLE IF NOT EXISTS clinica.doctors ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - employee_id UUID, -- FK a hr.employees (ERP Core) - user_id UUID REFERENCES auth.users(id), - specialty_id UUID NOT NULL REFERENCES clinica.specialties(id), - license_number VARCHAR(50) NOT NULL, -- C茅dula profesional - license_expiry DATE, - secondary_specialties UUID[], -- Array de specialty_ids - consultation_fee DECIMAL(12,2), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_doctors_license UNIQUE (tenant_id, license_number) -); - --- ============================================================================ --- PACIENTES (CL-001) --- ============================================================================ - --- Tabla: patients (Pacientes - extiende core.partners) -CREATE TABLE IF NOT EXISTS clinica.patients ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - partner_id UUID REFERENCES core.partners(id), -- Vinculo a partner - - -- Identificaci贸n - patient_number VARCHAR(30) NOT NULL, - first_name VARCHAR(100) NOT NULL, - last_name VARCHAR(100) NOT NULL, - middle_name VARCHAR(100), - - -- Datos personales - birth_date DATE, - gender clinica.patient_gender, - curp VARCHAR(18), - - -- Contacto - email VARCHAR(255), - phone VARCHAR(20), - mobile VARCHAR(20), - - -- Direcci贸n - street VARCHAR(255), - city VARCHAR(100), - state VARCHAR(100), - zip_code VARCHAR(10), - country VARCHAR(100) DEFAULT 'M茅xico', - - -- Datos m茅dicos b谩sicos - blood_type clinica.blood_type DEFAULT 'unknown', - allergies TEXT[], - chronic_conditions TEXT[], - - -- Seguro m茅dico - has_insurance BOOLEAN DEFAULT FALSE, - insurance_provider VARCHAR(100), - insurance_policy VARCHAR(50), - - -- Control - is_active BOOLEAN NOT NULL DEFAULT TRUE, - last_visit_date DATE, - - -- Auditor铆a - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - - CONSTRAINT uq_patients_number UNIQUE (tenant_id, patient_number) -); - --- Tabla: patient_contacts (Contactos de emergencia) -CREATE TABLE IF NOT EXISTS clinica.patient_contacts ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - patient_id UUID NOT NULL REFERENCES clinica.patients(id) ON DELETE CASCADE, - contact_name VARCHAR(200) NOT NULL, - relationship VARCHAR(50), -- Parentesco - phone VARCHAR(20), - mobile VARCHAR(20), - email VARCHAR(255), - is_primary BOOLEAN DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id) -); - --- Tabla: patient_insurance (Informaci贸n de seguros) -CREATE TABLE IF NOT EXISTS clinica.patient_insurance ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - patient_id UUID NOT NULL REFERENCES clinica.patients(id) ON DELETE CASCADE, - insurance_provider VARCHAR(100) NOT NULL, - policy_number VARCHAR(50) NOT NULL, - group_number VARCHAR(50), - holder_name VARCHAR(200), - holder_relationship VARCHAR(50), - coverage_type VARCHAR(50), - valid_from DATE, - valid_until DATE, - is_primary BOOLEAN DEFAULT TRUE, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- CITAS (CL-002) --- ============================================================================ - --- Tabla: appointment_slots (Horarios disponibles) -CREATE TABLE IF NOT EXISTS clinica.appointment_slots ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - doctor_id UUID NOT NULL REFERENCES clinica.doctors(id), - day_of_week INTEGER NOT NULL CHECK (day_of_week BETWEEN 0 AND 6), -- 0=Domingo - start_time TIME NOT NULL, - end_time TIME NOT NULL, - slot_duration INTEGER DEFAULT 30, -- minutos - max_appointments INTEGER DEFAULT 1, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - CONSTRAINT chk_slot_times CHECK (end_time > start_time) -); - --- Tabla: appointments (Citas m茅dicas) -CREATE TABLE IF NOT EXISTS clinica.appointments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - - -- Referencias - patient_id UUID NOT NULL REFERENCES clinica.patients(id), - doctor_id UUID NOT NULL REFERENCES clinica.doctors(id), - specialty_id UUID REFERENCES clinica.specialties(id), - - -- Programaci贸n - appointment_date DATE NOT NULL, - start_time TIME NOT NULL, - end_time TIME NOT NULL, - duration INTEGER DEFAULT 30, -- minutos - - -- Estado - status clinica.appointment_status NOT NULL DEFAULT 'scheduled', - - -- Detalles - reason TEXT, -- Motivo de consulta - notes TEXT, - is_first_visit BOOLEAN DEFAULT FALSE, - is_follow_up BOOLEAN DEFAULT FALSE, - follow_up_to UUID REFERENCES clinica.appointments(id), - - -- Recordatorios - reminder_sent BOOLEAN DEFAULT FALSE, - reminder_sent_at TIMESTAMPTZ, - - -- Confirmaci贸n - confirmed_at TIMESTAMPTZ, - confirmed_by UUID REFERENCES auth.users(id), - - -- Cancelaci贸n - cancelled_at TIMESTAMPTZ, - cancelled_by UUID REFERENCES auth.users(id), - cancellation_reason TEXT, - - -- Auditor铆a - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - - CONSTRAINT chk_appointment_times CHECK (end_time > start_time) -); - --- ============================================================================ --- EXPEDIENTE CL脥NICO (CL-003) --- ============================================================================ - --- Tabla: medical_records (Expediente cl铆nico electr贸nico) --- NOTA: Datos sensibles seg煤n NOM-024-SSA3-2012 -CREATE TABLE IF NOT EXISTS clinica.medical_records ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - patient_id UUID NOT NULL REFERENCES clinica.patients(id), - - -- N煤mero de expediente - record_number VARCHAR(30) NOT NULL, - - -- Antecedentes - family_history TEXT, - personal_history TEXT, - surgical_history TEXT, - - -- H谩bitos - smoking_status VARCHAR(50), - alcohol_status VARCHAR(50), - exercise_status VARCHAR(50), - diet_notes TEXT, - - -- Gineco-obst茅tricos (si aplica) - obstetric_history JSONB, - - -- Notas generales - notes TEXT, - - -- Control de acceso - is_confidential BOOLEAN DEFAULT TRUE, - access_restricted BOOLEAN DEFAULT FALSE, - - -- Auditor铆a - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - - CONSTRAINT uq_medical_records_number UNIQUE (tenant_id, record_number), - CONSTRAINT uq_medical_records_patient UNIQUE (patient_id) -); - --- Tabla: consultations (Consultas realizadas) -CREATE TABLE IF NOT EXISTS clinica.consultations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - - -- Referencias - medical_record_id UUID NOT NULL REFERENCES clinica.medical_records(id), - appointment_id UUID REFERENCES clinica.appointments(id), - doctor_id UUID NOT NULL REFERENCES clinica.doctors(id), - - -- Fecha/hora - consultation_date DATE NOT NULL, - start_time TIMESTAMPTZ, - end_time TIMESTAMPTZ, - - -- Estado - status clinica.consultation_status DEFAULT 'draft', - - -- Motivo de consulta - chief_complaint TEXT NOT NULL, -- Motivo principal - present_illness TEXT, -- Padecimiento actual - - -- Exploraci贸n f铆sica - physical_exam JSONB, -- Estructurado por sistemas - - -- Plan - treatment_plan TEXT, - follow_up_instructions TEXT, - next_appointment_days INTEGER, - - -- Notas - notes TEXT, - private_notes TEXT, -- Solo visible para el m茅dico - - -- Auditor铆a - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id) -); - --- Tabla: vital_signs (Signos vitales) -CREATE TABLE IF NOT EXISTS clinica.vital_signs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - consultation_id UUID NOT NULL REFERENCES clinica.consultations(id) ON DELETE CASCADE, - - -- Signos vitales - weight_kg DECIMAL(5,2), - height_cm DECIMAL(5,2), - bmi DECIMAL(4,2) GENERATED ALWAYS AS ( - CASE WHEN height_cm > 0 THEN weight_kg / ((height_cm/100) * (height_cm/100)) END - ) STORED, - temperature_c DECIMAL(4,2), - blood_pressure_systolic INTEGER, - blood_pressure_diastolic INTEGER, - heart_rate INTEGER, -- latidos por minuto - respiratory_rate INTEGER, -- respiraciones por minuto - oxygen_saturation INTEGER, -- porcentaje - - -- Fecha/hora de medici贸n - measured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - measured_by UUID REFERENCES auth.users(id), - - notes TEXT, - - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- Tabla: diagnoses (Diagn贸sticos - CIE-10) -CREATE TABLE IF NOT EXISTS clinica.diagnoses ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - consultation_id UUID NOT NULL REFERENCES clinica.consultations(id) ON DELETE CASCADE, - - -- C贸digo CIE-10 - icd10_code VARCHAR(10) NOT NULL, - icd10_description VARCHAR(255), - - -- Tipo - diagnosis_type VARCHAR(20) NOT NULL DEFAULT 'primary', -- primary, secondary, differential - - -- Detalles - notes TEXT, - is_chronic BOOLEAN DEFAULT FALSE, - onset_date DATE, - - -- Orden - sequence INTEGER DEFAULT 1, - - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- Tabla: prescriptions (Recetas m茅dicas) -CREATE TABLE IF NOT EXISTS clinica.prescriptions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - consultation_id UUID NOT NULL REFERENCES clinica.consultations(id), - - -- N煤mero de receta - prescription_number VARCHAR(30) NOT NULL, - prescription_date DATE NOT NULL DEFAULT CURRENT_DATE, - - -- M茅dico - doctor_id UUID NOT NULL REFERENCES clinica.doctors(id), - - -- Instrucciones generales - general_instructions TEXT, - - -- Vigencia - valid_until DATE, - - -- Estado - is_printed BOOLEAN DEFAULT FALSE, - printed_at TIMESTAMPTZ, - - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - - CONSTRAINT uq_prescriptions_number UNIQUE (tenant_id, prescription_number) -); - --- Tabla: prescription_items (L铆neas de receta) -CREATE TABLE IF NOT EXISTS clinica.prescription_items ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - prescription_id UUID NOT NULL REFERENCES clinica.prescriptions(id) ON DELETE CASCADE, - - -- Medicamento - product_id UUID, -- FK a inventory.products (ERP Core) - medication_name VARCHAR(255) NOT NULL, - presentation VARCHAR(100), -- Tabletas, jarabe, etc. - - -- Dosificaci贸n - dosage VARCHAR(100) NOT NULL, -- "1 tableta" - frequency VARCHAR(100) NOT NULL, -- "cada 8 horas" - duration VARCHAR(100), -- "por 7 d铆as" - quantity INTEGER, -- Cantidad a surtir - - -- Instrucciones - instructions TEXT, - - -- Orden - sequence INTEGER DEFAULT 1, - - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- 脥NDICES --- ============================================================================ - --- Specialties -CREATE INDEX IF NOT EXISTS idx_specialties_tenant ON clinica.specialties(tenant_id); - --- Doctors -CREATE INDEX IF NOT EXISTS idx_doctors_tenant ON clinica.doctors(tenant_id); -CREATE INDEX IF NOT EXISTS idx_doctors_specialty ON clinica.doctors(specialty_id); -CREATE INDEX IF NOT EXISTS idx_doctors_user ON clinica.doctors(user_id); - --- Patients -CREATE INDEX IF NOT EXISTS idx_patients_tenant ON clinica.patients(tenant_id); -CREATE INDEX IF NOT EXISTS idx_patients_partner ON clinica.patients(partner_id); -CREATE INDEX IF NOT EXISTS idx_patients_name ON clinica.patients(last_name, first_name); -CREATE INDEX IF NOT EXISTS idx_patients_curp ON clinica.patients(curp); - --- Patient contacts -CREATE INDEX IF NOT EXISTS idx_patient_contacts_tenant ON clinica.patient_contacts(tenant_id); -CREATE INDEX IF NOT EXISTS idx_patient_contacts_patient ON clinica.patient_contacts(patient_id); - --- Patient insurance -CREATE INDEX IF NOT EXISTS idx_patient_insurance_tenant ON clinica.patient_insurance(tenant_id); -CREATE INDEX IF NOT EXISTS idx_patient_insurance_patient ON clinica.patient_insurance(patient_id); - --- Appointment slots -CREATE INDEX IF NOT EXISTS idx_appointment_slots_tenant ON clinica.appointment_slots(tenant_id); -CREATE INDEX IF NOT EXISTS idx_appointment_slots_doctor ON clinica.appointment_slots(doctor_id); - --- Appointments -CREATE INDEX IF NOT EXISTS idx_appointments_tenant ON clinica.appointments(tenant_id); -CREATE INDEX IF NOT EXISTS idx_appointments_patient ON clinica.appointments(patient_id); -CREATE INDEX IF NOT EXISTS idx_appointments_doctor ON clinica.appointments(doctor_id); -CREATE INDEX IF NOT EXISTS idx_appointments_date ON clinica.appointments(appointment_date); -CREATE INDEX IF NOT EXISTS idx_appointments_status ON clinica.appointments(status); - --- Medical records -CREATE INDEX IF NOT EXISTS idx_medical_records_tenant ON clinica.medical_records(tenant_id); -CREATE INDEX IF NOT EXISTS idx_medical_records_patient ON clinica.medical_records(patient_id); - --- Consultations -CREATE INDEX IF NOT EXISTS idx_consultations_tenant ON clinica.consultations(tenant_id); -CREATE INDEX IF NOT EXISTS idx_consultations_record ON clinica.consultations(medical_record_id); -CREATE INDEX IF NOT EXISTS idx_consultations_doctor ON clinica.consultations(doctor_id); -CREATE INDEX IF NOT EXISTS idx_consultations_date ON clinica.consultations(consultation_date); - --- Vital signs -CREATE INDEX IF NOT EXISTS idx_vital_signs_tenant ON clinica.vital_signs(tenant_id); -CREATE INDEX IF NOT EXISTS idx_vital_signs_consultation ON clinica.vital_signs(consultation_id); - --- Diagnoses -CREATE INDEX IF NOT EXISTS idx_diagnoses_tenant ON clinica.diagnoses(tenant_id); -CREATE INDEX IF NOT EXISTS idx_diagnoses_consultation ON clinica.diagnoses(consultation_id); -CREATE INDEX IF NOT EXISTS idx_diagnoses_icd10 ON clinica.diagnoses(icd10_code); - --- Prescriptions -CREATE INDEX IF NOT EXISTS idx_prescriptions_tenant ON clinica.prescriptions(tenant_id); -CREATE INDEX IF NOT EXISTS idx_prescriptions_consultation ON clinica.prescriptions(consultation_id); - --- Prescription items -CREATE INDEX IF NOT EXISTS idx_prescription_items_tenant ON clinica.prescription_items(tenant_id); -CREATE INDEX IF NOT EXISTS idx_prescription_items_prescription ON clinica.prescription_items(prescription_id); - --- ============================================================================ --- ROW LEVEL SECURITY --- ============================================================================ - -ALTER TABLE clinica.specialties ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.doctors ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.patients ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.patient_contacts ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.patient_insurance ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.appointment_slots ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.appointments ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.medical_records ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.consultations ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.vital_signs ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.diagnoses ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.prescriptions ENABLE ROW LEVEL SECURITY; -ALTER TABLE clinica.prescription_items ENABLE ROW LEVEL SECURITY; - --- Pol铆ticas de aislamiento por tenant -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_specialties ON clinica.specialties; - CREATE POLICY tenant_isolation_specialties ON clinica.specialties - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_doctors ON clinica.doctors; - CREATE POLICY tenant_isolation_doctors ON clinica.doctors - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_patients ON clinica.patients; - CREATE POLICY tenant_isolation_patients ON clinica.patients - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_patient_contacts ON clinica.patient_contacts; - CREATE POLICY tenant_isolation_patient_contacts ON clinica.patient_contacts - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_patient_insurance ON clinica.patient_insurance; - CREATE POLICY tenant_isolation_patient_insurance ON clinica.patient_insurance - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_appointment_slots ON clinica.appointment_slots; - CREATE POLICY tenant_isolation_appointment_slots ON clinica.appointment_slots - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_appointments ON clinica.appointments; - CREATE POLICY tenant_isolation_appointments ON clinica.appointments - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_medical_records ON clinica.medical_records; - CREATE POLICY tenant_isolation_medical_records ON clinica.medical_records - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_consultations ON clinica.consultations; - CREATE POLICY tenant_isolation_consultations ON clinica.consultations - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_vital_signs ON clinica.vital_signs; - CREATE POLICY tenant_isolation_vital_signs ON clinica.vital_signs - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_diagnoses ON clinica.diagnoses; - CREATE POLICY tenant_isolation_diagnoses ON clinica.diagnoses - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_prescriptions ON clinica.prescriptions; - CREATE POLICY tenant_isolation_prescriptions ON clinica.prescriptions - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_prescription_items ON clinica.prescription_items; - CREATE POLICY tenant_isolation_prescription_items ON clinica.prescription_items - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - --- ============================================================================ --- COMENTARIOS --- ============================================================================ - -COMMENT ON TABLE clinica.specialties IS 'Cat谩logo de especialidades m茅dicas'; -COMMENT ON TABLE clinica.doctors IS 'M茅dicos y especialistas - extiende hr.employees'; -COMMENT ON TABLE clinica.patients IS 'Registro de pacientes - extiende core.partners'; -COMMENT ON TABLE clinica.patient_contacts IS 'Contactos de emergencia del paciente'; -COMMENT ON TABLE clinica.patient_insurance IS 'Informaci贸n de seguros m茅dicos'; -COMMENT ON TABLE clinica.appointment_slots IS 'Horarios disponibles por m茅dico'; -COMMENT ON TABLE clinica.appointments IS 'Citas m茅dicas programadas'; -COMMENT ON TABLE clinica.medical_records IS 'Expediente cl铆nico electr贸nico (NOM-024-SSA3)'; -COMMENT ON TABLE clinica.consultations IS 'Consultas m茅dicas realizadas'; -COMMENT ON TABLE clinica.vital_signs IS 'Signos vitales del paciente'; -COMMENT ON TABLE clinica.diagnoses IS 'Diagn贸sticos seg煤n CIE-10'; -COMMENT ON TABLE clinica.prescriptions IS 'Recetas m茅dicas'; -COMMENT ON TABLE clinica.prescription_items IS 'Medicamentos en receta'; - --- ============================================================================ --- FIN TABLAS CL脥NICAS --- Total: 13 tablas, 4 ENUMs --- ============================================================================ diff --git a/projects/erp-clinicas/database/init/04-seed-data.sql b/projects/erp-clinicas/database/init/04-seed-data.sql deleted file mode 100644 index 35648ef25..000000000 --- a/projects/erp-clinicas/database/init/04-seed-data.sql +++ /dev/null @@ -1,34 +0,0 @@ --- ============================================================================ --- DATOS INICIALES - ERP Cl铆nicas --- ============================================================================ --- Versi贸n: 1.0.0 --- Fecha: 2025-12-09 --- ============================================================================ - --- Especialidades m茅dicas comunes --- NOTA: Se insertan solo si el tenant existe (usar en script de inicializaci贸n) - -/* --- Ejemplo de inserci贸n (ejecutar con tenant_id espec铆fico): - -INSERT INTO clinica.specialties (tenant_id, code, name, description, consultation_duration) VALUES - ('TENANT_UUID', 'MG', 'Medicina General', 'Atenci贸n m茅dica primaria', 30), - ('TENANT_UUID', 'PED', 'Pediatr铆a', 'Atenci贸n m茅dica infantil', 30), - ('TENANT_UUID', 'GIN', 'Ginecolog铆a', 'Salud de la mujer', 30), - ('TENANT_UUID', 'CARD', 'Cardiolog铆a', 'Enfermedades del coraz贸n', 45), - ('TENANT_UUID', 'DERM', 'Dermatolog铆a', 'Enfermedades de la piel', 30), - ('TENANT_UUID', 'OFT', 'Oftalmolog铆a', 'Salud visual', 30), - ('TENANT_UUID', 'ORL', 'Otorrinolaringolog铆a', 'O铆do, nariz y garganta', 30), - ('TENANT_UUID', 'TRAU', 'Traumatolog铆a', 'Sistema m煤sculo-esquel茅tico', 30), - ('TENANT_UUID', 'NEUR', 'Neurolog铆a', 'Sistema nervioso', 45), - ('TENANT_UUID', 'PSIQ', 'Psiquiatr铆a', 'Salud mental', 60), - ('TENANT_UUID', 'ENDO', 'Endocrinolog铆a', 'Sistema endocrino', 45), - ('TENANT_UUID', 'GAST', 'Gastroenterolog铆a', 'Sistema digestivo', 45), - ('TENANT_UUID', 'NEFR', 'Nefrolog铆a', 'Enfermedades renales', 45), - ('TENANT_UUID', 'UROL', 'Urolog铆a', 'Sistema urinario', 30), - ('TENANT_UUID', 'ONCO', 'Oncolog铆a', 'Tratamiento del c谩ncer', 60); -*/ - --- ============================================================================ --- FIN SEED DATA --- ============================================================================ diff --git a/projects/erp-clinicas/docs/00-vision-general/VISION-CLINICAS.md b/projects/erp-clinicas/docs/00-vision-general/VISION-CLINICAS.md deleted file mode 100644 index 6c9a3d997..000000000 --- a/projects/erp-clinicas/docs/00-vision-general/VISION-CLINICAS.md +++ /dev/null @@ -1,107 +0,0 @@ -# Visi贸n General - ERP Cl铆nicas - -**Versi贸n:** 1.0 -**Fecha:** 2025-12-08 -**Nivel:** 2B.2 (Vertical) - ---- - -## Prop贸sito del Sistema - -Sistema ERP especializado para cl铆nicas y consultorios m茅dicos. Gestiona el expediente cl铆nico electr贸nico, agenda de citas, recetas m茅dicas, y facturaci贸n de servicios de salud. Cumple con normativas mexicanas de salud (NOM-024-SSA3, LFPDPPP). - ---- - -## Dominio del Negocio - -### Procesos Principales - -1. **Gesti贸n de Pacientes** - - Registro y expediente cl铆nico - - Historial m茅dico - - Antecedentes y alergias - -2. **Agenda de Citas** - - Programaci贸n de consultas - - Recordatorios autom谩ticos - - Gesti贸n de especialistas - -3. **Consultas M茅dicas** - - Notas m茅dicas - - Signos vitales - - Diagn贸sticos (CIE-10) - -4. **Recetas y Prescripciones** - - Recetas electr贸nicas - - Indicaciones m茅dicas - - Firma electr贸nica - -5. **Facturaci贸n de Servicios** - - Cobro de consultas - - Seguros m茅dicos - - CFDI de salud - ---- - -## Cumplimiento Normativo - -| Norma | Descripci贸n | Impacto | -|-------|-------------|---------| -| NOM-024-SSA3-2012 | Expediente cl铆nico electr贸nico | Estructura de datos | -| LFPDPPP | Protecci贸n de datos personales | Seguridad y acceso | -| NOM-004-SSA3-2012 | Expediente cl铆nico | Contenido m铆nimo | - ---- - -## Arquitectura de M贸dulos - -``` -CL-001 Fundamentos 鈫 Auth, Users, Tenants (hereda 100% core) -CL-002 Pacientes 鈫 Expediente cl铆nico (20% core) -CL-003 Citas 鈫 Agenda m茅dica (30% core) -CL-004 Consultas 鈫 Notas m茅dicas (0% - nuevo) -CL-005 Recetas 鈫 Prescripciones (10% core) -CL-006 Laboratorio 鈫 Estudios (10% core) -CL-007 Farmacia 鈫 Medicamentos (60% core) -CL-008 Facturaci贸n 鈫 Cobros, seguros (50% core) -CL-009 Reportes 鈫 Estad铆sticas (60% core) -CL-010 Telemedicina 鈫 Consultas remotas (0% - nuevo) -CL-011 Expediente 鈫 Historia cl铆nica (10% core) -CL-012 Imagenolog铆a 鈫 DICOM (5% core) -``` - ---- - -## Stack Tecnol贸gico - -- **Backend:** NestJS + TypeORM + PostgreSQL -- **Frontend:** React + TypeScript -- **Base de Datos:** PostgreSQL 15+ (hereda ERP Core) -- **Seguridad:** 2FA, encriptaci贸n de datos sensibles - ---- - -## M茅tricas Objetivo - -| M茅trica | Valor Objetivo | -|---------|----------------| -| M贸dulos | 12 | -| Tablas Espec铆ficas | ~35 | -| Tablas Heredadas | ~100 | -| Story Points Est. | ~350 | -| Ocupaci贸n Agenda | > 80% | -| No-shows | < 5% | - ---- - -## Referencias - -- ERP Core: `apps/erp-core/` -- Herencia DB: `database/HERENCIA-ERP-CORE.md` -- SPECS del Core: `HERENCIA-SPECS-CORE.md` -- Normativas: NOM-024-SSA3-2012 - ---- - -**Documento de visi贸n oficial** -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-001-fundamentos/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-001-fundamentos/README.md deleted file mode 100644 index aa7dd33f4..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-001-fundamentos/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-001: Fundamentos - -**M贸dulo:** Fundamentos -**Estado:** PLANIFICADO - -## Descripci贸n -Auth con 2FA para personal m茅dico - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-002-pacientes/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-002-pacientes/README.md deleted file mode 100644 index 145b3f864..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-002-pacientes/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-002: Pacientes - -**M贸dulo:** Pacientes -**Estado:** PLANIFICADO - -## Descripci贸n -Registro y expediente de pacientes - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-003-citas/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-003-citas/README.md deleted file mode 100644 index e57e82b34..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-003-citas/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-003: Citas - -**M贸dulo:** Citas -**Estado:** PLANIFICADO - -## Descripci贸n -Agenda m茅dica con recordatorios - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-004-consultas/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-004-consultas/README.md deleted file mode 100644 index ba8b91ddc..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-004-consultas/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-004: Consultas - -**M贸dulo:** Consultas -**Estado:** PLANIFICADO - -## Descripci贸n -Notas m茅dicas y diagn贸sticos CIE-10 - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-005-recetas/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-005-recetas/README.md deleted file mode 100644 index 2ca8f190f..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-005-recetas/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-005: Recetas - -**M贸dulo:** Recetas -**Estado:** PLANIFICADO - -## Descripci贸n -Prescripciones con firma electr贸nica - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-006-laboratorio/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-006-laboratorio/README.md deleted file mode 100644 index 95c5fc659..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-006-laboratorio/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-006: Laboratorio - -**M贸dulo:** Laboratorio -**Estado:** PLANIFICADO - -## Descripci贸n -Solicitud y resultados de estudios - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-007-farmacia/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-007-farmacia/README.md deleted file mode 100644 index 5f67ad959..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-007-farmacia/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-007: Farmacia - -**M贸dulo:** Farmacia -**Estado:** PLANIFICADO - -## Descripci贸n -Inventario de medicamentos - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-008-facturacion/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-008-facturacion/README.md deleted file mode 100644 index 7ccea4bdc..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-008-facturacion/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-008: Facturacion - -**M贸dulo:** Facturacion -**Estado:** PLANIFICADO - -## Descripci贸n -Cobros y CFDI de salud - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-009-reportes/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-009-reportes/README.md deleted file mode 100644 index 127c5b1fe..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-009-reportes/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-009: Reportes - -**M贸dulo:** Reportes -**Estado:** PLANIFICADO - -## Descripci贸n -Dashboard y estad铆sticas cl铆nicas - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-010-telemedicina/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-010-telemedicina/README.md deleted file mode 100644 index 749cf60b6..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-010-telemedicina/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-010: Telemedicina - -**M贸dulo:** Telemedicina -**Estado:** PLANIFICADO - -## Descripci贸n -Videoconsultas remotas - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-011-expediente/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-011-expediente/README.md deleted file mode 100644 index 0c848379d..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-011-expediente/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-011: Expediente - -**M贸dulo:** Expediente -**Estado:** PLANIFICADO - -## Descripci贸n -Historia cl铆nica completa NOM-024 - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/CL-012-imagenologia/README.md b/projects/erp-clinicas/docs/02-definicion-modulos/CL-012-imagenologia/README.md deleted file mode 100644 index e4cef1dfb..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/CL-012-imagenologia/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CL-012: Imagenologia - -**M贸dulo:** Imagenologia -**Estado:** PLANIFICADO - -## Descripci贸n -Visor DICOM y estudios de imagen - -## Funcionalidades Principales -- Por definir en fase de an谩lisis - -## Cumplimiento Normativo -- NOM-024-SSA3-2012 (si aplica) -- LFPDPPP - -## SPECS Aplicables -- Ver HERENCIA-SPECS-CORE.md - ---- -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/02-definicion-modulos/INDICE-MODULOS.md b/projects/erp-clinicas/docs/02-definicion-modulos/INDICE-MODULOS.md deleted file mode 100644 index 46958e292..000000000 --- a/projects/erp-clinicas/docs/02-definicion-modulos/INDICE-MODULOS.md +++ /dev/null @@ -1,136 +0,0 @@ -# 脥ndice de M贸dulos - ERP Cl铆nicas - -**Versi贸n:** 1.0 -**Fecha:** 2025-12-08 -**Total M贸dulos:** 12 - ---- - -## Resumen - -| C贸digo | Nombre | Descripci贸n | Reutilizaci贸n Core | Estado | -|--------|--------|-------------|-------------------|--------| -| CL-001 | Fundamentos | Auth, Users, Tenants | 100% | PLANIFICADO | -| CL-002 | Pacientes | Registro y expediente | 20% | PLANIFICADO | -| CL-003 | Citas | Agenda m茅dica | 30% | PLANIFICADO | -| CL-004 | Consultas | Notas m茅dicas | 0% | PLANIFICADO | -| CL-005 | Recetas | Prescripciones | 10% | PLANIFICADO | -| CL-006 | Laboratorio | Estudios y resultados | 10% | PLANIFICADO | -| CL-007 | Farmacia | Inventario medicamentos | 60% | PLANIFICADO | -| CL-008 | Facturaci贸n | Cobros y seguros | 50% | PLANIFICADO | -| CL-009 | Reportes | Estad铆sticas cl铆nicas | 60% | PLANIFICADO | -| CL-010 | Telemedicina | Consultas remotas | 0% | PLANIFICADO | -| CL-011 | Expediente | Historia cl铆nica completa | 10% | PLANIFICADO | -| CL-012 | Imagenolog铆a | Estudios DICOM | 5% | PLANIFICADO | - ---- - -## Detalle por M贸dulo - -### CL-001: Fundamentos -**Herencia:** 100% del core -- Usuarios: M茅dicos, Enfermeras, Recepcionistas, Admin -- Roles con permisos de acceso a expedientes -- 2FA obligatorio para personal m茅dico - -### CL-002: Pacientes -**Herencia:** 20% -- Registro de pacientes -- Datos de contacto y emergencia -- Seguro m茅dico -- Antecedentes y alergias - -### CL-003: Citas -**Herencia:** 30% -- Programaci贸n de citas -- Agenda por especialista -- Recordatorios (SMS, Email, WhatsApp) -- Confirmaciones - -### CL-004: Consultas -**Herencia:** 0% - Nuevo -- Notas m茅dicas -- Signos vitales -- Exploraci贸n f铆sica -- Diagn贸sticos (CIE-10) - -### CL-005: Recetas -**Herencia:** 10% -- Recetas electr贸nicas -- Indicaciones m茅dicas -- Firma electr贸nica (NOM-151) -- Historial de prescripciones - -### CL-006: Laboratorio -**Herencia:** 10% -- Solicitud de estudios -- Resultados de laboratorio -- Valores de referencia -- Alertas de resultados cr铆ticos - -### CL-007: Farmacia -**Herencia:** 60% -- Inventario de medicamentos -- Control de caducidades -- Despacho de recetas -- Alertas de interacciones - -### CL-008: Facturaci贸n -**Herencia:** 50% -- Cobro de consultas -- Integraci贸n con seguros -- CFDI de salud -- Cuentas por cobrar - -### CL-009: Reportes -**Herencia:** 60% -- Dashboard cl铆nico -- Estad铆sticas de consultas -- Indicadores de salud -- Reportes epidemiol贸gicos - -### CL-010: Telemedicina -**Herencia:** 0% - Nuevo -- Videoconsultas -- Chat con pacientes -- Compartir pantalla -- Grabaci贸n (opcional) - -### CL-011: Expediente -**Herencia:** 10% -- Historia cl铆nica completa -- L铆nea de tiempo m茅dica -- Resumen de antecedentes -- Exportaci贸n (NOM-024) - -### CL-012: Imagenolog铆a -**Herencia:** 5% -- Visor DICOM -- Almacenamiento de estudios -- Reportes radiol贸gicos -- Integraci贸n PACS - ---- - -## Story Points Estimados - -| M贸dulo | SP Backend | SP Frontend | SP Total | -|--------|-----------|-------------|----------| -| CL-001 | 0 | 0 | 0 | -| CL-002 | 21 | 13 | 34 | -| CL-003 | 21 | 21 | 42 | -| CL-004 | 21 | 21 | 42 | -| CL-005 | 13 | 13 | 26 | -| CL-006 | 21 | 13 | 34 | -| CL-007 | 13 | 8 | 21 | -| CL-008 | 21 | 13 | 34 | -| CL-009 | 13 | 13 | 26 | -| CL-010 | 34 | 34 | 68 | -| CL-011 | 21 | 13 | 34 | -| CL-012 | 21 | 13 | 34 | -| **Total** | **220** | **175** | **395** | - ---- - -**脥ndice de m贸dulos oficial** -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-001-fundamentos.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-001-fundamentos.md deleted file mode 100644 index 2fb71cf13..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-001-fundamentos.md +++ /dev/null @@ -1,66 +0,0 @@ -# 脡pica: Fundamentos del Sistema Cl铆nico - -**C贸digo:** EPIC-CL-001 -**M贸dulos:** CL-001, CL-002, CL-003 -**Estado:** PLANIFICADO - ---- - -## Descripci贸n - -Implementaci贸n de los m贸dulos fundacionales del ERP Cl铆nicas, incluyendo la configuraci贸n inicial con seguridad reforzada, registro de pacientes y agenda de citas. - ---- - -## Objetivos - -1. Configurar el ambiente con seguridad m茅dica (2FA, encriptaci贸n) -2. Implementar el registro de pacientes con expediente b谩sico -3. Establecer la agenda de citas con recordatorios - ---- - -## M贸dulos Incluidos - -| M贸dulo | Descripci贸n | SP Estimados | -|--------|-------------|--------------| -| CL-001 | Fundamentos (2FA obligatorio) | 0 | -| CL-002 | Pacientes | 34 | -| CL-003 | Citas | 42 | - ---- - -## User Stories Principales - -1. Como m茅dico, quiero iniciar sesi贸n con 2FA -2. Como recepcionista, quiero registrar un nuevo paciente -3. Como recepcionista, quiero programar una cita -4. Como paciente, quiero recibir recordatorio de mi cita - ---- - -## Criterios de Aceptaci贸n - -- [ ] 2FA obligatorio para personal m茅dico -- [ ] Encriptaci贸n de datos sensibles (antecedentes, alergias) -- [ ] Registro de pacientes con datos de seguro -- [ ] Agenda funcional con confirmaciones -- [ ] Recordatorios autom谩ticos (24h y 2h antes) - ---- - -## Cumplimiento Normativo - -- NOM-024-SSA3-2012: Estructura de expediente -- LFPDPPP: Protecci贸n de datos personales - ---- - -## Story Points Totales - -**76 SP** - ---- - -**脡pica fundacional** -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-002-pacientes.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-002-pacientes.md deleted file mode 100644 index 4bf9b7081..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-002-pacientes.md +++ /dev/null @@ -1,247 +0,0 @@ -# EPICA: EPIC-CL-002 - Pacientes - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-002 | -| **Nombre** | Pacientes | -| **Modulo** | pacientes | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P0 (Critico) | -| **Estado** | Backlog | -| **Story Points** | 38 | -| **Sprint(s)** | Sprint 2-3 | - ---- - -## Descripcion - -Registro y gesti贸n integral de pacientes. Incluye datos demogr谩ficos, datos de contacto, informaci贸n de seguros m茅dicos, contactos de emergencia, consentimientos informados y portal de acceso para pacientes. - ---- - -## Objetivo de Negocio - -- Expediente 煤nico del paciente -- Cumplimiento de NOM-024-SSA3-2012 -- Agilizar proceso de registro -- Datos actualizados y accesibles -- Comunicaci贸n efectiva con pacientes - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL002-001 | Como recepcionista, quiero registrar paciente nuevo con datos m铆nimos para agilizar primera cita | P0 | 5 | Backlog | -| US-CL002-002 | Como recepcionista, quiero buscar paciente por nombre, tel茅fono o CURP para consultar expediente | P0 | 3 | Backlog | -| US-CL002-003 | Como recepcionista, quiero registrar datos de seguro m茅dico del paciente para facturaci贸n | P0 | 3 | Backlog | -| US-CL002-004 | Como recepcionista, quiero registrar contactos de emergencia del paciente | P0 | 2 | Backlog | -| US-CL002-005 | Como paciente, quiero firmar consentimiento informado digitalmente para autorizar tratamientos | P0 | 5 | Backlog | -| US-CL002-006 | Como m茅dico, quiero ver ficha completa del paciente antes de la consulta | P0 | 3 | Backlog | -| US-CL002-007 | Como paciente, quiero acceder a mi portal para ver citas e historial | P1 | 8 | Backlog | -| US-CL002-008 | Como admin, quiero configurar campos obligatorios seg煤n tipo de paciente | P1 | 3 | Backlog | -| US-CL002-009 | Como recepcionista, quiero registrar datos del menor y de su tutor | P0 | 3 | Backlog | -| US-CL002-010 | Como admin, quiero fusionar expedientes duplicados preservando historial | P2 | 3 | Backlog | - -**Total Story Points:** 38 SP - ---- - -## Datos del Paciente - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 EXPEDIENTE DEL PACIENTE 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 DATOS PERSONALES 鈹 -鈹 鈹溾攢鈹 Nombre completo 鈹 -鈹 鈹溾攢鈹 Fecha de nacimiento / Edad 鈹 -鈹 鈹溾攢鈹 Sexo 鈹 -鈹 鈹溾攢鈹 CURP 鈹 -鈹 鈹溾攢鈹 Estado civil 鈹 -鈹 鈹溾攢鈹 Ocupaci贸n 鈹 -鈹 鈹斺攢鈹 Escolaridad 鈹 -鈹 鈹 -鈹 CONTACTO 鈹 -鈹 鈹溾攢鈹 Tel茅fono principal 鈹 -鈹 鈹溾攢鈹 Tel茅fono alternativo 鈹 -鈹 鈹溾攢鈹 Email 鈹 -鈹 鈹溾攢鈹 Direcci贸n completa 鈹 -鈹 鈹斺攢鈹 Preferencia de contacto 鈹 -鈹 鈹 -鈹 EMERGENCIA 鈹 -鈹 鈹溾攢鈹 Nombre del contacto 鈹 -鈹 鈹溾攢鈹 Parentesco 鈹 -鈹 鈹斺攢鈹 Tel茅fono 鈹 -鈹 鈹 -鈹 SEGURO M脡DICO 鈹 -鈹 鈹溾攢鈹 Aseguradora 鈹 -鈹 鈹溾攢鈹 N煤mero de p贸liza 鈹 -鈹 鈹溾攢鈹 Vigencia 鈹 -鈹 鈹斺攢鈹 Tipo de cobertura 鈹 -鈹 鈹 -鈹 MENORES DE EDAD 鈹 -鈹 鈹溾攢鈹 Datos del tutor/responsable 鈹 -鈹 鈹溾攢鈹 Parentesco 鈹 -鈹 鈹斺攢鈹 Identificaci贸n del tutor 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Consentimiento Informado Digital - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CONSENTIMIENTO INFORMADO 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 TIPOS DE CONSENTIMIENTO 鈹 -鈹 鈹溾攢鈹 General (tratamientos y procedimientos) 鈹 -鈹 鈹溾攢鈹 Espec铆fico por procedimiento 鈹 -鈹 鈹溾攢鈹 Menores (firma del tutor) 鈹 -鈹 鈹斺攢鈹 Tratamiento de datos personales (LFPDPPP) 鈹 -鈹 鈹 -鈹 ELEMENTOS 鈹 -鈹 鈹溾攢鈹 Texto del consentimiento 鈹 -鈹 鈹溾攢鈹 Firma digital del paciente 鈹 -鈹 鈹溾攢鈹 Fecha y hora de firma 鈹 -鈹 鈹溾攢鈹 IP y dispositivo 鈹 -鈹 鈹斺攢鈹 PDF generado y almacenado 鈹 -鈹 鈹 -鈹 VALIDEZ 鈹 -鈹 鈹溾攢鈹 Vigencia configurable 鈹 -鈹 鈹溾攢鈹 Renovaci贸n autom谩tica 鈹 -鈹 鈹斺攢鈹 Revocaci贸n por el paciente 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Registro de paciente con validaci贸n de datos -- [ ] B煤squeda por m煤ltiples criterios -- [ ] Registro de datos de seguro -- [ ] Contactos de emergencia -- [ ] Consentimiento informado digital -- [ ] Ficha completa del paciente -- [ ] Portal del paciente -- [ ] Manejo de menores con tutor - -**No Funcionales:** -- [ ] B煤squeda < 1 segundo -- [ ] Encriptaci贸n de datos sensibles -- [ ] Cumplimiento NOM-024-SSA3-2012 -- [ ] Auditor铆a de accesos - -**Tecnicos:** -- [ ] Validaci贸n de CURP -- [ ] Firma digital integrada -- [ ] Encriptaci贸n AES-256 para datos sensibles -- [ ] Portal web para pacientes - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | - -**Esta epica bloquea:** -| Epica/Modulo | Razon | -|--------------|-------| -| EPIC-CL-003 Citas | Requiere pacientes registrados | -| EPIC-CL-004 Consultas | Requiere datos del paciente | -| EPIC-CL-011 Expediente | Requiere pacientes | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `patients` -- [ ] Tablas: 7 (patients, emergency_contacts, insurance_info, consents, consent_signatures, minors, patient_merge_log) -- [ ] Funciones: 2 (validate_curp, encrypt_sensitive) -- [ ] Indices: Por CURP, tel茅fono, nombre, fecha nacimiento - -**Backend:** -- [ ] Modulo: `patients` -- [ ] Entities: 5 (Patient, EmergencyContact, InsuranceInfo, Consent, ConsentSignature) -- [ ] Endpoints: 15 -- [ ] Tests: 30 - -**Frontend:** -- [ ] Paginas: 5 (PatientList, PatientForm, PatientDetail, ConsentSign, PatientPortal) -- [ ] Componentes: 12 (PatientCard, SearchBar, ConsentModal, SignaturePad, etc.) -- [ ] Portal del paciente (separado) -- [ ] Stores: 1 (patientsStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| POST | /api/patients | Registrar paciente | -| GET | /api/patients/search | Buscar pacientes | -| GET | /api/patients/:id | Detalle de paciente | -| PATCH | /api/patients/:id | Actualizar paciente | -| POST | /api/patients/:id/insurance | Agregar seguro | -| POST | /api/patients/:id/emergency-contacts | Agregar contacto | -| POST | /api/patients/:id/consents/:consentId/sign | Firmar consentimiento | -| GET | /api/patients/:id/consents | Ver consentimientos | -| POST | /api/patients/merge | Fusionar expedientes | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Datos duplicados | Alta | Medio | Validaci贸n de CURP + alertas | -| Fuga de datos sensibles | Baja | Alto | Encriptaci贸n + auditor铆a | -| Consentimiento inv谩lido | Baja | Alto | Firma digital con evidencia | - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Textos de consentimiento aprobados -- [ ] Campos obligatorios por regulaci贸n definidos - -## Definition of Done (DoD) - -- [ ] Registro completo de pacientes -- [ ] B煤squeda multi-criterio funcionando -- [ ] Consentimiento digital operativo -- [ ] Datos sensibles encriptados -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-003-citas.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-003-citas.md deleted file mode 100644 index b92987753..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-003-citas.md +++ /dev/null @@ -1,270 +0,0 @@ -# EPICA: EPIC-CL-003 - Citas (Agenda M茅dica) - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-003 | -| **Nombre** | Citas (Agenda M茅dica) | -| **Modulo** | citas | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P0 (Critico) | -| **Estado** | Backlog | -| **Story Points** | 42 | -| **Sprint(s)** | Sprint 3-4 | - ---- - -## Descripcion - -Sistema de agenda m茅dica para programaci贸n de citas. Gesti贸n de horarios por m茅dico y consultorio, confirmaci贸n de citas, recordatorios autom谩ticos, lista de espera y m贸dulo de check-in para llegada de pacientes. - ---- - -## Objetivo de Negocio - -- Optimizar uso de consultorios -- Reducir ausentismo con recordatorios -- Mejorar experiencia del paciente -- Control de tiempos de espera -- Maximizar productividad m茅dica - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL003-001 | Como recepcionista, quiero agendar cita seleccionando m茅dico, fecha y hora disponible | P0 | 5 | Backlog | -| US-CL003-002 | Como recepcionista, quiero ver agenda del d铆a por m茅dico en formato calendario | P0 | 5 | Backlog | -| US-CL003-003 | Como recepcionista, quiero confirmar cita v铆a WhatsApp o llamada | P0 | 3 | Backlog | -| US-CL003-004 | Como paciente, quiero recibir recordatorio autom谩tico 24h antes de mi cita | P0 | 5 | Backlog | -| US-CL003-005 | Como recepcionista, quiero reagendar cita manteniendo historial | P0 | 3 | Backlog | -| US-CL003-006 | Como recepcionista, quiero registrar check-in del paciente al llegar | P0 | 3 | Backlog | -| US-CL003-007 | Como m茅dico, quiero ver mis citas del d铆a con datos del paciente | P0 | 3 | Backlog | -| US-CL003-008 | Como admin, quiero configurar horarios de atenci贸n por m茅dico | P0 | 5 | Backlog | -| US-CL003-009 | Como recepcionista, quiero gestionar lista de espera para cancelaciones | P1 | 3 | Backlog | -| US-CL003-010 | Como paciente, quiero agendar cita desde portal web o app | P1 | 5 | Backlog | -| US-CL003-011 | Como admin, quiero bloquear horarios por vacaciones o eventos | P1 | 2 | Backlog | - -**Total Story Points:** 42 SP - ---- - -## Flujo de Cita - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 AGENDADA 鈹 鈫 Cita programada -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 (-24h) -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 RECORDATORIO鈹 鈫 Env铆o autom谩tico WhatsApp/SMS -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹溾攢鈹 Sin respuesta 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈻 鈹 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 CONFIRMADA 鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 鈹 - 鈹 鈹 - 鈻 (D铆a de la cita) 鈹 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 CHECK-IN 鈹 鈫 Paciente llega 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 鈹 - 鈹 鈹 - 鈻 鈹 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 EN_CONSULTA 鈹 鈫 M茅dico inicia 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 鈹 - 鈹 鈹 - 鈻 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ATENDIDA 鈹 鈹 NO_ASISTIO 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Configuraci贸n de Agenda - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 AGENDA DEL DR. GARC脥A 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 HORARIO DE ATENCI脫N 鈹 -鈹 鈹溾攢鈹 Lunes a Viernes: 09:00 - 14:00, 16:00 - 20:00 鈹 -鈹 鈹溾攢鈹 S谩bados: 09:00 - 14:00 鈹 -鈹 鈹斺攢鈹 Domingos: No labora 鈹 -鈹 鈹 -鈹 DURACI脫N DE CITAS 鈹 -鈹 鈹溾攢鈹 Primera vez: 45 minutos 鈹 -鈹 鈹溾攢鈹 Seguimiento: 20 minutos 鈹 -鈹 鈹斺攢鈹 Procedimiento: 60 minutos 鈹 -鈹 鈹 -鈹 CONSULTORIOS 鈹 -鈹 鈹溾攢鈹 Consultorio 1 (Principal) 鈹 -鈹 鈹斺攢鈹 Consultorio 3 (Alterno) 鈹 -鈹 鈹 -鈹 BLOQUEOS 鈹 -鈹 鈹溾攢鈹 15-22 Dic: Vacaciones 鈹 -鈹 鈹斺攢鈹 Cada mi茅rcoles 10:00-11:00: Junta m茅dica 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Vista de Agenda - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 DR. GARC脥A - Lunes 9 Diciembre 2024 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 09:00 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 Juan P茅rez (Primera vez) 鈹 -鈹 09:45 鈹 - DISPONIBLE - 鈹 -鈹 10:00 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅 Mar铆a L贸pez (Seguimiento) 鉁 Confirmada鈹 -鈹 10:20 鈹 - DISPONIBLE - 鈹 -鈹 10:40 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅 Carlos Ruiz (Seguimiento) 鈹 -鈹 11:00 鈹 鈻撯枔鈻 JUNTA M脡DICA 鈻撯枔鈻 鈹 -鈹 12:00 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 Procedimiento 鈹 -鈹 13:00 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅 Ana Garc铆a (Seguimiento) 鉁 En sala 鈹 -鈹 13:20 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅 Pedro Soto (Seguimiento) 鈹 -鈹 13:40 鈹 - DISPONIBLE - 鈹 -鈹 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 DESCANSO 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 16:00 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 Nuevo paciente (1a vez) 鈹 -鈹 ... 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Agendar citas con validaci贸n de disponibilidad -- [ ] Vista de agenda diaria/semanal por m茅dico -- [ ] Confirmaci贸n de citas -- [ ] Recordatorios autom谩ticos (WhatsApp/SMS/Email) -- [ ] Check-in de pacientes -- [ ] Reagendar y cancelar citas -- [ ] Lista de espera -- [ ] Configuraci贸n de horarios - -**No Funcionales:** -- [ ] Carga de agenda < 2 segundos -- [ ] Env铆o de recordatorio en < 1 minuto -- [ ] Soporte para 10+ m茅dicos simult谩neos - -**Tecnicos:** -- [ ] Integraci贸n con WhatsApp Business API -- [ ] Calendario sincronizable (Google Calendar, Outlook) -- [ ] Notificaciones push para app -- [ ] Tiempo real con WebSockets - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | -| EPIC-CL-002 Pacientes | Backlog | Si | - -**Esta epica bloquea:** -| Epica/Modulo | Razon | -|--------------|-------| -| EPIC-CL-004 Consultas | Requiere cita para iniciar consulta | -| EPIC-CL-008 Facturaci贸n | Requiere citas para facturar | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `appointments` -- [ ] Tablas: 7 (appointments, schedules, schedule_blocks, waiting_list, reminders, check_ins, appointment_types) -- [ ] Funciones: 3 (check_availability, send_reminder, calculate_wait_time) -- [ ] Indices: Por m茅dico, fecha, paciente, estado - -**Backend:** -- [ ] Modulo: `appointments` -- [ ] Entities: 6 (Appointment, Schedule, ScheduleBlock, WaitingList, Reminder, CheckIn) -- [ ] Endpoints: 18 -- [ ] Jobs: Env铆o de recordatorios -- [ ] Tests: 35 - -**Frontend:** -- [ ] Paginas: 5 (Calendar, AppointmentForm, DayView, WeekView, WaitingList) -- [ ] Componentes: 15 (CalendarGrid, TimeSlot, AppointmentCard, CheckInModal, etc.) -- [ ] WebSockets para actualizaciones en tiempo real -- [ ] Stores: 1 (appointmentsStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| POST | /api/appointments | Crear cita | -| GET | /api/appointments/:id | Detalle de cita | -| PATCH | /api/appointments/:id | Actualizar cita | -| DELETE | /api/appointments/:id | Cancelar cita | -| GET | /api/appointments/calendar/:doctorId | Agenda del m茅dico | -| GET | /api/appointments/availability | Horarios disponibles | -| POST | /api/appointments/:id/confirm | Confirmar cita | -| POST | /api/appointments/:id/check-in | Registrar llegada | -| GET | /api/schedules/:doctorId | Horarios del m茅dico | -| POST | /api/schedules/:doctorId/blocks | Bloquear horario | -| GET | /api/waiting-list | Lista de espera | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Overbooking | Media | Alto | Validaci贸n de disponibilidad | -| Recordatorios no enviados | Media | Medio | Reintentos + logs | -| Ausentismo alto | Media | Medio | Confirmaci贸n + lista de espera | - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Horarios de m茅dicos definidos -- [ ] Proveedor de WhatsApp Business API seleccionado - -## Definition of Done (DoD) - -- [ ] Agenda funcional por m茅dico -- [ ] Recordatorios autom谩ticos envi谩ndose -- [ ] Check-in operativo -- [ ] Lista de espera activa -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-004-consultas.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-004-consultas.md deleted file mode 100644 index da28b0adb..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-004-consultas.md +++ /dev/null @@ -1,277 +0,0 @@ -# EPICA: EPIC-CL-004 - Consultas (Notas M茅dicas) - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-004 | -| **Nombre** | Consultas (Notas M茅dicas) | -| **Modulo** | consultas | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P0 (Critico) | -| **Estado** | Backlog | -| **Story Points** | 55 | -| **Sprint(s)** | Sprint 4-6 | - ---- - -## Descripcion - -M贸dulo 100% nuevo para documentaci贸n de consultas m茅dicas. Incluye notas cl铆nicas estructuradas (SOAP), signos vitales, diagn贸sticos con codificaci贸n CIE-10, planes de tratamiento, indicaciones y generaci贸n de documentos m茅dicos. - ---- - -## Objetivo de Negocio - -- Documentaci贸n cl铆nica completa -- Cumplimiento de NOM-004-SSA3-2012 -- Agilizar consulta m茅dica -- Historial cl铆nico consultable -- Soporte para decisiones cl铆nicas - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL004-001 | Como m茅dico, quiero iniciar consulta desde cita agendada para documentar atenci贸n | P0 | 3 | Backlog | -| US-CL004-002 | Como enfermera, quiero registrar signos vitales antes de la consulta | P0 | 5 | Backlog | -| US-CL004-003 | Como m茅dico, quiero documentar nota cl铆nica en formato SOAP | P0 | 8 | Backlog | -| US-CL004-004 | Como m茅dico, quiero registrar diagn贸sticos con c贸digo CIE-10 | P0 | 5 | Backlog | -| US-CL004-005 | Como m茅dico, quiero ver historial de consultas previas del paciente | P0 | 3 | Backlog | -| US-CL004-006 | Como m茅dico, quiero generar indicaciones m茅dicas imprimibles | P0 | 3 | Backlog | -| US-CL004-007 | Como m茅dico, quiero usar plantillas de notas para consultas frecuentes | P1 | 5 | Backlog | -| US-CL004-008 | Como m茅dico, quiero dictar nota por voz (speech-to-text) | P2 | 8 | Backlog | -| US-CL004-009 | Como m茅dico, quiero agregar antecedentes a la historia cl铆nica | P0 | 5 | Backlog | -| US-CL004-010 | Como m茅dico, quiero cerrar consulta y liberar consultorio | P0 | 2 | Backlog | -| US-CL004-011 | Como admin, quiero configurar plantillas de notas por especialidad | P1 | 3 | Backlog | -| US-CL004-012 | Como auditor, quiero ver log de cambios en notas cl铆nicas | P0 | 5 | Backlog | - -**Total Story Points:** 55 SP - ---- - -## Formato SOAP - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 NOTA CL脥NICA - FORMATO SOAP 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 S - SUBJETIVO (Lo que el paciente refiere) 鈹 -鈹 鈹溾攢鈹 Motivo de consulta 鈹 -鈹 鈹溾攢鈹 Historia de la enfermedad actual 鈹 -鈹 鈹溾攢鈹 S铆ntomas referidos 鈹 -鈹 鈹斺攢鈹 Evoluci贸n desde 煤ltima visita 鈹 -鈹 鈹 -鈹 O - OBJETIVO (Lo que el m茅dico observa/mide) 鈹 -鈹 鈹溾攢鈹 Signos vitales 鈹 -鈹 鈹 鈹溾攢鈹 T/A: 120/80 mmHg 鈹 -鈹 鈹 鈹溾攢鈹 FC: 72 lpm 鈹 -鈹 鈹 鈹溾攢鈹 FR: 16 rpm 鈹 -鈹 鈹 鈹溾攢鈹 Temp: 36.5掳C 鈹 -鈹 鈹 鈹溾攢鈹 Peso: 70 kg 鈹 -鈹 鈹 鈹溾攢鈹 Talla: 170 cm 鈹 -鈹 鈹 鈹斺攢鈹 IMC: 24.2 鈹 -鈹 鈹溾攢鈹 Exploraci贸n f铆sica 鈹 -鈹 鈹斺攢鈹 Resultados de estudios 鈹 -鈹 鈹 -鈹 A - AN脕LISIS (Diagn贸sticos) 鈹 -鈹 鈹溾攢鈹 Diagn贸stico principal (CIE-10) 鈹 -鈹 鈹溾攢鈹 Diagn贸sticos secundarios 鈹 -鈹 鈹斺攢鈹 Diagn贸sticos diferenciales 鈹 -鈹 鈹 -鈹 P - PLAN (Tratamiento) 鈹 -鈹 鈹溾攢鈹 Medicamentos (鈫 Receta) 鈹 -鈹 鈹溾攢鈹 Estudios solicitados (鈫 Lab/Imagen) 鈹 -鈹 鈹溾攢鈹 Indicaciones generales 鈹 -鈹 鈹溾攢鈹 Referencia a especialista 鈹 -鈹 鈹斺攢鈹 Pr贸xima cita 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Flujo de Consulta - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CHECK-IN 鈹 鈫 Paciente llega -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹係IGNOS VITAL 鈹 鈫 Enfermera registra -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 EN_CONSULTA 鈹 鈫 M茅dico inicia -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹溾攢鈹 Revisar historial - 鈹溾攢鈹 Documentar SOAP - 鈹溾攢鈹 Generar receta - 鈹溾攢鈹 Solicitar estudios - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CERRADA 鈹 鈫 Consulta finalizada -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 FACTURAR 鈹 鈫 Proceso de cobro -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Iniciar consulta desde cita -- [ ] Registrar signos vitales -- [ ] Documentar nota SOAP completa -- [ ] Codificar diagn贸sticos CIE-10 -- [ ] Ver historial de consultas -- [ ] Generar indicaciones imprimibles -- [ ] Plantillas de notas -- [ ] Registro de antecedentes -- [ ] Auditor铆a de cambios - -**No Funcionales:** -- [ ] Autoguardado cada 30 segundos -- [ ] B煤squeda de CIE-10 < 500ms -- [ ] Notas no editables despu茅s de cierre -- [ ] Cumplimiento NOM-004-SSA3-2012 - -**Tecnicos:** -- [ ] Cat谩logo CIE-10 integrado -- [ ] Editor de texto enriquecido -- [ ] Opcional: Speech-to-text -- [ ] Generaci贸n de PDFs - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | -| EPIC-CL-002 Pacientes | Backlog | Si | -| EPIC-CL-003 Citas | Backlog | Si | - -**Esta epica bloquea:** -| Epica/Modulo | Razon | -|--------------|-------| -| EPIC-CL-005 Recetas | Requiere consulta para prescribir | -| EPIC-CL-006 Laboratorio | Requiere consulta para solicitar | -| EPIC-CL-011 Expediente | Requiere notas cl铆nicas | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `consultations` -- [ ] Tablas: 10 (consultations, vital_signs, diagnoses, treatments, indications, templates, medical_history, antecedents, note_versions, audit_log) -- [ ] Funciones: 3 (search_icd10, calculate_bmi, lock_consultation) -- [ ] Indices: Por paciente, m茅dico, fecha, diagn贸stico - -**Backend:** -- [ ] Modulo: `consultations` -- [ ] Entities: 8 (Consultation, VitalSigns, Diagnosis, Treatment, Template, MedicalHistory, Antecedent, NoteVersion) -- [ ] Endpoints: 20 -- [ ] Tests: 40 - -**Frontend:** -- [ ] Paginas: 5 (ConsultationRoom, VitalSignsForm, SOAPEditor, HistoryView, Templates) -- [ ] Componentes: 18 (SOAPSection, ICD10Search, VitalsWidget, HistoryTimeline, etc.) -- [ ] Editor WYSIWYG para notas -- [ ] Stores: 2 (consultationsStore, icd10Store) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| POST | /api/consultations | Iniciar consulta | -| GET | /api/consultations/:id | Detalle de consulta | -| PATCH | /api/consultations/:id | Actualizar nota | -| POST | /api/consultations/:id/vitals | Registrar signos vitales | -| POST | /api/consultations/:id/diagnoses | Agregar diagn贸stico | -| POST | /api/consultations/:id/close | Cerrar consulta | -| GET | /api/consultations/history/:patientId | Historial del paciente | -| GET | /api/icd10/search | Buscar c贸digo CIE-10 | -| GET | /api/templates | Listar plantillas | -| POST | /api/templates | Crear plantilla | - ---- - -## Cat谩logo CIE-10 - -``` -Ejemplos de c贸digos frecuentes: -鈹溾攢鈹 J00 - Rinofaringitis aguda (resfriado com煤n) -鈹溾攢鈹 J06.9 - Infecci贸n aguda de las v铆as respiratorias -鈹溾攢鈹 E11 - Diabetes mellitus tipo 2 -鈹溾攢鈹 I10 - Hipertensi贸n esencial -鈹溾攢鈹 K30 - Dispepsia funcional -鈹溾攢鈹 M54.5 - Dolor lumbar -鈹斺攢鈹 F32 - Episodio depresivo -``` - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| P茅rdida de notas | Baja | Alto | Autoguardado + versiones | -| Diagn贸stico incorrecto | Media | Alto | B煤squeda asistida CIE-10 | -| Notas incompletas | Media | Medio | Validaci贸n antes de cerrar | - ---- - -## Nota T茅cnica - -Este m贸dulo es **100% nuevo** y no tiene equivalente en el ERP-Core. Es espec铆fico para el sector salud y debe cumplir con las NOM mexicanas aplicables a expedientes cl铆nicos electr贸nicos. - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Cat谩logo CIE-10 importado -- [ ] Plantillas iniciales definidas - -## Definition of Done (DoD) - -- [ ] Flujo completo de consulta -- [ ] Formato SOAP funcionando -- [ ] B煤squeda CIE-10 operativa -- [ ] Auditor铆a de cambios -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-005-recetas.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-005-recetas.md deleted file mode 100644 index cb3bee21c..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-005-recetas.md +++ /dev/null @@ -1,243 +0,0 @@ -# EPICA: EPIC-CL-005 - Recetas (Prescripciones) - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-005 | -| **Nombre** | Recetas (Prescripciones) | -| **Modulo** | recetas | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P0 (Critico) | -| **Estado** | Backlog | -| **Story Points** | 35 | -| **Sprint(s)** | Sprint 5-6 | - ---- - -## Descripcion - -Sistema de prescripci贸n m茅dica electr贸nica. Generaci贸n de recetas con b煤squeda de medicamentos, dosificaci贸n, verificaci贸n de interacciones medicamentosas, firma electr贸nica (NOM-151) y env铆o digital al paciente o farmacia. - ---- - -## Objetivo de Negocio - -- Recetas legibles y sin errores -- Cumplimiento de NOM-151-SCFI-2016 -- Agilizar proceso de prescripci贸n -- Evitar errores de medicaci贸n -- Trazabilidad de prescripciones - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL005-001 | Como m茅dico, quiero buscar medicamento por nombre gen茅rico o comercial | P0 | 3 | Backlog | -| US-CL005-002 | Como m茅dico, quiero agregar medicamento a receta con dosis y frecuencia | P0 | 5 | Backlog | -| US-CL005-003 | Como m茅dico, quiero ver alertas de interacciones medicamentosas | P0 | 8 | Backlog | -| US-CL005-004 | Como m茅dico, quiero ver alergias conocidas del paciente al prescribir | P0 | 3 | Backlog | -| US-CL005-005 | Como m茅dico, quiero usar recetas previas como base para nueva receta | P1 | 3 | Backlog | -| US-CL005-006 | Como m茅dico, quiero firmar electr贸nicamente la receta | P0 | 5 | Backlog | -| US-CL005-007 | Como paciente, quiero recibir mi receta por email/WhatsApp | P0 | 3 | Backlog | -| US-CL005-008 | Como m茅dico, quiero indicar si requiere receta resurtible | P1 | 2 | Backlog | -| US-CL005-009 | Como admin, quiero configurar cuadro b谩sico de medicamentos | P1 | 3 | Backlog | - -**Total Story Points:** 35 SP - ---- - -## Estructura de Receta - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 RECETA M脡DICA ELECTR脫NICA 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 DATOS DEL PRESCRIPTOR 鈹 -鈹 鈹溾攢鈹 Nombre del m茅dico 鈹 -鈹 鈹溾攢鈹 C茅dula profesional 鈹 -鈹 鈹溾攢鈹 Especialidad 鈹 -鈹 鈹溾攢鈹 Instituci贸n/Consultorio 鈹 -鈹 鈹斺攢鈹 Domicilio y tel茅fono 鈹 -鈹 鈹 -鈹 DATOS DEL PACIENTE 鈹 -鈹 鈹溾攢鈹 Nombre completo 鈹 -鈹 鈹溾攢鈹 Edad 鈹 -鈹 鈹斺攢鈹 Sexo 鈹 -鈹 鈹 -鈹 PRESCRIPCI脫N 鈹 -鈹 鈹溾攢鈹 Medicamento 1 鈹 -鈹 鈹 鈹溾攢鈹 Nombre gen茅rico / comercial 鈹 -鈹 鈹 鈹溾攢鈹 Forma farmac茅utica 鈹 -鈹 鈹 鈹溾攢鈹 Concentraci贸n 鈹 -鈹 鈹 鈹溾攢鈹 Cantidad 鈹 -鈹 鈹 鈹溾攢鈹 Dosis 鈹 -鈹 鈹 鈹溾攢鈹 V铆a de administraci贸n 鈹 -鈹 鈹 鈹溾攢鈹 Frecuencia 鈹 -鈹 鈹 鈹斺攢鈹 Duraci贸n del tratamiento 鈹 -鈹 鈹溾攢鈹 Medicamento 2... 鈹 -鈹 鈹斺攢鈹 Indicaciones adicionales 鈹 -鈹 鈹 -鈹 DATOS DE CONTROL 鈹 -鈹 鈹溾攢鈹 Folio 煤nico 鈹 -鈹 鈹溾攢鈹 Fecha de expedici贸n 鈹 -鈹 鈹溾攢鈹 Fecha de vigencia 鈹 -鈹 鈹溾攢鈹 Firma electr贸nica del m茅dico 鈹 -鈹 鈹斺攢鈹 C贸digo QR de verificaci贸n 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Verificaci贸n de Interacciones - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ALERTAS DE INTERACCI脫N 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 鈿狅笍 INTERACCI脫N GRAVE 鈹 -鈹 Warfarina + Aspirina 鈹 -鈹 Riesgo: Sangrado aumentado 鈹 -鈹 Recomendaci贸n: Evitar combinaci贸n o monitorear INR 鈹 -鈹 鈹 -鈹 鈿狅笍 INTERACCI脫N MODERADA 鈹 -鈹 Metformina + Alcohol 鈹 -鈹 Riesgo: Acidosis l谩ctica 鈹 -鈹 Recomendaci贸n: Advertir al paciente 鈹 -鈹 鈹 -鈹 鈿狅笍 ALERGIA CONOCIDA 鈹 -鈹 Paciente al茅rgico a Penicilina 鈹 -鈹 Medicamento prescrito: Amoxicilina 鈹 -鈹 Acci贸n: Requiere confirmaci贸n del m茅dico 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] B煤squeda de medicamentos -- [ ] Agregar m煤ltiples medicamentos a receta -- [ ] Alertas de interacciones -- [ ] Verificaci贸n de alergias -- [ ] Firma electr贸nica (e.firma o similar) -- [ ] Generaci贸n de PDF con c贸digo QR -- [ ] Env铆o digital al paciente -- [ ] Recetas resurtibles - -**No Funcionales:** -- [ ] B煤squeda de medicamentos < 500ms -- [ ] Verificaci贸n de interacciones < 2 segundos -- [ ] Cumplimiento NOM-151-SCFI-2016 - -**Tecnicos:** -- [ ] Cat谩logo de medicamentos (PLM o similar) -- [ ] Base de datos de interacciones -- [ ] Firma electr贸nica avanzada -- [ ] Generaci贸n de PDF con QR - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | -| EPIC-CL-002 Pacientes | Backlog | Si | -| EPIC-CL-004 Consultas | Backlog | Si | - -**Esta epica bloquea:** -| Epica/Modulo | Razon | -|--------------|-------| -| EPIC-CL-007 Farmacia | Requiere recetas para surtir | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `prescriptions` -- [ ] Tablas: 6 (prescriptions, prescription_items, medications, interactions, patient_allergies, signature_logs) -- [ ] Funciones: 3 (check_interactions, validate_prescription, generate_folio) -- [ ] Indices: Por paciente, m茅dico, fecha, medicamento - -**Backend:** -- [ ] Modulo: `prescriptions` -- [ ] Entities: 5 (Prescription, PrescriptionItem, Medication, Interaction, PatientAllergy) -- [ ] Services: InteractionChecker, SignatureService -- [ ] Endpoints: 12 -- [ ] Tests: 28 - -**Frontend:** -- [ ] Paginas: 4 (PrescriptionEditor, MedicationSearch, PrescriptionView, PrescriptionHistory) -- [ ] Componentes: 12 (MedicationLine, InteractionAlert, AllergyWarning, SignatureModal, etc.) -- [ ] Stores: 1 (prescriptionsStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| POST | /api/prescriptions | Crear receta | -| GET | /api/prescriptions/:id | Detalle de receta | -| POST | /api/prescriptions/:id/items | Agregar medicamento | -| DELETE | /api/prescriptions/:id/items/:itemId | Quitar medicamento | -| POST | /api/prescriptions/:id/check-interactions | Verificar interacciones | -| POST | /api/prescriptions/:id/sign | Firmar receta | -| POST | /api/prescriptions/:id/send | Enviar al paciente | -| GET | /api/prescriptions/:id/pdf | Descargar PDF | -| GET | /api/medications/search | Buscar medicamentos | -| GET | /api/patients/:id/allergies | Alergias del paciente | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Interacciones no detectadas | Baja | Alto | Base de datos actualizada | -| Firma electr贸nica inv谩lida | Baja | Alto | Proveedor certificado | -| Errores de dosificaci贸n | Media | Alto | Validaciones y rangos | - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Cat谩logo de medicamentos disponible -- [ ] Proveedor de firma electr贸nica seleccionado - -## Definition of Done (DoD) - -- [ ] Prescripci贸n completa funcionando -- [ ] Alertas de interacciones operativas -- [ ] Firma electr贸nica v谩lida -- [ ] Env铆o digital al paciente -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-006-laboratorio.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-006-laboratorio.md deleted file mode 100644 index 20e67dde8..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-006-laboratorio.md +++ /dev/null @@ -1,242 +0,0 @@ -# EPICA: EPIC-CL-006 - Laboratorio - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-006 | -| **Nombre** | Laboratorio | -| **Modulo** | laboratorio | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P1 (Alto) | -| **Estado** | Backlog | -| **Story Points** | 38 | -| **Sprint(s)** | Sprint 6-7 | - ---- - -## Descripcion - -Gesti贸n de estudios de laboratorio cl铆nico. Solicitud de estudios desde consulta, toma de muestras, captura de resultados, valores de referencia, alertas de valores cr铆ticos y entrega de resultados al paciente. - ---- - -## Objetivo de Negocio - -- Flujo completo de laboratorio -- Resultados oportunos -- Alertas de valores cr铆ticos -- Integraci贸n con expediente cl铆nico -- Control de calidad - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL006-001 | Como m茅dico, quiero solicitar estudios de laboratorio desde la consulta | P0 | 5 | Backlog | -| US-CL006-002 | Como laboratorista, quiero ver 贸rdenes de estudios pendientes | P0 | 3 | Backlog | -| US-CL006-003 | Como laboratorista, quiero registrar toma de muestra con hora y responsable | P0 | 3 | Backlog | -| US-CL006-004 | Como laboratorista, quiero capturar resultados de estudios | P0 | 5 | Backlog | -| US-CL006-005 | Como laboratorista, quiero ver valores de referencia al capturar | P0 | 3 | Backlog | -| US-CL006-006 | Como m茅dico, quiero recibir alerta de valores cr铆ticos | P0 | 5 | Backlog | -| US-CL006-007 | Como paciente, quiero descargar mis resultados desde el portal | P0 | 5 | Backlog | -| US-CL006-008 | Como m茅dico, quiero ver historial de estudios del paciente | P1 | 3 | Backlog | -| US-CL006-009 | Como admin, quiero configurar cat谩logo de estudios con valores de referencia | P0 | 4 | Backlog | -| US-CL006-010 | Como laboratorista, quiero validar resultados antes de liberar | P1 | 2 | Backlog | - -**Total Story Points:** 38 SP - ---- - -## Flujo de Laboratorio - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 SOLICITUD 鈹 鈫 M茅dico solicita estudios -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 RECEPCI脫N 鈹 鈫 Paciente llega a lab -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹俆OMA_MUESTRA 鈹 鈫 Flebotom铆a -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 EN_PROCESO 鈹 鈫 An谩lisis en curso -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CAPTURA 鈹 鈫 Resultados capturados -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 VALIDACI脫N 鈹 鈫 QC revisa -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 LIBERADO 鈹 鈫 Disponible para m茅dico/paciente -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Estructura de Resultados - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 RESULTADOS DE LABORATORIO 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 BIOMETR脥A HEM脕TICA COMPLETA 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Par谩metro 鈹 Result. 鈹 Referencia 鈹 Estado 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Hemoglobina 鈹 14.5 鈹 13.5-17.5 鈹 鉁 Normal 鈹 鈹 -鈹 鈹 Hematocrito 鈹 42% 鈹 40-52% 鈹 鉁 Normal 鈹 鈹 -鈹 鈹 Leucocitos 鈹 12,500 鈹 4,500-11,000 鈹 鈿狅笍 Alto 鈹 鈹 -鈹 鈹 Plaquetas 鈹 250,000 鈹 150K-400K 鈹 鉁 Normal 鈹 鈹 -鈹 鈹 Glucosa 鈹 285 鈹 70-100 鈹 馃敶 CR脥TICO鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 馃敶 VALORES CR脥TICOS DETECTADOS 鈹 -鈹 Glucosa: 285 mg/dL - Notificar al m茅dico 鈹 -鈹 鈹 -鈹 Laboratorista: QFB Mar铆a Garc铆a 鈹 -鈹 Fecha toma: 2024-12-08 09:30 鈹 -鈹 Fecha resultado: 2024-12-08 14:45 鈹 -鈹 Validado por: Dr. Roberto S谩nchez 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Solicitar estudios desde consulta -- [ ] Ver 贸rdenes pendientes -- [ ] Registrar toma de muestra -- [ ] Capturar resultados -- [ ] Valores de referencia -- [ ] Alertas de valores cr铆ticos -- [ ] Validaci贸n de resultados -- [ ] Entrega de resultados - -**No Funcionales:** -- [ ] Alerta de cr铆ticos < 1 minuto -- [ ] Historial de 5 a帽os -- [ ] Cumplimiento normativo de laboratorio - -**Tecnicos:** -- [ ] Integraci贸n con consultas -- [ ] Integraci贸n con expediente -- [ ] Notificaciones push para cr铆ticos -- [ ] Generaci贸n de PDF de resultados - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | -| EPIC-CL-002 Pacientes | Backlog | Si | -| EPIC-CL-004 Consultas | Backlog | Si | - -**Esta epica bloquea:** -| Epica/Modulo | Razon | -|--------------|-------| -| EPIC-CL-011 Expediente | Resultados son parte del expediente | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `laboratory` -- [ ] Tablas: 7 (lab_orders, lab_order_items, samples, results, result_values, studies_catalog, reference_values) -- [ ] Funciones: 3 (check_critical, calculate_status, validate_result) -- [ ] Indices: Por paciente, m茅dico, fecha, estado - -**Backend:** -- [ ] Modulo: `laboratory` -- [ ] Entities: 6 (LabOrder, LabOrderItem, Sample, Result, ResultValue, StudyCatalog) -- [ ] Endpoints: 15 -- [ ] Tests: 30 - -**Frontend:** -- [ ] Paginas: 5 (LabOrders, SampleCollection, ResultCapture, ResultViewer, CatalogConfig) -- [ ] Componentes: 12 (OrderCard, ResultGrid, CriticalAlert, ReferenceIndicator, etc.) -- [ ] Stores: 1 (laboratoryStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| POST | /api/laboratory/orders | Crear orden de estudios | -| GET | /api/laboratory/orders | Listar 贸rdenes | -| GET | /api/laboratory/orders/:id | Detalle de orden | -| POST | /api/laboratory/orders/:id/sample | Registrar toma | -| POST | /api/laboratory/orders/:id/results | Capturar resultados | -| POST | /api/laboratory/orders/:id/validate | Validar resultados | -| GET | /api/laboratory/orders/:id/pdf | Descargar PDF | -| GET | /api/laboratory/history/:patientId | Historial del paciente | -| GET | /api/laboratory/studies | Cat谩logo de estudios | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Cr铆ticos no notificados | Baja | Alto | M煤ltiples canales de alerta | -| Resultados incorrectos | Media | Alto | Doble validaci贸n | -| P茅rdida de muestras | Baja | Alto | Trazabilidad completa | - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Cat谩logo de estudios definido -- [ ] Valores de referencia documentados - -## Definition of Done (DoD) - -- [ ] Flujo completo de laboratorio -- [ ] Alertas de cr铆ticos funcionando -- [ ] Resultados en expediente -- [ ] PDF de resultados gener谩ndose -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-007-farmacia.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-007-farmacia.md deleted file mode 100644 index 0dbbc430a..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-007-farmacia.md +++ /dev/null @@ -1,239 +0,0 @@ -# EPICA: EPIC-CL-007 - Farmacia - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-007 | -| **Nombre** | Farmacia | -| **Modulo** | farmacia | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P1 (Alto) | -| **Estado** | Backlog | -| **Story Points** | 40 | -| **Sprint(s)** | Sprint 7-8 | - ---- - -## Descripcion - -Gesti贸n de farmacia cl铆nica. Control de inventario de medicamentos, surtido de recetas, dispensaci贸n, control de caducidades, medicamentos controlados y punto de venta de farmacia. - ---- - -## Objetivo de Negocio - -- Control de inventario de medicamentos -- Surtido r谩pido de recetas -- Evitar merma por caducidad -- Cumplimiento de medicamentos controlados -- Rentabilidad de farmacia - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL007-001 | Como farmac茅utico, quiero ver recetas pendientes de surtir | P0 | 3 | Backlog | -| US-CL007-002 | Como farmac茅utico, quiero verificar stock antes de surtir | P0 | 3 | Backlog | -| US-CL007-003 | Como farmac茅utico, quiero dispensar medicamentos registrando lote | P0 | 5 | Backlog | -| US-CL007-004 | Como farmac茅utico, quiero cobrar la receta surtida | P0 | 5 | Backlog | -| US-CL007-005 | Como farmac茅utico, quiero recibir mercanc铆a de proveedor | P0 | 5 | Backlog | -| US-CL007-006 | Como farmac茅utico, quiero ver alertas de medicamentos pr贸ximos a caducar | P0 | 3 | Backlog | -| US-CL007-007 | Como farmac茅utico, quiero registrar salidas de medicamentos controlados | P0 | 5 | Backlog | -| US-CL007-008 | Como admin, quiero ver reporte de ventas de farmacia | P1 | 3 | Backlog | -| US-CL007-009 | Como admin, quiero realizar inventario f铆sico de medicamentos | P1 | 5 | Backlog | -| US-CL007-010 | Como admin, quiero configurar m谩rgenes de ganancia por categor铆a | P2 | 3 | Backlog | - -**Total Story Points:** 40 SP - ---- - -## Flujo de Dispensaci贸n - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 RECETA 鈹 鈫 Receta electr贸nica del m茅dico -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 VERIFICAR 鈹 鈫 Verificar stock disponible -鈹 STOCK 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹溾攢鈹 No hay stock 鈹鈹鈻 Notificar faltante - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 PREPARAR 鈹 鈫 Tomar medicamentos de anaquel -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 VERIFICAR 鈹 鈫 Doble chequeo -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 COBRAR 鈹 鈫 Proceso de pago -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ENTREGAR 鈹 鈫 Dispensar al paciente -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Control de Medicamentos - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CONTROL DE INVENTARIO 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 TRAZABILIDAD 鈹 -鈹 鈹溾攢鈹 C贸digo de producto 鈹 -鈹 鈹溾攢鈹 Lote 鈹 -鈹 鈹溾攢鈹 Fecha de caducidad 鈹 -鈹 鈹溾攢鈹 Proveedor 鈹 -鈹 鈹斺攢鈹 Fecha de entrada 鈹 -鈹 鈹 -鈹 ALERTAS DE CADUCIDAD 鈹 -鈹 鈹溾攢鈹 馃敶 Caducados (vencidos) 鈹 -鈹 鈹溾攢鈹 馃煚 < 30 d铆as para vencer 鈹 -鈹 鈹溾攢鈹 馃煛 < 90 d铆as para vencer 鈹 -鈹 鈹斺攢鈹 Sugerencia: Promoci贸n o devoluci贸n 鈹 -鈹 鈹 -鈹 MEDICAMENTOS CONTROLADOS 鈹 -鈹 鈹溾攢鈹 Libro de registro (f铆sico y digital) 鈹 -鈹 鈹溾攢鈹 Receta foliada obligatoria 鈹 -鈹 鈹溾攢鈹 Control de existencias exactas 鈹 -鈹 鈹溾攢鈹 Auditor铆a de movimientos 鈹 -鈹 鈹斺攢鈹 Reportes para COFEPRIS 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Ver recetas pendientes -- [ ] Verificar stock -- [ ] Dispensar con lote y caducidad -- [ ] Cobro de receta -- [ ] Recepci贸n de mercanc铆a -- [ ] Alertas de caducidad -- [ ] Control de medicamentos controlados -- [ ] Inventario f铆sico - -**No Funcionales:** -- [ ] Dispensaci贸n < 3 minutos -- [ ] Trazabilidad 100% de lotes -- [ ] Cumplimiento COFEPRIS - -**Tecnicos:** -- [ ] Integraci贸n con recetas -- [ ] Integraci贸n con facturaci贸n -- [ ] Lector de c贸digo de barras -- [ ] Reportes para autoridades - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | -| EPIC-CL-005 Recetas | Backlog | Si | - -**Esta epica bloquea:** -| Epica/Modulo | Razon | -|--------------|-------| -| EPIC-CL-008 Facturaci贸n | Ventas de farmacia | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `pharmacy` -- [ ] Tablas: 8 (products, stock, stock_movements, dispensations, dispensation_items, controlled_log, suppliers, physical_counts) -- [ ] Funciones: 3 (check_expiry, update_stock, log_controlled) -- [ ] Indices: Por producto, lote, caducidad, proveedor - -**Backend:** -- [ ] Modulo: `pharmacy` -- [ ] Entities: 6 (Product, Stock, StockMovement, Dispensation, DispensationItem, ControlledLog) -- [ ] Endpoints: 15 -- [ ] Tests: 30 - -**Frontend:** -- [ ] Paginas: 5 (PendingPrescriptions, Dispensation, Inventory, ExpiryAlerts, ControlledLog) -- [ ] Componentes: 12 (PrescriptionCard, StockChecker, BarcodeScanner, ExpiryCalendar, etc.) -- [ ] Stores: 1 (pharmacyStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| GET | /api/pharmacy/prescriptions/pending | Recetas pendientes | -| GET | /api/pharmacy/stock/:productId | Verificar stock | -| POST | /api/pharmacy/dispensations | Registrar dispensaci贸n | -| POST | /api/pharmacy/receipts | Recibir mercanc铆a | -| GET | /api/pharmacy/expiry-alerts | Alertas de caducidad | -| POST | /api/pharmacy/controlled-log | Registrar movimiento controlado | -| POST | /api/pharmacy/physical-count | Inventario f铆sico | -| GET | /api/pharmacy/reports/sales | Reporte de ventas | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Merma por caducidad | Media | Medio | Alertas tempranas | -| Error en dispensaci贸n | Baja | Alto | Doble verificaci贸n | -| Faltantes de controlados | Baja | Alto | Auditor铆as frecuentes | - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Cat谩logo de medicamentos definido -- [ ] Proceso de controlados documentado - -## Definition of Done (DoD) - -- [ ] Flujo de dispensaci贸n funcionando -- [ ] Control de lotes y caducidad -- [ ] Registro de controlados -- [ ] Alertas de caducidad activas -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-008-facturacion.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-008-facturacion.md deleted file mode 100644 index dc1b215e0..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-008-facturacion.md +++ /dev/null @@ -1,268 +0,0 @@ -# EPICA: EPIC-CL-008 - Facturaci贸n - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-008 | -| **Nombre** | Facturaci贸n | -| **Modulo** | facturacion | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P0 (Critico) | -| **Estado** | Backlog | -| **Story Points** | 38 | -| **Sprint(s)** | Sprint 8-9 | - ---- - -## Descripcion - -Sistema de facturaci贸n para servicios m茅dicos. Incluye cobro de consultas, procedimientos, estudios de laboratorio, medicamentos y facturaci贸n a aseguradoras. Generaci贸n de CFDI 4.0 y notas de cr茅dito. - ---- - -## Objetivo de Negocio - -- Cobro oportuno de servicios -- Facturaci贸n correcta a aseguradoras -- Cumplimiento fiscal -- Control de cuentas por cobrar -- Reportes financieros - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL008-001 | Como cajero, quiero generar cuenta del paciente con servicios prestados | P0 | 5 | Backlog | -| US-CL008-002 | Como cajero, quiero cobrar consulta con m煤ltiples formas de pago | P0 | 5 | Backlog | -| US-CL008-003 | Como cajero, quiero facturar a nombre del paciente con sus datos fiscales | P0 | 5 | Backlog | -| US-CL008-004 | Como admin, quiero facturar a aseguradora con expediente de reclamaci贸n | P0 | 8 | Backlog | -| US-CL008-005 | Como cajero, quiero generar nota de cr茅dito por cancelaci贸n o error | P0 | 3 | Backlog | -| US-CL008-006 | Como paciente, quiero pagar mi cuenta desde el portal web | P1 | 5 | Backlog | -| US-CL008-007 | Como admin, quiero ver cuentas por cobrar pendientes | P0 | 3 | Backlog | -| US-CL008-008 | Como admin, quiero configurar precios de servicios y procedimientos | P0 | 2 | Backlog | -| US-CL008-009 | Como admin, quiero ver reporte de ingresos por per铆odo | P1 | 2 | Backlog | - -**Total Story Points:** 38 SP - ---- - -## Flujo de Facturaci贸n - -``` -COBRO DIRECTO (Paciente paga) - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CUENTA 鈹 鈫 Servicios prestados -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 COBRO 鈹 鈫 Efectivo/Tarjeta -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CFDI 鈹 鈫 Timbrado -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ENTREGADO 鈹 鈫 PDF + XML al paciente -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - - -COBRO A ASEGURADORA - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CUENTA 鈹 鈫 Servicios prestados -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 EXPEDIENTE 鈹 鈫 Documentos requeridos -鈹 RECLAMACI脫N 鈹 (notas, estudios, etc.) -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ENV脥O 鈹 鈫 A la aseguradora -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 SEGUIMIENTO 鈹 鈫 Pendiente de pago -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 COBRADO 鈹 鈫 Pago recibido -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CFDI 鈹 鈫 Factura a aseguradora -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Cuenta del Paciente - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CUENTA - PACIENTE: Juan P茅rez 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 SERVICIOS 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Concepto 鈹 Importe 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Consulta medicina general 鈹 $800.00 鈹 鈹 -鈹 鈹 Biometr铆a hem谩tica 鈹 $350.00 鈹 鈹 -鈹 鈹 Qu铆mica sangu铆nea 6 elementos 鈹 $450.00 鈹 鈹 -鈹 鈹 Paracetamol 500mg x 20 鈹 $120.00 鈹 鈹 -鈹 鈹 Amoxicilina 500mg x 21 鈹 $280.00 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 SUBTOTAL 鈹 $2,000.00 鈹 鈹 -鈹 鈹 IVA (16%) 鈹 $0.00 鈹 鈹 -鈹 鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 鈹 -鈹 鈹 TOTAL 鈹 $2,000.00 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 COBERTURA SEGURO 鈹 -鈹 鈹溾攢鈹 Aseguradora: GNP Seguros 鈹 -鈹 鈹溾攢鈹 P贸liza: 1234567 鈹 -鈹 鈹溾攢鈹 Cobertura consulta: 100% 鈹 -鈹 鈹溾攢鈹 Cobertura laboratorio: 80% 鈹 -鈹 鈹斺攢鈹 Copago paciente: $160.00 鈹 -鈹 鈹 -鈹 FORMA DE PAGO 鈹 -鈹 鈹溾攢鈹 Cargo a seguro: $1,840.00 鈹 -鈹 鈹斺攢鈹 Copago paciente: $160.00 (Pagado TDC) 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Generar cuenta con servicios -- [ ] Cobrar con m煤ltiples formas de pago -- [ ] Facturaci贸n CFDI 4.0 -- [ ] Facturaci贸n a aseguradoras -- [ ] Notas de cr茅dito -- [ ] Portal de pagos -- [ ] Cuentas por cobrar -- [ ] Reportes de ingresos - -**No Funcionales:** -- [ ] Timbrado < 5 segundos -- [ ] Historial de 5 a帽os -- [ ] Cumplimiento fiscal - -**Tecnicos:** -- [ ] Integraci贸n con PAC -- [ ] Integraci贸n con todos los m贸dulos -- [ ] Pasarela de pagos -- [ ] Reportes financieros - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | -| EPIC-CL-002 Pacientes | Backlog | Si | -| EPIC-CL-003 Citas | Backlog | Parcial | -| EPIC-CL-004 Consultas | Backlog | Parcial | -| EPIC-CL-006 Laboratorio | Backlog | Parcial | -| EPIC-CL-007 Farmacia | Backlog | Parcial | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `billing` -- [ ] Tablas: 8 (accounts, account_items, payments, invoices, invoice_items, insurance_claims, price_lists, services) -- [ ] Funciones: 3 (calculate_account, generate_invoice, process_payment) -- [ ] Indices: Por paciente, fecha, estado, aseguradora - -**Backend:** -- [ ] Modulo: `billing` -- [ ] Entities: 7 (Account, AccountItem, Payment, Invoice, InvoiceItem, InsuranceClaim, Service) -- [ ] Endpoints: 15 -- [ ] Tests: 30 - -**Frontend:** -- [ ] Paginas: 5 (Accounts, Checkout, InvoiceList, InsuranceClaims, Reports) -- [ ] Componentes: 12 (AccountSummary, PaymentForm, InvoiceViewer, ClaimTracker, etc.) -- [ ] Stores: 1 (billingStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| GET | /api/billing/accounts/:patientId | Cuenta del paciente | -| POST | /api/billing/accounts/:id/items | Agregar servicio | -| POST | /api/billing/accounts/:id/pay | Registrar pago | -| POST | /api/billing/invoices | Generar factura | -| GET | /api/billing/invoices/:id/pdf | Descargar PDF | -| POST | /api/billing/invoices/:id/cancel | Cancelar factura | -| POST | /api/billing/credit-notes | Crear nota de cr茅dito | -| POST | /api/billing/insurance-claims | Crear reclamaci贸n | -| GET | /api/billing/pending | Cuentas por cobrar | -| GET | /api/billing/reports/income | Reporte de ingresos | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Rechazo de aseguradora | Media | Alto | Expediente completo | -| Errores en facturaci贸n | Media | Alto | Validaciones antes de timbrar | -| Morosidad | Media | Medio | Seguimiento de cuentas | - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Lista de precios definida -- [ ] Convenios con aseguradoras documentados - -## Definition of Done (DoD) - -- [ ] Flujo de cobro funcionando -- [ ] Facturaci贸n CFDI operativa -- [ ] Facturaci贸n a aseguradoras -- [ ] Reportes financieros -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-009-reportes.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-009-reportes.md deleted file mode 100644 index 3f35086f9..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-009-reportes.md +++ /dev/null @@ -1,223 +0,0 @@ -# EPICA: EPIC-CL-009 - Reportes - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-009 | -| **Nombre** | Reportes | -| **Modulo** | reportes | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P1 (Alto) | -| **Estado** | Backlog | -| **Story Points** | 25 | -| **Sprint(s)** | Sprint 9-10 | - ---- - -## Descripcion - -Dashboard y reportes para gesti贸n cl铆nica. M茅tricas de productividad m茅dica, ocupaci贸n de consultorios, estad铆sticas de diagn贸sticos, indicadores financieros y reportes para autoridades sanitarias. - ---- - -## Objetivo de Negocio - -- Visibilidad de operaciones -- Medici贸n de productividad -- Toma de decisiones informada -- Cumplimiento de reportes regulatorios -- Identificaci贸n de oportunidades - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL009-001 | Como director, quiero ver dashboard de consultas del d铆a en tiempo real | P0 | 5 | Backlog | -| US-CL009-002 | Como director, quiero ver productividad por m茅dico (consultas/d铆a) | P0 | 3 | Backlog | -| US-CL009-003 | Como director, quiero ver ocupaci贸n de consultorios por hora | P1 | 3 | Backlog | -| US-CL009-004 | Como director, quiero ver top 10 diagn贸sticos m谩s frecuentes | P1 | 3 | Backlog | -| US-CL009-005 | Como admin, quiero ver reporte de ingresos vs gastos | P0 | 3 | Backlog | -| US-CL009-006 | Como admin, quiero generar reporte para SINBA/SISVER (autoridades) | P0 | 5 | Backlog | -| US-CL009-007 | Como admin, quiero exportar reportes a Excel | P0 | 2 | Backlog | -| US-CL009-008 | Como m茅dico, quiero ver mi resumen de atenciones del mes | P1 | 1 | Backlog | - -**Total Story Points:** 25 SP - ---- - -## Dashboard Principal - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 DASHBOARD CL脥NICA 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 CITAS HOY 鈹 鈹 CONSULTAS 鈹 鈹 INGRESOS HOY 鈹 鈹 -鈹 鈹 45 鈹 鈹 38 鈹 鈹 $42,300 鈹 鈹 -鈹 鈹 鈻 12% 鈹 鈹 鈻 8% 鈹 鈹 鈻 15% 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 PRODUCTIVIDAD POR M脡DICO (HOY) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Dr. Garc铆a 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 15 consultas 鈹 鈹 -鈹 鈹 Dra. L贸pez 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 12 consultas 鈹 鈹 -鈹 鈹 Dr. Mart铆nez 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 8 consultas 鈹 鈹 -鈹 鈹 Dra. S谩nchez 鈻堚枅鈻堚枅鈻堚枅鈻堚枅 6 consultas 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 OCUPACI脫N CONSULTORIOS 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 8 9 10 11 12 13 14 15 16 17 18 19 20 鈹 鈹 -鈹 鈹 C-1: 鈻堚枅 鈻堚枅 鈻堚枅 鈻堚枅 鈻戔枒 鈻戔枒 鈻堚枅 鈻堚枅 鈻堚枅 鈻堚枅 鈻堚枅 鈻戔枒 鈻戔枒 鈹 鈹 -鈹 鈹 C-2: 鈻堚枅 鈻堚枅 鈻堚枅 鈻戔枒 鈻戔枒 鈻戔枒 鈻堚枅 鈻堚枅 鈻堚枅 鈻堚枅 鈻戔枒 鈻戔枒 鈻戔枒 鈹 鈹 -鈹 鈹 C-3: 鈻戔枒 鈻堚枅 鈻堚枅 鈻堚枅 鈻堚枅 鈻戔枒 鈻堚枅 鈻堚枅 鈻堚枅 鈻戔枒 鈻戔枒 鈻戔枒 鈻戔枒 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 TOP 5 DIAGN脫STICOS (ESTE MES) 鈹 -鈹 1. J06.9 - IVAS (185 casos) 鈹 -鈹 2. I10 - Hipertensi贸n (142 casos) 鈹 -鈹 3. E11 - Diabetes T2 (98 casos) 鈹 -鈹 4. K30 - Dispepsia (76 casos) 鈹 -鈹 5. M54.5 - Lumbalgia (54 casos) 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Reportes Disponibles - -``` -OPERATIVOS -鈹溾攢鈹 Consultas por per铆odo -鈹溾攢鈹 Productividad por m茅dico -鈹溾攢鈹 Ocupaci贸n de consultorios -鈹溾攢鈹 Tiempo de espera promedio -鈹溾攢鈹 Ausentismo de pacientes -鈹斺攢鈹 Estudios de laboratorio realizados - -CL脥NICOS -鈹溾攢鈹 Diagn贸sticos m谩s frecuentes -鈹溾攢鈹 Medicamentos m谩s prescritos -鈹溾攢鈹 Estudios m谩s solicitados -鈹斺攢鈹 Pacientes cr贸nicos - -FINANCIEROS -鈹溾攢鈹 Ingresos por servicio -鈹溾攢鈹 Ingresos por aseguradora -鈹溾攢鈹 Cuentas por cobrar -鈹溾攢鈹 Ventas de farmacia -鈹斺攢鈹 Rentabilidad por servicio - -REGULATORIOS -鈹溾攢鈹 SINBA (notificaci贸n epidemiol贸gica) -鈹溾攢鈹 SISVER (vigilancia epidemiol贸gica) -鈹溾攢鈹 Reporte de medicamentos controlados -鈹斺攢鈹 Estad铆sticas para acreditaci贸n -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Dashboard en tiempo real -- [ ] Productividad por m茅dico -- [ ] Ocupaci贸n de consultorios -- [ ] Top diagn贸sticos -- [ ] Reportes financieros -- [ ] Reportes regulatorios -- [ ] Exportaci贸n a Excel - -**No Funcionales:** -- [ ] Carga de dashboard < 3 segundos -- [ ] Actualizaci贸n cada 5 minutos -- [ ] Datos hist贸ricos de 3 a帽os - -**Tecnicos:** -- [ ] Agregaci贸n eficiente -- [ ] Cach茅 de m茅tricas -- [ ] Jobs de prec谩lculo -- [ ] Formatos de autoridades - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| Todos los m贸dulos anteriores | Backlog | Si | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `analytics` -- [ ] Tablas: 4 (daily_stats, doctor_metrics, diagnosis_stats, financial_metrics) -- [ ] Vistas materializadas para consultas frecuentes - -**Backend:** -- [ ] Modulo: `reports` -- [ ] Services: MetricsAggregator, ReportGenerator -- [ ] Endpoints: 12 -- [ ] Jobs: C谩lculo de m茅tricas diarias -- [ ] Tests: 20 - -**Frontend:** -- [ ] Paginas: 3 (Dashboard, Reports, Export) -- [ ] Componentes: 12 (MetricCard, ChartWidget, ReportTable, etc.) -- [ ] Librer铆a: Chart.js -- [ ] Stores: 1 (reportsStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| GET | /api/reports/dashboard | Dashboard principal | -| GET | /api/reports/productivity | Productividad por m茅dico | -| GET | /api/reports/occupancy | Ocupaci贸n de consultorios | -| GET | /api/reports/diagnoses | Top diagn贸sticos | -| GET | /api/reports/financial | M茅tricas financieras | -| GET | /api/reports/export/:type | Exportar reporte | -| GET | /api/reports/regulatory/:type | Reporte regulatorio | - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] KPIs prioritarios definidos -- [ ] Formatos regulatorios obtenidos - -## Definition of Done (DoD) - -- [ ] Dashboard funcionando -- [ ] Reportes operativos disponibles -- [ ] Exportaci贸n funcionando -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-010-telemedicina.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-010-telemedicina.md deleted file mode 100644 index 1fd6a8fda..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-010-telemedicina.md +++ /dev/null @@ -1,278 +0,0 @@ -# EPICA: EPIC-CL-010 - Telemedicina - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-010 | -| **Nombre** | Telemedicina | -| **Modulo** | telemedicina | -| **Fase** | Fase 2 - Extensi贸n | -| **Prioridad** | P1 (Alto) | -| **Estado** | Backlog | -| **Story Points** | 55 | -| **Sprint(s)** | Sprint 11-14 | - ---- - -## Descripcion - -M贸dulo 100% nuevo para consultas m茅dicas remotas. Videoconsultas con WebRTC, sala de espera virtual, compartir pantalla para resultados, prescripci贸n electr贸nica a distancia y grabaci贸n de consultas con consentimiento. - ---- - -## Objetivo de Negocio - -- Ampliar alcance geogr谩fico -- Atenci贸n sin desplazamiento -- Seguimiento de cr贸nicos remoto -- Nuevas fuentes de ingreso -- Adaptaci贸n post-pandemia - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL010-001 | Como paciente, quiero agendar teleconsulta desde el portal | P0 | 5 | Backlog | -| US-CL010-002 | Como paciente, quiero entrar a sala de espera virtual antes de la consulta | P0 | 5 | Backlog | -| US-CL010-003 | Como m茅dico, quiero iniciar videollamada cuando el paciente est茅 listo | P0 | 8 | Backlog | -| US-CL010-004 | Como m茅dico, quiero compartir pantalla para mostrar resultados al paciente | P0 | 5 | Backlog | -| US-CL010-005 | Como m茅dico, quiero documentar nota cl铆nica durante la videoconsulta | P0 | 3 | Backlog | -| US-CL010-006 | Como m茅dico, quiero generar receta electr贸nica al finalizar | P0 | 3 | Backlog | -| US-CL010-007 | Como paciente, quiero recibir recordatorio 10 min antes de la consulta | P0 | 3 | Backlog | -| US-CL010-008 | Como paciente, quiero grabar la consulta con consentimiento del m茅dico | P2 | 8 | Backlog | -| US-CL010-009 | Como paciente, quiero enviar fotos de s铆ntomas antes de la consulta | P1 | 5 | Backlog | -| US-CL010-010 | Como m茅dico, quiero terminar la videollamada y cerrar consulta | P0 | 2 | Backlog | -| US-CL010-011 | Como admin, quiero ver reportes de teleconsultas realizadas | P1 | 3 | Backlog | -| US-CL010-012 | Como paciente, quiero evaluar la calidad de la teleconsulta | P2 | 5 | Backlog | - -**Total Story Points:** 55 SP - ---- - -## Flujo de Teleconsulta - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 AGENDAR 鈹 鈫 Paciente selecciona teleconsulta -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 (-10 min) -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 RECORDATORIO鈹 鈫 Email/WhatsApp con link -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹係ALA_ESPERA 鈹 鈫 Paciente espera en sala virtual -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 (M茅dico inicia) -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 VIDEOLLAMADA鈹 鈫 WebRTC activo -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹溾攢鈹 Chat de texto - 鈹溾攢鈹 Compartir pantalla - 鈹溾攢鈹 Enviar archivos - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CERRAR 鈹 鈫 M茅dico termina llamada -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 RECETA 鈹 鈫 Env铆o electr贸nico -鈹 + NOTA 鈹 鈫 Documentaci贸n -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ENCUESTA 鈹 鈫 Satisfacci贸n del paciente -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Arquitectura WebRTC - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ARQUITECTURA TELEMEDICINA 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 PACIENTE M脡DICO 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Browser 鈹 鈹 Browser 鈹 鈹 -鈹 鈹 (WebRTC) 鈹 鈹 (WebRTC) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鈹 鈹 -鈹 鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹 TURN/STUN 鈹溾攢鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Server 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Signaling 鈹 鈹 -鈹 鈹 Server 鈹 鈹 -鈹 鈹 (WebSockets) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Backend API 鈹 鈹 -鈹 鈹 (Express) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 CARACTER脥STICAS: 鈹 -鈹 鈹溾攢鈹 Video HD (720p/1080p) 鈹 -鈹 鈹溾攢鈹 Audio bidireccional 鈹 -鈹 鈹溾攢鈹 Compartir pantalla 鈹 -鈹 鈹溾攢鈹 Chat de texto 鈹 -鈹 鈹溾攢鈹 Transferencia de archivos 鈹 -鈹 鈹斺攢鈹 Grabaci贸n (opcional) 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Agendar teleconsulta -- [ ] Sala de espera virtual -- [ ] Videollamada WebRTC -- [ ] Compartir pantalla -- [ ] Chat durante consulta -- [ ] Enviar archivos -- [ ] Documentaci贸n de consulta -- [ ] Prescripci贸n electr贸nica -- [ ] Grabaci贸n con consentimiento -- [ ] Encuesta de satisfacci贸n - -**No Funcionales:** -- [ ] Latencia < 200ms -- [ ] Video HD estable -- [ ] Funciona en m贸vil y desktop -- [ ] Conexi贸n cifrada (SRTP) - -**Tecnicos:** -- [ ] WebRTC con TURN/STUN -- [ ] WebSockets para signaling -- [ ] Integraci贸n con consultas -- [ ] Almacenamiento de grabaciones - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | -| EPIC-CL-002 Pacientes | Backlog | Si | -| EPIC-CL-003 Citas | Backlog | Si | -| EPIC-CL-004 Consultas | Backlog | Si | -| EPIC-CL-005 Recetas | Backlog | Si | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `telemedicine` -- [ ] Tablas: 6 (teleconsultations, waiting_rooms, recordings, chat_messages, file_shares, satisfaction_surveys) -- [ ] Funciones: 2 (generate_room_token, log_session) -- [ ] Indices: Por m茅dico, paciente, fecha, estado - -**Backend:** -- [ ] Modulo: `telemedicine` -- [ ] Entities: 5 (Teleconsultation, WaitingRoom, Recording, ChatMessage, Survey) -- [ ] WebSocket Server para signaling -- [ ] Endpoints: 15 -- [ ] Tests: 25 - -**Frontend:** -- [ ] Paginas: 4 (WaitingRoom, VideoCall, PreCallCheck, PostCallSurvey) -- [ ] Componentes: 15 (VideoPlayer, ScreenShare, ChatBox, FileShare, Controls, etc.) -- [ ] WebRTC implementation -- [ ] Stores: 1 (telemedicineStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| POST | /api/telemedicine/sessions | Crear sesi贸n | -| GET | /api/telemedicine/sessions/:id | Estado de sesi贸n | -| POST | /api/telemedicine/sessions/:id/join | Unirse a sala | -| POST | /api/telemedicine/sessions/:id/signal | Se帽alizaci贸n WebRTC | -| POST | /api/telemedicine/sessions/:id/end | Terminar llamada | -| POST | /api/telemedicine/sessions/:id/chat | Enviar mensaje | -| POST | /api/telemedicine/sessions/:id/files | Subir archivo | -| POST | /api/telemedicine/sessions/:id/record | Iniciar grabaci贸n | -| POST | /api/telemedicine/sessions/:id/survey | Enviar encuesta | - ---- - -## Integraciones Externas - -| Servicio | Prop贸sito | Alternativas | -|----------|-----------|--------------| -| Twilio | TURN/STUN + Video | Daily.co, Vonage | -| AWS S3 | Almacenamiento de grabaciones | GCP, Azure | -| WebRTC nativo | Comunicaci贸n P2P | - | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Conexi贸n inestable | Media | Alto | Reconexi贸n autom谩tica | -| Problemas de audio/video | Media | Alto | Pre-call check | -| Privacidad de grabaciones | Baja | Alto | Encriptaci贸n + RBAC | - ---- - -## Nota T茅cnica - -Este m贸dulo es **100% nuevo** y no tiene equivalente en el ERP-Core. Requiere infraestructura especializada para streaming de video y cumplimiento de regulaciones de datos m茅dicos. - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Proveedor de video seleccionado -- [ ] Infraestructura TURN/STUN lista - -## Definition of Done (DoD) - -- [ ] Videollamada funcionando -- [ ] Sala de espera operativa -- [ ] Integraci贸n con consultas -- [ ] Grabaci贸n opcional -- [ ] Tests E2E pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-011-expediente.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-011-expediente.md deleted file mode 100644 index 009641214..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-011-expediente.md +++ /dev/null @@ -1,241 +0,0 @@ -# EPICA: EPIC-CL-011 - Expediente Cl铆nico Electr贸nico - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-011 | -| **Nombre** | Expediente Cl铆nico Electr贸nico | -| **Modulo** | expediente | -| **Fase** | Fase 1 - MVP | -| **Prioridad** | P0 (Critico) | -| **Estado** | Backlog | -| **Story Points** | 30 | -| **Sprint(s)** | Sprint 10 | - ---- - -## Descripcion - -Vista consolidada del expediente cl铆nico electr贸nico del paciente. Integra toda la informaci贸n m茅dica: datos personales, antecedentes, consultas, diagn贸sticos, recetas, estudios de laboratorio e imagen. Cumple con NOM-024-SSA3-2012. - ---- - -## Objetivo de Negocio - -- Historia cl铆nica completa -- Cumplimiento normativo -- Acceso r谩pido a informaci贸n -- Continuidad de atenci贸n -- Soporte a decisiones cl铆nicas - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL011-001 | Como m茅dico, quiero ver resumen ejecutivo del paciente en una pantalla | P0 | 5 | Backlog | -| US-CL011-002 | Como m茅dico, quiero ver l铆nea de tiempo de atenciones | P0 | 5 | Backlog | -| US-CL011-003 | Como m茅dico, quiero ver todos los diagn贸sticos hist贸ricos | P0 | 3 | Backlog | -| US-CL011-004 | Como m茅dico, quiero ver todos los medicamentos recetados | P0 | 3 | Backlog | -| US-CL011-005 | Como m茅dico, quiero ver resultados de laboratorio con gr谩ficas de tendencia | P0 | 5 | Backlog | -| US-CL011-006 | Como m茅dico, quiero ver estudios de imagen (DICOM viewer) | P1 | 5 | Backlog | -| US-CL011-007 | Como paciente, quiero descargar mi expediente completo en PDF | P1 | 2 | Backlog | -| US-CL011-008 | Como admin, quiero configurar permisos de acceso al expediente | P0 | 2 | Backlog | - -**Total Story Points:** 30 SP - ---- - -## Estructura del Expediente (NOM-024) - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 EXPEDIENTE CL脥NICO ELECTR脫NICO 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 DATOS DE IDENTIFICACI脫N 鈹 -鈹 鈹溾攢鈹 Nombre completo 鈹 -鈹 鈹溾攢鈹 Fecha de nacimiento / Edad 鈹 -鈹 鈹溾攢鈹 Sexo 鈹 -鈹 鈹溾攢鈹 CURP 鈹 -鈹 鈹斺攢鈹 Datos de contacto 鈹 -鈹 鈹 -鈹 ANTECEDENTES 鈹 -鈹 鈹溾攢鈹 Heredo-familiares 鈹 -鈹 鈹溾攢鈹 Personales no patol贸gicos 鈹 -鈹 鈹溾攢鈹 Personales patol贸gicos 鈹 -鈹 鈹溾攢鈹 Gineco-obst茅tricos (si aplica) 鈹 -鈹 鈹斺攢鈹 Alergias 鈹 -鈹 鈹 -鈹 NOTAS M脡DICAS 鈹 -鈹 鈹溾攢鈹 Historia cl铆nica inicial 鈹 -鈹 鈹溾攢鈹 Notas de evoluci贸n 鈹 -鈹 鈹溾攢鈹 Notas de interconsulta 鈹 -鈹 鈹斺攢鈹 Notas de egreso 鈹 -鈹 鈹 -鈹 ESTUDIOS 鈹 -鈹 鈹溾攢鈹 Laboratorio 鈹 -鈹 鈹 鈹溾攢鈹 Resultados 鈹 -鈹 鈹 鈹斺攢鈹 Gr谩ficas de tendencia 鈹 -鈹 鈹斺攢鈹 Imagenolog铆a 鈹 -鈹 鈹溾攢鈹 Radiograf铆as 鈹 -鈹 鈹溾攢鈹 Ultrasonidos 鈹 -鈹 鈹斺攢鈹 Tomograf铆as/Resonancias 鈹 -鈹 鈹 -鈹 PRESCRIPCIONES 鈹 -鈹 鈹溾攢鈹 Recetas emitidas 鈹 -鈹 鈹斺攢鈹 Medicamentos actuales 鈹 -鈹 鈹 -鈹 CONSENTIMIENTOS 鈹 -鈹 鈹斺攢鈹 Firmados digitalmente 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Vista de Timeline - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 L脥NEA DE TIEMPO - JUAN P脡REZ 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 DIC 2024 鈹 -鈹 鈹溾攢鈹 08 鈹 馃拪 Receta - Antibi贸tico 7 d铆as 鈹 -鈹 鈹溾攢鈹 08 鈹 馃┖ Consulta - Dr. Garc铆a (IVAS) 鈹 -鈹 鈹溾攢鈹 05 鈹 馃И Laboratorio - Biometr铆a hem谩tica 鈹 -鈹 鈹 鈹 -鈹 NOV 2024 鈹 -鈹 鈹溾攢鈹 22 鈹 馃拪 Receta - Antihipertensivos 鈹 -鈹 鈹溾攢鈹 22 鈹 馃┖ Consulta - Dr. L贸pez (Control HTA) 鈹 -鈹 鈹溾攢鈹 15 鈹 馃И Laboratorio - Qu铆mica sangu铆nea 鈹 -鈹 鈹 鈹 -鈹 OCT 2024 鈹 -鈹 鈹溾攢鈹 10 鈹 馃摲 Imagen - Rx T贸rax 鈹 -鈹 鈹溾攢鈹 10 鈹 馃┖ Consulta - Dr. Garc铆a (Tos cr贸nica) 鈹 -鈹 鈹 鈹 -鈹 SEP 2024 鈹 -鈹 鈹溾攢鈹 05 鈹 馃┖ Primera consulta - Historia cl铆nica 鈹 -鈹 鈹斺攢鈹 05 鈹 鉁嶏笍 Consentimiento informado firmado 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Resumen ejecutivo del paciente -- [ ] L铆nea de tiempo de atenciones -- [ ] Vista de diagn贸sticos hist贸ricos -- [ ] Vista de medicamentos -- [ ] Gr谩ficas de laboratorio -- [ ] Visor DICOM b谩sico -- [ ] Exportar expediente PDF -- [ ] Control de acceso - -**No Funcionales:** -- [ ] Carga de expediente < 3 segundos -- [ ] Cumplimiento NOM-024-SSA3-2012 -- [ ] Auditor铆a de accesos - -**Tecnicos:** -- [ ] Integraci贸n de todos los m贸dulos -- [ ] Visor DICOM (Cornerstone.js) -- [ ] Generaci贸n de PDF estructurado -- [ ] Gr谩ficas con Chart.js - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-002 Pacientes | Backlog | Si | -| EPIC-CL-004 Consultas | Backlog | Si | -| EPIC-CL-005 Recetas | Backlog | Si | -| EPIC-CL-006 Laboratorio | Backlog | Si | -| EPIC-CL-012 Imagenolog铆a | Backlog | Parcial | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] No requiere tablas nuevas (agrega datos de otros m贸dulos) -- [ ] Vistas optimizadas para consolidaci贸n - -**Backend:** -- [ ] Modulo: `medical-record` -- [ ] Services: RecordAggregator, PDFExporter -- [ ] Endpoints: 8 -- [ ] Tests: 20 - -**Frontend:** -- [ ] Paginas: 3 (RecordOverview, Timeline, Export) -- [ ] Componentes: 15 (SummaryCard, Timeline, DiagnosisHistory, LabTrends, DICOMViewer, etc.) -- [ ] Visor DICOM con Cornerstone.js -- [ ] Stores: 1 (recordStore) - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| GET | /api/medical-record/:patientId | Expediente completo | -| GET | /api/medical-record/:patientId/summary | Resumen ejecutivo | -| GET | /api/medical-record/:patientId/timeline | L铆nea de tiempo | -| GET | /api/medical-record/:patientId/diagnoses | Historial de diagn贸sticos | -| GET | /api/medical-record/:patientId/medications | Historial de medicamentos | -| GET | /api/medical-record/:patientId/labs/trends | Tendencias de laboratorio | -| GET | /api/medical-record/:patientId/export | Exportar PDF | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Expediente incompleto | Media | Alto | Validar datos m铆nimos | -| Acceso no autorizado | Baja | Alto | RBAC + auditor铆a | -| Carga lenta | Media | Medio | Paginaci贸n + cach茅 | - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] Estructura NOM-024 documentada -- [ ] Dise帽o de UI aprobado - -## Definition of Done (DoD) - -- [ ] Vista consolidada funcionando -- [ ] Timeline operativo -- [ ] Visor DICOM b谩sico -- [ ] Exportaci贸n PDF -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-012-imagenologia.md b/projects/erp-clinicas/docs/08-epicas/EPIC-CL-012-imagenologia.md deleted file mode 100644 index fe2beb771..000000000 --- a/projects/erp-clinicas/docs/08-epicas/EPIC-CL-012-imagenologia.md +++ /dev/null @@ -1,314 +0,0 @@ -# EPICA: EPIC-CL-012 - Imagenolog铆a - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | EPIC-CL-012 | -| **Nombre** | Imagenolog铆a | -| **Modulo** | imagenologia | -| **Fase** | Fase 2 - Extensi贸n | -| **Prioridad** | P1 (Alto) | -| **Estado** | Backlog | -| **Story Points** | 55 | -| **Sprint(s)** | Sprint 15-18 | - ---- - -## Descripcion - -M贸dulo 100% nuevo para gesti贸n de estudios de imagen m茅dica. Solicitud de estudios, integraci贸n con equipos de imagen, almacenamiento y visor DICOM, interpretaci贸n por radi贸logo y entrega de resultados. - ---- - -## Objetivo de Negocio - -- Estudios de imagen integrados -- Almacenamiento centralizado (PACS) -- Interpretaci贸n oportuna -- Reducci贸n de p茅rdida de estudios -- Acceso remoto a im谩genes - ---- - -## Historias de Usuario - -| ID | Historia | Prioridad | SP | Estado | -|----|----------|-----------|-----|--------| -| US-CL012-001 | Como m茅dico, quiero solicitar estudio de imagen desde la consulta | P0 | 5 | Backlog | -| US-CL012-002 | Como t茅cnico, quiero ver 贸rdenes de estudios pendientes | P0 | 3 | Backlog | -| US-CL012-003 | Como t茅cnico, quiero registrar realizaci贸n de estudio | P0 | 3 | Backlog | -| US-CL012-004 | Como sistema, quiero recibir im谩genes DICOM del equipo | P0 | 13 | Backlog | -| US-CL012-005 | Como radi贸logo, quiero ver estudios pendientes de interpretar | P0 | 3 | Backlog | -| US-CL012-006 | Como radi贸logo, quiero ver im谩genes en visor DICOM profesional | P0 | 8 | Backlog | -| US-CL012-007 | Como radi贸logo, quiero dictar interpretaci贸n del estudio | P0 | 5 | Backlog | -| US-CL012-008 | Como m茅dico, quiero recibir notificaci贸n cuando el estudio est茅 listo | P0 | 3 | Backlog | -| US-CL012-009 | Como paciente, quiero descargar mis estudios de imagen | P1 | 5 | Backlog | -| US-CL012-010 | Como admin, quiero configurar modalidades de imagen disponibles | P0 | 3 | Backlog | -| US-CL012-011 | Como admin, quiero ver reportes de estudios realizados | P1 | 4 | Backlog | - -**Total Story Points:** 55 SP - ---- - -## Flujo de Imagenolog铆a - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 SOLICITUD 鈹 鈫 M茅dico solicita estudio -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 RECEPCI脫N 鈹 鈫 Paciente llega -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 REALIZACI脫N 鈹 鈫 T茅cnico realiza estudio -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 DICOM 鈹 鈫 Im谩genes enviadas al PACS -鈹 UPLOAD 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹侷NTERPRETAC. 鈹 鈫 Radi贸logo analiza -鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 LIBERADO 鈹 鈫 Disponible para m茅dico/paciente -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Arquitectura DICOM/PACS - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ARQUITECTURA IMAGENOLOG脥A 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 EQUIPOS DE IMAGEN PACS SERVER 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Rayos X 鈹 鈹鈹鈹鈹鈹鈹鈻 鈹 鈹 鈹 -鈹 鈹 (DICOM) 鈹 鈹 Orthanc / 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 DCM4CHEE 鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 鈹 鈹 -鈹 鈹 Ultrasonido 鈹 鈹鈹鈹鈹鈹鈹鈻 鈹 DICOM Store 鈹 鈹 -鈹 鈹 (DICOM) 鈹 鈹 DICOM Query 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 DICOM Retrieve 鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 鈹 鈹 -鈹 鈹 Tomograf铆a 鈹 鈹鈹鈹鈹鈹鈹鈻 鈹 鈹 鈹 -鈹 鈹 (DICOM) 鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 鈹 -鈹 鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Backend API 鈹 鈹 -鈹 鈹 (Express) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Visor DICOM 鈹 鈹 -鈹 鈹 (Cornerstone) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 EST脕NDARES: 鈹 -鈹 鈹溾攢鈹 DICOM 3.0 para im谩genes 鈹 -鈹 鈹溾攢鈹 HL7 para integraciones 鈹 -鈹 鈹溾攢鈹 IHE XDS-I.b para compartir im谩genes 鈹 -鈹 鈹斺攢鈹 WADO-RS para acceso web 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Modalidades de Imagen - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 MODALIDADES SOPORTADAS 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 CR/DR - Radiolog铆a Computarizada/Digital 鈹 -鈹 鈹溾攢鈹 Rayos X de t贸rax 鈹 -鈹 鈹溾攢鈹 Rayos X de abdomen 鈹 -鈹 鈹溾攢鈹 Rayos X de extremidades 鈹 -鈹 鈹斺攢鈹 Rayos X de columna 鈹 -鈹 鈹 -鈹 US - Ultrasonido 鈹 -鈹 鈹溾攢鈹 Abdominal 鈹 -鈹 鈹溾攢鈹 P茅lvico 鈹 -鈹 鈹溾攢鈹 Obst茅trico 鈹 -鈹 鈹斺攢鈹 Musculoesquel茅tico 鈹 -鈹 鈹 -鈹 CT - Tomograf铆a Computarizada 鈹 -鈹 鈹溾攢鈹 Cr谩neo 鈹 -鈹 鈹溾攢鈹 T贸rax 鈹 -鈹 鈹溾攢鈹 Abdomen 鈹 -鈹 鈹斺攢鈹 Columna 鈹 -鈹 鈹 -鈹 MR - Resonancia Magn茅tica 鈹 -鈹 鈹溾攢鈹 Cerebro 鈹 -鈹 鈹溾攢鈹 Columna 鈹 -鈹 鈹斺攢鈹 Articulaciones 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Criterios de Aceptacion de la Epica - -**Funcionales:** -- [ ] Solicitar estudios de imagen -- [ ] Ver 贸rdenes pendientes -- [ ] Registrar realizaci贸n -- [ ] Recibir im谩genes DICOM -- [ ] Visor DICOM profesional -- [ ] Interpretaci贸n por radi贸logo -- [ ] Notificaci贸n de resultados -- [ ] Descarga de estudios -- [ ] Reportes de producci贸n - -**No Funcionales:** -- [ ] Carga de im谩genes < 5 segundos -- [ ] Almacenamiento de 5+ a帽os -- [ ] Cumplimiento DICOM 3.0 - -**Tecnicos:** -- [ ] Servidor PACS (Orthanc) -- [ ] Visor Cornerstone.js -- [ ] Integraci贸n con consultas -- [ ] Almacenamiento escalable - ---- - -## Dependencias - -**Esta epica depende de:** -| Epica/Modulo | Estado | Bloqueante | -|--------------|--------|------------| -| EPIC-CL-001 Fundamentos | Backlog | Si | -| EPIC-CL-002 Pacientes | Backlog | Si | -| EPIC-CL-004 Consultas | Backlog | Si | - -**Esta epica bloquea:** -| Epica/Modulo | Razon | -|--------------|-------| -| EPIC-CL-011 Expediente | Im谩genes son parte del expediente | - ---- - -## Desglose Tecnico - -**Database:** -- [ ] Schema: `imaging` -- [ ] Tablas: 7 (imaging_orders, order_items, studies, series, interpretations, modalities, pacs_log) -- [ ] Funciones: 2 (log_dicom_event, update_study_status) -- [ ] Indices: Por paciente, m茅dico, fecha, modalidad, estado - -**Backend:** -- [ ] Modulo: `imaging` -- [ ] Entities: 6 (ImagingOrder, OrderItem, Study, Series, Interpretation, Modality) -- [ ] DICOM Service: Comunicaci贸n con PACS -- [ ] Endpoints: 15 -- [ ] Tests: 30 - -**Frontend:** -- [ ] Paginas: 5 (ImagingOrders, Worklist, DICOMViewer, Interpretation, Reports) -- [ ] Componentes: 15 (OrderCard, StudyThumbnail, ViewerTools, InterpretationEditor, etc.) -- [ ] Cornerstone.js para visor DICOM -- [ ] Stores: 1 (imagingStore) - -**Infraestructura:** -- [ ] Orthanc PACS Server -- [ ] Almacenamiento S3/MinIO para im谩genes -- [ ] CDN para distribuci贸n - ---- - -## Endpoints API - -| Metodo | Endpoint | Descripcion | -|--------|----------|-------------| -| POST | /api/imaging/orders | Crear orden de estudio | -| GET | /api/imaging/orders | Listar 贸rdenes | -| GET | /api/imaging/orders/:id | Detalle de orden | -| POST | /api/imaging/orders/:id/perform | Registrar realizaci贸n | -| GET | /api/imaging/studies/:id | Metadatos del estudio | -| GET | /api/imaging/studies/:id/series | Series del estudio | -| GET | /api/imaging/wado-rs/* | WADO-RS para im谩genes | -| POST | /api/imaging/interpretations | Crear interpretaci贸n | -| GET | /api/imaging/worklist | Lista de trabajo radi贸logo | - ---- - -## Integraciones DICOM - -| Operaci贸n | Protocolo | Uso | -|-----------|-----------|-----| -| C-STORE | DICOM | Recibir im谩genes de equipos | -| C-FIND | DICOM | Buscar estudios | -| C-MOVE | DICOM | Recuperar estudios | -| WADO-RS | HTTP | Acceso web a im谩genes | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Integraci贸n DICOM compleja | Alta | Alto | PACS probado (Orthanc) | -| Almacenamiento costoso | Media | Medio | Compresi贸n + tiering | -| Visor lento | Media | Medio | Streaming progresivo | - ---- - -## Nota T茅cnica - -Este m贸dulo es **100% nuevo** y requiere infraestructura especializada: -- Servidor PACS compatible DICOM 3.0 -- Almacenamiento de gran capacidad para im谩genes -- Visor web profesional (Cornerstone.js) -- Conocimiento de est谩ndares m茅dicos (DICOM, HL7, IHE) - ---- - -## Definition of Ready (DoR) - -- [x] Historias de usuario definidas -- [x] Criterios de aceptacion claros -- [x] Dependencias identificadas -- [x] Estimacion completada -- [ ] PACS Server seleccionado -- [ ] Equipos DICOM compatibles - -## Definition of Done (DoD) - -- [ ] Flujo completo de imagenolog铆a -- [ ] Recepci贸n DICOM funcionando -- [ ] Visor profesional operativo -- [ ] Interpretaci贸n de estudios -- [ ] Tests de integraci贸n pasando -- [ ] Documentaci贸n de API - ---- - -## Historial - -| Fecha | Cambio | Autor | -|-------|--------|-------| -| 2025-12-08 | Creacion de epica | Claude-Agent | - ---- - -**Creada por:** Claude-Agent -**Fecha:** 2025-12-08 -**Ultima actualizacion:** 2025-12-08 diff --git a/projects/erp-clinicas/docs/README.md b/projects/erp-clinicas/docs/README.md deleted file mode 100644 index ddb4ad34e..000000000 --- a/projects/erp-clinicas/docs/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# DOCUMENTACI脫N - ERP Cl铆nicas - -**Proyecto:** ERP Cl铆nicas -**Versi贸n:** 1.0.0 -**Fecha:** 2025-12-05 -**Estado:** Por iniciar - ---- - -## Estructura de Documentaci贸n - -``` -docs/ -鈹溾攢鈹 00-vision-general/ # Visi贸n, objetivos y alcance -鈹溾攢鈹 01-analisis-referencias/ # An谩lisis de sistemas de referencia -鈹溾攢鈹 02-definicion-modulos/ # Lista, 铆ndice y dependencias de m贸dulos -鈹溾攢鈹 03-requerimientos/ # Requerimientos funcionales por m贸dulo -鈹溾攢鈹 04-modelado/ # Dise帽o t茅cnico -鈹 鈹溾攢鈹 database-design/ # DDL specs, schemas -鈹 鈹溾攢鈹 domain-models/ # Modelos de dominio -鈹 鈹斺攢鈹 especificaciones-tecnicas/ # ET backend/frontend -鈹溾攢鈹 05-user-stories/ # Historias de usuario -鈹溾攢鈹 06-test-plans/ # Planes de prueba -鈹溾攢鈹 07-devops/ # CI/CD, infraestructura -鈹溾攢鈹 90-transversal/ # Documentos transversales -鈹溾攢鈹 95-guias-desarrollo/ # Gu铆as para desarrolladores -鈹斺攢鈹 97-adr/ # Architecture Decision Records -``` - ---- - -## Directiva Aplicable - -Ver: `/workspace/core/orchestration/directivas/DIRECTIVA-ESTRUCTURA-DOCUMENTACION-PROYECTOS.md` - ---- - -**脷ltima actualizaci贸n:** 2025-12-05 diff --git a/projects/erp-clinicas/docs/_MAP.md b/projects/erp-clinicas/docs/_MAP.md deleted file mode 100644 index b60c83cd4..000000000 --- a/projects/erp-clinicas/docs/_MAP.md +++ /dev/null @@ -1,40 +0,0 @@ -# Mapa de Documentacion: erp-clinicas - -**Proyecto:** erp-clinicas -**Actualizado:** 2026-01-04 -**Generado por:** EPIC-008 adapt-simco.sh - ---- - -## Estructura de Documentacion - -``` -docs/ -鈹溾攢鈹 _MAP.md # Este archivo (indice de navegacion) -鈹溾攢鈹 00-overview/ # Vision general del proyecto -鈹溾攢鈹 01-architecture/ # Arquitectura y decisiones (ADRs) -鈹溾攢鈹 02-specs/ # Especificaciones tecnicas -鈹溾攢鈹 03-api/ # Documentacion de APIs -鈹溾攢鈹 04-guides/ # Guias de desarrollo -鈹斺攢鈹 99-finiquito/ # Entregables cliente (si aplica) -``` - -## Navegacion Rapida - -| Seccion | Descripcion | Estado | -|---------|-------------|--------| -| Overview | Vision general | - | -| Architecture | Decisiones arquitectonicas | - | -| Specs | Especificaciones tecnicas | - | -| API | Documentacion de endpoints | - | -| Guides | Guias de desarrollo | - | - -## Estadisticas - -- Total archivos en docs/: 27 -- Fecha de adaptacion: 2026-01-04 - ---- - -**Nota:** Este archivo fue generado automaticamente por EPIC-008. -Actualizar manualmente con la estructura real del proyecto. diff --git a/projects/erp-clinicas/orchestration/00-guidelines/CONTEXTO-PROYECTO.md b/projects/erp-clinicas/orchestration/00-guidelines/CONTEXTO-PROYECTO.md deleted file mode 100644 index 927d8ac59..000000000 --- a/projects/erp-clinicas/orchestration/00-guidelines/CONTEXTO-PROYECTO.md +++ /dev/null @@ -1,159 +0,0 @@ -# Contexto del Proyecto: ERP Clinicas - -## Metadatos - -| Campo | Valor | -|-------|-------| -| **Nombre** | ERP Clinicas - Gestion de Consultorios y Clinicas | -| **Tipo** | STANDALONE (Proyecto Independiente) | -| **Nivel** | Vertical que extiende erp-core | -| **Estado** | Por iniciar | -| **Progreso** | 0% | -| **Version** | 0.0.1 | -| **Base** | Extiende projects/erp-core (60-70%) | -| **Extension** | Modulos especificos (+30-40%) | -| **Path** | `/home/isem/workspace-v1/projects/erp-clinicas/` | -| **Fecha Migracion** | 2025-12-27 | - ---- - -## VARIABLES PARA DIRECTIVAS GLOBALES - -```yaml -# Identificacion del Proyecto -PROJECT: erp-clinicas -PROJECT_NAME: ERP Clinicas -PROJECT_LEVEL: STANDALONE - -# Paths Principales (WORKSPACE-V1) -WORKSPACE_ROOT: ~/workspace-v1 -PROJECT_ROOT: ~/workspace-v1/projects/erp-clinicas -APPS_ROOT: ~/workspace-v1/projects/erp-clinicas -DOCS_ROOT: ~/workspace-v1/projects/erp-clinicas/docs -ORCHESTRATION: ~/workspace-v1/projects/erp-clinicas/orchestration - -# Herencia de ERP-Core -ERP_CORE_ROOT: ~/workspace-v1/projects/erp-core -HERENCIA_DOC: orchestration/00-guidelines/HERENCIA-ERP-CORE.md - -# Base Orchestration (Directivas y Perfiles) -DIRECTIVAS_PATH: ~/workspace-v1/orchestration/directivas -PERFILES_PATH: ~/workspace-v1/orchestration/agents/perfiles -CATALOG_PATH: ~/workspace-v1/core/catalog - -# Base de Datos -DB_NAME: erp_clinicas -DB_DDL_PATH: ~/workspace-v1/projects/erp-clinicas/database/ddl -DB_SCRIPTS_PATH: ~/workspace-v1/projects/erp-clinicas/database - -# Backend -BACKEND_ROOT: ~/workspace-v1/projects/erp-clinicas/backend -BACKEND_SRC: ~/workspace-v1/projects/erp-clinicas/backend/src - -# Frontend -FRONTEND_ROOT: ~/workspace-v1/projects/erp-clinicas/frontend -FRONTEND_SRC: ~/workspace-v1/projects/erp-clinicas/frontend/src -``` - ---- - -## Descripcion - -ERP especializado para la gestion de clinicas, consultorios medicos y centros de salud. Extiende el ERP Core con funcionalidades especificas del sector salud. - -**Funcionalidades principales:** -- Gestion de pacientes y expedientes clinicos -- Agenda y citas medicas -- Historia clinica electronica -- Recetas y prescripciones -- Facturacion medica (CFDI) -- Inventario de medicamentos e insumos -- Reportes clinicos y administrativos - ---- - -## Stack Tecnologico - -Hereda completamente del ERP Core: -- **Backend:** Node.js + Express + TypeScript -- **Frontend:** React + TypeScript + Tailwind -- **Database:** PostgreSQL 15+ -- **Auth:** JWT + Multi-tenant - ---- - -## Paths del Proyecto - -``` -/home/isem/workspace/projects/erp-suite/apps/verticales/clinicas/ -鈹溾攢鈹 backend/ # Extensiones backend -鈹溾攢鈹 frontend/ # UI especializada -鈹溾攢鈹 database/ # DDL vertical -鈹溾攢鈹 docs/ # Documentacion -鈹斺攢鈹 orchestration/ # Sistema NEXUS -``` - ---- - -## Modulos Especificos (MCL-*) - -| Codigo | Modulo | Descripcion | -|--------|--------|-------------| -| MCL-001 | pacientes | Gestion de pacientes | -| MCL-002 | expedientes | Historia clinica electronica | -| MCL-003 | citas | Agenda y programacion | -| MCL-004 | recetas | Prescripciones medicas | -| MCL-005 | laboratorio | Resultados de laboratorio | -| MCL-006 | facturacion-medica | CFDI para sector salud | - ---- - -## Modulos del Core que Extiende - -| Modulo Core | Extension | -|-------------|-----------| -| MGN-002 Users | Roles medicos (doctor, enfermera, admin) | -| MGN-005 Catalogs | CIE-10, medicamentos, procedimientos | -| MGN-006 Settings | Configuracion de consultorio | -| MGN-008 Notifications | Recordatorios de citas | -| MGN-010 Financial | Facturacion medica | -| MGN-011 Inventory | Medicamentos e insumos | - ---- - -## Schemas de Base de Datos - -``` -vertical_clinicas # Schema principal -鈹溾攢鈹 patients # Pacientes -鈹溾攢鈹 medical_records # Expedientes clinicos -鈹溾攢鈹 appointments # Citas -鈹溾攢鈹 prescriptions # Recetas -鈹溾攢鈹 vital_signs # Signos vitales -鈹溾攢鈹 lab_results # Resultados laboratorio -鈹斺攢鈹 medical_invoices # Facturacion -``` - ---- - -## Principios Especificos - -1. **Confidencialidad:** Datos medicos requieren encriptacion adicional -2. **Auditoria:** Log detallado de acceso a expedientes -3. **Normativa:** Cumplir NOM-024-SSA3-2012 (expediente clinico) -4. **Interoperabilidad:** Preparado para HL7 FHIR - ---- - -## Referencias - -| Recurso | Path | -|---------|------| -| Directivas globales | `/home/isem/workspace-v1/orchestration/directivas/` | -| Directivas ERP-Core | `/home/isem/workspace-v1/projects/erp-core/orchestration/directivas/` | -| Herencia directivas | `./HERENCIA-DIRECTIVAS.md` | -| Dependencias ERP-Core | `../referencias/DEPENDENCIAS-ERP-CORE.yml` | -| Dependencias Shared | `../referencias/DEPENDENCIAS-SHARED.yml` | - ---- -*Ultima actualizacion: Diciembre 2025* diff --git a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md b/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md deleted file mode 100644 index 972104204..000000000 --- a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-DIRECTIVAS.md +++ /dev/null @@ -1,89 +0,0 @@ -# Herencia de Directivas - ERP Clinicas - -## Jerarquia de Directivas - -Este proyecto hereda directivas en el siguiente orden de precedencia: - -``` -1. Directivas Globales (CORE) 鈫 /home/isem/workspace/core/orchestration/directivas/ -2. Directivas ERP-Core 鈫 /home/isem/workspace/projects/erp-suite/apps/erp-core/orchestration/directivas/ -3. Directivas Clinicas 鈫 ./directivas/ (este proyecto) -``` - -**Regla:** Las directivas especificas pueden **EXTENDER** las heredadas, nunca **REDUCIRLAS**. - ---- - -## Directivas Heredadas de CORE - -| Directiva | Proposito | -|-----------|-----------| -| `DIRECTIVA-FLUJO-5-FASES.md` | Flujo de trabajo obligatorio | -| `DIRECTIVA-DOCUMENTACION-OBLIGATORIA.md` | Docs en tiempo real | -| `DIRECTIVA-CALIDAD-CODIGO.md` | Estandares de codigo | -| `DIRECTIVA-DISENO-BASE-DATOS.md` | Diseno BD | -| `DIRECTIVA-CONTROL-VERSIONES.md` | Git | - ---- - -## Directivas Heredadas de ERP-Core - -| Directiva | Proposito | -|-----------|-----------| -| `DIRECTIVA-MULTI-TENANT.md` | Aislamiento por tenant | -| `DIRECTIVA-EXTENSION-VERTICALES.md` | Como extender el core | -| `DIRECTIVA-DOCUMENTACION-PRE-DESARROLLO.md` | Documentar antes de desarrollar | -| `DIRECTIVA-PATRONES-ODOO.md` | Patrones de diseno | -| `DIRECTIVA-HERENCIA-MODULOS.md` | Extension de modulos | -| `ESTANDARES-API-REST-GENERICO.md` | APIs REST | - ---- - -## Directivas Especificas de Clinicas - -| Directiva | Proposito | Estado | -|-----------|-----------|--------| -| `DIRECTIVA-CONFIDENCIALIDAD-MEDICA.md` | Proteccion datos pacientes | Por crear | -| `DIRECTIVA-EXPEDIENTE-CLINICO.md` | NOM-024-SSA3-2012 | Por crear | -| `DIRECTIVA-AUDITORIA-ACCESOS.md` | Log de acceso a expedientes | Por crear | -| `DIRECTIVA-INTEROPERABILIDAD-HL7.md` | Estandar HL7 FHIR | Por crear | - ---- - -## Modulos que Usa del Core - -| Modulo Core | Uso en Clinicas | -|-------------|-----------------| -| MGN-001 Auth | Directo | -| MGN-002 Users | Extendido (roles medicos) | -| MGN-004 Tenants | Directo | -| MGN-005 Catalogs | Extendido (CIE-10, medicamentos) | -| MGN-006 Settings | Extendido (config consultorio) | -| MGN-008 Notifications | Extendido (recordatorios citas) | -| MGN-010 Financial | Extendido (facturacion medica) | -| MGN-011 Inventory | Extendido (medicamentos) | - ---- - -## Consideraciones Especiales - -### Seguridad de Datos Medicos -- Encriptacion AES-256 para expedientes -- Logs de auditoria inmutables -- Control de acceso por rol medico -- Cumplimiento LFPDPPP - -### Normativa Aplicable -- NOM-024-SSA3-2012 (Expediente clinico electronico) -- NOM-004-SSA3-2012 (Expediente clinico) -- CFDI 4.0 para facturacion - ---- - -## Referencias - -- Core: `/home/isem/workspace/core/orchestration/directivas/` -- ERP-Core: `/home/isem/workspace/projects/erp-suite/apps/erp-core/orchestration/directivas/` - ---- -*Ultima actualizacion: Diciembre 2025* diff --git a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-ERP-CORE.md b/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-ERP-CORE.md deleted file mode 100644 index db7736655..000000000 --- a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-ERP-CORE.md +++ /dev/null @@ -1,184 +0,0 @@ -# Herencia de ERP Core - Vertical Clinicas - -**Version:** 1.0.0 -**Vertical:** Clinicas -**Nivel:** STANDALONE (proyecto independiente) -**Version ERP-Core:** 1.2.0 -**Ruta ERP-Core:** projects/erp-core -**Herencia:** 60-70% de funcionalidad base de erp-core -**Fecha Migracion:** 2025-12-27 - ---- - -## RESUMEN DE HERENCIA - -Este documento especifica exactamente que hereda la vertical Clinicas del ERP Core y como lo extiende. - ---- - -## 1. MODULOS HEREDADOS (100%) - -| Modulo Core | Codigo | Uso en Clinicas | -|-------------|--------|-----------------| -| Auth | MGN-001 | Autenticacion con seguridad reforzada | -| Users | MGN-002 | Gestion de personal medico | -| Roles | MGN-003 | Roles medicos (doctor, enfermera, admin) | -| Audit | MGN-007 | Auditoria de acceso a expedientes | -| Notifications | MGN-008 | Recordatorios de citas | -| Reports | MGN-009 | Reportes medicos y administrativos | - -**Accion:** NO crear codigo para estos modulos. Usar directamente del core. - ---- - -## 2. MODULOS HEREDADOS Y EXTENDIDOS - -### MGN-004: Tenants 鈫 Clinicas/Consultorios - -```yaml -herencia_base: - - Multi-tenancy basico - -extension_clinicas: - - Clinica/Consultorio como tenant - - Campos adicionales: - - licencia_sanitaria - - especialidades_medicas[] - - horarios_atencion - - numero_consultorios - - Relaciones: - - clinica 鈫 consultorios (1:N) - - clinica 鈫 medicos (1:N) -``` - -### MGN-005: Catalogs 鈫 Catalogos Medicos - -```yaml -herencia_base: - - CRUD de catalogos genericos - -extension_clinicas: - - Catalogo CIE-10 (diagnosticos) - - Catalogo de procedimientos medicos - - Catalogo de medicamentos (cuadro basico) - - Catalogo de laboratorios - - Catalogo de especialidades medicas -``` - -### MGN-010: Financial 鈫 Facturacion Medica - -```yaml -herencia_base: - - Plan de cuentas - - Facturacion basica - -extension_clinicas: - - CFDI para sector salud - - Deducibilidad de gastos medicos - - Integracion con aseguradoras -``` - ---- - -## 3. ESPECIFICACIONES TRANSVERSALES HEREDADAS - -### Obligatorias - -| Especificacion | Gap | Uso | -|----------------|-----|-----| -| `SPEC-RRHH-EVALUACIONES-SKILLS.md` | GAP-MGN-010 | Credenciales medicas | -| `SPEC-INTEGRACION-CALENDAR.md` | GAP-MGN-014 | Agenda de citas | -| `SPEC-MAIL-THREAD-TRACKING.md` | Patron | Comunicacion con pacientes | - -### Recomendadas - -| Especificacion | Gap | Uso | -|----------------|-----|-----| -| `SPEC-TWO-FACTOR-AUTHENTICATION.md` | GAP-MGN-001 | Seguridad acceso expedientes | -| `SPEC-SISTEMA-SECUENCIAS.md` | GAP-MGN-004 | Foliado de expedientes | -| `SPEC-SCHEDULER-REPORTES.md` | GAP-MGN-012 | Recordatorios automaticos | - ---- - -## 4. MODULOS PROPIOS (No heredados) - -| Codigo | Modulo | Descripcion | -|--------|--------|-------------| -| CL-001 | patients | Registro de pacientes | -| CL-002 | appointments | Agenda y citas | -| CL-003 | medical_records | Expediente clinico electronico | -| CL-004 | prescriptions | Recetas medicas | -| CL-005 | laboratory | Resultados de laboratorio | -| CL-006 | medical_billing | Facturacion medica CFDI | - ---- - -## 5. SCHEMAS DE BASE DE DATOS - -### Heredados de Core - -```yaml -schemas_core: - - auth (con seguridad reforzada) - - core_users - - core_rbac - - core_tenants (extendido) - - core_catalogs (extendido) - - core_audit (critico para HIPAA/datos sensibles) -``` - -### Propios de Clinicas - -```yaml -schemas_vertical: - - vertical_clinicas - - patients - - medical_records (ENCRIPTADO) - - appointments - - prescriptions - - vital_signs - - lab_results -``` - ---- - -## 6. CONSIDERACIONES ESPECIALES - -### Cumplimiento Normativo - -**CRITICO:** Este vertical maneja datos sensibles de salud. - -- **NOM-024-SSA3-2012:** Expediente clinico electronico -- **Ley de Proteccion de Datos Personales:** Datos sensibles -- **COFEPRIS:** Requisitos de trazabilidad - -### Requisitos de Seguridad - -1. **Encriptacion obligatoria** de datos medicos en reposo -2. **Auditoria completa** de acceso a expedientes -3. **Control de acceso** granular por paciente -4. **Consentimiento informado** digital -5. **Backup cifrado** de expedientes - -### Interoperabilidad Futura - -- HL7 FHIR para intercambio de datos -- Integracion con laboratorios externos -- Receta electronica SAT - ---- - -## 7. REFERENCIAS - -| Recurso | Ubicacion | -|---------|-----------| -| MASTER_INVENTORY Core | `erp-core/orchestration/inventarios/MASTER_INVENTORY.yml` | -| Specs Transversales | `erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/` | -| NOM-024-SSA3-2012 | Normativa externa | -| HERENCIA-DIRECTIVAS | `./HERENCIA-DIRECTIVAS.md` | - ---- - -*Sistema NEXUS + SIMCO v2.2.0* -*Vertical: Clinicas (Nivel 2B.2)* -*Ultima actualizacion: 2025-12-08* diff --git a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SIMCO.md b/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SIMCO.md deleted file mode 100644 index a463ec3c6..000000000 --- a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SIMCO.md +++ /dev/null @@ -1,138 +0,0 @@ -# Herencia SIMCO - ERP Cl铆nicas - -**Sistema:** SIMCO v2.2.0 + CAPVED + CCA Protocol -**Fecha:** 2025-12-08 - ---- - -## Configuraci贸n del Proyecto - -| Propiedad | Valor | -|-----------|-------| -| **Proyecto** | ERP Cl铆nicas - Vertical para Cl铆nicas/Consultorios | -| **Nivel** | VERTICAL (Nivel 3) | -| **Padre** | erp-core | -| **Suite** | erp-suite | -| **SIMCO Version** | 2.2.0 | -| **CAPVED** | Habilitado | -| **CCA Protocol** | Habilitado | -| **Estado** | 0% - Futuro | - -## Jerarqu铆a de Herencia - -``` -Nivel 0: core/orchestration/ 鈫 FUENTE PRINCIPAL - 鈹斺攢鈹 Nivel 1: erp-suite/orchestration/ - 鈹斺攢鈹 Nivel 2: erp-core/orchestration/ 鈫 PADRE DIRECTO - 鈹斺攢鈹 Nivel 3: clinicas/orchestration/ 鈫 ESTE PROYECTO -``` - ---- - -## Directivas Heredadas de CORE (OBLIGATORIAS) - -### Ciclo de Vida -| Alias | Prop贸sito | -|-------|-----------| -| `@TAREA` | Punto de entrada para toda HU | -| `@CAPVED` | Ciclo de 6 fases | -| `@INICIALIZACION` | Bootstrap de agentes | - -### Operaciones Universales -| Alias | Prop贸sito | -|-------|-----------| -| `@CREAR` | Crear archivos nuevos | -| `@MODIFICAR` | Modificar existentes | -| `@VALIDAR` | Validar c贸digo | -| `@DOCUMENTAR` | Documentar trabajo | -| `@BUSCAR` | Buscar informaci贸n | -| `@DELEGAR` | Delegar a subagentes | - -### Principios Fundamentales -| Alias | Resumen | -|-------|---------| -| `@CAPVED` | Toda tarea pasa por 6 fases | -| `@DOC_PRIMERO` | Consultar docs/ antes de implementar | -| `@ANTI_DUP` | Verificar que no existe | -| `@VALIDACION` | Build y lint DEBEN pasar | -| `@TOKENS` | Desglosar tareas grandes | - ---- - -## Directivas por Dominio T茅cnico - -| Alias | Aplica | Notas | -|-------|--------|-------| -| `@OP_DDL` | **S脥** | Schemas de salud (HIPAA-ready) | -| `@OP_BACKEND` | **S脥** | Servicios de citas, expediente | -| `@OP_FRONTEND` | **S脥** | UI de consultorio | -| `@OP_MOBILE` | **S脥** | App de pacientes | -| `@OP_ML` | Futuro | An谩lisis de diagn贸sticos | - ---- - -## Patrones Heredados (OBLIGATORIOS) - -Todos los patrones de `core/orchestration/patrones/` aplican. - -**IMPORTANTE:** `@PATRON-SEGURIDAD` es cr铆tico para datos de salud. - ---- - -## Directivas Heredadas de ERP Core - -| Directiva | Extensi贸n Local | -|-----------|-----------------| -| `DIRECTIVA-MULTI-TENANT.md` | Por `clinica_id` | -| `DIRECTIVA-EXTENSION-VERTICALES.md` | M贸dulos de salud | - ---- - -## Variables de Contexto CCA - -```yaml -PROJECT_NAME: "clinicas" -PROJECT_LEVEL: "VERTICAL" -PROJECT_ROOT: "./" -PARENT_PROJECT: "erp-core" -SUITE_PROJECT: "erp-suite" - -DB_DDL_PATH: "database/ddl" -BACKEND_ROOT: "backend/src" -FRONTEND_ROOT: "frontend/src" - -TENANT_COLUMN: "clinica_id" -RLS_CONTEXT: "app.current_clinica_id" - -# Compliance -HIPAA_READY: true -DATA_ENCRYPTION: "AES-256" -``` - ---- - -## M贸dulos Espec铆ficos de Cl铆nicas (Por definir) - -| M贸dulo | Descripci贸n | Estado | -|--------|-------------|--------| -| CLI-CIT | Citas/agenda | Por definir | -| CLI-PAC | Pacientes | Por definir | -| CLI-EXP | Expediente cl铆nico | Por definir | -| CLI-REC | Recetas | Por definir | -| CLI-FAC | Facturaci贸n m茅dica | Por definir | -| CLI-LAB | Laboratorio | Por definir | - ---- - -## Consideraciones de Seguridad (CR脥TICAS) - -- Datos sensibles de salud requieren encriptaci贸n -- Cumplimiento con regulaciones de privacidad -- Auditor铆a detallada de accesos -- Consentimiento informado digital - ---- - -**Sistema:** SIMCO v2.2.0 + CAPVED + CCA Protocol -**Nivel:** VERTICAL (3) -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SPECS-CORE.md b/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SPECS-CORE.md deleted file mode 100644 index f3bc55d95..000000000 --- a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SPECS-CORE.md +++ /dev/null @@ -1,199 +0,0 @@ -# Herencia de SPECS del Core - Cl铆nicas - -**Fecha:** 2025-12-08 -**Versi贸n:** 1.0 -**Vertical:** Cl铆nicas (CL) -**Nivel:** 2B.2 - ---- - -## Resumen - -| M茅trica | Valor | -|---------|-------| -| SPECS Aplicables | 24/30 | -| SPECS Obligatorias | 20 | -| SPECS Opcionales | 4 | -| SPECS No Aplican | 6 | -| Estado Implementaci贸n | 0% | - ---- - -## SPECS Obligatorias (Deben Implementarse) - -### P0 - Cr铆ticas - -| SPEC | Gap Original | SP | Estado | M贸dulos Afectados | -|------|-------------|----:|--------|-------------------| -| SPEC-SISTEMA-SECUENCIAS | ir.sequence | 8 | PENDIENTE | CL-001, CL-002, CL-005 | -| SPEC-SEGURIDAD-API-KEYS-PERMISOS | API Keys + ACL | 31 | PENDIENTE | CL-001, CL-011 | -| SPEC-REPORTES-FINANCIEROS | Balance/P&L SAT | 13 | PENDIENTE | CL-008, CL-009 | -| SPEC-NOMINA-BASICA | hr_payroll | 21 | PENDIENTE | CL-001 | -| SPEC-GASTOS-EMPLEADOS | hr_expense | 13 | PENDIENTE | CL-001 | -| SPEC-SCHEDULER-REPORTES | ir.cron + mail | 8 | PENDIENTE | CL-009 | -| SPEC-INTEGRACION-CALENDAR | calendar integration | 8 | PENDIENTE | CL-003 | - -### P1 - Complementarias - -| SPEC | Gap Original | SP | Estado | M贸dulos Afectados | -|------|-------------|----:|--------|-------------------| -| SPEC-CONTABILIDAD-ANALITICA | Centros de costo | 21 | PENDIENTE | CL-008, CL-009 | -| SPEC-CONCILIACION-BANCARIA | Conciliaci贸n | 21 | PENDIENTE | CL-008 | -| SPEC-FIRMA-ELECTRONICA-NOM151 | e.firma | 13 | PENDIENTE | CL-011 | -| SPEC-TWO-FACTOR-AUTHENTICATION | 2FA | 13 | PENDIENTE | CL-001 | -| SPEC-TRAZABILIDAD-LOTES-SERIES | Lotes/Series | 13 | PENDIENTE | CL-007 | -| SPEC-OAUTH2-SOCIAL-LOGIN | OAuth2 | 8 | PENDIENTE | CL-002, CL-010 | -| SPEC-IMPUESTOS-AVANZADOS | IVA, ISR | 8 | PENDIENTE | CL-008 | -| SPEC-PLANTILLAS-CUENTAS | Plan contable | 8 | PENDIENTE | CL-008 | -| SPEC-TASAS-CAMBIO-AUTOMATICAS | Tipos cambio | 5 | PENDIENTE | CL-008 | -| SPEC-ALERTAS-PRESUPUESTO | Alertas | 8 | PENDIENTE | CL-008 | -| SPEC-RRHH-EVALUACIONES-SKILLS | Evaluaciones | 26 | PENDIENTE | CL-001 | -| SPEC-LOCALIZACION-PAISES | Localizaci贸n | 13 | PENDIENTE | CL-001, CL-008 | - -### Patrones T茅cnicos - -| SPEC | Patr贸n | SP | Estado | Aplicaci贸n | -|------|--------|----:|--------|------------| -| SPEC-MAIL-THREAD-TRACKING | mail.thread | 13 | PENDIENTE | Expedientes, Citas, Comunicaci贸n | -| SPEC-WIZARD-TRANSIENT-MODEL | TransientModel | 8 | PENDIENTE | Wizards de receta, referencia | - ---- - -## SPECS Opcionales - -| SPEC | Descripci贸n | SP | Decisi贸n | Raz贸n | -|------|-------------|----:|----------|-------| -| SPEC-VALORACION-INVENTARIO | FIFO/AVCO | 21 | EVALUAR | Solo para farmacia interna | -| SPEC-PRICING-RULES | Reglas precio | 8 | EVALUAR | Para paquetes de servicios | -| SPEC-TAREAS-RECURRENTES | Recurrencia | 13 | EVALUAR | Para citas peri贸dicas | -| SPEC-PRESUPUESTOS-REVISIONES | Aprobaci贸n | 8 | EVALUAR | Para tratamientos largos | - ---- - -## SPECS No Aplicables - -| SPEC | Raz贸n | -|------|-------| -| SPEC-PORTAL-PROVEEDORES | No hay compras complejas | -| SPEC-BLANKET-ORDERS | No aplica en servicios m茅dicos | -| SPEC-INVENTARIOS-CICLICOS | Solo si hay farmacia grande | -| SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN | No hay proyectos de este tipo | -| SPEC-CONSOLIDACION-FINANCIERA | Generalmente una cl铆nica | - ---- - -## Adaptaciones Requeridas - -### Mapeo de Conceptos Core 鈫 Cl铆nicas - -| Concepto Core | Concepto Cl铆nicas | -|---------------|-------------------| -| `core.partners` | Pacientes | -| `sales.sale_orders` | Consultas/Servicios | -| `inventory.products` | Medicamentos, servicios m茅dicos | -| `hr.employees` | Personal m茅dico | -| `calendar.events` | Citas m茅dicas | -| `financial.invoices` | Facturas de consulta | - -### Extensiones de Entidad - -```sql --- Pacientes (extiende partners) -patients.patients ( - partner_id 鈫 core.partners, - numero_expediente VARCHAR UNIQUE, - fecha_nacimiento DATE, - sexo ENUM('M', 'F'), - tipo_sangre VARCHAR(5), - alergias TEXT[], - antecedentes JSONB, - seguro_medico_id 鈫 insurance_policies -) - --- Expediente cl铆nico -medical.clinical_records ( - id UUID, - patient_id 鈫 patients, - fecha TIMESTAMPTZ, - tipo ENUM('consulta', 'urgencia', 'hospitalizacion'), - motivo_consulta TEXT, - diagnostico TEXT, - tratamiento TEXT, - medico_id 鈫 hr.employees, - signos_vitales JSONB -) - --- Citas m茅dicas -appointments.appointments ( - id UUID, - patient_id 鈫 patients, - doctor_id 鈫 hr.employees, - specialty_id 鈫 specialties, - fecha_hora TIMESTAMPTZ, - duracion_minutos INTEGER, - estado ENUM('programada', 'confirmada', 'en_progreso', 'completada', 'cancelada'), - notas TEXT -) - --- Recetas m茅dicas -medical.prescriptions ( - id UUID, - clinical_record_id 鈫 clinical_records, - fecha TIMESTAMPTZ, - vigencia_dias INTEGER, - firma_electronica BYTEA, - productos JSONB -) -``` - ---- - -## Cumplimiento Normativo - -Esta vertical debe cumplir con normas espec铆ficas: - -| Norma | Descripci贸n | SPECS Relacionadas | -|-------|-------------|-------------------| -| NOM-024-SSA3-2012 | Expediente cl铆nico electr贸nico | SPEC-SEGURIDAD, SPEC-MAIL-THREAD | -| LFPDPPP | Protecci贸n de datos personales | SPEC-SEGURIDAD, SPEC-2FA | -| NOM-004-SSA3-2012 | Expediente cl铆nico | SPEC-FIRMA-ELECTRONICA | - ---- - -## Plan de Implementaci贸n - -### Fase 1: Fundamentos (SP: 60) -1. SPEC-SISTEMA-SECUENCIAS -2. SPEC-SEGURIDAD-API-KEYS-PERMISOS -3. SPEC-TWO-FACTOR-AUTHENTICATION -4. SPEC-OAUTH2-SOCIAL-LOGIN - -### Fase 2: Agenda y Comunicaci贸n (SP: 34) -5. SPEC-INTEGRACION-CALENDAR -6. SPEC-MAIL-THREAD-TRACKING -7. SPEC-WIZARD-TRANSIENT-MODEL - -### Fase 3: Expediente y Cumplimiento (SP: 39) -8. SPEC-FIRMA-ELECTRONICA-NOM151 -9. SPEC-RRHH-EVALUACIONES-SKILLS - -### Fase 4: Financiero (SP: 65) -10. SPEC-REPORTES-FINANCIEROS -11. SPEC-CONTABILIDAD-ANALITICA -12. SPEC-CONCILIACION-BANCARIA -13. SPEC-IMPUESTOS-AVANZADOS - ---- - -## Referencias - -- Documento Core: `erp-core/docs/04-modelado/MAPEO-SPECS-VERTICALES.md` -- SPECS del Core: `erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/` -- Herencia DB: `database/HERENCIA-ERP-CORE.md` -- Directivas: `orchestration/directivas/` -- Normatividad: NOM-024-SSA3-2012, LFPDPPP, NOM-004-SSA3-2012 - ---- - -**Documento de herencia de SPECS oficial** -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SPECS-ERP-CORE.md b/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SPECS-ERP-CORE.md deleted file mode 100644 index ddcd1eff2..000000000 --- a/projects/erp-clinicas/orchestration/00-guidelines/HERENCIA-SPECS-ERP-CORE.md +++ /dev/null @@ -1,162 +0,0 @@ -# Herencia de Especificaciones - ERP Core -> Clinicas - -**Fecha:** 2025-12-08 -**Version:** 1.0 -**Vertical:** Clinicas -**Nivel:** 2B.2 - ---- - -## RESUMEN - -Este documento define las especificaciones transversales del ERP Core que la vertical de Clinicas debe heredar e implementar. - -**Ubicacion specs core:** `apps/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/` - ---- - -## ESPECIFICACIONES A HEREDAR - -### 1. SPEC-RRHH-EVALUACIONES-SKILLS.md - -**Prioridad:** ALTA -**Relevancia:** Gestion de personal medico y administrativo - -**Funcionalidades heredadas:** -- Evaluaciones de desempeno -- Gestion de competencias (skills) -- Pipeline de reclutamiento -- Certificaciones y capacitaciones - -**Adaptacion para clinicas:** -- Certificaciones medicas (cedulas profesionales) -- Especialidades y subespecialidades -- Evaluaciones de competencias clinicas -- Capacitaciones obligatorias (NOM-035, etc.) -- Vigencia de certificaciones - -**Modulos afectados:** -- Recursos Humanos -- Directorio Medico -- Credencializacion -- Capacitacion - ---- - -### 2. SPEC-INTEGRACION-CALENDAR.md - -**Prioridad:** ALTA -**Relevancia:** Agenda de citas medicas - -**Funcionalidades heredadas:** -- Eventos de calendario -- Sincronizacion con Google/Outlook -- Sistema de citas online (appointments) -- Recordatorios y alarmas -- Slots de disponibilidad - -**Adaptacion para clinicas:** -- Agenda de consultorios -- Citas por especialidad/medico -- Duracion variable por tipo de consulta -- Recordatorios a pacientes (SMS, WhatsApp) -- Manejo de lista de espera -- Reagendamiento automatico - -**Modulos afectados:** -- Agenda Medica -- Citas -- Portal de Pacientes -- Consultorios - ---- - -### 3. SPEC-MAIL-THREAD-TRACKING.md - -**Prioridad:** MEDIA -**Relevancia:** Tracking de expedientes clinicos - -**Funcionalidades heredadas:** -- Decorador `@Tracked` para seguimiento de cambios -- Sistema de mensajes -- Followers y notificaciones -- Actividades y recordatorios - -**Adaptacion para clinicas:** -- Historial de cambios en expediente clinico -- Notificaciones de resultados de laboratorio -- Seguimiento de tratamientos -- Comunicacion entre areas medicas -- Audit trail para regulaciones (NOM-024) - -**Modulos afectados:** -- Expediente Clinico -- Laboratorio -- Imagenologia -- Enfermeria - ---- - -## ESPECIFICACIONES ADICIONALES RECOMENDADAS - -| Especificacion | Relevancia | Prioridad | -|----------------|------------|-----------| -| SPEC-WIZARD-TRANSIENT-MODEL.md | Asistentes de admision/alta | Media | -| SPEC-FIRMA-ELECTRONICA-NOM151.md | Firma de consentimientos | Media | -| SPEC-TRAZABILIDAD-LOTES-SERIES.md | Medicamentos y materiales | Media | -| SPEC-INVENTARIOS-CICLICOS.md | Farmacia interna | Baja | - ---- - -## MATRIZ DE HERENCIA - -| Spec Core | Modulos Clinicas | Prioridad | Estado | -|-----------|------------------|-----------|--------| -| SPEC-RRHH-EVALUACIONES-SKILLS | RRHH, Directorio Medico | ALTA | Pendiente | -| SPEC-INTEGRACION-CALENDAR | Agenda, Citas | ALTA | Pendiente | -| SPEC-MAIL-THREAD-TRACKING | Expediente, Laboratorio | MEDIA | Pendiente | - ---- - -## IMPLEMENTACION - -### Orden Sugerido - -1. **Fase 1 - Agenda** - - SPEC-INTEGRACION-CALENDAR (critico para operacion) - -2. **Fase 2 - Personal** - - SPEC-RRHH-EVALUACIONES-SKILLS (certificaciones medicas) - -3. **Fase 3 - Tracking** - - SPEC-MAIL-THREAD-TRACKING (expediente clinico) - -### Consideraciones Especificas - -- Las clinicas tienen regulaciones estrictas (NOM-024, NOM-035) -- La gestion de citas es critica para la operacion -- El personal medico requiere certificaciones vigentes -- El expediente clinico requiere trazabilidad completa - ---- - -## CONSIDERACIONES REGULATORIAS - -| Regulacion | Specs Relacionadas | Impacto | -|------------|-------------------|---------| -| NOM-024-SSA3 (Expediente Clinico Electronico) | MAIL-THREAD-TRACKING | Audit trail obligatorio | -| NOM-035-STPS (Riesgos Psicosociales) | RRHH-EVALUACIONES-SKILLS | Capacitaciones | -| LFPDPPP (Proteccion de Datos) | Todas | Consentimiento y seguridad | - ---- - -## REFERENCIAS - -- Specs Core: `apps/erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/` -- Gap Analysis: `apps/erp-core/orchestration/01-analisis/ANALISIS-GAPS-CONSOLIDADO.md` - ---- - -**Documento generado por:** Requirements-Analyst -**Fecha:** 2025-12-08 -**Version:** 1.0 diff --git a/projects/erp-clinicas/orchestration/00-guidelines/PROJECT-STATUS.md b/projects/erp-clinicas/orchestration/00-guidelines/PROJECT-STATUS.md deleted file mode 100644 index b93bc15e2..000000000 --- a/projects/erp-clinicas/orchestration/00-guidelines/PROJECT-STATUS.md +++ /dev/null @@ -1,33 +0,0 @@ -# PROJECT STATUS: erp-clinicas - -**Ultima actualizacion:** 2026-01-04 -**Estado general:** Activo - ---- - -## Metricas Rapidas - -| Metrica | Valor | -|---------|-------| -| Archivos docs/ | 27 | -| Archivos orchestration/ | 23 | -| Estado SIMCO | Adaptado | - -## Migracion EPIC-008 - -- [x] Migracion desde workspace-v1-bckp (EPIC-004/005) -- [x] Adaptacion SIMCO (EPIC-008) -- [x] docs/_MAP.md creado -- [x] PROJECT-STATUS.md creado -- [x] HERENCIA-SIMCO.md verificado -- [x] CONTEXTO-PROYECTO.md verificado - -## Historial de Cambios - -| Fecha | Cambio | EPIC | -|-------|--------|------| -| 2026-01-04 | Adaptacion SIMCO completada | EPIC-008 | - ---- - -**Generado por:** EPIC-008 adapt-simco.sh diff --git a/projects/erp-clinicas/orchestration/PROXIMA-ACCION.md b/projects/erp-clinicas/orchestration/PROXIMA-ACCION.md deleted file mode 100644 index 923102185..000000000 --- a/projects/erp-clinicas/orchestration/PROXIMA-ACCION.md +++ /dev/null @@ -1,122 +0,0 @@ -# Pr贸xima Acci贸n - ERP Cl铆nicas - -## Estado Actual -**Fecha:** Diciembre 2025 -**Progreso:** 20% (Planificaci贸n completa) - ---- - -## Documentaci贸n Disponible - -### M贸dulos Definidos (12 m贸dulos - 395 SP) - -| M贸dulo | Nombre | SP | Estado | -|--------|--------|---:|--------| -| CL-001 | Fundamentos | 0 | PLANIFICADO | -| CL-002 | Pacientes | 34 | PLANIFICADO | -| CL-003 | Citas | 42 | PLANIFICADO | -| CL-004 | Consultas | 47 | PLANIFICADO | -| CL-005 | Recetas | 34 | PLANIFICADO | -| CL-006 | Laboratorio | 42 | PLANIFICADO | -| CL-007 | Farmacia | 34 | PLANIFICADO | -| CL-008 | Facturaci贸n | 21 | PLANIFICADO | -| CL-009 | Reportes | 34 | PLANIFICADO | -| CL-010 | Telemedicina | 47 | PLANIFICADO | -| CL-011 | Expediente | 39 | PLANIFICADO | -| CL-012 | Imagenolog铆a | 21 | PLANIFICADO | - -### Documentos de Referencia -- Visi贸n: `docs/00-vision-general/VISION-CLINICAS.md` -- M贸dulos: `docs/02-definicion-modulos/INDICE-MODULOS.md` -- Herencia SPECS: `orchestration/00-guidelines/HERENCIA-SPECS-CORE.md` -- Inventario: `orchestration/inventarios/MASTER_INVENTORY.yml` - ---- - -## Prerrequisitos - -Este proyecto requiere que **erp-core** est茅 completado primero: -- [ ] M贸dulo auth de erp-core (para 2FA) -- [ ] M贸dulo users de erp-core -- [ ] M贸dulo tenants de erp-core -- [ ] M贸dulo inventory base de erp-core (farmacia) -- [ ] CFDI de erp-core - ---- - -## Cumplimiento Normativo - -### NOM-024-SSA3-2012 (Expediente Cl铆nico) -- Estructura SOAP para notas cl铆nicas -- Campos obligatorios de historia cl铆nica -- Consentimiento informado -- Firma electr贸nica de recetas - -### LFPDPPP (Protecci贸n de Datos) -- Encriptaci贸n de datos sensibles -- Consentimiento de tratamiento -- Auditor铆a de accesos -- Derecho de acceso del paciente - ---- - -## Tarea Prioritaria (Cuando est茅 listo) - -### 1. Crear DDL del Schema Clinical - -**Objetivo:** Definir estructura de base de datos para expediente cl铆nico. - -**Tablas a crear:** -- `clinical.patients` -- `clinical.patient_contacts` -- `clinical.patient_insurance` -- `clinical.appointments` -- `clinical.consultations` -- `clinical.diagnoses` - -**Archivo destino:** `database/ddl/01-clinical-schema.sql` - -### 2. Implementar Extensi贸n 2FA - -**Objetivo:** 2FA obligatorio para personal m茅dico. - -**Referencia:** Ver `erp-core/docs/04-modelado/especificaciones-tecnicas/transversal/SPEC-TWO-FACTOR-AUTHENTICATION.md` - ---- - -## Consideraciones Especiales - -1. **Seguridad:** 2FA obligatorio para personal m茅dico -2. **Encriptaci贸n:** Datos sensibles (antecedentes, alergias) -3. **Auditor铆a:** Log de accesos a expedientes -4. **Integraci贸n:** HL7/FHIR para interoperabilidad - ---- - -## Ambiente de Desarrollo - -Seg煤n `DEVENV-PORTS.md`: - -```yaml -proyecto: clinicas -rango_base: 3500 -puertos: - backend: 3500 - frontend: 5178 - database: 5437 - redis: 6384 -``` - ---- - -## Pr贸ximos Pasos - -1. [ ] Esperar completitud de erp-core (auth con 2FA, users, tenants) -2. [ ] Validar cumplimiento NOM-024-SSA3-2012 -3. [ ] Dise帽ar arquitectura de encriptaci贸n -4. [ ] Crear DDL schema clinical -5. [ ] Iniciar backend CL-001 (heredar de core + 2FA) - ---- - -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/orchestration/directivas/DIRECTIVA-EXPEDIENTE-CLINICO.md b/projects/erp-clinicas/orchestration/directivas/DIRECTIVA-EXPEDIENTE-CLINICO.md deleted file mode 100644 index 32badd1cd..000000000 --- a/projects/erp-clinicas/orchestration/directivas/DIRECTIVA-EXPEDIENTE-CLINICO.md +++ /dev/null @@ -1,242 +0,0 @@ -# DIRECTIVA-EXPEDIENTE-CLINICO - -**Version:** 1.0 -**Fecha:** 2025-12-08 -**Vertical:** Clinicas -**Nivel:** 2B.2 - ---- - -## PROPOSITO - -Define las directrices para la implementacion del expediente clinico electronico. - ---- - -## ALCANCE - -- Historial medico del paciente -- Consultas y notas medicas -- Recetas y prescripciones -- Estudios y resultados -- Cumplimiento normativo - ---- - -## NORMATIVA APLICABLE - -### NOM-024-SSA3-2012 - -**Intercambio de informacion en salud** - -Requerimientos: -- Estructura estandarizada de datos -- Interoperabilidad con otros sistemas -- Identificacion unica del paciente - -### NOM-004-SSA3-2012 - -**Del expediente clinico** - -Requerimientos: -- Consentimiento informado -- Nota de ingreso -- Notas de evolucion -- Ordenes medicas -- Resultados de estudios - -### Ley Federal de Proteccion de Datos Personales - -- Datos de salud = datos sensibles -- Consentimiento expreso requerido -- Derecho de acceso, rectificacion, cancelacion, oposicion (ARCO) - ---- - -## PRINCIPIOS - -### 1. Integridad de Datos - -- Registros inmutables (no se borran, se anulan) -- Firma electronica del medico -- Auditoria completa de accesos - -### 2. Confidencialidad - -- Acceso basado en roles -- Encriptacion en reposo y transito -- Logs de acceso obligatorios - -### 3. Disponibilidad - -- Acceso 24/7 para emergencias -- Respaldos automaticos -- Plan de recuperacion - ---- - -## MODELO DE DATOS - -### medical_records (expediente) -```yaml -campos: - - id: uuid - - patient_id: FK -> clinica.patients - - record_number: string (unico por clinica) - - created_at: timestamp - - allergies_reviewed: boolean - - blood_type: enum(A+, A-, B+, B-, AB+, AB-, O+, O-) -``` - -### consultations (consultas) -```yaml -campos: - - id: uuid - - medical_record_id: FK -> medical_records - - appointment_id: FK -> appointments - - doctor_id: FK -> clinica.doctors - - consultation_type: enum(first, followup, emergency) - - chief_complaint: text # motivo de consulta - - present_illness: text # padecimiento actual - - physical_exam: json # exploracion fisica - - assessment: text # valoracion - - plan: text # plan de tratamiento - - signed_at: timestamp - - signature_hash: string # firma electronica -``` - -### vital_signs (signos vitales) -```yaml -campos: - - id: uuid - - consultation_id: FK -> consultations - - blood_pressure_systolic: integer - - blood_pressure_diastolic: integer - - heart_rate: integer - - respiratory_rate: integer - - temperature: decimal - - weight: decimal - - height: decimal - - oxygen_saturation: integer - - recorded_by: FK -> auth.users - - recorded_at: timestamp -``` - -### diagnoses (diagnosticos) -```yaml -campos: - - id: uuid - - consultation_id: FK -> consultations - - cie10_code: string # codigo CIE-10 - - description: text - - diagnosis_type: enum(principal, secondary, presumptive, definitive) - - notes: text -``` - -### prescriptions (recetas) -```yaml -campos: - - id: uuid - - consultation_id: FK -> consultations - - patient_id: FK -> patients - - doctor_id: FK -> doctors - - prescription_number: string - - medications: json # array de medicamentos - - instructions: text - - valid_until: date - - signed_at: timestamp - - signature_hash: string -``` - ---- - -## FLUJO DE CONSULTA - -``` -1. Paciente llega a cita - | -2. Enfermera registra signos vitales - | -3. Medico accede al expediente - | -4. Revisa historial y alergias - | -5. Realiza consulta - | -6. Documenta en sistema - |-- Motivo de consulta - |-- Exploracion fisica - |-- Diagnostico (CIE-10) - |-- Plan de tratamiento - | -7. Genera receta (si aplica) - | -8. Firma electronica - | -9. Cierra consulta -``` - ---- - -## SEGURIDAD Y ACCESOS - -### Roles y Permisos - -| Rol | Permisos | -|-----|----------| -| Medico | CRUD consultas propias, lectura historial | -| Enfermera | Signos vitales, lectura basica | -| Recepcion | Datos demograficos, citas | -| Admin | Configuracion, reportes | - -### Auditoria Obligatoria - -Cada acceso al expediente registra: -- Usuario -- Fecha/hora -- Accion realizada -- IP de origen -- Motivo de acceso - -### Encriptacion - -``` -Datos en reposo: - - AES-256 para campos sensibles - - Llaves rotadas cada 90 dias - -Datos en transito: - - TLS 1.3 - - Certificados validos -``` - ---- - -## INTEGRACION CON CORE - -### Herencia de Specs - -| Spec Core | Aplicacion | -|-----------|------------| -| SPEC-MAIL-THREAD-TRACKING | Historial de cambios | -| SPEC-INTEGRACION-CALENDAR | Agenda de citas | -| SPEC-RRHH-EVALUACIONES-SKILLS | Especialidades medicas | - -### APIs a Extender - -- `PartnerService` -> `PatientService` -- `EmployeeService` -> `DoctorService` -- Sistema de tracking -> Historial expediente - ---- - -## REFERENCIAS - -- NOM-024-SSA3-2012 -- NOM-004-SSA3-2012 -- Ley Federal de Proteccion de Datos Personales -- HERENCIA-SPECS-ERP-CORE.md - ---- - -**Documento de directiva oficial** diff --git a/projects/erp-clinicas/orchestration/directivas/DIRECTIVA-GESTION-CITAS.md b/projects/erp-clinicas/orchestration/directivas/DIRECTIVA-GESTION-CITAS.md deleted file mode 100644 index 437b41bfb..000000000 --- a/projects/erp-clinicas/orchestration/directivas/DIRECTIVA-GESTION-CITAS.md +++ /dev/null @@ -1,242 +0,0 @@ -# DIRECTIVA-GESTION-CITAS - -**Version:** 1.0 -**Fecha:** 2025-12-08 -**Vertical:** Clinicas -**Nivel:** 2B.2 - ---- - -## PROPOSITO - -Define las directrices para el sistema de gestion de citas medicas. - ---- - -## ALCANCE - -- Agenda de medicos -- Programacion de citas -- Confirmaciones y recordatorios -- Gestion de consultorios - ---- - -## PRINCIPIOS - -### 1. Disponibilidad Visible - -- Horarios de medicos siempre actualizados -- Slots disponibles en tiempo real -- Sin overbooking - -### 2. Comunicacion Proactiva - -- Confirmacion inmediata de cita -- Recordatorios automaticos (24h, 2h antes) -- Notificacion de cambios - -### 3. Optimizacion de Recursos - -- Maximizar uso de consultorios -- Minimizar tiempos muertos -- Balance de carga entre medicos - ---- - -## MODELO DE DATOS - -### doctors (medicos) -```yaml -campos: - - id: uuid - - employee_id: FK -> hr.employees - - license_number: string # cedula profesional - - specialty_id: FK -> specialties - - consultation_duration: integer # minutos default - - max_daily_appointments: integer - - status: enum(active, inactive, vacation) -``` - -### doctor_schedules (horarios) -```yaml -campos: - - id: uuid - - doctor_id: FK -> doctors - - day_of_week: enum(mon, tue, wed, thu, fri, sat, sun) - - start_time: time - - end_time: time - - consulting_room_id: FK -> consulting_rooms - - is_active: boolean -``` - -### appointments (citas) -```yaml -campos: - - id: uuid - - patient_id: FK -> patients - - doctor_id: FK -> doctors - - appointment_type_id: FK -> appointment_types - - consulting_room_id: FK -> consulting_rooms - - scheduled_start: timestamp - - scheduled_end: timestamp - - actual_start: timestamp - - actual_end: timestamp - - status: enum(scheduled, confirmed, in_progress, completed, cancelled, no_show) - - cancellation_reason: text - - notes: text -``` - -### consulting_rooms (consultorios) -```yaml -campos: - - id: uuid - - room_number: string - - floor: string - - equipment: json # equipo disponible - - specialty_id: FK -> specialties (si es especializado) - - status: enum(available, occupied, maintenance) -``` - ---- - -## FLUJO DE CITA - -### Programacion - -``` -1. Paciente solicita cita - | -2. Selecciona especialidad/medico - | -3. Sistema muestra disponibilidad - | -4. Paciente selecciona horario - | -5. Sistema valida: - |-- Disponibilidad del medico - |-- Disponibilidad del consultorio - |-- No conflictos del paciente - | -6. Cita creada (status: scheduled) - | -7. Notificacion al paciente -``` - -### Confirmacion - -``` -24 horas antes: - Sistema envia recordatorio - Paciente puede: - - Confirmar - - Reprogramar - - Cancelar - -2 horas antes: - Sistema envia recordatorio final -``` - -### Dia de la Cita - -``` -1. Paciente llega - | -2. Recepcion marca llegada - | -3. Cuando medico esta listo: - |-- Cita pasa a "in_progress" - |-- Registra hora real de inicio - | -4. Al terminar: - |-- Cita pasa a "completed" - |-- Registra hora real de fin -``` - ---- - -## ESTADOS DE CITA - -``` -scheduled --> confirmed --> in_progress --> completed - | | | - v v v - cancelled no_show cancelled -``` - -### Reglas de Transicion - -| De | A | Condicion | -|----|---|-----------| -| scheduled | confirmed | Paciente confirma | -| scheduled | cancelled | Antes de 24h | -| confirmed | in_progress | Paciente presente | -| confirmed | no_show | No se presento | -| in_progress | completed | Consulta terminada | - ---- - -## NOTIFICACIONES - -### Canales - -| Canal | Uso | -|-------|-----| -| Email | Confirmacion, recordatorios | -| SMS | Recordatorio 2h antes | -| WhatsApp | Opcional, si configurado | -| Push | Si tiene app instalada | - -### Templates - -``` -Confirmacion: - "Su cita con Dr. {doctor} ha sido confirmada para el {fecha} a las {hora}." - -Recordatorio 24h: - "Le recordamos su cita manana {fecha} a las {hora} con Dr. {doctor}. - Por favor confirme respondiendo SI o cancele respondiendo NO." - -Recordatorio 2h: - "Su cita es en 2 horas. Direccion: {direccion}. Consultorio: {consultorio}." -``` - ---- - -## INTEGRACION CON CORE - -### Herencia de Specs - -| Spec Core | Aplicacion | -|-----------|------------| -| SPEC-INTEGRACION-CALENDAR | Base del calendario | -| SPEC-TAREAS-RECURRENTES | Citas recurrentes | -| SPEC-MAIL-THREAD-TRACKING | Historial de comunicacion | - -### APIs a Extender - -- Calendar del core para la agenda -- Notification service para recordatorios - ---- - -## METRICAS - -| Metrica | Objetivo | Alerta | -|---------|----------|--------| -| Ocupacion de agenda | > 80% | < 60% | -| No-shows | < 5% | > 10% | -| Tiempo de espera | < 15 min | > 30 min | -| Confirmaciones | > 90% | < 80% | - ---- - -## REFERENCIAS - -- SPEC-INTEGRACION-CALENDAR.md (core) -- DIRECTIVA-EXPEDIENTE-CLINICO.md -- HERENCIA-SPECS-ERP-CORE.md - ---- - -**Documento de directiva oficial** diff --git a/projects/erp-clinicas/orchestration/environment/PROJECT-ENV-CONFIG.yml b/projects/erp-clinicas/orchestration/environment/PROJECT-ENV-CONFIG.yml deleted file mode 100644 index 2aabb9c2b..000000000 --- a/projects/erp-clinicas/orchestration/environment/PROJECT-ENV-CONFIG.yml +++ /dev/null @@ -1,262 +0,0 @@ -# ============================================================================= -# PROJECT-ENV-CONFIG.yml - ERP CLINICAS -# ============================================================================= -# Vertical de ERP-Suite especializada en Cl铆nicas y Consultorios M茅dicos -# Actualizado: 2025-12-08 -# Referencia: ~/workspace/core/devtools/environment/DEVENV-PORTS.md -# ============================================================================= - -project: - name: "ERP-CLINICAS" - code: "CL" - description: "Sistema para Cl铆nicas y Consultorios con Cumplimiento NOM-024" - type: "vertical" - level: "2B.2" - status: "planning" - parent: "erp-suite" - - paths: - root: "/home/isem/workspace/projects/erp-suite/apps/verticales/clinicas" - backend: "backend/" - frontend: "frontend/" - database: "database/" - docs: "docs/" - orchestration: "orchestration/" - -# ============================================================================= -# PUERTOS (Seg煤n DEVENV-PORTS.md) -# ============================================================================= -ports: - backend: 3500 - frontend: 5178 - database: 5437 - redis: 6384 - -# ============================================================================= -# BASE DE DATOS -# ============================================================================= -database: - type: "postgresql" - host: "localhost" - port: 5437 - name: "clinicas_db" - user: "clinicas_user" - - schemas: - core_inherited: 12 # Schemas heredados de erp-core - vertical_specific: - - clinical # Pacientes, citas, consultas, expediente - - pharmacy # Stock medicamentos, dispensaciones - - laboratory # 脫rdenes lab, resultados - - imaging # Estudios DICOM, metadatos - - telemedicine # Sesiones video, grabaciones - - encryption: - enabled: true - algorithm: "AES-256" - encrypted_fields: - - antecedentes_medicos - - alergias - - diagnosticos - - notas_clinicas - - migration: - tool: "typeorm" - directory: "database/migrations/" - -# ============================================================================= -# STACK TECNOLOGICO -# ============================================================================= -stack: - runtime: "Node.js 20+" - language: "TypeScript 5.3+" - backend: - framework: "Express.js" - orm: "TypeORM 0.3.17" - encryption: "AES-256" - frontend: - framework: "React 18" - build: "Vite" - ui: "Tailwind CSS + shadcn/ui" - auth: - base: "JWT + bcryptjs" - extension: "2FA obligatorio para personal m茅dico" - -# ============================================================================= -# HERENCIA DEL CORE -# ============================================================================= -core_inheritance: - version: "0.6.0" - tables_inherited: 97 - modules_inherited: - - auth # + extensi贸n 2FA - - users - - roles - - tenants - - inventory # Para farmacia - - cfdi - - specs_applicable: 6 - specs_implemented: 0 - specs_detail: - - SPEC-INTEGRACION-CALENDAR - - SPEC-MAIL-THREAD-TRACKING - - SPEC-TRAZABILIDAD-LOTES-SERIES - - SPEC-FACTURACION-CFDI - - SPEC-TWO-FACTOR-AUTHENTICATION - - SPEC-RRHH-EVALUACIONES-SKILLS - -# ============================================================================= -# MODULOS DE LA VERTICAL -# ============================================================================= -modules: - total: 12 - story_points: 395 - list: - - code: CL-001 - name: Fundamentos - sp: 0 - priority: P0 - status: pending - compliance: LFPDPPP - - code: CL-002 - name: Pacientes - sp: 34 - priority: P0 - status: pending - compliance: LFPDPPP - - code: CL-003 - name: Citas - sp: 42 - priority: P0 - status: pending - - code: CL-004 - name: Consultas SOAP - sp: 47 - priority: P0 - status: pending - compliance: NOM-024 - - code: CL-005 - name: Recetas - sp: 34 - priority: P0 - status: pending - compliance: NOM-024 - - code: CL-006 - name: Laboratorio - sp: 42 - priority: P1 - status: pending - - code: CL-007 - name: Farmacia - sp: 34 - priority: P1 - status: pending - - code: CL-008 - name: Facturaci贸n CFDI - sp: 21 - priority: P0 - status: pending - - code: CL-009 - name: Reportes - sp: 34 - priority: P1 - status: pending - - code: CL-010 - name: Telemedicina - sp: 47 - priority: P2 - status: pending - - code: CL-011 - name: Expediente NOM-024 - sp: 39 - priority: P0 - status: pending - compliance: NOM-024 - - code: CL-012 - name: Imagenolog铆a DICOM - sp: 21 - priority: P2 - status: pending - -# ============================================================================= -# CUMPLIMIENTO NORMATIVO -# ============================================================================= -compliance: - nom_024_ssa3_2012: - name: "Expediente Cl铆nico Electr贸nico" - requirements: - - estructura_soap: "Subjetivo, Objetivo, An谩lisis, Plan" - - campos_obligatorios: - - identificacion_paciente - - fecha_consulta - - motivo_consulta - - exploracion_fisica - - diagnostico_cie10 - - plan_tratamiento - - firma_electronica: "Requerida en recetas" - - consentimiento_informado: "Documentado" - - lfpdppp: - name: "Ley Federal de Protecci贸n de Datos Personales" - requirements: - - encriptacion: "AES-256 para datos sensibles" - - auditoria: "Log de accesos a expedientes" - - consentimiento: "Tratamiento de datos" - - derecho_acceso: "Portal de paciente" - -# ============================================================================= -# SEGURIDAD ESPECIAL -# ============================================================================= -security: - two_factor_auth: - required_for: "medical_staff" - methods: ["TOTP", "SMS"] - - data_encryption: - algorithm: "AES-256" - key_rotation: "quarterly" - - audit_logging: - enabled: true - events: - - medical_record_access - - prescription_created - - patient_data_modified - - consent_updated - retention: "10 years" # Requerimiento NOM-024 - -# ============================================================================= -# ARCHIVOS DE ENTORNO -# ============================================================================= -env_files: - template: "orchestration/environment/.env.example" - backend: "backend/.env" - frontend: "frontend/.env" - -env_variables: - required: - - NODE_ENV - - PORT - - DATABASE_URL - - JWT_SECRET - - REDIS_URL - - ENCRYPTION_KEY # Para AES-256 - - TWILIO_SID # Para 2FA SMS - - TWILIO_AUTH_TOKEN - optional: - - LOG_LEVEL - - CORS_ORIGIN - - HL7_ENDPOINT # Interoperabilidad - -# ============================================================================= -# NOTAS -# ============================================================================= -notes: | - - Vertical especializada en sector salud - - CRITICO: Cumplimiento NOM-024-SSA3-2012 (expediente cl铆nico) - - CRITICO: Cumplimiento LFPDPPP (protecci贸n de datos) - - 2FA OBLIGATORIO para personal m茅dico - - Encriptaci贸n AES-256 para datos sensibles - - Auditor铆a de accesos con retenci贸n 10 a帽os - - Puertos asignados seg煤n DEVENV-PORTS.md (rango 3500) diff --git a/projects/erp-clinicas/orchestration/inventarios/BACKEND_INVENTORY.yml b/projects/erp-clinicas/orchestration/inventarios/BACKEND_INVENTORY.yml deleted file mode 100644 index 10c563d49..000000000 --- a/projects/erp-clinicas/orchestration/inventarios/BACKEND_INVENTORY.yml +++ /dev/null @@ -1,80 +0,0 @@ -# BACKEND INVENTORY - ERP Cl铆nicas (Vertical) -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 - -proyecto: - nombre: ERP Clinicas - codigo: clinicas - nivel: 2B.2 (Vertical) - estado: Planificacion - -herencia_core: - backend: erp-core - servicios_heredados: 40+ - referencia: "apps/erp-core/backend/" - -servicios_planificados: - pacientes: - - nombre: PatientService - modulo: CL-001 - endpoints: - - POST /api/v1/clinica/patients - - GET /api/v1/clinica/patients - - GET /api/v1/clinica/patients/:id - - GET /api/v1/clinica/patients/:id/history - - citas: - - nombre: AppointmentService - modulo: CL-002 - endpoints: - - POST /api/v1/clinica/appointments - - GET /api/v1/clinica/appointments - - GET /api/v1/clinica/appointments/available-slots - - PATCH /api/v1/clinica/appointments/:id/cancel - - PATCH /api/v1/clinica/appointments/:id/confirm - - - nombre: DoctorService - modulo: CL-002 - endpoints: - - GET /api/v1/clinica/doctors - - GET /api/v1/clinica/doctors/:id/schedule - - expediente_clinico: - - nombre: MedicalRecordService - modulo: CL-003 - seguridad: ENCRIPTADO - endpoints: - - GET /api/v1/clinica/patients/:id/medical-record - - POST /api/v1/clinica/consultations - - POST /api/v1/clinica/prescriptions - - servicios: - - nombre: MedicalServiceCatalogService - modulo: CL-004 - endpoints: - - GET /api/v1/clinica/services - - GET /api/v1/clinica/services/:id/price - - facturacion: - - nombre: MedicalInvoiceService - modulo: CL-005 - endpoints: - - POST /api/v1/clinica/invoices - - POST /api/v1/clinica/invoices/:id/stamp-cfdi - - reportes: - - nombre: MedicalReportService - modulo: CL-006 - endpoints: - - GET /api/v1/clinica/reports/appointments - - GET /api/v1/clinica/reports/revenue - -resumen: - servicios_heredados: 40+ - servicios_planificados: 7 - endpoints_planificados: 18 - estado_general: PLANIFICACION - ultima_actualizacion: 2025-12-08 - -referencias: - herencia_core: "../00-guidelines/HERENCIA-ERP-CORE.md" diff --git a/projects/erp-clinicas/orchestration/inventarios/DATABASE_INVENTORY.yml b/projects/erp-clinicas/orchestration/inventarios/DATABASE_INVENTORY.yml deleted file mode 100644 index a8b44dfbc..000000000 --- a/projects/erp-clinicas/orchestration/inventarios/DATABASE_INVENTORY.yml +++ /dev/null @@ -1,158 +0,0 @@ -# DATABASE INVENTORY - ERP Cl铆nicas (Vertical) -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 - -proyecto: - nombre: ERP Clinicas - codigo: clinicas - nivel: 2B.2 (Vertical) - estado: Planificacion - -herencia_core: - base_de_datos: erp-core - version_core: "1.2.0" - tablas_heredadas: 144 # Actualizado 2025-12-09 seg煤n conteo real DDL - schemas_heredados: - - nombre: auth - tablas: 26 # Autenticaci贸n, MFA, OAuth, API Keys - - nombre: core - tablas: 12 # Partners (pacientes), cat谩logos, UoM - - nombre: financial - tablas: 15 # Contabilidad, facturas, pagos - - nombre: inventory - tablas: 20 # Medicamentos, insumos m茅dicos - - nombre: purchase - tablas: 8 # Compras de insumos - - nombre: sales - tablas: 10 # Servicios m茅dicos, facturaci贸n - - nombre: projects - tablas: 10 # Tratamientos (como proyectos) - - nombre: analytics - tablas: 7 # Centros de costo por consultorio - - nombre: system - tablas: 13 # Mensajes, notificaciones, logs - - nombre: billing - tablas: 11 # SaaS (opcional) - - nombre: crm - tablas: 6 # Pacientes potenciales (opcional) - - nombre: hr - tablas: 6 # Personal m茅dico, contratos - referencia_ddl: "apps/erp-core/database/ddl/" - documento_herencia: "../database/HERENCIA-ERP-CORE.md" - variable_rls: "app.current_tenant_id" - -schemas_especificos: - - nombre: clinica - descripcion: Schema para operaciones de clinica/consultorio - estado: PLANIFICADO - modulos_relacionados: [CL-001, CL-002, CL-003, CL-004, CL-005, CL-006] - nota: "Datos sensibles - Requiere encriptacion" - -tablas_planificadas: - pacientes: - - nombre: clinica.patients - descripcion: Registro de pacientes - modulo: CL-001 - prioridad: P0 - seguridad: DATOS_SENSIBLES - - - nombre: clinica.patient_contacts - descripcion: Contactos de emergencia - modulo: CL-001 - prioridad: P1 - - - nombre: clinica.patient_insurance - descripcion: Informacion de seguros - modulo: CL-001 - prioridad: P1 - - citas: - - nombre: clinica.appointments - descripcion: Citas medicas - modulo: CL-002 - prioridad: P0 - - - nombre: clinica.appointment_slots - descripcion: Horarios disponibles - modulo: CL-002 - prioridad: P0 - - - nombre: clinica.doctors - descripcion: Medicos y especialistas - modulo: CL-002 - prioridad: P0 - - - nombre: clinica.specialties - descripcion: Catalogo de especialidades - modulo: CL-002 - prioridad: P0 - - expediente_clinico: - - nombre: clinica.medical_records - descripcion: Expediente clinico electronico - modulo: CL-003 - prioridad: P0 - seguridad: ENCRIPTADO - normativa: NOM-024-SSA3-2012 - - - nombre: clinica.consultations - descripcion: Consultas realizadas - modulo: CL-003 - prioridad: P0 - - - nombre: clinica.diagnoses - descripcion: Diagnosticos (CIE-10) - modulo: CL-003 - prioridad: P0 - - - nombre: clinica.prescriptions - descripcion: Recetas medicas - modulo: CL-003 - prioridad: P1 - - - nombre: clinica.vital_signs - descripcion: Signos vitales - modulo: CL-003 - prioridad: P1 - - servicios: - - nombre: clinica.medical_services - descripcion: Catalogo de servicios medicos - modulo: CL-004 - prioridad: P0 - - - nombre: clinica.service_prices - descripcion: Precios por servicio - modulo: CL-004 - prioridad: P1 - - facturacion: - - nombre: clinica.invoices - descripcion: Facturas medicas - modulo: CL-005 - prioridad: P1 - nota: CFDI para sector salud - -specs_core_requeridas: - - spec: SPEC-RRHH-EVALUACIONES-SKILLS.md - aplicacion: Credenciales medicas - - spec: SPEC-INTEGRACION-CALENDAR.md - aplicacion: Agenda de citas - - spec: SPEC-TWO-FACTOR-AUTHENTICATION.md - aplicacion: Seguridad acceso expedientes - -consideraciones_seguridad: - - Encriptacion obligatoria de expedientes medicos - - Auditoria completa de acceso a datos sensibles - - Cumplimiento NOM-024-SSA3-2012 - - Proteccion de datos personales de salud - -resumen: - tablas_heredadas: 120+ - tablas_especificas_planificadas: 15 - schemas_especificos: 1 - estado_general: PLANIFICACION - ultima_actualizacion: 2025-12-08 - -referencias: - herencia_core: "../00-guidelines/HERENCIA-ERP-CORE.md" diff --git a/projects/erp-clinicas/orchestration/inventarios/DEPENDENCY_GRAPH.yml b/projects/erp-clinicas/orchestration/inventarios/DEPENDENCY_GRAPH.yml deleted file mode 100644 index 94b290584..000000000 --- a/projects/erp-clinicas/orchestration/inventarios/DEPENDENCY_GRAPH.yml +++ /dev/null @@ -1,61 +0,0 @@ -# DEPENDENCY GRAPH - ERP Cl铆nicas (Vertical) -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 - -proyecto: - nombre: ERP Clinicas - nivel: 2B.2 (Vertical) - -modulos_verticales: - CL-001_pacientes: - depende_de: [] - core: [auth, users, tenants] - - CL-002_citas: - depende_de: - - CL-001_pacientes - core: [auth, users, tenants, notifications] - - CL-003_expediente_clinico: - depende_de: - - CL-001_pacientes - - CL-002_citas - core: [auth, users, audit] - seguridad: CRITICO - - CL-004_servicios: - depende_de: [] - core: [auth, catalogs] - - CL-005_facturacion: - depende_de: - - CL-001_pacientes - - CL-002_citas - - CL-004_servicios - core: [auth, financial] - - CL-006_reportes: - depende_de: - - CL-002_citas - - CL-005_facturacion - core: [auth, reports] - -modulos_core_heredados: - - MGN-001_auth (100% + 2FA reforzado) - - MGN-002_users (100%) - - MGN-003_roles (100%) - - MGN-004_tenants (extendido) - - MGN-007_audit (CRITICO para HIPAA) - - MGN-008_notifications (100%) - -orden_implementacion: - fase_1: [CL-001, CL-004] - fase_2: [CL-002] - fase_3: [CL-003] - fase_4: [CL-005, CL-006] - -resumen: - total_modulos: 6 - dependencias_internas: 7 - estado: PLANIFICACION - ultima_actualizacion: 2025-12-08 diff --git a/projects/erp-clinicas/orchestration/inventarios/FRONTEND_INVENTORY.yml b/projects/erp-clinicas/orchestration/inventarios/FRONTEND_INVENTORY.yml deleted file mode 100644 index 205ea373e..000000000 --- a/projects/erp-clinicas/orchestration/inventarios/FRONTEND_INVENTORY.yml +++ /dev/null @@ -1,57 +0,0 @@ -# FRONTEND INVENTORY - ERP Cl铆nicas (Vertical) -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 - -proyecto: - nombre: ERP Clinicas - codigo: clinicas - nivel: 2B.2 (Vertical) - estado: Planificacion - -herencia_core: - frontend: erp-core - componentes_heredados: 80+ - referencia: "apps/erp-core/frontend/" - -componentes_planificados: - pacientes: - - PatientList - - PatientForm - - PatientDetail - - PatientSearch - - citas: - - AppointmentCalendar - - AppointmentForm - - AppointmentList - - DoctorScheduleView - - SlotSelector - - expediente_clinico: - - MedicalRecordView - - ConsultationForm - - PrescriptionForm - - VitalSignsForm - - DiagnosisSelector - - servicios: - - ServiceCatalog - - ServicePriceList - - facturacion: - - MedicalInvoiceForm - - InvoiceList - - reportes: - - AppointmentReport - - RevenueReport - - PatientStatistics - -resumen: - componentes_heredados: 80+ - componentes_planificados: 18 - estado_general: PLANIFICACION - ultima_actualizacion: 2025-12-08 - -referencias: - herencia_core: "../00-guidelines/HERENCIA-ERP-CORE.md" diff --git a/projects/erp-clinicas/orchestration/inventarios/MASTER_INVENTORY.yml b/projects/erp-clinicas/orchestration/inventarios/MASTER_INVENTORY.yml deleted file mode 100644 index 4185b68fa..000000000 --- a/projects/erp-clinicas/orchestration/inventarios/MASTER_INVENTORY.yml +++ /dev/null @@ -1,213 +0,0 @@ -# MASTER INVENTORY - ERP Cl铆nicas (Vertical) -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 - -proyecto: - nombre: ERP Cl铆nicas - codigo: CL - nivel: 2B.2 (Vertical) - estado: EPICAS_COMPLETAS - version: 0.3.0 - path: /home/isem/workspace/projects/erp-suite/apps/verticales/clinicas - herencia: - core_version: "0.6.0" - tablas_heredadas: 144 - schemas_heredados: 12 - specs_aplicables: 22 - specs_implementadas: 0 - -resumen_general: - total_modulos: 12 - total_schemas_planificados: 1 - total_tablas_planificadas: 13 - total_tablas_implementadas: 13 - total_servicios_backend: 0 - total_componentes_frontend: 0 - story_points_estimados: 451 - test_coverage: N/A - ultima_actualizacion: 2025-12-09 - -modulos: - total: 12 - lista: - - codigo: CL-001 - nombre: Fundamentos - descripcion: Auth con 2FA obligatorio, seguridad m茅dica - herencia: 90% - prioridad: P0 - estado: PLANIFICADO - sp: 0 - - - codigo: CL-002 - nombre: Pacientes - descripcion: Registro y expediente b谩sico - herencia: 40% - prioridad: P0 - estado: EPICA_COMPLETA - sp: 38 - epica: docs/08-epicas/EPIC-CL-002-pacientes.md - - - codigo: CL-003 - nombre: Citas - descripcion: Agenda m茅dica con recordatorios - herencia: 30% - prioridad: P0 - estado: EPICA_COMPLETA - sp: 42 - epica: docs/08-epicas/EPIC-CL-003-citas.md - - - codigo: CL-004 - nombre: Consultas - descripcion: Registro de consultas y SOAP - herencia: 0% - prioridad: P0 - estado: EPICA_COMPLETA - sp: 55 - epica: docs/08-epicas/EPIC-CL-004-consultas.md - - - codigo: CL-005 - nombre: Recetas - descripcion: Prescripciones y medicamentos - herencia: 20% - prioridad: P0 - estado: EPICA_COMPLETA - sp: 35 - epica: docs/08-epicas/EPIC-CL-005-recetas.md - - - codigo: CL-006 - nombre: Laboratorio - descripcion: 脫rdenes y resultados - herencia: 10% - prioridad: P1 - estado: EPICA_COMPLETA - sp: 38 - epica: docs/08-epicas/EPIC-CL-006-laboratorio.md - - - codigo: CL-007 - nombre: Farmacia - descripcion: Inventario de medicamentos - herencia: 60% - prioridad: P1 - estado: EPICA_COMPLETA - sp: 40 - epica: docs/08-epicas/EPIC-CL-007-farmacia.md - - - codigo: CL-008 - nombre: Facturacion - descripcion: CFDI servicios m茅dicos - herencia: 70% - prioridad: P0 - estado: EPICA_COMPLETA - sp: 38 - epica: docs/08-epicas/EPIC-CL-008-facturacion.md - - - codigo: CL-009 - nombre: Reportes - descripcion: Estad铆sticas cl铆nicas - herencia: 40% - prioridad: P1 - estado: EPICA_COMPLETA - sp: 25 - epica: docs/08-epicas/EPIC-CL-009-reportes.md - - - codigo: CL-010 - nombre: Telemedicina - descripcion: Videoconsultas remotas - herencia: 0% - prioridad: P2 - estado: EPICA_COMPLETA - sp: 55 - epica: docs/08-epicas/EPIC-CL-010-telemedicina.md - - - codigo: CL-011 - nombre: Expediente - descripcion: Historia cl铆nica NOM-024 - herencia: 10% - prioridad: P0 - estado: EPICA_COMPLETA - sp: 30 - epica: docs/08-epicas/EPIC-CL-011-expediente.md - - - codigo: CL-012 - nombre: Imagenologia - descripcion: Visor DICOM y estudios - herencia: 0% - prioridad: P2 - estado: EPICA_COMPLETA - sp: 55 - epica: docs/08-epicas/EPIC-CL-012-imagenologia.md - -specs_core: - aplicables: 22 - implementadas: 0 - por_implementar: 22 - documento: orchestration/00-guidelines/HERENCIA-SPECS-CORE.md - detalle: - - spec: SPEC-SISTEMA-SECUENCIAS - estado: PENDIENTE - - spec: SPEC-SEGURIDAD-API-KEYS-PERMISOS - estado: PENDIENTE - - spec: SPEC-TRAZABILIDAD-LOTES-SERIES - estado: PENDIENTE - - spec: SPEC-MAIL-THREAD-TRACKING - estado: PENDIENTE - - spec: SPEC-RRHH-EVALUACIONES - estado: PENDIENTE - - spec: SPEC-INTEGRACION-CALENDAR - estado: PENDIENTE - - spec: SPEC-WIZARD-TRANSIENT-MODEL - estado: PENDIENTE - - spec: SPEC-FACTURACION-CFDI - estado: PENDIENTE - -capas: - database: - inventario: DATABASE_INVENTORY.yml - schemas_implementados: [clinical] - tablas_implementadas: 13 - enums_implementados: 4 - ddl_files: - - init/00-extensions.sql - - init/01-create-schemas.sql - - init/02-rls-functions.sql - - init/03-clinical-tables.sql - estado: DDL_COMPLETO - - backend: - inventario: BACKEND_INVENTORY.yml - modulos_planificados: 12 - estado: PLANIFICADO - - frontend: - inventario: FRONTEND_INVENTORY.yml - paginas_planificadas: 25 - estado: PLANIFICADO - -dependencias_core: - obligatorias: - - auth (MGN-001) - - users (MGN-002) - - roles (MGN-003) - - tenants (MGN-004) - opcionales: - - catalogs (MGN-005) - - financial (MGN-010) - - inventory (MGN-011) - - cfdi (MGN-016) - - calendar (MGN-020) - -consideraciones_especiales: - - Cumplimiento NOM-024-SSA3-2012 - - LFPDPPP - Protecci贸n datos personales de salud - - 2FA obligatorio para personal m茅dico - - Encriptaci贸n de datos sensibles (antecedentes, alergias) - - Integraci贸n HL7/FHIR para interoperabilidad - - Firma electr贸nica de recetas - -referencias: - docs: docs/ - vision: docs/00-vision-general/VISION-CLINICAS.md - modulos: docs/02-definicion-modulos/INDICE-MODULOS.md - orchestration: orchestration/ - herencia_specs: orchestration/00-guidelines/HERENCIA-SPECS-CORE.md - herencia_db: database/HERENCIA-ERP-CORE.md diff --git a/projects/erp-clinicas/orchestration/inventarios/README.md b/projects/erp-clinicas/orchestration/inventarios/README.md deleted file mode 100644 index 8020d926e..000000000 --- a/projects/erp-clinicas/orchestration/inventarios/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# Inventarios - ERP Cl铆nicas - -**Version:** 1.0.0 -**Fecha:** 2025-12-08 -**Nivel SIMCO:** 2B.2 - ---- - -## Descripci贸n - -Este directorio contiene los inventarios YAML que sirven como **Single Source of Truth (SSOT)** para el proyecto ERP Cl铆nicas. Estos archivos son la referencia can贸nica para m茅tricas, trazabilidad y componentes del sistema. - ---- - -## Archivos de Inventario - -| Archivo | Descripci贸n | Estado | -|---------|-------------|--------| -| [MASTER_INVENTORY.yml](./MASTER_INVENTORY.yml) | Inventario maestro con m茅tricas globales | Completo | -| [DATABASE_INVENTORY.yml](./DATABASE_INVENTORY.yml) | Inventario de objetos de base de datos | Planificado | -| [BACKEND_INVENTORY.yml](./BACKEND_INVENTORY.yml) | Inventario de componentes backend | Planificado | -| [FRONTEND_INVENTORY.yml](./FRONTEND_INVENTORY.yml) | Inventario de componentes frontend | Planificado | -| [TRACEABILITY_MATRIX.yml](./TRACEABILITY_MATRIX.yml) | Matriz de trazabilidad RF->ET->US | Completo | -| [DEPENDENCY_GRAPH.yml](./DEPENDENCY_GRAPH.yml) | Grafo de dependencias entre m贸dulos | Completo | - ---- - -## Herencia del Core - -Este proyecto hereda del **ERP Core** (nivel 2B.1): - -| Aspecto | Heredado | Espec铆fico | -|---------|----------|------------| -| **Tablas DB** | ~100 | Planificado | -| **Schemas** | 8+ | Planificado | -| **Specs** | 3 | - | - -### Specs Heredadas - -1. SPEC-RRHH-EVALUACIONES-SKILLS.md -2. SPEC-INTEGRACION-CALENDAR.md -3. SPEC-MAIL-THREAD-TRACKING.md - ---- - -## Resumen Ejecutivo - -### M茅tricas del Proyecto - -| M茅trica | Valor | -|---------|-------| -| **M贸dulos** | 5 (CL-001 a CL-005) | -| **Estado** | PLANIFICACION_COMPLETA | -| **Completitud** | 15% | - -### Dominio del Negocio - -- Expediente cl铆nico electr贸nico -- Gesti贸n de citas m茅dicas -- Control de consultorios -- Facturaci贸n de servicios m茅dicos - ---- - -## Directivas Espec铆ficas - -1. [DIRECTIVA-EXPEDIENTE-CLINICO.md](../directivas/DIRECTIVA-EXPEDIENTE-CLINICO.md) -2. [DIRECTIVA-GESTION-CITAS.md](../directivas/DIRECTIVA-GESTION-CITAS.md) - ---- - -## Configuraci贸n de Puertos (Planificado) - -| Servicio | Puerto | -|----------|--------| -| Backend API | 3500 | -| Frontend Web | 5179 | -| Patient Portal | 5180 | - ---- - -## Cumplimiento Normativo - -Este proyecto debe cumplir con: -- NOM-024-SSA3-2012 (Expediente cl铆nico electr贸nico) -- Ley de Protecci贸n de Datos Personales en Posesi贸n de Particulares - ---- - -## Alineaci贸n con ERP Core - -Estos inventarios siguen la misma estructura que: -- `/erp-core/orchestration/inventarios/` (proyecto padre) - -### Referencias - -- Suite Master: `orchestration/inventarios/SUITE_MASTER_INVENTORY.yml` -- Core: `apps/erp-core/orchestration/inventarios/` -- Status Global: `orchestration/inventarios/STATUS.yml` - ---- - -**脷ltima actualizaci贸n:** 2025-12-08 diff --git a/projects/erp-clinicas/orchestration/inventarios/TRACEABILITY_MATRIX.yml b/projects/erp-clinicas/orchestration/inventarios/TRACEABILITY_MATRIX.yml deleted file mode 100644 index 0d3af0152..000000000 --- a/projects/erp-clinicas/orchestration/inventarios/TRACEABILITY_MATRIX.yml +++ /dev/null @@ -1,514 +0,0 @@ -# ============================================================================= -# TRACEABILITY MATRIX - ERP Cl铆nicas (Vertical) -# ============================================================================= -# Generado: 2025-12-08 -# Sistema: NEXUS + SIMCO v2.2.0 -# Prop贸sito: Matriz de trazabilidad M贸dulos -> SPECS -> Componentes -# ============================================================================= - -metadata: - proyecto: ERP Cl铆nicas - codigo: CL - version: 1.0.0 - fecha_actualizacion: 2025-12-08 - base_core: erp-core v0.6.0 - normativa: NOM-024-SSA3-2012, LFPDPPP - -# ============================================================================= -# RESUMEN GLOBAL -# ============================================================================= -resumen: - modulos_total: 12 - modulos_documentados: 12 - story_points_total: 395 - specs_core_aplicables: 22 - specs_implementadas: 0 - cobertura_specs: 0% - estado: PLANIFICACION_COMPLETA - -# ============================================================================= -# TRAZABILIDAD POR M脫DULO -# ============================================================================= -trazabilidad: - # --------------------------------------------------------------------------- - # CL-001: Fundamentos (90% herencia + 2FA obligatorio) - # --------------------------------------------------------------------------- - CL-001: - nombre: Fundamentos - herencia: 90% - prioridad: P0 - sp: 0 - extiende: - - MGN-001 (auth) - - MGN-002 (users) - - MGN-003 (roles) - - MGN-004 (tenants) - database: - heredadas: [auth.users, auth.sessions, auth.roles, tenants.tenants] - extensiones: - - auth.two_factor_configs - - auth.medical_certifications - backend: - heredados: [AuthService, UserService, RoleService, TenantService] - extensiones: - - TwoFactorService - - MedicalAuthService - frontend: - heredados: [LoginForm, UserProfile, RoleSelector] - extensiones: - - TwoFactorSetup - - TwoFactorVerify - specs_core: - - SPEC-SISTEMA-SECUENCIAS - - SPEC-SEGURIDAD-API-KEYS-PERMISOS - seguridad: - - 2FA_obligatorio_personal_medico - - encriptacion_datos_sensibles - - # --------------------------------------------------------------------------- - # CL-002: Pacientes - # --------------------------------------------------------------------------- - CL-002: - nombre: Pacientes - herencia: 40% - prioridad: P0 - sp: 34 - database: - tablas: - - clinical.patients - - clinical.patient_contacts - - clinical.patient_insurance - - clinical.patient_allergies - - clinical.emergency_contacts - backend: - servicios: - - PatientService - - PatientSearchService - - InsuranceService - frontend: - componentes: - - PatientList - - PatientForm - - PatientDetail - - PatientSearch - - InsuranceCard - - AllergyBadge - specs_core: - - SPEC-MAIL-THREAD-TRACKING (comunicaci贸n pacientes) - seguridad: - - datos_encriptados: [allergies, medical_history] - - consentimiento_requerido: true - - # --------------------------------------------------------------------------- - # CL-003: Citas - # --------------------------------------------------------------------------- - CL-003: - nombre: Citas - herencia: 30% - prioridad: P0 - sp: 42 - database: - tablas: - - clinical.appointments - - clinical.appointment_slots - - clinical.doctors - - clinical.specialties - - clinical.appointment_reminders - backend: - servicios: - - AppointmentService - - DoctorService - - ScheduleService - - ReminderService - frontend: - componentes: - - AppointmentCalendar - - AppointmentForm - - DoctorScheduleView - - SlotSelector - - ReminderConfig - - AppointmentConfirmation - specs_core: - - SPEC-INTEGRACION-CALENDAR (Google Calendar, Outlook) - - # --------------------------------------------------------------------------- - # CL-004: Consultas - # --------------------------------------------------------------------------- - CL-004: - nombre: Consultas - herencia: 0% - prioridad: P0 - sp: 47 - database: - tablas: - - clinical.consultations - - clinical.vital_signs - - clinical.diagnoses - - clinical.icd10_codes - - clinical.consultation_notes - backend: - servicios: - - ConsultationService - - VitalSignsService - - DiagnosisService - - ICD10Service - frontend: - componentes: - - ConsultationForm - - SOAPNote - - VitalSignsCapture - - DiagnosisSearch - - ICD10Selector - - ConsultationHistory - specs_core: [] - normativa: - - NOM-024-SSA3-2012 (estructura SOAP) - - # --------------------------------------------------------------------------- - # CL-005: Recetas - # --------------------------------------------------------------------------- - CL-005: - nombre: Recetas - herencia: 20% - prioridad: P0 - sp: 34 - database: - tablas: - - clinical.prescriptions - - clinical.prescription_items - - clinical.medications - - clinical.drug_interactions - backend: - servicios: - - PrescriptionService - - MedicationService - - InteractionChecker - - PrescriptionPrintService - frontend: - componentes: - - PrescriptionForm - - MedicationSearch - - DosageCalculator - - InteractionAlert - - PrescriptionPrint - specs_core: - - SPEC-WIZARD-TRANSIENT-MODEL (asistente receta) - caracteristicas: - - firma_electronica: true - - validacion_interacciones: true - - # --------------------------------------------------------------------------- - # CL-006: Laboratorio - # --------------------------------------------------------------------------- - CL-006: - nombre: Laboratorio - herencia: 10% - prioridad: P1 - sp: 42 - database: - tablas: - - laboratory.lab_orders - - laboratory.lab_tests - - laboratory.test_results - - laboratory.reference_ranges - - laboratory.test_panels - backend: - servicios: - - LabOrderService - - TestResultService - - ReferenceRangeService - frontend: - componentes: - - LabOrderForm - - TestSelector - - ResultsCapture - - ResultsViewer - - AbnormalHighlight - - TrendChart - specs_core: - - SPEC-TRAZABILIDAD-LOTES-SERIES (muestras) - - # --------------------------------------------------------------------------- - # CL-007: Farmacia - # --------------------------------------------------------------------------- - CL-007: - nombre: Farmacia - herencia: 60% - prioridad: P1 - sp: 34 - database: - tablas: - - pharmacy.pharmacy_stock - - pharmacy.medication_lots - - pharmacy.dispensations - - pharmacy.controlled_substances - backend: - servicios: - - PharmacyStockService - - DispensationService - - ControlledSubstanceService - frontend: - componentes: - - PharmacyStock - - DispensationForm - - LotSelector - - ExpirationAlert - - ControlledLog - specs_core: - - SPEC-VALORACION-INVENTARIO - - SPEC-TRAZABILIDAD-LOTES-SERIES - - SPEC-INVENTARIOS-CICLICOS - caracteristicas: - - control_caducidades: true - - sustancias_controladas: true - - # --------------------------------------------------------------------------- - # CL-008: Facturaci贸n - # --------------------------------------------------------------------------- - CL-008: - nombre: Facturacion - herencia: 70% - prioridad: P0 - sp: 21 - database: - tablas: - - invoicing.medical_invoices - - invoicing.insurance_claims - - invoicing.service_charges - backend: - servicios: - - MedicalInvoiceService - - InsuranceClaimService - - CFDIService - frontend: - componentes: - - MedicalInvoiceForm - - InvoiceList - - InsuranceClaimForm - - PaymentCapture - specs_core: - - SPEC-FACTURACION-CFDI (servicios m茅dicos) - - # --------------------------------------------------------------------------- - # CL-009: Reportes - # --------------------------------------------------------------------------- - CL-009: - nombre: Reportes - herencia: 40% - prioridad: P1 - sp: 34 - database: - tablas: [] - backend: - servicios: - - MedicalReportService - - StatisticsService - - ExportService - frontend: - componentes: - - AppointmentReport - - RevenueReport - - PatientStatistics - - DoctorProductivity - - DiagnosisDistribution - specs_core: [] - - # --------------------------------------------------------------------------- - # CL-010: Telemedicina - # --------------------------------------------------------------------------- - CL-010: - nombre: Telemedicina - herencia: 0% - prioridad: P2 - sp: 47 - database: - tablas: - - telemedicine.video_sessions - - telemedicine.session_recordings - - telemedicine.waiting_room - backend: - servicios: - - VideoSessionService - - WaitingRoomService - - RecordingService - frontend: - componentes: - - VideoConsultation - - WaitingRoom - - VirtualExamRoom - - ScreenShare - - ChatPanel - specs_core: [] - caracteristicas: - - encriptacion_e2e: true - - grabacion_opcional: true - - # --------------------------------------------------------------------------- - # CL-011: Expediente Cl铆nico - # --------------------------------------------------------------------------- - CL-011: - nombre: Expediente - herencia: 10% - prioridad: P0 - sp: 39 - database: - tablas: - - clinical.medical_records - - clinical.clinical_notes - - clinical.attachments - - clinical.consent_forms - - clinical.record_access_log - backend: - servicios: - - MedicalRecordService - - ClinicalNoteService - - AttachmentService - - ConsentService - - AuditService - frontend: - componentes: - - MedicalRecordView - - TimelineView - - NoteEditor - - AttachmentViewer - - ConsentCapture - - AccessLog - specs_core: - - SPEC-MAIL-THREAD-TRACKING (notas cl铆nicas) - normativa: - - NOM-024-SSA3-2012 (estructura expediente) - seguridad: - - encriptacion_completa: true - - auditoria_accesos: true - - # --------------------------------------------------------------------------- - # CL-012: Imagenolog铆a - # --------------------------------------------------------------------------- - CL-012: - nombre: Imagenologia - herencia: 0% - prioridad: P2 - sp: 21 - database: - tablas: - - imaging.imaging_studies - - imaging.dicom_metadata - - imaging.study_reports - backend: - servicios: - - ImagingStudyService - - DICOMService - - ReportService - frontend: - componentes: - - DICOMViewer - - StudyList - - ReportEditor - - ImageAnnotation - specs_core: [] - caracteristicas: - - dicom_viewer: true - - integracion_pacs: true - -# ============================================================================= -# REFERENCIAS CRUZADAS CON ERP-CORE -# ============================================================================= -referencias_core: - specs_implementadas: [] - - specs_pendientes: - - spec: SPEC-SISTEMA-SECUENCIAS - modulos: [CL-001, CL-003, CL-004] - prioridad: P0 - estado: PENDIENTE - - - spec: SPEC-SEGURIDAD-API-KEYS-PERMISOS - modulos: [CL-001] - prioridad: P0 - estado: PENDIENTE - adaptacion: "Permisos por especialidad m茅dica" - - - spec: SPEC-INTEGRACION-CALENDAR - modulos: [CL-003] - prioridad: P0 - estado: PENDIENTE - adaptacion: "Sincronizaci贸n agenda m茅dica" - - - spec: SPEC-TRAZABILIDAD-LOTES-SERIES - modulos: [CL-006, CL-007] - prioridad: P1 - estado: PENDIENTE - adaptacion: "Trazabilidad muestras y medicamentos" - - - spec: SPEC-MAIL-THREAD-TRACKING - modulos: [CL-002, CL-011] - prioridad: P1 - estado: PENDIENTE - adaptacion: "Comunicaci贸n segura pacientes" - - modulos_extendidos: - - core: MGN-001 (auth) - vertical: CL-001 - tipo: extension_2fa_obligatorio - - - core: MGN-011 (inventory) - vertical: CL-007 - tipo: extension_farmacia - - - core: MGN-016 (cfdi) - vertical: CL-008 - tipo: extension_servicios_medicos - -# ============================================================================= -# CUMPLIMIENTO NORMATIVO -# ============================================================================= -cumplimiento_normativo: - NOM-024-SSA3-2012: - descripcion: "Expediente cl铆nico electr贸nico" - modulos_afectados: [CL-004, CL-011] - requisitos: - - estructura_soap: CL-004 - - historia_clinica: CL-011 - - consentimiento_informado: CL-011 - - firma_electronica: CL-005 - - LFPDPPP: - descripcion: "Protecci贸n de datos personales" - modulos_afectados: [CL-002, CL-011] - requisitos: - - encriptacion_datos_sensibles: true - - consentimiento_tratamiento: true - - derecho_acceso: true - - auditoria_accesos: true - -# ============================================================================= -# VALIDACIONES -# ============================================================================= -validaciones: - modulos_huerfanos: 0 - specs_sin_modulo: 0 - - alertas: - - tipo: implementacion_pendiente - mensaje: "0% de c贸digo implementado - fase planificaci贸n" - - - tipo: seguridad_critica - modulo: CL-001 - mensaje: "2FA obligatorio para personal m茅dico" - - - tipo: normativa - modulos: [CL-004, CL-011] - mensaje: "Requiere validaci贸n NOM-024-SSA3-2012" - - - tipo: encriptacion - modulos: [CL-002, CL-011] - mensaje: "Datos sensibles requieren encriptaci贸n" - -# ============================================================================= -# METADATA -# ============================================================================= -metadata_documento: - creado_por: Claude-Code - fecha_creacion: 2025-12-08 - ultima_actualizacion: 2025-12-08 - version_documento: 1.0.0 diff --git a/projects/erp-clinicas/orchestration/prompts/PROMPT-CL-BACKEND-AGENT.md b/projects/erp-clinicas/orchestration/prompts/PROMPT-CL-BACKEND-AGENT.md deleted file mode 100644 index d791cd16b..000000000 --- a/projects/erp-clinicas/orchestration/prompts/PROMPT-CL-BACKEND-AGENT.md +++ /dev/null @@ -1,182 +0,0 @@ -# Prompt: Cl铆nicas Backend Agent - -## Identidad - -Eres un agente especializado en desarrollo backend para ERP Cl铆nicas. Tu expertise est谩 en Node.js, Express, TypeScript, TypeORM y PostgreSQL, con conocimiento espec铆fico del dominio de salud y cumplimiento normativo mexicano (NOM-024-SSA3-2012, LFPDPPP). - -## Contexto del Proyecto - -```yaml -proyecto: ERP Cl铆nicas -codigo: CL -tipo: Vertical de ERP-Suite -nivel: 2B.2 -stack: - runtime: Node.js 20+ - framework: Express.js - lenguaje: TypeScript 5.3+ - orm: TypeORM 0.3.17 - database: PostgreSQL 15+ - auth: JWT + bcryptjs + 2FA (heredado + extendido) - encriptacion: AES-256 para datos sensibles - -paths: - vertical: /home/isem/workspace/projects/erp-suite/apps/verticales/clinicas/ - backend: /home/isem/workspace/projects/erp-suite/apps/verticales/clinicas/backend/ - docs: /home/isem/workspace/projects/erp-suite/apps/verticales/clinicas/docs/ - core: /home/isem/workspace/projects/erp-suite/apps/erp-core/ - directivas: orchestration/directivas/ - -puertos: - backend: 3500 - frontend: 5178 - database: 5437 -``` - -## Herencia del Core - -Este proyecto HEREDA del ERP-Core: -- M贸dulos: auth (+ 2FA), users, roles, tenants, inventory, cfdi -- SPECS: Ver `orchestration/00-guidelines/HERENCIA-SPECS-CORE.md` -- Base de datos: 97 tablas heredadas - -**REGLA:** Extender, NUNCA modificar el core. - -## M贸dulos de la Vertical - -| M贸dulo | Descripci贸n | Prioridad | Normativa | -|--------|-------------|-----------|-----------| -| CL-001 | Fundamentos (+ 2FA) | P0 | LFPDPPP | -| CL-002 | Pacientes | P0 | LFPDPPP | -| CL-003 | Citas | P0 | - | -| CL-004 | Consultas (SOAP) | P0 | NOM-024 | -| CL-005 | Recetas | P0 | NOM-024 | -| CL-006 | Laboratorio | P1 | - | -| CL-007 | Farmacia | P1 | - | -| CL-008 | Facturaci贸n CFDI | P0 | - | -| CL-009 | Reportes | P1 | - | -| CL-010 | Telemedicina | P2 | - | -| CL-011 | Expediente NOM-024 | P0 | NOM-024 | -| CL-012 | Imagenolog铆a DICOM | P2 | - | - -## Directivas Obligatorias - -### 1. Multi-Tenant (Heredada) -``` -OBLIGATORIO: Toda operaci贸n debe filtrar por tenant_id. -Ver: core/orchestration/directivas/DIRECTIVA-MULTI-TENANT.md -``` - -### 2. Expediente Cl铆nico -``` -ESPEC脥FICO: Cumplimiento NOM-024-SSA3-2012. -Ver: directivas/DIRECTIVA-EXPEDIENTE-CLINICO.md -``` - -### 3. Gesti贸n de Citas -``` -ESPEC脥FICO: Agenda m茅dica, recordatorios. -Ver: directivas/DIRECTIVA-GESTION-CITAS.md -``` - -## Cumplimiento Normativo - -### NOM-024-SSA3-2012 (Expediente Cl铆nico) -```yaml -requisitos: - - estructura_soap: Subjetivo, Objetivo, An谩lisis, Plan - - campos_obligatorios: - - identificacion_paciente - - fecha_consulta - - motivo_consulta - - exploracion_fisica - - diagnostico_cie10 - - plan_tratamiento - - firma_electronica: En recetas - - consentimiento_informado: Documentado -``` - -### LFPDPPP (Protecci贸n de Datos) -```yaml -requisitos: - - encriptacion: AES-256 para datos sensibles - - campos_encriptados: - - antecedentes_medicos - - alergias - - diagnosticos - - notas_clinicas - - auditoria: Log de accesos a expedientes - - consentimiento: Tratamiento de datos -``` - -## Schemas de Base de Datos - -```yaml -schemas_especificos: - - clinical: Pacientes, citas, consultas, expediente - - pharmacy: Stock medicamentos, dispensaciones - - laboratory: 脫rdenes lab, resultados - - imaging: Estudios DICOM, metadatos - - telemedicine: Sesiones video, grabaciones -``` - -## SPECS del Core Aplicables - -- SPEC-INTEGRACION-CALENDAR (agenda m茅dica) -- SPEC-MAIL-THREAD-TRACKING (comunicaci贸n pacientes) -- SPEC-TRAZABILIDAD-LOTES-SERIES (muestras, medicamentos) -- SPEC-FACTURACION-CFDI (servicios m茅dicos) -- SPEC-TWO-FACTOR-AUTHENTICATION (2FA obligatorio) -- SPEC-RRHH-EVALUACIONES-SKILLS (certificaciones m茅dicas) - -## Seguridad Especial - -### 2FA Obligatorio -```typescript -// Personal m茅dico REQUIERE 2FA -// Implementar en extensi贸n de auth -@UseGuards(TwoFactorGuard) -export class MedicalController { } -``` - -### Encriptaci贸n de Datos Sensibles -```typescript -// Usar decorador para campos sensibles -@EncryptedColumn() -antecedentes_medicos: string; - -@EncryptedColumn() -alergias: string; -``` - -### Auditor铆a de Accesos -```typescript -// Log autom谩tico de accesos a expediente -@AuditAccess('medical_record') -async getMedicalRecord(patientId: string) { } -``` - -## Flujo de Trabajo - -``` -1. Leer especificaci贸n del m贸dulo en docs/02-definicion-modulos/ -2. Verificar cumplimiento normativo (NOM-024, LFPDPPP) -3. Verificar SPECS aplicables en HERENCIA-SPECS-CORE.md -4. Revisar DDL existente en database/ -5. Implementar con encriptaci贸n y auditor铆a -6. Actualizar TRAZA-TAREAS-BACKEND.md -7. Actualizar BACKEND_INVENTORY.yml -``` - -## Referencias - -- Inventario: `orchestration/inventarios/MASTER_INVENTORY.yml` -- Trazabilidad: `orchestration/inventarios/TRACEABILITY_MATRIX.yml` -- Herencia: `orchestration/00-guidelines/HERENCIA-SPECS-CORE.md` -- NOM-024: Normativa expediente cl铆nico electr贸nico -- LFPDPPP: Ley Federal de Protecci贸n de Datos Personales - ---- - -**Versi贸n:** 1.0.0 -**Sistema:** SIMCO v2.2.0 diff --git a/projects/erp-clinicas/orchestration/referencias/DEPENDENCIAS-ERP-CORE.yml b/projects/erp-clinicas/orchestration/referencias/DEPENDENCIAS-ERP-CORE.yml deleted file mode 100644 index b263b42f4..000000000 --- a/projects/erp-clinicas/orchestration/referencias/DEPENDENCIAS-ERP-CORE.yml +++ /dev/null @@ -1,97 +0,0 @@ -# Dependencias de ERP-Core para ERP Clinicas -# ============================================ - -version: "1.0.0" -fecha_actualizacion: "2025-12-27" -proyecto: "erp-clinicas" - -# Base de la que hereda -base: - proyecto: "erp-core" - version_minima: "1.2.0" - ruta: "projects/erp-core" - ruta_absoluta: "/home/isem/workspace-v1/projects/erp-core" - -# Schemas de base de datos heredados -database: - herencia: "completa" - - schemas_usados: - - nombre: "auth_management" - tablas_heredadas: 26 - tablas_extendidas: 2 - uso: "Autenticacion con seguridad reforzada para datos medicos" - extensiones: - - "MFA obligatorio para acceso expedientes" - - "Consentimiento informado digital" - - - nombre: "core_management" - tablas_heredadas: 12 - tablas_extendidas: 3 - uso: "Partners (pacientes, proveedores medicos)" - extensiones: - - "Datos sensibles encriptados" - - "Historial medico referenciado" - - - nombre: "core_catalogs" - tablas_heredadas: 8 - tablas_extendidas: 5 - uso: "Catalogos medicos especializados" - extensiones: - - "CIE-10 (diagnosticos)" - - "Procedimientos medicos" - - "Medicamentos (cuadro basico)" - - "Laboratorios" - - "Especialidades medicas" - -# Schemas propios de clinicas (no heredados) -schemas_propios: - - nombre: "medical_management" - tablas: 15 - descripcion: "Expedientes, citas, recetas, laboratorio" - - - nombre: "billing_medical" - tablas: 8 - descripcion: "Facturacion CFDI sector salud, aseguradoras" - -# Variable RLS obligatoria -rls: - variable: "app.current_tenant_id" - tipo: "UUID" - nota: "TODAS las queries deben filtrar por esta variable" - -# Modulos backend importados -backend: - modulos_importados: - - nombre: "AuthModule" - desde: "@erp-core/auth" - version: "1.0.0" - - - nombre: "UsersModule" - desde: "@erp-core/users" - version: "1.0.0" - - - nombre: "RolesModule" - desde: "@erp-core/roles" - version: "1.0.0" - - - nombre: "TenantsModule" - desde: "@erp-core/tenants" - version: "1.0.0" - -# Cumplimiento normativo -normativas: - - codigo: "NOM-024-SSA3-2012" - descripcion: "Expediente clinico electronico" - - codigo: "LFPDPPP" - descripcion: "Ley de Proteccion de Datos Personales" - - codigo: "COFEPRIS" - descripcion: "Requisitos de trazabilidad" - -# Validaciones requeridas -validaciones: - - "Variable RLS correcta en todo DDL" - - "Encriptacion de datos medicos" - - "Auditoria completa de accesos" - - "Imports de erp-core funcionando" - - "Tests pasando" diff --git a/projects/erp-clinicas/orchestration/referencias/DEPENDENCIAS-SHARED.yml b/projects/erp-clinicas/orchestration/referencias/DEPENDENCIAS-SHARED.yml deleted file mode 100644 index 9bdbe9c9d..000000000 --- a/projects/erp-clinicas/orchestration/referencias/DEPENDENCIAS-SHARED.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Dependencias de Modulos Compartidos para ERP Clinicas -# ====================================================== - -version: "1.0.0" -fecha_actualizacion: "2025-12-27" -proyecto: "erp-clinicas" - -# Modulos del catalogo usados -modulos_catalogo: - - id: "auth" - ruta: "core/catalog/auth" - version_usada: "1.0.0" - fecha_implementacion: "pendiente" - adaptaciones: - - descripcion: "MFA obligatorio para acceso expedientes" - archivo: "Por implementar" - tests_pasando: false - - - id: "multi-tenancy" - ruta: "core/catalog/multi-tenancy" - version_usada: "1.0.0" - fecha_implementacion: "pendiente" - adaptaciones: null - tests_pasando: false - - - id: "notifications" - ruta: "core/catalog/notifications" - version_usada: "1.0.0" - fecha_implementacion: "pendiente" - adaptaciones: - - descripcion: "Recordatorios de citas" - archivo: "Por implementar" - - descripcion: "Notificaciones de resultados laboratorio" - archivo: "Por implementar" - tests_pasando: false - - - id: "rate-limiting" - ruta: "core/catalog/rate-limiting" - version_usada: "1.0.0" - fecha_implementacion: "pendiente" - adaptaciones: null - tests_pasando: false - -# Modulos de core/modules usados -modulos_core: [] - -# Librerias de shared/libs usadas -librerias_shared: [] - -# Modulos pendientes de implementar -pendientes: - - id: "audit-logs" - prioridad: "alta" - justificacion: "Auditoria completa de acceso a expedientes medicos (COFEPRIS)" - - - id: "two-factor-auth" - prioridad: "alta" - justificacion: "Seguridad reforzada para datos sensibles" - - - id: "encryption" - prioridad: "alta" - justificacion: "Encriptacion de datos medicos en reposo" diff --git a/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-BACKEND.md b/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-BACKEND.md deleted file mode 100644 index ebc83f9d6..000000000 --- a/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-BACKEND.md +++ /dev/null @@ -1,38 +0,0 @@ -# TRAZA DE TAREAS - BACKEND LAYER -# Proyecto: ERP CLINICAS (Vertical) -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto en planificaci贸n - ---- - -## Formato de Registro - -```yaml -[FECHA] - [ID_TAREA] - [OPERACION] -Descripcion: {descripcion} -Archivos: - - {archivo_1} -Estado: {COMPLETADO | EN_PROGRESO | BLOQUEADO} -Ejecutado_por: {AGENTE | USUARIO} -``` - ---- - -## Historial de Tareas - -*Sin tareas registradas - Proyecto en planificaci贸n* - ---- - -## Resumen - -| M茅trica | Valor | -|---------|-------| -| Total tareas | 0 | -| Completadas | 0 | -| En progreso | 0 | -| Bloqueadas | 0 | -| 脷ltima actualizaci贸n | 2025-12-08 | - ---- -*Traza de tareas - Sistema NEXUS* diff --git a/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-DATABASE.md b/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-DATABASE.md deleted file mode 100644 index 4048a7b2f..000000000 --- a/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-DATABASE.md +++ /dev/null @@ -1,38 +0,0 @@ -# TRAZA DE TAREAS - DATABASE LAYER -# Proyecto: ERP CLINICAS (Vertical) -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto en planificaci贸n - ---- - -## Formato de Registro - -```yaml -[FECHA] - [ID_TAREA] - [OPERACION] -Descripcion: {descripcion} -Archivos: - - {archivo_1} -Estado: {COMPLETADO | EN_PROGRESO | BLOQUEADO} -Ejecutado_por: {AGENTE | USUARIO} -``` - ---- - -## Historial de Tareas - -*Sin tareas registradas - Proyecto en planificaci贸n* - ---- - -## Resumen - -| M茅trica | Valor | -|---------|-------| -| Total tareas | 0 | -| Completadas | 0 | -| En progreso | 0 | -| Bloqueadas | 0 | -| 脷ltima actualizaci贸n | 2025-12-08 | - ---- -*Traza de tareas - Sistema NEXUS* diff --git a/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md b/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md deleted file mode 100644 index 92c2683ea..000000000 --- a/projects/erp-clinicas/orchestration/trazas/TRAZA-TAREAS-FRONTEND.md +++ /dev/null @@ -1,38 +0,0 @@ -# TRAZA DE TAREAS - FRONTEND LAYER -# Proyecto: ERP CLINICAS (Vertical) -# Sistema: NEXUS + SIMCO v2.2.0 -# Estado: TEMPLATE - Proyecto en planificaci贸n - ---- - -## Formato de Registro - -```yaml -[FECHA] - [ID_TAREA] - [OPERACION] -Descripcion: {descripcion} -Archivos: - - {archivo_1} -Estado: {COMPLETADO | EN_PROGRESO | BLOQUEADO} -Ejecutado_por: {AGENTE | USUARIO} -``` - ---- - -## Historial de Tareas - -*Sin tareas registradas - Proyecto en planificaci贸n* - ---- - -## Resumen - -| M茅trica | Valor | -|---------|-------| -| Total tareas | 0 | -| Completadas | 0 | -| En progreso | 0 | -| Bloqueadas | 0 | -| 脷ltima actualizaci贸n | 2025-12-08 | - ---- -*Traza de tareas - Sistema NEXUS* diff --git a/projects/erp-construccion/.env.example b/projects/erp-construccion/.env.example deleted file mode 100644 index 07c02155b..000000000 --- a/projects/erp-construccion/.env.example +++ /dev/null @@ -1,119 +0,0 @@ -# ============================================================================= -# ERP Construccion - Environment Variables -# ============================================================================= -# Copia este archivo a .env y configura los valores -# cp .env.example .env - -# ----------------------------------------------------------------------------- -# APPLICATION -# ----------------------------------------------------------------------------- -NODE_ENV=development -APP_PORT=3000 -API_VERSION=v1 - -# ----------------------------------------------------------------------------- -# DATABASE - PostgreSQL -# ----------------------------------------------------------------------------- -DB_HOST=localhost -DB_PORT=5432 -DB_USER=construccion -DB_PASSWORD=construccion_dev_2024 -DB_NAME=erp_construccion -DB_SCHEMA=public - -# Database Pool -DB_POOL_MIN=2 -DB_POOL_MAX=10 - -# ----------------------------------------------------------------------------- -# REDIS -# ----------------------------------------------------------------------------- -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD=redis_dev_2024 - -# ----------------------------------------------------------------------------- -# JWT & AUTHENTICATION -# ----------------------------------------------------------------------------- -JWT_SECRET=your-super-secret-jwt-key-change-in-production-minimum-32-chars -JWT_EXPIRES_IN=1d -JWT_REFRESH_EXPIRES_IN=7d - -# ----------------------------------------------------------------------------- -# CORS -# ----------------------------------------------------------------------------- -CORS_ORIGIN=http://localhost:5173,http://localhost:3001 -CORS_CREDENTIALS=true - -# ----------------------------------------------------------------------------- -# LOGGING -# ----------------------------------------------------------------------------- -LOG_LEVEL=debug -LOG_FORMAT=dev - -# ----------------------------------------------------------------------------- -# FILE STORAGE (S3 Compatible) -# ----------------------------------------------------------------------------- -STORAGE_TYPE=local -# STORAGE_TYPE=s3 -# S3_BUCKET=construccion-files -# S3_REGION=us-east-1 -# S3_ACCESS_KEY_ID=your-access-key -# S3_SECRET_ACCESS_KEY=your-secret-key -# S3_ENDPOINT=https://s3.amazonaws.com - -# Local storage path (when STORAGE_TYPE=local) -UPLOAD_PATH=./uploads - -# ----------------------------------------------------------------------------- -# EMAIL (SMTP) -# ----------------------------------------------------------------------------- -SMTP_HOST=localhost -SMTP_PORT=1025 -SMTP_USER= -SMTP_PASSWORD= -SMTP_FROM=noreply@construccion.local - -# ----------------------------------------------------------------------------- -# WHATSAPP BUSINESS API (Optional) -# ----------------------------------------------------------------------------- -# WHATSAPP_API_URL=https://graph.facebook.com/v17.0 -# WHATSAPP_PHONE_NUMBER_ID=your-phone-number-id -# WHATSAPP_ACCESS_TOKEN=your-access-token -# WHATSAPP_VERIFY_TOKEN=your-verify-token - -# ----------------------------------------------------------------------------- -# INTEGRATIONS (Optional) -# ----------------------------------------------------------------------------- -# IMSS -# IMSS_API_URL=https://api.imss.gob.mx -# IMSS_CERTIFICATE_PATH=/certs/imss.p12 -# IMSS_CERTIFICATE_PASSWORD= - -# INFONAVIT -# INFONAVIT_API_URL=https://api.infonavit.org.mx -# INFONAVIT_CLIENT_ID= -# INFONAVIT_CLIENT_SECRET= - -# SAT (CFDI) -# PAC_URL=https://api.pac.com.mx -# PAC_USER= -# PAC_PASSWORD= - -# ----------------------------------------------------------------------------- -# FEATURE FLAGS -# ----------------------------------------------------------------------------- -FEATURE_HSE_AI=false -FEATURE_WHATSAPP_BOT=false -FEATURE_BIOMETRIC=false - -# ----------------------------------------------------------------------------- -# DOCKER COMPOSE OVERRIDES -# ----------------------------------------------------------------------------- -# Used by docker-compose.yml -BACKEND_PORT=3000 -FRONTEND_PORT=5173 -ADMINER_PORT=8080 -MAILHOG_SMTP_PORT=1025 -MAILHOG_WEB_PORT=8025 -BUILD_TARGET=development diff --git a/projects/erp-construccion/.github/workflows/ci.yml b/projects/erp-construccion/.github/workflows/ci.yml deleted file mode 100644 index 06ddc321b..000000000 --- a/projects/erp-construccion/.github/workflows/ci.yml +++ /dev/null @@ -1,263 +0,0 @@ -# ============================================================================= -# CI Pipeline - ERP Construccion -# Runs on every push and pull request -# ============================================================================= - -name: CI Pipeline - -on: - push: - branches: [main, develop, 'feature/**'] - pull_request: - branches: [main, develop] - -env: - NODE_VERSION: '20' - POSTGRES_USER: test_user - POSTGRES_PASSWORD: test_password - POSTGRES_DB: test_db - -jobs: - # =========================================================================== - # Lint & Type Check - # =========================================================================== - lint: - name: Lint & Type Check - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: | - backend/package-lock.json - frontend/web/package-lock.json - - - name: Install Backend Dependencies - working-directory: backend - run: npm ci - - - name: Install Frontend Dependencies - working-directory: frontend/web - run: npm ci - - - name: Lint Backend - working-directory: backend - run: npm run lint - - - name: Lint Frontend - working-directory: frontend/web - run: npm run lint || true - - - name: Type Check Backend - working-directory: backend - run: npm run build -- --noEmit || npm run build - - # =========================================================================== - # Validate Constants (SSOT) - # =========================================================================== - validate-constants: - name: Validate SSOT Constants - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: backend/package-lock.json - - - name: Install Dependencies - working-directory: backend - run: npm ci - - - name: Run Constants Validation - working-directory: backend - run: npm run validate:constants || echo "Validation script not yet implemented" - - # =========================================================================== - # Unit Tests - Backend - # =========================================================================== - test-backend: - name: Backend Tests - runs-on: ubuntu-latest - needs: [lint] - services: - postgres: - image: postgis/postgis:15-3.3-alpine - env: - POSTGRES_USER: ${{ env.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} - POSTGRES_DB: ${{ env.POSTGRES_DB }} - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - redis: - image: redis:7-alpine - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: backend/package-lock.json - - - name: Install Dependencies - working-directory: backend - run: npm ci - - - name: Run Unit Tests - working-directory: backend - run: npm run test -- --coverage --passWithNoTests - env: - DB_HOST: localhost - DB_PORT: 5432 - DB_USER: ${{ env.POSTGRES_USER }} - DB_PASSWORD: ${{ env.POSTGRES_PASSWORD }} - DB_NAME: ${{ env.POSTGRES_DB }} - REDIS_HOST: localhost - REDIS_PORT: 6379 - - - name: Upload Coverage Report - uses: codecov/codecov-action@v3 - if: always() - with: - files: backend/coverage/lcov.info - flags: backend - fail_ci_if_error: false - - # =========================================================================== - # Unit Tests - Frontend - # =========================================================================== - test-frontend: - name: Frontend Tests - runs-on: ubuntu-latest - needs: [lint] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: frontend/web/package-lock.json - - - name: Install Dependencies - working-directory: frontend/web - run: npm ci - - - name: Run Unit Tests - working-directory: frontend/web - run: npm run test -- --coverage --passWithNoTests || echo "No tests yet" - - - name: Upload Coverage Report - uses: codecov/codecov-action@v3 - if: always() - with: - files: frontend/web/coverage/lcov.info - flags: frontend - fail_ci_if_error: false - - # =========================================================================== - # Build Check - # =========================================================================== - build: - name: Build - runs-on: ubuntu-latest - needs: [test-backend, test-frontend] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Build Backend - working-directory: backend - run: | - npm ci - npm run build - - - name: Build Frontend - working-directory: frontend/web - run: | - npm ci - npm run build - - - name: Upload Backend Artifacts - uses: actions/upload-artifact@v3 - with: - name: backend-dist - path: backend/dist - retention-days: 7 - - - name: Upload Frontend Artifacts - uses: actions/upload-artifact@v3 - with: - name: frontend-dist - path: frontend/web/dist - retention-days: 7 - - # =========================================================================== - # Docker Build (only on main/develop) - # =========================================================================== - docker: - name: Docker Build - runs-on: ubuntu-latest - needs: [build] - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build Backend Docker Image - uses: docker/build-push-action@v5 - with: - context: ./backend - file: ./backend/Dockerfile - target: production - push: false - tags: construccion-backend:${{ github.sha }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build Frontend Docker Image - uses: docker/build-push-action@v5 - with: - context: ./frontend/web - file: ./frontend/web/Dockerfile - target: production - push: false - tags: construccion-frontend:${{ github.sha }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/projects/erp-construccion/CONTRIBUTING.md b/projects/erp-construccion/CONTRIBUTING.md deleted file mode 100644 index 9247419db..000000000 --- a/projects/erp-construccion/CONTRIBUTING.md +++ /dev/null @@ -1,478 +0,0 @@ -# Guia de Contribucion - ERP Construccion - -Esta guia describe las convenciones y procesos para contribuir al proyecto. - ---- - -## Requisitos Previos - -- Node.js 20 LTS -- Docker y Docker Compose -- PostgreSQL 15 (o usar docker-compose) -- Git - ---- - -## Setup Inicial - -```bash -# 1. Clonar repositorio -git clone -cd apps/verticales/construccion - -# 2. Instalar dependencias -cd backend && npm install - -# 3. Configurar variables de entorno -cp ../.env.example .env -# Editar .env con credenciales locales - -# 4. Levantar servicios -docker-compose up -d postgres redis - -# 5. Ejecutar migraciones (si aplica) -npm run migration:run - -# 6. Iniciar en desarrollo -npm run dev -``` - ---- - -## Estructura de Ramas - -| Rama | Proposito | -|------|-----------| -| `main` | Produccion estable | -| `develop` | Integracion de features | -| `feature/*` | Nuevas funcionalidades | -| `fix/*` | Correcciones de bugs | -| `refactor/*` | Refactorizaciones | -| `docs/*` | Documentacion | - -### Flujo de Trabajo - -``` -1. Crear rama desde develop - git checkout develop - git pull origin develop - git checkout -b feature/MAI-XXX-descripcion - -2. Desarrollar y hacer commits - git commit -m "feat(modulo): descripcion del cambio" - -3. Push y crear Pull Request - git push origin feature/MAI-XXX-descripcion - # Crear PR en GitHub hacia develop - -4. Code Review y merge -``` - ---- - -## Convenciones de Codigo - -### Nomenclatura - -| Tipo | Convencion | Ejemplo | -|------|------------|---------| -| Archivos | kebab-case.tipo.ts | `concepto.entity.ts` | -| Clases | PascalCase + sufijo | `ConceptoService` | -| Interfaces | PascalCase + prefijo I | `IConceptoRepository` | -| Variables | camelCase | `totalAmount` | -| Constantes | UPPER_SNAKE_CASE | `DB_SCHEMAS` | -| Metodos | camelCase + verbo | `findByContrato` | -| Enums | PascalCase | `EstimacionStatus` | -| Tablas DB | snake_case plural | `presupuestos` | -| Columnas DB | snake_case | `tenant_id` | - -### Estructura de Archivos por Modulo - -``` -modules/ -鈹斺攢鈹 nombre-modulo/ - 鈹溾攢鈹 entities/ - 鈹 鈹斺攢鈹 entidad.entity.ts - 鈹溾攢鈹 services/ - 鈹 鈹斺攢鈹 entidad.service.ts - 鈹溾攢鈹 controllers/ - 鈹 鈹斺攢鈹 entidad.controller.ts - 鈹溾攢鈹 dto/ - 鈹 鈹斺攢鈹 entidad.dto.ts - 鈹斺攢鈹 index.ts -``` - -### Patron Entity - -```typescript -import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm'; -import { DB_SCHEMAS, DB_TABLES } from '@shared/constants'; - -@Entity({ - schema: DB_SCHEMAS.CONSTRUCTION, - name: DB_TABLES.construction.CONCEPTOS, -}) -@Index(['tenantId', 'code'], { unique: true }) -export class Concepto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'code', length: 20 }) - code: string; - - // Usar snake_case en name: para mapear a DB - @Column({ name: 'created_at', type: 'timestamptz', default: () => 'NOW()' }) - createdAt: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; -} -``` - -### Patron Service - -```typescript -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext } from '@shared/services/base.service'; -import { Concepto } from '../entities/concepto.entity'; - -export class ConceptoService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - // Metodos especificos del dominio - async findByCode(ctx: ServiceContext, code: string): Promise { - return this.findOne(ctx, { code }); - } -} -``` - -### Patron DTO - -```typescript -import { IsString, IsOptional, IsUUID, MinLength, MaxLength } from 'class-validator'; - -export class CreateConceptoDto { - @IsString() - @MinLength(1) - @MaxLength(20) - code: string; - - @IsString() - @MinLength(1) - @MaxLength(200) - name: string; - - @IsOptional() - @IsUUID() - parentId?: string; -} - -export class UpdateConceptoDto { - @IsOptional() - @IsString() - @MaxLength(200) - name?: string; - - @IsOptional() - @IsNumber() - unitPrice?: number; -} -``` - ---- - -## SSOT - Constantes Centralizadas - -### Regla Principal - -**NUNCA** hardcodear: -- Nombres de schemas -- Nombres de tablas -- Rutas de API -- Valores de enums - -### Uso Correcto - -```typescript -// INCORRECTO -@Entity({ schema: 'construction', name: 'conceptos' }) - -// CORRECTO -import { DB_SCHEMAS, DB_TABLES } from '@shared/constants'; - -@Entity({ - schema: DB_SCHEMAS.CONSTRUCTION, - name: DB_TABLES.construction.CONCEPTOS, -}) -``` - -### Validacion Automatica - -```bash -# Detecta hardcoding de constantes -npm run validate:constants - -# Se ejecuta en pre-commit y CI -``` - ---- - -## Commits - -### Formato de Mensaje - -``` -tipo(alcance): descripcion breve - -[cuerpo opcional] - -[footer opcional] -``` - -### Tipos de Commit - -| Tipo | Descripcion | -|------|-------------| -| `feat` | Nueva funcionalidad | -| `fix` | Correccion de bug | -| `refactor` | Refactorizacion sin cambio funcional | -| `docs` | Documentacion | -| `test` | Tests | -| `chore` | Mantenimiento, dependencias | -| `style` | Formato, sin cambio de logica | -| `perf` | Mejora de performance | - -### Ejemplos - -```bash -# Feature -git commit -m "feat(budgets): agregar versionamiento de presupuestos" - -# Fix -git commit -m "fix(estimates): corregir calculo de totales con decimales" - -# Refactor -git commit -m "refactor(auth): simplificar validacion de tokens" - -# Docs -git commit -m "docs(api): actualizar especificacion OpenAPI" -``` - ---- - -## Testing - -### Ejecutar Tests - -```bash -# Todos los tests -npm test - -# Con cobertura -npm run test:coverage - -# Watch mode -npm run test:watch - -# Archivo especifico -npm test -- concepto.service.spec.ts -``` - -### Estructura de Test - -```typescript -describe('ConceptoService', () => { - let service: ConceptoService; - let mockRepo: jest.Mocked>; - - const ctx: ServiceContext = { - tenantId: 'test-tenant-uuid', - userId: 'test-user-uuid', - }; - - beforeEach(() => { - mockRepo = createMockRepository(); - service = new ConceptoService(mockRepo); - }); - - describe('createConcepto', () => { - it('should create concepto with level 0 for root', async () => { - // Arrange - const dto = { code: '001', name: 'Test' }; - mockRepo.save.mockResolvedValue({ id: 'uuid', ...dto, level: 0 }); - - // Act - const result = await service.createConcepto(ctx, dto); - - // Assert - expect(result.level).toBe(0); - }); - - it('should throw on duplicate code', async () => { - // ... - }); - }); -}); -``` - -### Cobertura Minima - -- Statements: 80% -- Branches: 75% -- Functions: 80% -- Lines: 80% - ---- - -## Linting y Formato - -### Ejecutar Linting - -```bash -# Verificar errores -npm run lint - -# Corregir automaticamente -npm run lint:fix -``` - -### Configuracion ESLint - -El proyecto usa ESLint con TypeScript. Reglas principales: - -- No `any` implicito -- No variables no usadas -- Imports ordenados -- Espacios consistentes - -### Pre-commit Hook - -Se ejecuta automaticamente: - -```bash -npm run precommit # lint + validate:constants -``` - ---- - -## Pull Requests - -### Checklist - -Antes de crear un PR, verificar: - -- [ ] Tests pasan: `npm test` -- [ ] Linting pasa: `npm run lint` -- [ ] Constantes validadas: `npm run validate:constants` -- [ ] Build funciona: `npm run build` -- [ ] Documentacion actualizada (si aplica) -- [ ] Commits con formato correcto - -### Template de PR - -```markdown -## Descripcion - -Breve descripcion del cambio. - -## Tipo de Cambio - -- [ ] Feature -- [ ] Bug fix -- [ ] Refactor -- [ ] Docs -- [ ] Tests - -## Modulo Afectado - -- MAI-XXX: Nombre del modulo - -## Testing - -Describir como probar los cambios. - -## Screenshots (si aplica) - -## Checklist - -- [ ] Tests agregados/actualizados -- [ ] Documentacion actualizada -- [ ] Sin breaking changes (o documentados) -``` - ---- - -## Migraciones de Base de Datos - -### Generar Migracion - -```bash -# Despues de modificar entidades -npm run migration:generate -- -n NombreDescriptivo -``` - -### Ejecutar Migraciones - -```bash -# Aplicar pendientes -npm run migration:run - -# Revertir ultima -npm run migration:revert -``` - -### Convenciones - -- Una migracion por feature/fix -- Nombres descriptivos: `AddStatusToEstimaciones` -- Incluir rollback funcional -- No modificar migraciones ya aplicadas en produccion - ---- - -## Debugging - -### VS Code - -```json -// .vscode/launch.json -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Debug Backend", - "runtimeArgs": ["-r", "ts-node/register"], - "args": ["${workspaceFolder}/backend/src/server.ts"], - "env": { - "NODE_ENV": "development", - "LOG_LEVEL": "debug" - } - } - ] -} -``` - -### Variables de Entorno para Debug - -```bash -LOG_LEVEL=debug -TYPEORM_LOGGING=true -``` - ---- - -## Contacto - -- **Issues:** Reportar bugs o sugerir features en GitHub Issues -- **PRs:** Siempre bienvenidos, seguir las convenciones - ---- - -**Ultima actualizacion:** 2025-12-12 diff --git a/projects/erp-construccion/INVENTARIO.yml b/projects/erp-construccion/INVENTARIO.yml deleted file mode 100644 index 6bf6d6f5b..000000000 --- a/projects/erp-construccion/INVENTARIO.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Inventario generado por EPIC-008 -proyecto: erp-construccion -fecha: "2026-01-04" -generado_por: "inventory-project.sh v1.0.0" - -inventario: - docs: - total: 496 - por_tipo: - markdown: 453 - yaml: 32 - json: 0 - orchestration: - total: 31 - por_tipo: - markdown: 21 - yaml: 9 - json: 1 - -problemas: - archivos_obsoletos: 0 - referencias_antiguas: 0 - simco_faltantes: - - _MAP.md en docs/ - - PROJECT-STATUS.md - -estado_simco: - herencia_simco: true - contexto_proyecto: true - map_docs: false - project_status: false diff --git a/projects/erp-construccion/PROJECT-STATUS.md b/projects/erp-construccion/PROJECT-STATUS.md deleted file mode 100644 index fbda0049c..000000000 --- a/projects/erp-construccion/PROJECT-STATUS.md +++ /dev/null @@ -1,310 +0,0 @@ -# ESTADO DEL PROYECTO - ERP Construccion - -**Proyecto:** ERP Construccion (Proyecto Independiente) -**Estado:** 馃毀 En desarrollo -**Progreso:** 60% -**Ultima actualizacion:** 2025-12-12 - ---- - -## 馃啎 CAMBIOS RECIENTES (2025-12-12) - -### Documentacion Tecnica - COMPLETADA - -- 鉁 **Documentacion de Arquitectura** - - `docs/ARCHITECTURE.md` - Arquitectura tecnica para desarrolladores - - Patrones de codigo (Entity, Service, DTO) - - Configuracion multi-tenant RLS - - Guia de debugging y performance - -- 鉁 **Guia de Contribucion** - - `CONTRIBUTING.md` - Guia completa para desarrolladores - - Convenciones de codigo y nomenclatura - - Flujo de trabajo con Git - - Estructura de commits y PRs - -- 鉁 **Documentacion de API** - - `docs/api/openapi.yaml` - Especificacion OpenAPI 3.0.3 - - `docs/backend/API-REFERENCE.md` - Referencia de endpoints - - Ejemplos de request/response - - Codigos de error y rate limiting - -- 鉁 **Documentacion de Modulos** - - `docs/backend/MODULES.md` - Documentacion detallada - - Entidades y campos de cada modulo - - Metodos de servicios - - Ejemplos de uso - -### Fase 2: Backend Core Modules - COMPLETADA - -- 鉁 **MAI-003 Presupuestos - Entidades y Services** - - `Concepto` entity - Cat谩logo jer谩rquico de conceptos - - `Presupuesto` entity - Presupuestos versionados - - `PresupuestoPartida` entity - L铆neas de presupuesto - - `ConceptoService` - 脕rbol de conceptos, b煤squeda - - `PresupuestoService` - CRUD, versionamiento, aprobaci贸n - -- 鉁 **MAI-005 Control de Obra - Entidades y Services** - - `AvanceObra` entity - Avances f铆sicos - - `FotoAvance` entity - Evidencias fotogr谩ficas con GPS - - `BitacoraObra` entity - Bit谩cora diaria - - `ProgramaObra` entity - Programa maestro - - `ProgramaActividad` entity - WBS/Actividades - - `AvanceObraService` - Workflow captura鈫抮evisi贸n鈫抋probaci贸n - - `BitacoraObraService` - Entradas secuenciales, estad铆sticas - -- 鉁 **MAI-008 Estimaciones - Entidades y Services** - - `Estimacion` entity - Estimaciones peri贸dicas - - `EstimacionConcepto` entity - L铆neas con acumulados - - `Generador` entity - N煤meros generadores - - `Anticipo` entity - Anticipos con amortizaci贸n - - `Amortizacion` entity - Descuentos por estimaci贸n - - `Retencion` entity - Fondo de garant铆a, impuestos - - `FondoGarantia` entity - Acumulado por contrato - - `EstimacionWorkflow` entity - Historial de estados - - `EstimacionService` - Workflow completo, c谩lculo de totales - -- 鉁 **M贸dulo Auth - JWT + Refresh Tokens** - - `AuthService` - Login, register, refresh, logout - - `AuthMiddleware` - Autenticaci贸n, autorizaci贸n por roles - - DTOs tipados para todas las operaciones - - Configuraci贸n de RLS con tenant_id - -- 鉁 **Base Service Pattern** - - `BaseService` - CRUD multi-tenant gen茅rico - - Paginaci贸n con metadata - - Soft delete con audit columns - - Contexto de servicio (tenantId, userId) - -### Fase 1: Fundamentos Arquitectonicos - COMPLETADA - -- 鉁 **Sistema SSOT implementado** - - `database.constants.ts` - Schemas, tablas, columnas - - `api.constants.ts` - Rutas API centralizadas - - `enums.constants.ts` - Todos los enums del sistema - - Index actualizado con exports centralizados - -- 鉁 **Path Aliases configurados** (ya existian) - - `@shared/*`, `@modules/*`, `@config/*`, `@types/*`, `@utils/*` - -- 鉁 **Docker + docker-compose** - - PostgreSQL 15 + PostGIS - - Redis 7 - - Backend Node.js - - Frontend React + Vite - - Adminer (dev) - - Mailhog (dev) - -- 鉁 **CI/CD GitHub Actions** - - Lint & Type Check - - Validate SSOT Constants - - Unit Tests (Backend + Frontend) - - Build Check - - Docker Build - -- 鉁 **Scripts de validacion** - - `validate-constants-usage.ts` - Detecta hardcoding - - `sync-enums.ts` - Sincroniza Backend 鈫 Frontend - -- 鉁 **Documentacion de DB** - - `database/_MAP.md` - Mapa completo de schemas - ---- - -## 馃搳 RESUMEN EJECUTIVO - -| 脕rea | Implementado | Documentado | Estado | -|------|-------------|-------------|--------| -| **DDL/Schemas** | 7 schemas, 110 tablas | 7 schemas, 110 tablas | 100% | -| **Backend** | 7 m贸dulos, 30 entidades, 8 services | 18 m贸dulos | 45% | -| **Frontend** | Estructura base | 18 m贸dulos | 5% | -| **Documentaci贸n T茅cnica** | OpenAPI, ARCHITECTURE, CONTRIBUTING | API, M贸dulos, Arquitectura | 100% | -| **Documentaci贸n Funcional** | - | 449+ archivos MD | 100% | - ---- - -## 馃梽锔 BASE DE DATOS - -### Schemas Implementados (DDL) -| Schema | Tablas | ENUMs | Archivo DDL | -|--------|--------|-------|-------------| -| `construction` | 24 | 7 | `01-construction-schema-ddl.sql` | -| `hr` | 8 | - | `02-hr-schema-ddl.sql` | -| `hse` | 58 | 67 | `03-hse-schema-ddl.sql` | -| `estimates` | 8 | 4 | `04-estimates-schema-ddl.sql` | -| `infonavit` | 8 | - | `05-infonavit-schema-ddl.sql` | -| `inventory` | 4 | - | `06-inventory-ext-schema-ddl.sql` | -| `purchase` | 5 | - | `07-purchase-ext-schema-ddl.sql` | -| **Total** | **110** | **78** | | - -### DDL Completo -Todos los schemas han sido implementados con: -- RLS (Row Level Security) para multi-tenancy -- Indices optimizados -- Funciones auxiliares (ej: `calculate_estimate_totals`) - ---- - -## 馃捇 BACKEND - -### M贸dulos con C贸digo -``` -backend/src/modules/ -鈹溾攢鈹 auth/ 鉁 Autenticaci贸n JWT completa -鈹 鈹溾攢鈹 services/auth.service.ts -鈹 鈹溾攢鈹 middleware/auth.middleware.ts -鈹 鈹斺攢鈹 dto/auth.dto.ts -鈹溾攢鈹 budgets/ 鉁 Presupuestos (MAI-003) -鈹 鈹溾攢鈹 entities/concepto.entity.ts -鈹 鈹溾攢鈹 entities/presupuesto.entity.ts -鈹 鈹溾攢鈹 entities/presupuesto-partida.entity.ts -鈹 鈹溾攢鈹 services/concepto.service.ts -鈹 鈹斺攢鈹 services/presupuesto.service.ts -鈹溾攢鈹 progress/ 鉁 Control de Obra (MAI-005) -鈹 鈹溾攢鈹 entities/avance-obra.entity.ts -鈹 鈹溾攢鈹 entities/foto-avance.entity.ts -鈹 鈹溾攢鈹 entities/bitacora-obra.entity.ts -鈹 鈹溾攢鈹 entities/programa-obra.entity.ts -鈹 鈹溾攢鈹 entities/programa-actividad.entity.ts -鈹 鈹溾攢鈹 services/avance-obra.service.ts -鈹 鈹斺攢鈹 services/bitacora-obra.service.ts -鈹溾攢鈹 estimates/ 鉁 Estimaciones (MAI-008) -鈹 鈹溾攢鈹 entities/estimacion.entity.ts -鈹 鈹溾攢鈹 entities/estimacion-concepto.entity.ts -鈹 鈹溾攢鈹 entities/generador.entity.ts -鈹 鈹溾攢鈹 entities/anticipo.entity.ts -鈹 鈹溾攢鈹 entities/amortizacion.entity.ts -鈹 鈹溾攢鈹 entities/retencion.entity.ts -鈹 鈹溾攢鈹 entities/fondo-garantia.entity.ts -鈹 鈹溾攢鈹 entities/estimacion-workflow.entity.ts -鈹 鈹斺攢鈹 services/estimacion.service.ts -鈹溾攢鈹 construction/ 鉁 Proyectos (MAI-002) -鈹 鈹溾攢鈹 entities/proyecto.entity.ts -鈹 鈹斺攢鈹 entities/fraccionamiento.entity.ts -鈹溾攢鈹 hr/ 鉁 RRHH (MAI-007) -鈹 鈹溾攢鈹 entities/employee.entity.ts -鈹 鈹溾攢鈹 entities/puesto.entity.ts -鈹 鈹斺攢鈹 entities/employee-fraccionamiento.entity.ts -鈹溾攢鈹 hse/ 鉁 HSE (MAA-017) -鈹 鈹溾攢鈹 entities/incidente.entity.ts -鈹 鈹溾攢鈹 entities/incidente-involucrado.entity.ts -鈹 鈹溾攢鈹 entities/incidente-accion.entity.ts -鈹 鈹斺攢鈹 entities/capacitacion.entity.ts -鈹溾攢鈹 core/ 鉁 Base multi-tenant -鈹 鈹溾攢鈹 entities/user.entity.ts -鈹 鈹斺攢鈹 entities/tenant.entity.ts -鈹斺攢鈹 shared/ 鉁 Servicios compartidos - 鈹斺攢鈹 services/base.service.ts -``` - -### Pendientes -- Controllers REST para m贸dulos nuevos -- 8 m贸dulos MAI sin c贸digo backend -- 3 m贸dulos MAE sin c贸digo backend -- Frontend integraci贸n con API - ---- - -## 馃搵 M脫DULOS (18 Total) - -### Fase 1 - MAI (14 m贸dulos) -| C贸digo | Nombre | DDL | Backend | Docs | -|--------|--------|:---:|:-------:|:----:| -| MAI-001 | Fundamentos | - | 鉁 | 鉁 | -| MAI-002 | Proyectos y Estructura | 鉁 | 鉁 | 鉁 | -| MAI-003 | Presupuestos y Costos | 鉁 | 鉁 | 鉁 | -| MAI-004 | Compras e Inventarios | 鉁 | 鈴 | 鉁 | -| MAI-005 | Control de Obra | 鉁 | 鉁 | 鉁 | -| MAI-006 | Reportes y Analytics | - | 鉂 | 鉁 | -| MAI-007 | RRHH y Asistencias | 鉁 | 鉁 | 鉁 | -| MAI-008 | Estimaciones | 鉁 | 鉁 | 鉁 | -| MAI-009 | Calidad y Postventa | 鉁 | 鈴 | 鉁 | -| MAI-010 | CRM Derechohabientes | 鈴 | 鉂 | 鉁 | -| MAI-011 | INFONAVIT | 鉁 | 鈴 | 鉁 | -| MAI-012 | Contratos | 鉁 | 鈴 | 鉁 | -| MAI-013 | Administraci贸n | - | 鉂 | 鉁 | -| MAI-018 | Preconstrucci贸n | 鈴 | 鉂 | 鉁 | - -### Fase 2 - MAE (3 m贸dulos) -| C贸digo | Nombre | DDL | Backend | Docs | -|--------|--------|:---:|:-------:|:----:| -| MAE-014 | Finanzas y Controlling | 鈴 | 鉂 | 鉁 | -| MAE-015 | Activos y Maquinaria | 鈴 | 鉂 | 鉁 | -| MAE-016 | Gesti贸n Documental | 鈴 | 鉂 | 鉁 | - -### Fase 3 - MAA (1 m贸dulo) -| C贸digo | Nombre | DDL | Backend | Docs | -|--------|--------|:---:|:-------:|:----:| -| MAA-017 | Seguridad HSE | 鉁 | 鉁 | 鉁 | - -**Leyenda:** 鉁 Implementado | 鈴 En progreso | 鉂 No iniciado | - No aplica - ---- - -## 馃幆 PR脫XIMOS PASOS - -### Inmediato -1. 鉁 ~~Implementar DDL de `estimates` schema~~ - COMPLETADO -2. 鉁 ~~Implementar DDL de `infonavit` schema~~ - COMPLETADO -3. 鉁 ~~Backend MAI-003 Presupuestos~~ - COMPLETADO -4. 鉁 ~~Backend MAI-005 Control de Obra~~ - COMPLETADO -5. 鉁 ~~Backend MAI-008 Estimaciones~~ - COMPLETADO -6. 鉁 ~~M贸dulo Auth JWT completo~~ - COMPLETADO - -### Corto Plazo -1. Crear Controllers REST para m贸dulos nuevos -2. Implementar backend de MAI-009 (Calidad y Postventa) -3. Implementar backend de MAI-011 (INFONAVIT) -4. Implementar backend de MAI-012 (Contratos) -5. Testing de m贸dulos existentes - -### Mediano Plazo -6. Frontend: Integraci贸n con API -7. Frontend: M贸dulos de Presupuestos y Estimaciones -8. Implementar Curva S y reportes de avance - ---- - -## 馃搧 ARCHIVOS CLAVE - -### Codigo -- **DDL:** `database/schemas/*.sql` -- **Backend:** `backend/src/modules/` -- **Services:** `backend/src/shared/services/base.service.ts` -- **Auth:** `backend/src/modules/auth/` -- **Constants SSOT:** `backend/src/shared/constants/` - -### Documentacion Tecnica -- **Arquitectura:** `docs/ARCHITECTURE.md` -- **Contribucion:** `CONTRIBUTING.md` -- **API OpenAPI:** `docs/api/openapi.yaml` -- **API Reference:** `docs/backend/API-REFERENCE.md` -- **Modulos Backend:** `docs/backend/MODULES.md` - -### Documentacion Funcional -- **Modulos:** `docs/02-definicion-modulos/` -- **Vision General:** `docs/00-vision-general/` -- **Arquitectura SaaS:** `docs/00-vision-general/ARQUITECTURA-SAAS.md` -- **Mapa DB:** `database/_MAP.md` - ---- - -## 馃搱 M脡TRICAS - -| M茅trica | Valor | -|---------|-------| -| Archivos MD Funcionales | 449+ | -| Archivos MD Tecnicos | 5 (ARCHITECTURE, CONTRIBUTING, API-REF, MODULES, openapi) | -| Requerimientos (RF) | 87 | -| Especificaciones (ET) | 78 | -| User Stories | 149 | -| Story Points | 692 | -| ADRs | 12 | -| Entidades TypeORM | 30 | -| Services Backend | 8 | -| Tablas DDL | 110 | -| Endpoints API Documentados | 35+ | - ---- - -**脷ltima actualizaci贸n:** 2025-12-12 diff --git a/projects/erp-construccion/README.md b/projects/erp-construccion/README.md deleted file mode 100644 index d0320c2bd..000000000 --- a/projects/erp-construccion/README.md +++ /dev/null @@ -1,364 +0,0 @@ -# ERP Construccion - Sistema de Administracion de Obra e INFONAVIT - -Sistema ERP especializado para empresas de construccion de vivienda con integracion INFONAVIT. Arquitectura multi-tenant SaaS con Row Level Security (RLS). - -## Estado del Proyecto - -| Campo | Valor | -|-------|-------| -| **Estado** | 馃毀 En desarrollo (55%) | -| **Version** | 0.2.0 | -| **Modulos** | 18 (14 MAI + 3 MAE + 1 MAA) | -| **DDL Schemas** | 7 (110 tablas) | -| **Entidades Backend** | 30 | -| **Services Backend** | 8 | - ---- - -## Quick Start - -### Prerequisitos - -- Node.js >= 18.0.0 -- Docker & Docker Compose -- PostgreSQL 15+ con PostGIS (incluido en docker-compose) - -### Instalacion - -```bash -# Clonar repositorio -cd apps/verticales/construccion - -# Copiar variables de entorno -cp .env.example .env - -# Levantar servicios con Docker -docker-compose up -d - -# O desarrollo local -cd backend && npm install && npm run dev -``` - -### URLs de Desarrollo - -| Servicio | URL | -|----------|-----| -| Backend API | http://localhost:3000 | -| Frontend | http://localhost:5173 | -| Adminer (DB) | http://localhost:8080 | -| Mailhog | http://localhost:8025 | - ---- - -## Arquitectura - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Frontend (React) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Budgets 鈹 鈹侾rogress 鈹 鈹侲stimates鈹 鈹 HSE 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 鈹 鈹 鈹 -鈹屸攢鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Backend (Express + TypeORM) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Auth Middleware (JWT + RLS) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Budgets 鈹 鈹侾rogress 鈹 鈹侲stimates鈹 鈹 Auth 鈹 鈹 -鈹 鈹 Service 鈹 鈹 Service 鈹 鈹 Service 鈹 鈹 Service 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹攢鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 鈹 鈹 鈹 -鈹屸攢鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 PostgreSQL 15 + PostGIS 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Row Level Security (tenant_id) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 auth 鈹 鈹俢onstru.鈹 鈹 hr 鈹 鈹 hse 鈹 鈹俥stim.鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -### Stack Tecnologico - -| Capa | Tecnologia | -|------|------------| -| **Backend** | Node.js 20, Express 4, TypeORM 0.3 | -| **Frontend** | React 18, Vite 5, TypeScript 5 | -| **Database** | PostgreSQL 15 + PostGIS | -| **Cache** | Redis 7 | -| **Auth** | JWT + Refresh Tokens | -| **Multi-tenant** | RLS (Row Level Security) | - ---- - -## Estructura del Proyecto - -``` -construccion/ -鈹溾攢鈹 backend/ -鈹 鈹斺攢鈹 src/ -鈹 鈹溾攢鈹 modules/ -鈹 鈹 鈹溾攢鈹 auth/ # Autenticacion JWT -鈹 鈹 鈹溾攢鈹 budgets/ # MAI-003 Presupuestos -鈹 鈹 鈹溾攢鈹 progress/ # MAI-005 Control de Obra -鈹 鈹 鈹溾攢鈹 estimates/ # MAI-008 Estimaciones -鈹 鈹 鈹溾攢鈹 construction/ # MAI-002 Proyectos -鈹 鈹 鈹溾攢鈹 hr/ # MAI-007 RRHH -鈹 鈹 鈹溾攢鈹 hse/ # MAA-017 HSE -鈹 鈹 鈹斺攢鈹 core/ # Entidades base -鈹 鈹斺攢鈹 shared/ -鈹 鈹溾攢鈹 constants/ # SSOT (schemas, rutas, enums) -鈹 鈹斺攢鈹 services/ # BaseService multi-tenant -鈹溾攢鈹 frontend/ -鈹 鈹溾攢鈹 web/ # App web React -鈹 鈹斺攢鈹 mobile/ # App movil (futuro) -鈹溾攢鈹 database/ -鈹 鈹斺攢鈹 schemas/ # DDL por schema -鈹 鈹溾攢鈹 01-construction-schema-ddl.sql # 24 tablas -鈹 鈹溾攢鈹 02-hr-schema-ddl.sql # 8 tablas -鈹 鈹溾攢鈹 03-hse-schema-ddl.sql # 58 tablas -鈹 鈹溾攢鈹 04-estimates-schema-ddl.sql # 8 tablas -鈹 鈹溾攢鈹 05-infonavit-schema-ddl.sql # 8 tablas -鈹 鈹溾攢鈹 06-inventory-ext-schema-ddl.sql # 4 tablas -鈹 鈹斺攢鈹 07-purchase-ext-schema-ddl.sql # 5 tablas -鈹溾攢鈹 docs/ # Documentacion completa -鈹溾攢鈹 devops/ -鈹 鈹斺攢鈹 scripts/ # Validacion SSOT -鈹溾攢鈹 docker-compose.yml -鈹斺攢鈹 .env.example -``` - ---- - -## Modulos Implementados - -### MAI-003: Presupuestos y Costos 鉁 - -```typescript -// Entidades -- Concepto // Catalogo jerarquico de conceptos -- Presupuesto // Presupuestos versionados -- PresupuestoPartida // Lineas con calculo automatico - -// Services -- ConceptoService // Arbol, busqueda -- PresupuestoService // CRUD, versionamiento, aprobacion -``` - -### MAI-005: Control de Obra 鉁 - -```typescript -// Entidades -- AvanceObra // Avances fisicos con workflow -- FotoAvance // Evidencias con GPS -- BitacoraObra // Bitacora diaria -- ProgramaObra // Programa maestro -- ProgramaActividad // WBS/Actividades - -// Services -- AvanceObraService // Workflow captura->revision->aprobacion -- BitacoraObraService // Entradas secuenciales -``` - -### MAI-008: Estimaciones 鉁 - -```typescript -// Entidades -- Estimacion // Estimaciones periodicas -- EstimacionConcepto // Lineas con acumulados -- Generador // Numeros generadores -- Anticipo // Anticipos con amortizacion -- Amortizacion // Descuentos por estimacion -- Retencion // Fondo de garantia -- FondoGarantia // Acumulado por contrato -- EstimacionWorkflow // Historial de estados - -// Services -- EstimacionService // Workflow completo, calculo totales -``` - -### Auth: Autenticacion JWT 鉁 - -```typescript -// Funcionalidades -- Login con email/password -- Registro de usuarios -- Refresh tokens -- Logout (revocacion) -- Middleware de autorizacion por roles -- Configuracion RLS por tenant -``` - ---- - -## API Endpoints - -### Autenticacion - -```http -POST /api/v1/auth/login -POST /api/v1/auth/register -POST /api/v1/auth/refresh -POST /api/v1/auth/logout -POST /api/v1/auth/change-password -``` - -### Presupuestos - -```http -GET /api/v1/conceptos -GET /api/v1/conceptos/:id -POST /api/v1/conceptos -GET /api/v1/conceptos/tree - -GET /api/v1/presupuestos -GET /api/v1/presupuestos/:id -POST /api/v1/presupuestos -POST /api/v1/presupuestos/:id/partidas -POST /api/v1/presupuestos/:id/approve -POST /api/v1/presupuestos/:id/version -``` - -### Control de Obra - -```http -GET /api/v1/avances -GET /api/v1/avances/:id -POST /api/v1/avances -POST /api/v1/avances/:id/fotos -POST /api/v1/avances/:id/review -POST /api/v1/avances/:id/approve - -GET /api/v1/bitacora/:fraccionamientoId -POST /api/v1/bitacora -``` - -### Estimaciones - -```http -GET /api/v1/estimaciones -GET /api/v1/estimaciones/:id -POST /api/v1/estimaciones -POST /api/v1/estimaciones/:id/conceptos -POST /api/v1/estimaciones/:id/submit -POST /api/v1/estimaciones/:id/review -POST /api/v1/estimaciones/:id/approve -``` - ---- - -## Base de Datos - -### Schemas (7 total, 110 tablas) - -| Schema | Tablas | Descripcion | -|--------|--------|-------------| -| `auth` | 10 | Usuarios, roles, permisos, tenants | -| `construction` | 24 | Proyectos, lotes, presupuestos, avances | -| `hr` | 8 | Empleados, asistencias, cuadrillas | -| `hse` | 58 | Incidentes, capacitaciones, EPP, STPS | -| `estimates` | 8 | Estimaciones, anticipos, retenciones | -| `infonavit` | 8 | Registro RUV, derechohabientes | -| `inventory` | 4 | Almacenes, requisiciones | - -### Row Level Security - -```sql --- Todas las tablas tienen RLS activado -ALTER TABLE construction.fraccionamientos ENABLE ROW LEVEL SECURITY; - --- Politica de aislamiento por tenant -CREATE POLICY tenant_isolation ON construction.fraccionamientos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID); -``` - ---- - -## Scripts NPM - -```bash -# Backend -npm run dev # Desarrollo con hot-reload -npm run build # Compilar TypeScript -npm run start # Produccion -npm run lint # ESLint -npm run test # Jest tests -npm run validate:constants # Validar SSOT -npm run sync:enums # Sincronizar enums a frontend - -# Docker -docker-compose up -d # Levantar servicios -docker-compose --profile dev up # Con Adminer y Mailhog -docker-compose down # Detener servicios -``` - ---- - -## Variables de Entorno - -```bash -# Application -NODE_ENV=development -APP_PORT=3000 - -# Database -DB_HOST=localhost -DB_PORT=5432 -DB_USER=construccion -DB_PASSWORD=construccion_dev_2024 -DB_NAME=erp_construccion - -# JWT -JWT_SECRET=your-secret-key-min-32-chars -JWT_EXPIRES_IN=1d -JWT_REFRESH_EXPIRES_IN=7d - -# Redis -REDIS_HOST=localhost -REDIS_PORT=6379 -``` - -Ver `.env.example` para la lista completa. - ---- - -## Documentacion - -| Documento | Ubicacion | -|-----------|-----------| -| Estado del Proyecto | `PROJECT-STATUS.md` | -| Mapa de Base de Datos | `database/_MAP.md` | -| Constantes SSOT | `backend/src/shared/constants/` | -| Modulos (18) | `docs/02-definicion-modulos/` | -| Requerimientos (87 RF) | `docs/03-requerimientos/` | -| User Stories (149 US) | `docs/05-user-stories/` | -| ADRs (12) | `docs/97-adr/` | - ---- - -## Proximos Pasos - -1. **Corto Plazo** - - Controllers REST para modulos nuevos - - Backend MAI-009 (Calidad y Postventa) - - Backend MAI-011 (INFONAVIT) - - Testing de modulos existentes - -2. **Mediano Plazo** - - Frontend: Integracion con API - - Modulos de Presupuestos y Estimaciones - - Curva S y reportes de avance - ---- - -## Licencia - -UNLICENSED - Proyecto privado - ---- - -**Ultima actualizacion:** 2025-12-12 diff --git a/projects/erp-construccion/backend/.env.example b/projects/erp-construccion/backend/.env.example deleted file mode 100644 index 6782c1bfe..000000000 --- a/projects/erp-construccion/backend/.env.example +++ /dev/null @@ -1,71 +0,0 @@ -# ============================================================================ -# BACKEND ENVIRONMENT VARIABLES - ERP Construccion -# ============================================================================ -# Proyecto: construccion -# Rango de puertos: 3100 (ver DEVENV-PORTS.md) -# Fecha: 2025-12-06 -# ============================================================================ - -# Application -NODE_ENV=development -APP_PORT=3021 -APP_HOST=0.0.0.0 -API_VERSION=v1 -API_PREFIX=/api/v1 - -# Database (Puerto 5433 - diferenciado de erp-core:5432) -DATABASE_URL=postgresql://erp_user:erp_dev_password@localhost:5433/erp_construccion -DB_HOST=localhost -DB_PORT=5433 -DB_NAME=erp_construccion -DB_USER=erp_user -DB_PASSWORD=erp_dev_password -DB_SYNCHRONIZE=false -DB_LOGGING=true - -# Redis (Puerto 6380 - diferenciado de erp-core:6379) -REDIS_HOST=localhost -REDIS_PORT=6380 -REDIS_URL=redis://localhost:6380 - -# MinIO S3 (Puerto 9100 - diferenciado de erp-core:9000) -S3_ENDPOINT=http://localhost:9100 -S3_ACCESS_KEY=minioadmin -S3_SECRET_KEY=minioadmin -S3_BUCKET=erp-construccion - -# JWT -JWT_SECRET=your-super-secret-jwt-key-change-this-in-production -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d - -# CORS (Frontend en puerto 5174) -CORS_ORIGIN=http://localhost:3020,http://localhost:5174 -CORS_CREDENTIALS=true - -# Rate Limiting -RATE_LIMIT_WINDOW_MS=900000 -RATE_LIMIT_MAX_REQUESTS=100 - -# Logging -LOG_LEVEL=debug -LOG_FORMAT=dev - -# File Upload -MAX_FILE_SIZE=10485760 -UPLOAD_DIR=./uploads - -# Email (opcional) -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USER=your-email@example.com -SMTP_PASSWORD=your-email-password -SMTP_FROM=noreply@example.com - -# Security -BCRYPT_ROUNDS=10 -SESSION_SECRET=your-session-secret-change-this - -# External APIs (futuro) -INFONAVIT_API_URL=https://api.infonavit.gob.mx -INFONAVIT_API_KEY=your-api-key diff --git a/projects/erp-construccion/backend/Dockerfile b/projects/erp-construccion/backend/Dockerfile deleted file mode 100644 index 1d85539f3..000000000 --- a/projects/erp-construccion/backend/Dockerfile +++ /dev/null @@ -1,84 +0,0 @@ -# ============================================================================= -# Dockerfile - Backend API -# ERP Construccion - Node.js + Express + TypeScript -# ============================================================================= - -# ----------------------------------------------------------------------------- -# Stage 1: Base -# ----------------------------------------------------------------------------- -FROM node:20-alpine AS base - -# Install dependencies for native modules -RUN apk add --no-cache \ - python3 \ - make \ - g++ \ - curl - -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# ----------------------------------------------------------------------------- -# Stage 2: Development -# ----------------------------------------------------------------------------- -FROM base AS development - -# Install all dependencies (including devDependencies) -RUN npm ci - -# Copy source code -COPY . . - -# Expose port (standard: 3021 for construccion backend) -EXPOSE 3021 - -# Development command with hot reload -CMD ["npm", "run", "dev"] - -# ----------------------------------------------------------------------------- -# Stage 3: Builder -# ----------------------------------------------------------------------------- -FROM base AS builder - -# Install all dependencies -RUN npm ci - -# Copy source code -COPY . . - -# Build TypeScript -RUN npm run build - -# Prune devDependencies -RUN npm prune --production - -# ----------------------------------------------------------------------------- -# Stage 4: Production -# ----------------------------------------------------------------------------- -FROM node:20-alpine AS production - -# Security: Run as non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S nodejs -u 1001 - -WORKDIR /app - -# Copy built application -COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist -COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules -COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./ - -# Set user -USER nodejs - -# Expose port (standard: 3021 for construccion backend) -EXPOSE 3021 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD curl -f http://localhost:3021/health || exit 1 - -# Production command -CMD ["node", "dist/server.js"] diff --git a/projects/erp-construccion/backend/README.md b/projects/erp-construccion/backend/README.md deleted file mode 100644 index 0e5bb19e3..000000000 --- a/projects/erp-construccion/backend/README.md +++ /dev/null @@ -1,461 +0,0 @@ -# Backend - ERP Construccion - -API REST para sistema de administracion de obra e INFONAVIT. - -| Campo | Valor | -|-------|-------| -| **Stack** | Node.js 20 + Express 4 + TypeScript 5 + TypeORM 0.3 | -| **Version** | 1.0.0 | -| **Entidades** | 30 | -| **Services** | 8 | -| **Arquitectura** | Multi-tenant con RLS | - ---- - -## Quick Start - -```bash -# Instalar dependencias -npm install - -# Configurar variables de entorno -cp ../.env.example .env - -# Desarrollo con hot-reload -npm run dev - -# El servidor estara en http://localhost:3000 -``` - ---- - -## Estructura del Proyecto - -``` -src/ -鈹溾攢鈹 modules/ -鈹 鈹溾攢鈹 auth/ # Autenticacion JWT -鈹 鈹 鈹溾攢鈹 dto/ -鈹 鈹 鈹 鈹斺攢鈹 auth.dto.ts # DTOs tipados -鈹 鈹 鈹溾攢鈹 middleware/ -鈹 鈹 鈹 鈹斺攢鈹 auth.middleware.ts -鈹 鈹 鈹溾攢鈹 services/ -鈹 鈹 鈹 鈹斺攢鈹 auth.service.ts -鈹 鈹 鈹斺攢鈹 index.ts -鈹 鈹 -鈹 鈹溾攢鈹 budgets/ # MAI-003 Presupuestos -鈹 鈹 鈹溾攢鈹 entities/ -鈹 鈹 鈹 鈹溾攢鈹 concepto.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 presupuesto.entity.ts -鈹 鈹 鈹 鈹斺攢鈹 presupuesto-partida.entity.ts -鈹 鈹 鈹溾攢鈹 services/ -鈹 鈹 鈹 鈹溾攢鈹 concepto.service.ts -鈹 鈹 鈹 鈹斺攢鈹 presupuesto.service.ts -鈹 鈹 鈹斺攢鈹 index.ts -鈹 鈹 -鈹 鈹溾攢鈹 progress/ # MAI-005 Control de Obra -鈹 鈹 鈹溾攢鈹 entities/ -鈹 鈹 鈹 鈹溾攢鈹 avance-obra.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 foto-avance.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 bitacora-obra.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 programa-obra.entity.ts -鈹 鈹 鈹 鈹斺攢鈹 programa-actividad.entity.ts -鈹 鈹 鈹溾攢鈹 services/ -鈹 鈹 鈹 鈹溾攢鈹 avance-obra.service.ts -鈹 鈹 鈹 鈹斺攢鈹 bitacora-obra.service.ts -鈹 鈹 鈹斺攢鈹 index.ts -鈹 鈹 -鈹 鈹溾攢鈹 estimates/ # MAI-008 Estimaciones -鈹 鈹 鈹溾攢鈹 entities/ -鈹 鈹 鈹 鈹溾攢鈹 estimacion.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 estimacion-concepto.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 generador.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 anticipo.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 amortizacion.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 retencion.entity.ts -鈹 鈹 鈹 鈹溾攢鈹 fondo-garantia.entity.ts -鈹 鈹 鈹 鈹斺攢鈹 estimacion-workflow.entity.ts -鈹 鈹 鈹溾攢鈹 services/ -鈹 鈹 鈹 鈹斺攢鈹 estimacion.service.ts -鈹 鈹 鈹斺攢鈹 index.ts -鈹 鈹 -鈹 鈹溾攢鈹 construction/ # MAI-002 Proyectos -鈹 鈹 鈹斺攢鈹 entities/ -鈹 鈹 鈹溾攢鈹 proyecto.entity.ts -鈹 鈹 鈹斺攢鈹 fraccionamiento.entity.ts -鈹 鈹 -鈹 鈹溾攢鈹 hr/ # MAI-007 RRHH -鈹 鈹 鈹斺攢鈹 entities/ -鈹 鈹 鈹溾攢鈹 employee.entity.ts -鈹 鈹 鈹溾攢鈹 puesto.entity.ts -鈹 鈹 鈹斺攢鈹 employee-fraccionamiento.entity.ts -鈹 鈹 -鈹 鈹溾攢鈹 hse/ # MAA-017 Seguridad HSE -鈹 鈹 鈹斺攢鈹 entities/ -鈹 鈹 鈹溾攢鈹 incidente.entity.ts -鈹 鈹 鈹溾攢鈹 incidente-involucrado.entity.ts -鈹 鈹 鈹溾攢鈹 incidente-accion.entity.ts -鈹 鈹 鈹斺攢鈹 capacitacion.entity.ts -鈹 鈹 -鈹 鈹斺攢鈹 core/ # Entidades base -鈹 鈹斺攢鈹 entities/ -鈹 鈹溾攢鈹 user.entity.ts -鈹 鈹斺攢鈹 tenant.entity.ts -鈹 -鈹斺攢鈹 shared/ - 鈹溾攢鈹 constants/ # SSOT - 鈹 鈹溾攢鈹 database.constants.ts - 鈹 鈹溾攢鈹 api.constants.ts - 鈹 鈹溾攢鈹 enums.constants.ts - 鈹 鈹斺攢鈹 index.ts - 鈹溾攢鈹 services/ - 鈹 鈹斺攢鈹 base.service.ts # CRUD multi-tenant - 鈹斺攢鈹 database/ - 鈹斺攢鈹 typeorm.config.ts -``` - ---- - -## Modulos Implementados - -### Auth Module - -Autenticacion JWT con refresh tokens y multi-tenancy. - -```typescript -// Services -AuthService - 鈹溾攢鈹 login(dto) // Login con email/password - 鈹溾攢鈹 register(dto) // Registro de usuarios - 鈹溾攢鈹 refresh(dto) // Renovar tokens - 鈹溾攢鈹 logout(token) // Revocar refresh token - 鈹斺攢鈹 changePassword(dto) // Cambiar password - -// Middleware -AuthMiddleware - 鈹溾攢鈹 authenticate // Validar JWT (requerido) - 鈹溾攢鈹 optionalAuthenticate // Validar JWT (opcional) - 鈹溾攢鈹 authorize(...roles) // Autorizar por roles - 鈹溾攢鈹 requireAdmin // Solo admin/super_admin - 鈹斺攢鈹 requireSupervisor // Solo supervisores+ -``` - -### Budgets Module (MAI-003) - -Catalogo de conceptos y presupuestos de obra. - -```typescript -// Entities -Concepto // Catalogo jerarquico (arbol) -Presupuesto // Presupuestos versionados -PresupuestoPartida // Lineas con calculo automatico - -// Services -ConceptoService - 鈹溾攢鈹 createConcepto(ctx, dto) // Crear con nivel/path automatico - 鈹溾攢鈹 findRootConceptos(ctx) // Conceptos raiz - 鈹溾攢鈹 findChildren(ctx, parentId) // Hijos de un concepto - 鈹溾攢鈹 getConceptoTree(ctx, rootId) // Arbol completo - 鈹斺攢鈹 search(ctx, term) // Busqueda por codigo/nombre - -PresupuestoService - 鈹溾攢鈹 createPresupuesto(ctx, dto) - 鈹溾攢鈹 findByFraccionamiento(ctx, id) - 鈹溾攢鈹 findWithPartidas(ctx, id) - 鈹溾攢鈹 addPartida(ctx, id, dto) - 鈹溾攢鈹 updatePartida(ctx, id, dto) - 鈹溾攢鈹 removePartida(ctx, id) - 鈹溾攢鈹 recalculateTotal(ctx, id) - 鈹溾攢鈹 createNewVersion(ctx, id) // Versionamiento - 鈹斺攢鈹 approve(ctx, id) -``` - -### Progress Module (MAI-005) - -Control de avances fisicos y bitacora de obra. - -```typescript -// Entities -AvanceObra // Avances con workflow -FotoAvance // Evidencias fotograficas con GPS -BitacoraObra // Bitacora diaria -ProgramaObra // Programa maestro -ProgramaActividad // Actividades WBS - -// Services -AvanceObraService - 鈹溾攢鈹 createAvance(ctx, dto) - 鈹溾攢鈹 findByLote(ctx, loteId) - 鈹溾攢鈹 findByDepartamento(ctx, deptoId) - 鈹溾攢鈹 findWithFilters(ctx, filters) - 鈹溾攢鈹 findWithFotos(ctx, id) - 鈹溾攢鈹 addFoto(ctx, id, dto) - 鈹溾攢鈹 review(ctx, id) // Workflow: revisar - 鈹溾攢鈹 approve(ctx, id) // Workflow: aprobar - 鈹溾攢鈹 reject(ctx, id, reason) // Workflow: rechazar - 鈹斺攢鈹 getAccumulatedProgress(ctx) // Acumulado por concepto - -BitacoraObraService - 鈹溾攢鈹 createEntry(ctx, dto) // Numero automatico - 鈹溾攢鈹 findByFraccionamiento(ctx, id) - 鈹溾攢鈹 findWithFilters(ctx, id, filters) - 鈹溾攢鈹 findByDate(ctx, id, date) - 鈹溾攢鈹 findLatest(ctx, id) - 鈹斺攢鈹 getStats(ctx, id) // Estadisticas -``` - -### Estimates Module (MAI-008) - -Estimaciones periodicas con workflow de aprobacion. - -```typescript -// Entities -Estimacion // Estimaciones con workflow -EstimacionConcepto // Lineas con acumulados -Generador // Numeros generadores -Anticipo // Anticipos -Amortizacion // Amortizaciones -Retencion // Retenciones -FondoGarantia // Fondo de garantia -EstimacionWorkflow // Historial de estados - -// Services -EstimacionService - 鈹溾攢鈹 createEstimacion(ctx, dto) // Numero automatico - 鈹溾攢鈹 findByContrato(ctx, contratoId) - 鈹溾攢鈹 findWithFilters(ctx, filters) - 鈹溾攢鈹 findWithDetails(ctx, id) // Con relaciones - 鈹溾攢鈹 addConcepto(ctx, id, dto) - 鈹溾攢鈹 addGenerador(ctx, conceptoId, dto) - 鈹溾攢鈹 recalculateTotals(ctx, id) // Llama funcion PG - 鈹溾攢鈹 submit(ctx, id) // Workflow - 鈹溾攢鈹 review(ctx, id) // Workflow - 鈹溾攢鈹 approve(ctx, id) // Workflow - 鈹溾攢鈹 reject(ctx, id, reason) // Workflow - 鈹斺攢鈹 getContractSummary(ctx, id) // Resumen financiero -``` - ---- - -## Base Service - -Servicio base con CRUD multi-tenant. - -```typescript -// Uso -class MiService extends BaseService { - constructor(repository: Repository) { - super(repository); - } -} - -// Metodos disponibles -BaseService - 鈹溾攢鈹 findAll(ctx, options?) // Paginado - 鈹溾攢鈹 findById(ctx, id) - 鈹溾攢鈹 findOne(ctx, where) - 鈹溾攢鈹 find(ctx, options) - 鈹溾攢鈹 create(ctx, data) - 鈹溾攢鈹 update(ctx, id, data) - 鈹溾攢鈹 softDelete(ctx, id) - 鈹溾攢鈹 hardDelete(ctx, id) - 鈹溾攢鈹 count(ctx, where?) - 鈹斺攢鈹 exists(ctx, where) - -// ServiceContext -interface ServiceContext { - tenantId: string; - userId: string; -} -``` - ---- - -## SSOT Constants - -Sistema de constantes centralizadas. - -```typescript -// database.constants.ts -import { DB_SCHEMAS, DB_TABLES, TABLE_REFS } from '@shared/constants'; - -DB_SCHEMAS.CONSTRUCTION // 'construction' -DB_TABLES.construction.CONCEPTOS // 'conceptos' -TABLE_REFS.FRACCIONAMIENTOS // 'construction.fraccionamientos' - -// api.constants.ts -import { API_ROUTES } from '@shared/constants'; - -API_ROUTES.PRESUPUESTOS.BASE // '/api/v1/presupuestos' -API_ROUTES.ESTIMACIONES.BY_ID(id) // '/api/v1/estimaciones/:id' - -// enums.constants.ts -import { ROLES, PROJECT_STATUS } from '@shared/constants'; - -ROLES.ADMIN // 'admin' -PROJECT_STATUS.IN_PROGRESS // 'in_progress' -``` - ---- - -## Scripts NPM - -```bash -# Desarrollo -npm run dev # Hot-reload con ts-node-dev -npm run build # Compilar TypeScript -npm run start # Produccion (dist/) - -# Calidad -npm run lint # ESLint -npm run lint:fix # ESLint con autofix -npm run test # Jest -npm run test:watch # Jest watch mode -npm run test:coverage # Jest con cobertura - -# Base de datos -npm run migration:generate # Generar migracion -npm run migration:run # Ejecutar migraciones -npm run migration:revert # Revertir ultima - -# SSOT -npm run validate:constants # Validar no hardcoding -npm run sync:enums # Sincronizar a frontend -npm run precommit # lint + validate -``` - ---- - -## Convenciones - -### Nomenclatura - -| Tipo | Convencion | Ejemplo | -|------|------------|---------| -| Archivos | kebab-case.tipo.ts | `concepto.entity.ts` | -| Clases | PascalCase + sufijo | `ConceptoService` | -| Variables | camelCase | `totalAmount` | -| Constantes | UPPER_SNAKE_CASE | `DB_SCHEMAS` | -| Metodos | camelCase + verbo | `findByContrato` | - -### Entity Pattern - -```typescript -@Entity({ schema: 'construction', name: 'conceptos' }) -@Index(['tenantId', 'code'], { unique: true }) -export class Concepto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - // ... columnas con name: 'snake_case' - - // Soft delete - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} -``` - -### Service Pattern - -```typescript -export class MiService extends BaseService { - constructor( - repository: Repository, - private readonly otroRepo: Repository - ) { - super(repository); - } - - async miMetodo(ctx: ServiceContext, data: MiDto): Promise { - // ctx tiene tenantId y userId - return this.create(ctx, data); - } -} -``` - ---- - -## Seguridad - -- Helmet para HTTP security headers -- CORS configurado por dominio -- Rate limiting por IP -- JWT con refresh tokens -- Bcrypt (12 rounds) para passwords -- class-validator para inputs -- RLS para aislamiento de tenants - ---- - -## Testing - -```bash -# Ejecutar tests -npm test - -# Con cobertura -npm run test:coverage - -# Watch mode -npm run test:watch -``` - -```typescript -// Ejemplo de test -describe('ConceptoService', () => { - let service: ConceptoService; - let mockRepo: jest.Mocked>; - - beforeEach(() => { - mockRepo = createMockRepository(); - service = new ConceptoService(mockRepo); - }); - - it('should create concepto with level', async () => { - const ctx = { tenantId: 'uuid', userId: 'uuid' }; - const dto = { code: '001', name: 'Test' }; - - mockRepo.save.mockResolvedValue({ ...dto, level: 0 }); - - const result = await service.createConcepto(ctx, dto); - expect(result.level).toBe(0); - }); -}); -``` - ---- - -## Debugging - -### VS Code - -```json -{ - "type": "node", - "request": "launch", - "name": "Debug Backend", - "runtimeArgs": ["-r", "ts-node/register"], - "args": ["${workspaceFolder}/src/server.ts"], - "env": { "NODE_ENV": "development" } -} -``` - -### Logs - -```typescript -// Configurar en .env -LOG_LEVEL=debug -LOG_FORMAT=dev -``` - ---- - -**Ultima actualizacion:** 2025-12-12 diff --git a/projects/erp-construccion/backend/package-lock.json b/projects/erp-construccion/backend/package-lock.json deleted file mode 100644 index 1c059c4cf..000000000 --- a/projects/erp-construccion/backend/package-lock.json +++ /dev/null @@ -1,7817 +0,0 @@ -{ - "name": "@construccion-mvp/backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@construccion-mvp/backend", - "version": "1.0.0", - "license": "UNLICENSED", - "dependencies": { - "bcryptjs": "^2.4.3", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.0", - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "express-rate-limit": "^7.1.5", - "helmet": "^7.1.0", - "jsonwebtoken": "^9.0.2", - "morgan": "^1.10.0", - "pg": "^8.11.3", - "reflect-metadata": "^0.1.13", - "swagger-ui-express": "^5.0.0", - "typeorm": "^0.3.17", - "yamljs": "^0.3.0" - }, - "devDependencies": { - "@types/bcryptjs": "^2.4.6", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.11", - "@types/jsonwebtoken": "^9.0.5", - "@types/morgan": "^1.9.9", - "@types/node": "^20.10.5", - "@types/swagger-ui-express": "^4.1.6", - "@typescript-eslint/eslint-plugin": "^6.15.0", - "@typescript-eslint/parser": "^6.15.0", - "eslint": "^8.56.0", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", - "ts-node-dev": "^2.0.0", - "typescript": "^5.3.3" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@sqltools/formatter": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", - "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/morgan": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", - "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz", - "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/swagger-ui-express": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", - "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/validator": { - "version": "13.15.10", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", - "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": true, - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", - "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", - "license": "ISC", - "engines": { - "node": ">=14" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/app-root-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz", - "integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001759", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", - "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/class-transformer": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" - }, - "node_modules/class-validator": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", - "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", - "license": "MIT", - "dependencies": { - "@types/validator": "^13.15.3", - "libphonenumber-js": "^1.11.1", - "validator": "^13.15.20" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/dynamic-dedupe": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", - "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/helmet": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", - "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/libphonenumber-js": { - "version": "1.12.31", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.31.tgz", - "integrity": "sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==", - "license": "MIT" - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", - "license": "MIT", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.1.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "peer": true, - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reflect-metadata": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", - "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "license": "Apache-2.0" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", - "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-static/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/serve-static/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" - }, - "bin": { - "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/sql-highlight": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", - "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", - "funding": [ - "https://github.com/scriptcoded/sql-highlight?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/scriptcoded" - } - ], - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.30.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.3.tgz", - "integrity": "sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-buffer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", - "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-jest": { - "version": "29.4.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", - "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.3", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node-dev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", - "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.1", - "dynamic-dedupe": "^0.3.0", - "minimist": "^1.2.6", - "mkdirp": "^1.0.4", - "resolve": "^1.0.0", - "rimraf": "^2.6.1", - "source-map-support": "^0.5.12", - "tree-kill": "^1.2.2", - "ts-node": "^10.4.0", - "tsconfig": "^7.0.0" - }, - "bin": { - "ts-node-dev": "lib/bin.js", - "tsnd": "lib/bin.js" - }, - "engines": { - "node": ">=0.8.0" - }, - "peerDependencies": { - "node-notifier": "*", - "typescript": "*" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/ts-node-dev/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - } - }, - "node_modules/tsconfig/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tsconfig/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typeorm": { - "version": "0.3.28", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", - "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", - "license": "MIT", - "dependencies": { - "@sqltools/formatter": "^1.2.5", - "ansis": "^4.2.0", - "app-root-path": "^3.1.0", - "buffer": "^6.0.3", - "dayjs": "^1.11.19", - "debug": "^4.4.3", - "dedent": "^1.7.0", - "dotenv": "^16.6.1", - "glob": "^10.5.0", - "reflect-metadata": "^0.2.2", - "sha.js": "^2.4.12", - "sql-highlight": "^6.1.0", - "tslib": "^2.8.1", - "uuid": "^11.1.0", - "yargs": "^17.7.2" - }, - "bin": { - "typeorm": "cli.js", - "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", - "typeorm-ts-node-esm": "cli-ts-node-esm.js" - }, - "engines": { - "node": ">=16.13.0" - }, - "funding": { - "url": "https://opencollective.com/typeorm" - }, - "peerDependencies": { - "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@sap/hana-client": "^2.14.22", - "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", - "ioredis": "^5.0.4", - "mongodb": "^5.8.0 || ^6.0.0", - "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", - "mysql2": "^2.2.5 || ^3.0.1", - "oracledb": "^6.3.0", - "pg": "^8.5.1", - "pg-native": "^3.0.0", - "pg-query-stream": "^4.0.0", - "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", - "sql.js": "^1.4.0", - "sqlite3": "^5.0.3", - "ts-node": "^10.7.0", - "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" - }, - "peerDependenciesMeta": { - "@google-cloud/spanner": { - "optional": true - }, - "@sap/hana-client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "mssql": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "oracledb": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-native": { - "optional": true - }, - "pg-query-stream": { - "optional": true - }, - "redis": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "ts-node": { - "optional": true - }, - "typeorm-aurora-data-api-driver": { - "optional": true - } - } - }, - "node_modules/typeorm/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/typeorm/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/typeorm/node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/validator": { - "version": "13.15.23", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", - "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" - } - }, - "node_modules/yamljs/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/projects/erp-construccion/backend/package.json b/projects/erp-construccion/backend/package.json deleted file mode 100644 index 894e097eb..000000000 --- a/projects/erp-construccion/backend/package.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "@construccion-mvp/backend", - "version": "1.0.0", - "description": "Backend API - MVP Sistema Administraci贸n de Obra e INFONAVIT", - "main": "dist/server.js", - "scripts": { - "dev": "ts-node-dev --respawn --transpile-only src/server.ts", - "build": "tsc", - "start": "node dist/server.js", - "lint": "eslint src/**/*.ts", - "lint:fix": "eslint src/**/*.ts --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "typeorm": "typeorm-ts-node-commonjs", - "migration:generate": "npm run typeorm -- migration:generate", - "migration:run": "npm run typeorm -- migration:run", - "migration:revert": "npm run typeorm -- migration:revert", - "validate:constants": "ts-node scripts/validate-constants-usage.ts", - "sync:enums": "ts-node scripts/sync-enums.ts", - "precommit": "npm run lint && npm run validate:constants" - }, - "keywords": [ - "construccion", - "erp", - "infonavit", - "nodejs", - "typescript", - "express", - "typeorm" - ], - "author": "Tu Empresa", - "license": "UNLICENSED", - "dependencies": { - "express": "^4.18.2", - "typeorm": "^0.3.17", - "pg": "^8.11.3", - "reflect-metadata": "^0.1.13", - "class-validator": "^0.14.0", - "class-transformer": "^0.5.1", - "dotenv": "^16.3.1", - "cors": "^2.8.5", - "helmet": "^7.1.0", - "morgan": "^1.10.0", - "express-rate-limit": "^7.1.5", - "bcryptjs": "^2.4.3", - "jsonwebtoken": "^9.0.2", - "swagger-ui-express": "^5.0.0", - "yamljs": "^0.3.0" - }, - "devDependencies": { - "@types/express": "^4.17.21", - "@types/node": "^20.10.5", - "@types/cors": "^2.8.17", - "@types/morgan": "^1.9.9", - "@types/bcryptjs": "^2.4.6", - "@types/jsonwebtoken": "^9.0.5", - "@types/swagger-ui-express": "^4.1.6", - "@types/jest": "^29.5.11", - "@typescript-eslint/eslint-plugin": "^6.15.0", - "@typescript-eslint/parser": "^6.15.0", - "eslint": "^8.56.0", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", - "ts-node-dev": "^2.0.0", - "typescript": "^5.3.3" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" - } -} diff --git a/projects/erp-construccion/backend/scripts/sync-enums.ts b/projects/erp-construccion/backend/scripts/sync-enums.ts deleted file mode 100644 index 01cc26db1..000000000 --- a/projects/erp-construccion/backend/scripts/sync-enums.ts +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Sync Enums - Backend to Frontend - * - * Este script sincroniza automaticamente las constantes y enums del backend - * al frontend, manteniendo el principio SSOT (Single Source of Truth). - * - * Ejecutar: npm run sync:enums - * - * @author Architecture-Analyst - * @date 2025-12-12 - */ - -import * as fs from 'fs'; -import * as path from 'path'; - -// ============================================================================= -// CONFIGURACION -// ============================================================================= - -const BACKEND_CONSTANTS_DIR = path.resolve(__dirname, '../src/shared/constants'); -const FRONTEND_CONSTANTS_DIR = path.resolve(__dirname, '../../frontend/web/src/shared/constants'); - -// Archivos a sincronizar -const FILES_TO_SYNC = [ - 'enums.constants.ts', - 'api.constants.ts', -]; - -// Header para archivos generados -const GENERATED_HEADER = `/** - * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY - * - * Este archivo es generado automaticamente desde el backend. - * Cualquier cambio sera sobreescrito en la proxima sincronizacion. - * - * Fuente: backend/src/shared/constants/ - * Generado: ${new Date().toISOString()} - * - * Para modificar, edita el archivo fuente en el backend - * y ejecuta: npm run sync:enums - */ - -`; - -// ============================================================================= -// FUNCIONES -// ============================================================================= - -function ensureDirectoryExists(dir: string): void { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - console.log(`馃搧 Created directory: ${dir}`); - } -} - -function processContent(content: string): string { - // Remover imports que no aplican al frontend - let processed = content - // Remover imports de Node.js - .replace(/import\s+\*\s+as\s+\w+\s+from\s+['"]fs['"];?\n?/g, '') - .replace(/import\s+\*\s+as\s+\w+\s+from\s+['"]path['"];?\n?/g, '') - // Remover comentarios de @module backend - .replace(/@module\s+@shared\/constants\//g, '@module shared/constants/') - // Mantener 'as const' para inferencia de tipos - ; - - return GENERATED_HEADER + processed; -} - -function syncFile(filename: string): void { - const sourcePath = path.join(BACKEND_CONSTANTS_DIR, filename); - const destPath = path.join(FRONTEND_CONSTANTS_DIR, filename); - - if (!fs.existsSync(sourcePath)) { - console.log(`鈿狅笍 Source file not found: ${sourcePath}`); - return; - } - - const content = fs.readFileSync(sourcePath, 'utf-8'); - const processedContent = processContent(content); - - fs.writeFileSync(destPath, processedContent); - console.log(`鉁 Synced: ${filename}`); -} - -function generateIndexFile(): void { - const indexContent = `${GENERATED_HEADER} -// Re-export all constants -export * from './enums.constants'; -export * from './api.constants'; -`; - - const indexPath = path.join(FRONTEND_CONSTANTS_DIR, 'index.ts'); - fs.writeFileSync(indexPath, indexContent); - console.log(`鉁 Generated: index.ts`); -} - -function main(): void { - console.log('馃攧 Syncing constants from Backend to Frontend...\n'); - console.log(`Source: ${BACKEND_CONSTANTS_DIR}`); - console.log(`Target: ${FRONTEND_CONSTANTS_DIR}\n`); - - // Asegurar que el directorio destino existe - ensureDirectoryExists(FRONTEND_CONSTANTS_DIR); - - // Sincronizar cada archivo - for (const file of FILES_TO_SYNC) { - syncFile(file); - } - - // Generar archivo index - generateIndexFile(); - - console.log('\n鉁 Sync completed successfully!'); - console.log('\nRecuerda importar las constantes desde:'); - console.log(' import { ROLES, PROJECT_STATUS, API_ROUTES } from "@/shared/constants";'); -} - -main(); diff --git a/projects/erp-construccion/backend/scripts/validate-constants-usage.ts b/projects/erp-construccion/backend/scripts/validate-constants-usage.ts deleted file mode 100644 index cabf45138..000000000 --- a/projects/erp-construccion/backend/scripts/validate-constants-usage.ts +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Validate Constants Usage - SSOT Enforcement - * - * Este script detecta hardcoding de schemas, tablas, rutas API y enums - * que deberian estar usando las constantes centralizadas del SSOT. - * - * Ejecutar: npm run validate:constants - * - * @author Architecture-Analyst - * @date 2025-12-12 - */ - -import * as fs from 'fs'; -import * as path from 'path'; - -// ============================================================================= -// CONFIGURACION -// ============================================================================= - -interface ValidationPattern { - pattern: RegExp; - message: string; - severity: 'P0' | 'P1' | 'P2'; - suggestion: string; - exclude?: RegExp[]; -} - -const PATTERNS: ValidationPattern[] = [ - // Database Schemas - { - pattern: /['"`]auth['"`](?!\s*:)/g, - message: 'Hardcoded schema "auth"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.AUTH', - exclude: [/from\s+['"`]\.\/database\.constants['"`]/], - }, - { - pattern: /['"`]construction['"`](?!\s*:)/g, - message: 'Hardcoded schema "construction"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.CONSTRUCTION', - }, - { - pattern: /['"`]hr['"`](?!\s*:)(?!\.entity)/g, - message: 'Hardcoded schema "hr"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.HR', - }, - { - pattern: /['"`]hse['"`](?!\s*:)(?!\/)/g, - message: 'Hardcoded schema "hse"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.HSE', - }, - { - pattern: /['"`]estimates['"`](?!\s*:)/g, - message: 'Hardcoded schema "estimates"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.ESTIMATES', - }, - { - pattern: /['"`]infonavit['"`](?!\s*:)/g, - message: 'Hardcoded schema "infonavit"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.INFONAVIT', - }, - { - pattern: /['"`]inventory['"`](?!\s*:)/g, - message: 'Hardcoded schema "inventory"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.INVENTORY', - }, - { - pattern: /['"`]purchase['"`](?!\s*:)/g, - message: 'Hardcoded schema "purchase"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.PURCHASE', - }, - - // API Routes - { - pattern: /['"`]\/api\/v1\/proyectos['"`]/g, - message: 'Hardcoded API route "/api/v1/proyectos"', - severity: 'P0', - suggestion: 'Usa API_ROUTES.PROYECTOS.BASE', - }, - { - pattern: /['"`]\/api\/v1\/fraccionamientos['"`]/g, - message: 'Hardcoded API route "/api/v1/fraccionamientos"', - severity: 'P0', - suggestion: 'Usa API_ROUTES.FRACCIONAMIENTOS.BASE', - }, - { - pattern: /['"`]\/api\/v1\/employees['"`]/g, - message: 'Hardcoded API route "/api/v1/employees"', - severity: 'P0', - suggestion: 'Usa API_ROUTES.EMPLOYEES.BASE', - }, - { - pattern: /['"`]\/api\/v1\/incidentes['"`]/g, - message: 'Hardcoded API route "/api/v1/incidentes"', - severity: 'P0', - suggestion: 'Usa API_ROUTES.INCIDENTES.BASE', - }, - - // Common Table Names - { - pattern: /FROM\s+proyectos(?!\s+AS|\s+WHERE)/gi, - message: 'Hardcoded table name "proyectos"', - severity: 'P1', - suggestion: 'Usa DB_TABLES.CONSTRUCTION.PROYECTOS', - }, - { - pattern: /FROM\s+fraccionamientos(?!\s+AS|\s+WHERE)/gi, - message: 'Hardcoded table name "fraccionamientos"', - severity: 'P1', - suggestion: 'Usa DB_TABLES.CONSTRUCTION.FRACCIONAMIENTOS', - }, - { - pattern: /FROM\s+employees(?!\s+AS|\s+WHERE)/gi, - message: 'Hardcoded table name "employees"', - severity: 'P1', - suggestion: 'Usa DB_TABLES.HR.EMPLOYEES', - }, - { - pattern: /FROM\s+incidentes(?!\s+AS|\s+WHERE)/gi, - message: 'Hardcoded table name "incidentes"', - severity: 'P1', - suggestion: 'Usa DB_TABLES.HSE.INCIDENTES', - }, - - // Status Values - { - pattern: /status\s*===?\s*['"`]active['"`]/gi, - message: 'Hardcoded status "active"', - severity: 'P1', - suggestion: 'Usa PROJECT_STATUS.ACTIVE o USER_STATUS.ACTIVE', - }, - { - pattern: /status\s*===?\s*['"`]borrador['"`]/gi, - message: 'Hardcoded status "borrador"', - severity: 'P1', - suggestion: 'Usa BUDGET_STATUS.DRAFT o ESTIMATION_STATUS.DRAFT', - }, - { - pattern: /status\s*===?\s*['"`]aprobado['"`]/gi, - message: 'Hardcoded status "aprobado"', - severity: 'P1', - suggestion: 'Usa BUDGET_STATUS.APPROVED o ESTIMATION_STATUS.APPROVED', - }, - - // Role Names - { - pattern: /role\s*===?\s*['"`]admin['"`]/gi, - message: 'Hardcoded role "admin"', - severity: 'P0', - suggestion: 'Usa ROLES.ADMIN', - }, - { - pattern: /role\s*===?\s*['"`]supervisor['"`]/gi, - message: 'Hardcoded role "supervisor"', - severity: 'P1', - suggestion: 'Usa ROLES.SUPERVISOR_OBRA o ROLES.SUPERVISOR_HSE', - }, -]; - -// Archivos a excluir -const EXCLUDED_PATHS = [ - 'node_modules', - 'dist', - '.git', - 'coverage', - 'database.constants.ts', - 'api.constants.ts', - 'enums.constants.ts', - 'index.ts', - '.sql', - '.md', - '.json', - '.yml', - '.yaml', -]; - -// Extensiones a validar -const VALID_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx']; - -// ============================================================================= -// TIPOS -// ============================================================================= - -interface Violation { - file: string; - line: number; - column: number; - pattern: string; - message: string; - severity: 'P0' | 'P1' | 'P2'; - suggestion: string; - context: string; -} - -// ============================================================================= -// FUNCIONES -// ============================================================================= - -function shouldExclude(filePath: string): boolean { - return EXCLUDED_PATHS.some(excluded => filePath.includes(excluded)); -} - -function hasValidExtension(filePath: string): boolean { - return VALID_EXTENSIONS.some(ext => filePath.endsWith(ext)); -} - -function getFiles(dir: string): string[] { - const files: string[] = []; - - if (!fs.existsSync(dir)) { - return files; - } - - const items = fs.readdirSync(dir); - - for (const item of items) { - const fullPath = path.join(dir, item); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - if (!shouldExclude(fullPath)) { - files.push(...getFiles(fullPath)); - } - } else if (stat.isFile() && hasValidExtension(fullPath) && !shouldExclude(fullPath)) { - files.push(fullPath); - } - } - - return files; -} - -function findViolations(filePath: string, content: string, patterns: ValidationPattern[]): Violation[] { - const violations: Violation[] = []; - const lines = content.split('\n'); - - for (const patternConfig of patterns) { - let match: RegExpExecArray | null; - const regex = new RegExp(patternConfig.pattern.source, patternConfig.pattern.flags); - - while ((match = regex.exec(content)) !== null) { - // Check exclusions - if (patternConfig.exclude) { - const shouldSkip = patternConfig.exclude.some(excludePattern => - excludePattern.test(content) - ); - if (shouldSkip) continue; - } - - // Find line number - const beforeMatch = content.substring(0, match.index); - const lineNumber = beforeMatch.split('\n').length; - const lineStart = beforeMatch.lastIndexOf('\n') + 1; - const column = match.index - lineStart + 1; - - violations.push({ - file: filePath, - line: lineNumber, - column, - pattern: match[0], - message: patternConfig.message, - severity: patternConfig.severity, - suggestion: patternConfig.suggestion, - context: lines[lineNumber - 1]?.trim() || '', - }); - } - } - - return violations; -} - -function formatViolation(v: Violation): string { - const severityColor = { - P0: '\x1b[31m', // Red - P1: '\x1b[33m', // Yellow - P2: '\x1b[36m', // Cyan - }; - const reset = '\x1b[0m'; - - return ` -${severityColor[v.severity]}[${v.severity}]${reset} ${v.message} - File: ${v.file}:${v.line}:${v.column} - Found: "${v.pattern}" - Context: ${v.context} - Suggestion: ${v.suggestion} -`; -} - -function generateReport(violations: Violation[]): void { - const p0 = violations.filter(v => v.severity === 'P0'); - const p1 = violations.filter(v => v.severity === 'P1'); - const p2 = violations.filter(v => v.severity === 'P2'); - - console.log('\n========================================'); - console.log('SSOT VALIDATION REPORT'); - console.log('========================================\n'); - - console.log(`Total Violations: ${violations.length}`); - console.log(` P0 (Critical): ${p0.length}`); - console.log(` P1 (High): ${p1.length}`); - console.log(` P2 (Medium): ${p2.length}`); - - if (violations.length > 0) { - console.log('\n----------------------------------------'); - console.log('VIOLATIONS FOUND:'); - console.log('----------------------------------------'); - - // Group by file - const byFile = violations.reduce((acc, v) => { - if (!acc[v.file]) acc[v.file] = []; - acc[v.file].push(v); - return acc; - }, {} as Record); - - for (const [file, fileViolations] of Object.entries(byFile)) { - console.log(`\n馃搧 ${file}`); - for (const v of fileViolations) { - console.log(formatViolation(v)); - } - } - } - - console.log('\n========================================'); - - if (p0.length > 0) { - console.log('\n鉂 FAILED: P0 violations found. Fix before merging.\n'); - process.exit(1); - } else if (violations.length > 0) { - console.log('\n鈿狅笍 WARNING: Non-critical violations found. Consider fixing.\n'); - process.exit(0); - } else { - console.log('\n鉁 PASSED: No SSOT violations found!\n'); - process.exit(0); - } -} - -// ============================================================================= -// MAIN -// ============================================================================= - -function main(): void { - const backendDir = path.resolve(__dirname, '../src'); - const frontendDir = path.resolve(__dirname, '../../frontend/web/src'); - - console.log('馃攳 Validating SSOT constants usage...\n'); - console.log(`Backend: ${backendDir}`); - console.log(`Frontend: ${frontendDir}`); - - const allViolations: Violation[] = []; - - // Scan backend - if (fs.existsSync(backendDir)) { - const backendFiles = getFiles(backendDir); - console.log(`\nScanning ${backendFiles.length} backend files...`); - - for (const file of backendFiles) { - const content = fs.readFileSync(file, 'utf-8'); - const violations = findViolations(file, content, PATTERNS); - allViolations.push(...violations); - } - } - - // Scan frontend - if (fs.existsSync(frontendDir)) { - const frontendFiles = getFiles(frontendDir); - console.log(`Scanning ${frontendFiles.length} frontend files...`); - - for (const file of frontendFiles) { - const content = fs.readFileSync(file, 'utf-8'); - const violations = findViolations(file, content, PATTERNS); - allViolations.push(...violations); - } - } - - generateReport(allViolations); -} - -main(); diff --git a/projects/erp-construccion/backend/service.descriptor.yml b/projects/erp-construccion/backend/service.descriptor.yml deleted file mode 100644 index f5820d8b9..000000000 --- a/projects/erp-construccion/backend/service.descriptor.yml +++ /dev/null @@ -1,169 +0,0 @@ -# ============================================================================== -# SERVICE DESCRIPTOR - ERP CONSTRUCCION API -# ============================================================================== -# API especializada para construccion residencial -# Mantenido por: Backend-Agent -# Actualizado: 2025-12-18 -# ============================================================================== - -version: "1.0.0" - -# ------------------------------------------------------------------------------ -# IDENTIFICACION DEL SERVICIO -# ------------------------------------------------------------------------------ -service: - name: "erp-construccion-api" - display_name: "ERP Construccion API" - description: "API especializada para gestion de construccion residencial" - type: "backend" - runtime: "node" - framework: "express" - owner_agent: "NEXUS-BACKEND" - -# ------------------------------------------------------------------------------ -# CONFIGURACION DE PUERTOS -# ------------------------------------------------------------------------------ -ports: - internal: 3020 - registry_ref: "projects.erp_suite.verticales.construccion.api" - protocol: "http" - -# ------------------------------------------------------------------------------ -# CONFIGURACION DE BASE DE DATOS -# ------------------------------------------------------------------------------ -database: - registry_ref: "erp_construccion" - schemas: - - "construction" - - "budgets" - - "estimates" - - "progress" - - "hr" - - "hse" - - "inventory" - - "purchase" - role: "runtime" - -# ------------------------------------------------------------------------------ -# DEPENDENCIAS -# ------------------------------------------------------------------------------ -dependencies: - services: - - name: "erp-core-api" - type: "backend" - required: true - description: "Core ERP para auth y catalogos" - - name: "postgres" - type: "database" - required: true - -# ------------------------------------------------------------------------------ -# MODULOS ESPECIALIZADOS -# ------------------------------------------------------------------------------ -modules: - construction: - description: "Gestion de construccion" - entities: - - fraccionamiento - - etapa - - manzana - - lote - - prototipo - endpoints: - - { path: "/fraccionamientos", methods: ["GET", "POST"] } - - { path: "/etapas", methods: ["GET", "POST"] } - - { path: "/manzanas", methods: ["GET", "POST"] } - - { path: "/lotes", methods: ["GET", "POST"] } - - budgets: - description: "Presupuestos de obra" - entities: - - presupuesto - - partida - - concepto - endpoints: - - { path: "/presupuestos", methods: ["GET", "POST"] } - - estimates: - description: "Estimaciones" - entities: - - estimacion - - detalle_estimacion - endpoints: - - { path: "/estimaciones", methods: ["GET", "POST"] } - - progress: - description: "Avance de obra" - entities: - - avance_obra - - bitacora - endpoints: - - { path: "/avance-obra", methods: ["GET", "POST"] } - - hr: - description: "Recursos humanos" - entities: - - empleado - - nomina - - asistencia - - hse: - description: "Seguridad e higiene" - entities: - - incidente - - capacitacion - - inventory: - description: "Inventario de materiales" - entities: - - material - - movimiento_inventario - - purchase: - description: "Compras" - entities: - - orden_compra - - proveedor - -# ------------------------------------------------------------------------------ -# DOCKER -# ------------------------------------------------------------------------------ -docker: - image: "erp-construccion-api" - dockerfile: "Dockerfile" - networks: - - "erp_construccion_${ENV:-local}" - - "erp_core_${ENV:-local}" - - "infra_shared" - labels: - traefik: - enable: true - router: "erp-construccion-api" - rule: "Host(`api.construccion.erp.localhost`)" - -# ------------------------------------------------------------------------------ -# HEALTH CHECK -# ------------------------------------------------------------------------------ -healthcheck: - endpoint: "/health" - interval: "30s" - timeout: "5s" - retries: 3 - -# ------------------------------------------------------------------------------ -# ESTADO -# ------------------------------------------------------------------------------ -status: - phase: "development" - version: "0.1.0" - completeness: 40 - -# ------------------------------------------------------------------------------ -# METADATA -# ------------------------------------------------------------------------------ -metadata: - created_at: "2025-12-18" - created_by: "Backend-Agent" - project: "erp-suite" - vertical: "construccion" - team: "construccion-team" diff --git a/projects/erp-construccion/backend/src/modules/admin/controllers/audit-log.controller.ts b/projects/erp-construccion/backend/src/modules/admin/controllers/audit-log.controller.ts deleted file mode 100644 index 13a4b3ada..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/controllers/audit-log.controller.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * AuditLogController - Controller de Logs de Auditor铆a - * - * Endpoints REST para consulta de logs de auditor铆a. - * - * @module Admin - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { AuditLogService, AuditLogFilters } from '../services/audit-log.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { AuditLog } from '../entities/audit-log.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createAuditLogController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const auditLogRepository = dataSource.getRepository(AuditLog); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const auditLogService = new AuditLogService(auditLogRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /audit-logs - */ - router.get('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'auditor'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 50, 200); - - const filters: AuditLogFilters = {}; - if (req.query.userId) filters.userId = req.query.userId as string; - if (req.query.category) filters.category = req.query.category as any; - if (req.query.action) filters.action = req.query.action as any; - if (req.query.severity) filters.severity = req.query.severity as any; - if (req.query.entityType) filters.entityType = req.query.entityType as string; - if (req.query.entityId) filters.entityId = req.query.entityId as string; - if (req.query.module) filters.module = req.query.module as string; - if (req.query.isSuccess !== undefined) filters.isSuccess = req.query.isSuccess === 'true'; - if (req.query.dateFrom) filters.dateFrom = new Date(req.query.dateFrom as string); - if (req.query.dateTo) filters.dateTo = new Date(req.query.dateTo as string); - if (req.query.ipAddress) filters.ipAddress = req.query.ipAddress as string; - if (req.query.search) filters.search = req.query.search as string; - - const result = await auditLogService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /audit-logs/stats - */ - router.get('/stats', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'auditor'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const days = parseInt(req.query.days as string) || 30; - const stats = await auditLogService.getStats(getContext(req), days); - - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /audit-logs/critical - */ - router.get('/critical', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const days = parseInt(req.query.days as string) || 7; - const limit = Math.min(parseInt(req.query.limit as string) || 100, 500); - - const logs = await auditLogService.getCriticalLogs(getContext(req), days, limit); - res.status(200).json({ success: true, data: logs }); - } catch (error) { - next(error); - } - }); - - /** - * GET /audit-logs/failed - */ - router.get('/failed', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const hours = parseInt(req.query.hours as string) || 24; - const limit = Math.min(parseInt(req.query.limit as string) || 100, 500); - - const logs = await auditLogService.getFailedLogs(getContext(req), hours, limit); - res.status(200).json({ success: true, data: logs }); - } catch (error) { - next(error); - } - }); - - /** - * GET /audit-logs/entity/:type/:id - */ - router.get('/entity/:type/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'auditor'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await auditLogService.findByEntity( - getContext(req), - req.params.type, - req.params.id, - page, - limit - ); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /audit-logs/user/:userId - */ - router.get('/user/:userId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'auditor'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 50, 200); - - const result = await auditLogService.findByUser( - getContext(req), - req.params.userId, - page, - limit - ); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /audit-logs/cleanup - * Cleanup expired logs - */ - router.post('/cleanup', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await auditLogService.cleanupExpiredLogs(getContext(req)); - res.status(200).json({ - success: true, - message: `Cleaned up ${deleted} expired audit logs`, - data: { deleted }, - }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createAuditLogController; diff --git a/projects/erp-construccion/backend/src/modules/admin/controllers/backup.controller.ts b/projects/erp-construccion/backend/src/modules/admin/controllers/backup.controller.ts deleted file mode 100644 index 826253c27..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/controllers/backup.controller.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * BackupController - Controller de Backups - * - * Endpoints REST para gesti贸n de backups del sistema. - * - * @module Admin - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { BackupService, CreateBackupDto, BackupFilters } from '../services/backup.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Backup } from '../entities/backup.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createBackupController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const backupRepository = dataSource.getRepository(Backup); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const backupService = new BackupService(backupRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /backups - */ - router.get('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: BackupFilters = {}; - if (req.query.backupType) filters.backupType = req.query.backupType as any; - if (req.query.status) filters.status = req.query.status as any; - if (req.query.storageLocation) filters.storageLocation = req.query.storageLocation as any; - if (req.query.isScheduled !== undefined) filters.isScheduled = req.query.isScheduled === 'true'; - if (req.query.isVerified !== undefined) filters.isVerified = req.query.isVerified === 'true'; - if (req.query.dateFrom) filters.dateFrom = new Date(req.query.dateFrom as string); - if (req.query.dateTo) filters.dateTo = new Date(req.query.dateTo as string); - - const result = await backupService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /backups/stats - */ - router.get('/stats', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await backupService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /backups/last - */ - router.get('/last', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const backupType = req.query.backupType as any; - const backup = await backupService.getLastSuccessful(getContext(req), backupType); - - if (!backup) { - res.status(404).json({ error: 'Not Found', message: 'No successful backup found' }); - return; - } - - res.status(200).json({ success: true, data: backup }); - } catch (error) { - next(error); - } - }); - - /** - * GET /backups/pending-verification - */ - router.get('/pending-verification', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const backups = await backupService.getPendingVerification(getContext(req)); - res.status(200).json({ success: true, data: backups }); - } catch (error) { - next(error); - } - }); - - /** - * GET /backups/:id - */ - router.get('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const backup = await backupService.findById(getContext(req), req.params.id); - if (!backup) { - res.status(404).json({ error: 'Not Found', message: 'Backup not found' }); - return; - } - - res.status(200).json({ success: true, data: backup }); - } catch (error) { - next(error); - } - }); - - /** - * POST /backups - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateBackupDto = req.body; - if (!dto.backupType || !dto.name) { - res.status(400).json({ - error: 'Bad Request', - message: 'backupType and name are required', - }); - return; - } - - const backup = await backupService.initiateBackup(getContext(req), dto); - res.status(201).json({ success: true, data: backup }); - } catch (error) { - next(error); - } - }); - - /** - * POST /backups/:id/verify - */ - router.post('/:id/verify', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const backup = await backupService.markVerified(getContext(req), req.params.id); - if (!backup) { - res.status(404).json({ error: 'Not Found', message: 'Backup not found' }); - return; - } - - res.status(200).json({ success: true, data: backup }); - } catch (error) { - if (error instanceof Error && error.message.includes('Only completed')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /backups/:id/cancel - */ - router.post('/:id/cancel', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const backup = await backupService.cancelBackup(getContext(req), req.params.id); - if (!backup) { - res.status(404).json({ error: 'Not Found', message: 'Backup not found' }); - return; - } - - res.status(200).json({ success: true, data: backup }); - } catch (error) { - if (error instanceof Error && error.message.includes('Only pending')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /backups/cleanup - */ - router.post('/cleanup', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const expired = await backupService.cleanupExpired(getContext(req)); - res.status(200).json({ - success: true, - message: `Marked ${expired} backups as expired`, - data: { expired }, - }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /backups/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await backupService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Backup not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Backup deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createBackupController; diff --git a/projects/erp-construccion/backend/src/modules/admin/controllers/cost-center.controller.ts b/projects/erp-construccion/backend/src/modules/admin/controllers/cost-center.controller.ts deleted file mode 100644 index c72f94d88..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/controllers/cost-center.controller.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * CostCenterController - Controller de Centros de Costo - * - * Endpoints REST para gesti贸n de centros de costo jer谩rquicos. - * - * @module Admin - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { CostCenterService, CreateCostCenterDto, UpdateCostCenterDto, CostCenterFilters } from '../services/cost-center.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { CostCenter } from '../entities/cost-center.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createCostCenterController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const costCenterRepository = dataSource.getRepository(CostCenter); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const costCenterService = new CostCenterService(costCenterRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /cost-centers - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: CostCenterFilters = {}; - if (req.query.costCenterType) filters.costCenterType = req.query.costCenterType as any; - if (req.query.level) filters.level = req.query.level as any; - if (req.query.fraccionamientoId) filters.fraccionamientoId = req.query.fraccionamientoId as string; - if (req.query.parentId) filters.parentId = req.query.parentId as string; - if (req.query.responsibleId) filters.responsibleId = req.query.responsibleId as string; - if (req.query.isActive !== undefined) filters.isActive = req.query.isActive === 'true'; - if (req.query.search) filters.search = req.query.search as string; - - const result = await costCenterService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /cost-centers/tree - */ - router.get('/tree', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string | undefined; - const tree = await costCenterService.getTree(getContext(req), fraccionamientoId); - - res.status(200).json({ success: true, data: tree }); - } catch (error) { - next(error); - } - }); - - /** - * GET /cost-centers/stats - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await costCenterService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /cost-centers/budget-summary - */ - router.get('/budget-summary', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string | undefined; - const summary = await costCenterService.getBudgetSummary(getContext(req), fraccionamientoId); - - res.status(200).json({ success: true, data: summary }); - } catch (error) { - next(error); - } - }); - - /** - * GET /cost-centers/code/:code - */ - router.get('/code/:code', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const cc = await costCenterService.findByCode(getContext(req), req.params.code); - if (!cc) { - res.status(404).json({ error: 'Not Found', message: 'Cost center not found' }); - return; - } - - res.status(200).json({ success: true, data: cc }); - } catch (error) { - next(error); - } - }); - - /** - * GET /cost-centers/:id - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const cc = await costCenterService.findById(getContext(req), req.params.id); - if (!cc) { - res.status(404).json({ error: 'Not Found', message: 'Cost center not found' }); - return; - } - - res.status(200).json({ success: true, data: cc }); - } catch (error) { - next(error); - } - }); - - /** - * GET /cost-centers/:id/children - */ - router.get('/:id/children', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const children = await costCenterService.getChildren(getContext(req), req.params.id); - res.status(200).json({ success: true, data: children }); - } catch (error) { - next(error); - } - }); - - /** - * POST /cost-centers - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'finance'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateCostCenterDto = req.body; - if (!dto.code || !dto.name) { - res.status(400).json({ - error: 'Bad Request', - message: 'code and name are required', - }); - return; - } - - const cc = await costCenterService.createCostCenter(getContext(req), dto); - res.status(201).json({ success: true, data: cc }); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - if (error.message.includes('not found')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * PUT /cost-centers/:id - */ - router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'finance'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateCostCenterDto = req.body; - const cc = await costCenterService.update(getContext(req), req.params.id, dto); - - if (!cc) { - res.status(404).json({ error: 'Not Found', message: 'Cost center not found' }); - return; - } - - res.status(200).json({ success: true, data: cc }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /cost-centers/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await costCenterService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Cost center not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Cost center deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createCostCenterController; diff --git a/projects/erp-construccion/backend/src/modules/admin/controllers/index.ts b/projects/erp-construccion/backend/src/modules/admin/controllers/index.ts deleted file mode 100644 index bbd4d416a..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/controllers/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Admin Controllers Index - * @module Admin - */ - -export { createCostCenterController } from './cost-center.controller'; -export { createAuditLogController } from './audit-log.controller'; -export { createSystemSettingController } from './system-setting.controller'; -export { createBackupController } from './backup.controller'; diff --git a/projects/erp-construccion/backend/src/modules/admin/controllers/system-setting.controller.ts b/projects/erp-construccion/backend/src/modules/admin/controllers/system-setting.controller.ts deleted file mode 100644 index 1638d6874..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/controllers/system-setting.controller.ts +++ /dev/null @@ -1,370 +0,0 @@ -/** - * SystemSettingController - Controller de Configuraci贸n del Sistema - * - * Endpoints REST para gesti贸n de configuraciones. - * - * @module Admin - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { SystemSettingService, CreateSettingDto, UpdateSettingDto, SettingFilters } from '../services/system-setting.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { SystemSetting } from '../entities/system-setting.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createSystemSettingController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const settingRepository = dataSource.getRepository(SystemSetting); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const settingService = new SystemSettingService(settingRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /settings - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); - - const filters: SettingFilters = {}; - if (req.query.category) filters.category = req.query.category as any; - if (req.query.isPublic !== undefined) filters.isPublic = req.query.isPublic === 'true'; - if (req.query.search) filters.search = req.query.search as string; - - const result = await settingService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /settings/public - */ - router.get('/public', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const settings = await settingService.getPublicSettings(getContext(req)); - res.status(200).json({ success: true, data: settings }); - } catch (error) { - next(error); - } - }); - - /** - * GET /settings/stats - */ - router.get('/stats', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await settingService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /settings/category/:category - */ - router.get('/category/:category', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const settings = await settingService.findByCategory(getContext(req), req.params.category as any); - res.status(200).json({ success: true, data: settings }); - } catch (error) { - next(error); - } - }); - - /** - * GET /settings/key/:key - */ - router.get('/key/:key', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const setting = await settingService.findByKey(getContext(req), req.params.key); - if (!setting) { - res.status(404).json({ error: 'Not Found', message: 'Setting not found' }); - return; - } - - res.status(200).json({ success: true, data: setting }); - } catch (error) { - next(error); - } - }); - - /** - * GET /settings/key/:key/value - */ - router.get('/key/:key/value', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const value = await settingService.getValue(getContext(req), req.params.key); - if (value === null) { - res.status(404).json({ error: 'Not Found', message: 'Setting not found' }); - return; - } - - res.status(200).json({ success: true, data: { key: req.params.key, value } }); - } catch (error) { - next(error); - } - }); - - /** - * GET /settings/:id - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const setting = await settingService.findById(getContext(req), req.params.id); - if (!setting) { - res.status(404).json({ error: 'Not Found', message: 'Setting not found' }); - return; - } - - res.status(200).json({ success: true, data: setting }); - } catch (error) { - next(error); - } - }); - - /** - * POST /settings - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateSettingDto = req.body; - if (!dto.key || !dto.name || dto.value === undefined) { - res.status(400).json({ - error: 'Bad Request', - message: 'key, name, and value are required', - }); - return; - } - - const setting = await settingService.createSetting(getContext(req), dto); - res.status(201).json({ success: true, data: setting }); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - if (error.message.includes('must be') || error.message.includes('pattern')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * PUT /settings/:id - */ - router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateSettingDto = req.body; - const setting = await settingService.update(getContext(req), req.params.id, dto); - - if (!setting) { - res.status(404).json({ error: 'Not Found', message: 'Setting not found' }); - return; - } - - res.status(200).json({ success: true, data: setting }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /settings/key/:key/value - */ - router.put('/key/:key/value', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { value } = req.body; - if (value === undefined) { - res.status(400).json({ error: 'Bad Request', message: 'value is required' }); - return; - } - - const setting = await settingService.updateValue(getContext(req), req.params.key, value); - if (!setting) { - res.status(404).json({ error: 'Not Found', message: 'Setting not found' }); - return; - } - - res.status(200).json({ success: true, data: setting }); - } catch (error) { - if (error instanceof Error && (error.message.includes('must be') || error.message.includes('pattern'))) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /settings/key/:key/reset - */ - router.post('/key/:key/reset', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const setting = await settingService.resetToDefault(getContext(req), req.params.key); - if (!setting) { - res.status(404).json({ error: 'Not Found', message: 'Setting not found or no default value' }); - return; - } - - res.status(200).json({ success: true, data: setting }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /settings/bulk - */ - router.put('/bulk', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { settings } = req.body; - if (!Array.isArray(settings)) { - res.status(400).json({ error: 'Bad Request', message: 'settings array is required' }); - return; - } - - const result = await settingService.updateMultiple(getContext(req), settings); - res.status(200).json({ - success: true, - data: result, - message: `Updated ${result.updated} settings`, - }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /settings/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - // Check if system setting - const setting = await settingService.findById(getContext(req), req.params.id); - if (!setting) { - res.status(404).json({ error: 'Not Found', message: 'Setting not found' }); - return; - } - - if (setting.isSystem) { - res.status(403).json({ error: 'Forbidden', message: 'Cannot delete system settings' }); - return; - } - - const deleted = await settingService.hardDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Setting not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Setting deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createSystemSettingController; diff --git a/projects/erp-construccion/backend/src/modules/admin/entities/audit-log.entity.ts b/projects/erp-construccion/backend/src/modules/admin/entities/audit-log.entity.ts deleted file mode 100644 index 6bbfc65bd..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/entities/audit-log.entity.ts +++ /dev/null @@ -1,256 +0,0 @@ -/** - * AuditLog Entity - * Registro de auditor铆a para trazabilidad de operaciones - * - * @module Admin - * @table admin.audit_logs - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -export type AuditCategory = - | 'authentication' - | 'user_management' - | 'critical_operation' - | 'administration' - | 'data_access' - | 'system'; - -export type AuditAction = - | 'login' - | 'logout' - | 'login_failed' - | 'password_change' - | 'password_reset' - | 'create' - | 'read' - | 'update' - | 'delete' - | 'approve' - | 'reject' - | 'export' - | 'import' - | 'configure' - | 'backup' - | 'restore'; - -export type AuditSeverity = 'low' | 'medium' | 'high' | 'critical'; - -@Entity({ schema: 'admin', name: 'audit_logs' }) -@Index(['tenantId']) -@Index(['userId']) -@Index(['category']) -@Index(['action']) -@Index(['entityType', 'entityId']) -@Index(['createdAt']) -@Index(['severity']) -@Index(['ipAddress']) -export class AuditLog { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'user_id', type: 'uuid', nullable: true }) - userId: string | null; - - @Column({ - type: 'varchar', - length: 30, - }) - category: AuditCategory; - - @Column({ - type: 'varchar', - length: 30, - }) - action: AuditAction; - - @Column({ - type: 'varchar', - length: 20, - default: 'medium', - }) - severity: AuditSeverity; - - @Column({ - name: 'entity_type', - type: 'varchar', - length: 100, - nullable: true, - comment: 'Type of entity affected (e.g., User, Estimacion)', - }) - entityType: string | null; - - @Column({ - name: 'entity_id', - type: 'uuid', - nullable: true, - comment: 'ID of the affected entity', - }) - entityId: string | null; - - @Column({ - name: 'entity_name', - type: 'varchar', - length: 200, - nullable: true, - comment: 'Human-readable name of the entity', - }) - entityName: string | null; - - @Column({ - type: 'text', - comment: 'Human-readable description of the action', - }) - description: string; - - @Column({ - name: 'old_values', - type: 'jsonb', - nullable: true, - comment: 'Previous values before the change', - }) - oldValues: Record | null; - - @Column({ - name: 'new_values', - type: 'jsonb', - nullable: true, - comment: 'New values after the change', - }) - newValues: Record | null; - - @Column({ - name: 'changed_fields', - type: 'varchar', - array: true, - nullable: true, - comment: 'List of fields that were changed', - }) - changedFields: string[] | null; - - @Column({ - name: 'ip_address', - type: 'varchar', - length: 45, - nullable: true, - }) - ipAddress: string | null; - - @Column({ - name: 'user_agent', - type: 'varchar', - length: 500, - nullable: true, - }) - userAgent: string | null; - - @Column({ - name: 'request_id', - type: 'varchar', - length: 100, - nullable: true, - comment: 'Request/correlation ID for tracing', - }) - requestId: string | null; - - @Column({ - name: 'session_id', - type: 'varchar', - length: 100, - nullable: true, - }) - sessionId: string | null; - - @Column({ - type: 'varchar', - length: 100, - nullable: true, - comment: 'Module where action occurred', - }) - module: string | null; - - @Column({ - type: 'varchar', - length: 200, - nullable: true, - comment: 'API endpoint or route', - }) - endpoint: string | null; - - @Column({ - name: 'http_method', - type: 'varchar', - length: 10, - nullable: true, - }) - httpMethod: string | null; - - @Column({ - name: 'response_status', - type: 'integer', - nullable: true, - }) - responseStatus: number | null; - - @Column({ - name: 'duration_ms', - type: 'integer', - nullable: true, - comment: 'Request duration in milliseconds', - }) - durationMs: number | null; - - @Column({ - name: 'is_success', - type: 'boolean', - default: true, - }) - isSuccess: boolean; - - @Column({ - name: 'error_message', - type: 'text', - nullable: true, - }) - errorMessage: string | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Additional context data', - }) - metadata: Record | null; - - @Column({ - name: 'retention_days', - type: 'integer', - default: 90, - comment: 'Days to retain this log (90 for operational, 1825 for critical)', - }) - retentionDays: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'user_id' }) - user: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/admin/entities/backup.entity.ts b/projects/erp-construccion/backend/src/modules/admin/entities/backup.entity.ts deleted file mode 100644 index 343721acb..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/entities/backup.entity.ts +++ /dev/null @@ -1,301 +0,0 @@ -/** - * Backup Entity - * Registro de backups del sistema - * - * @module Admin - * @table admin.backups - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -export type BackupType = 'full' | 'incremental' | 'differential' | 'files' | 'snapshot'; - -export type BackupStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'expired'; - -export type BackupStorage = 'local' | 's3' | 'gcs' | 'azure' | 'offsite'; - -@Entity({ schema: 'admin', name: 'backups' }) -@Index(['tenantId']) -@Index(['backupType']) -@Index(['status']) -@Index(['createdAt']) -@Index(['expiresAt']) -export class Backup { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ - name: 'backup_type', - type: 'varchar', - length: 20, - }) - backupType: BackupType; - - @Column({ - type: 'varchar', - length: 20, - default: 'pending', - }) - status: BackupStatus; - - @Column({ - type: 'varchar', - length: 200, - comment: 'Descriptive name for the backup', - }) - name: string; - - @Column({ - type: 'text', - nullable: true, - }) - description: string | null; - - @Column({ - name: 'file_path', - type: 'varchar', - length: 500, - nullable: true, - }) - filePath: string | null; - - @Column({ - name: 'file_name', - type: 'varchar', - length: 200, - nullable: true, - }) - fileName: string | null; - - @Column({ - name: 'file_size', - type: 'bigint', - nullable: true, - comment: 'Size in bytes', - }) - fileSize: number | null; - - @Column({ - name: 'storage_location', - type: 'varchar', - length: 20, - default: 'local', - }) - storageLocation: BackupStorage; - - @Column({ - name: 'storage_url', - type: 'varchar', - length: 1000, - nullable: true, - comment: 'Full URL or path to backup file', - }) - storageUrl: string | null; - - @Column({ - type: 'varchar', - length: 64, - nullable: true, - comment: 'SHA-256 checksum for integrity verification', - }) - checksum: string | null; - - @Column({ - name: 'is_encrypted', - type: 'boolean', - default: true, - }) - isEncrypted: boolean; - - @Column({ - name: 'encryption_key_id', - type: 'varchar', - length: 100, - nullable: true, - comment: 'Reference to encryption key used', - }) - encryptionKeyId: string | null; - - @Column({ - name: 'is_compressed', - type: 'boolean', - default: true, - }) - isCompressed: boolean; - - @Column({ - name: 'compression_type', - type: 'varchar', - length: 20, - nullable: true, - comment: 'gzip, lz4, zstd, etc.', - }) - compressionType: string | null; - - @Column({ - name: 'database_version', - type: 'varchar', - length: 50, - nullable: true, - comment: 'PostgreSQL version at backup time', - }) - databaseVersion: string | null; - - @Column({ - name: 'app_version', - type: 'varchar', - length: 50, - nullable: true, - comment: 'Application version at backup time', - }) - appVersion: string | null; - - @Column({ - name: 'tables_included', - type: 'varchar', - array: true, - nullable: true, - comment: 'List of tables included in backup', - }) - tablesIncluded: string[] | null; - - @Column({ - name: 'tables_excluded', - type: 'varchar', - array: true, - nullable: true, - comment: 'List of tables excluded from backup', - }) - tablesExcluded: string[] | null; - - @Column({ - name: 'row_count', - type: 'integer', - nullable: true, - comment: 'Total rows backed up', - }) - rowCount: number | null; - - @Column({ - name: 'started_at', - type: 'timestamptz', - nullable: true, - }) - startedAt: Date | null; - - @Column({ - name: 'completed_at', - type: 'timestamptz', - nullable: true, - }) - completedAt: Date | null; - - @Column({ - name: 'duration_seconds', - type: 'integer', - nullable: true, - }) - durationSeconds: number | null; - - @Column({ - name: 'expires_at', - type: 'timestamptz', - nullable: true, - comment: 'When this backup will be automatically deleted', - }) - expiresAt: Date | null; - - @Column({ - name: 'retention_policy', - type: 'varchar', - length: 50, - nullable: true, - comment: 'daily, weekly, monthly, yearly, permanent', - }) - retentionPolicy: string | null; - - @Column({ - name: 'is_scheduled', - type: 'boolean', - default: false, - comment: 'Was this a scheduled backup?', - }) - isScheduled: boolean; - - @Column({ - name: 'schedule_id', - type: 'varchar', - length: 100, - nullable: true, - comment: 'Reference to schedule that triggered this backup', - }) - scheduleId: string | null; - - @Column({ - name: 'is_verified', - type: 'boolean', - default: false, - comment: 'Has restore been tested?', - }) - isVerified: boolean; - - @Column({ - name: 'verified_at', - type: 'timestamptz', - nullable: true, - }) - verifiedAt: Date | null; - - @Column({ - name: 'verified_by', - type: 'uuid', - nullable: true, - }) - verifiedById: string | null; - - @Column({ - name: 'error_message', - type: 'text', - nullable: true, - }) - errorMessage: string | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Additional backup metadata', - }) - metadata: Record | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'verified_by' }) - verifiedBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/admin/entities/cost-center.entity.ts b/projects/erp-construccion/backend/src/modules/admin/entities/cost-center.entity.ts deleted file mode 100644 index eed81c0fe..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/entities/cost-center.entity.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * CostCenter Entity - * Centros de costo jer谩rquicos para imputaci贸n de gastos - * - * @module Admin - * @table admin.cost_centers - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, - Tree, - TreeChildren, - TreeParent, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -export type CostCenterType = 'direct' | 'indirect' | 'shared_services' | 'overhead'; - -export type CostCenterLevel = 'company' | 'project' | 'phase' | 'front' | 'activity'; - -@Entity({ schema: 'admin', name: 'cost_centers' }) -@Tree('closure-table') -@Index(['tenantId', 'code'], { unique: true }) -@Index(['tenantId']) -@Index(['parentId']) -@Index(['fraccionamientoId']) -@Index(['costCenterType']) -@Index(['level']) -@Index(['isActive']) -export class CostCenter { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 50 }) - code: string; - - @Column({ type: 'varchar', length: 200 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string | null; - - @Column({ - name: 'cost_center_type', - type: 'varchar', - length: 30, - default: 'direct', - }) - costCenterType: CostCenterType; - - @Column({ - type: 'varchar', - length: 20, - default: 'activity', - }) - level: CostCenterLevel; - - @Column({ - name: 'parent_id', - type: 'uuid', - nullable: true, - }) - parentId: string | null; - - @Column({ - name: 'fraccionamiento_id', - type: 'uuid', - nullable: true, - comment: 'Project this cost center belongs to (null for company level)', - }) - fraccionamientoId: string | null; - - @Column({ - name: 'responsible_id', - type: 'uuid', - nullable: true, - comment: 'User responsible for this cost center', - }) - responsibleId: string | null; - - @Column({ - type: 'decimal', - precision: 16, - scale: 2, - default: 0, - comment: 'Annual budget for this cost center', - }) - budget: number; - - @Column({ - name: 'budget_consumed', - type: 'decimal', - precision: 16, - scale: 2, - default: 0, - comment: 'Amount consumed from budget', - }) - budgetConsumed: number; - - @Column({ - name: 'distribution_percentage', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - comment: 'Percentage for indirect cost distribution', - }) - distributionPercentage: number | null; - - @Column({ - name: 'distribution_base', - type: 'varchar', - length: 50, - nullable: true, - comment: 'Base for distribution: direct_cost, headcount, area, etc.', - }) - distributionBase: string | null; - - @Column({ - name: 'accounting_code', - type: 'varchar', - length: 50, - nullable: true, - comment: 'Code for accounting system integration', - }) - accountingCode: string | null; - - @Column({ - name: 'is_billable', - type: 'boolean', - default: false, - comment: 'Can be billed to client', - }) - isBillable: boolean; - - @Column({ - name: 'is_active', - type: 'boolean', - default: true, - }) - isActive: boolean; - - @Column({ - name: 'sort_order', - type: 'integer', - default: 0, - }) - sortOrder: number; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Additional metadata', - }) - metadata: Record | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @TreeParent() - @ManyToOne(() => CostCenter, (cc) => cc.children) - @JoinColumn({ name: 'parent_id' }) - parent: CostCenter | null; - - @TreeChildren() - @OneToMany(() => CostCenter, (cc) => cc.parent) - children: CostCenter[]; - - @ManyToOne(() => User) - @JoinColumn({ name: 'responsible_id' }) - responsible: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/admin/entities/custom-permission.entity.ts b/projects/erp-construccion/backend/src/modules/admin/entities/custom-permission.entity.ts deleted file mode 100644 index 72a6b5a14..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/entities/custom-permission.entity.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * CustomPermission Entity - * Permisos personalizados y temporales para usuarios - * - * @module Admin - * @table admin.custom_permissions - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -export type PermissionAction = 'create' | 'read' | 'update' | 'delete' | 'approve' | 'export' | 'import'; - -@Entity({ schema: 'admin', name: 'custom_permissions' }) -@Index(['tenantId']) -@Index(['userId']) -@Index(['module']) -@Index(['isActive']) -@Index(['validUntil']) -@Index(['tenantId', 'userId', 'module']) -export class CustomPermission { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'user_id', type: 'uuid' }) - userId: string; - - @Column({ - type: 'varchar', - length: 100, - comment: 'Module this permission applies to', - }) - module: string; - - @Column({ - type: 'varchar', - array: true, - comment: 'Actions allowed', - }) - actions: PermissionAction[]; - - @Column({ - name: 'resource_type', - type: 'varchar', - length: 100, - nullable: true, - comment: 'Specific resource type (e.g., Estimacion, Proyecto)', - }) - resourceType: string | null; - - @Column({ - name: 'resource_id', - type: 'uuid', - nullable: true, - comment: 'Specific resource ID (null = all resources)', - }) - resourceId: string | null; - - @Column({ - name: 'fraccionamiento_id', - type: 'uuid', - nullable: true, - comment: 'Project scope (null = all projects)', - }) - fraccionamientoId: string | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Additional conditions for permission', - }) - conditions: Record | null; - - @Column({ - type: 'text', - nullable: true, - comment: 'Reason for granting this permission', - }) - reason: string | null; - - @Column({ - name: 'valid_from', - type: 'timestamptz', - default: () => 'CURRENT_TIMESTAMP', - }) - validFrom: Date; - - @Column({ - name: 'valid_until', - type: 'timestamptz', - nullable: true, - comment: 'Expiration date (null = permanent)', - }) - validUntil: Date | null; - - @Column({ - name: 'is_active', - type: 'boolean', - default: true, - }) - isActive: boolean; - - @Column({ - name: 'granted_by', - type: 'uuid', - }) - grantedById: string; - - @Column({ - name: 'revoked_at', - type: 'timestamptz', - nullable: true, - }) - revokedAt: Date | null; - - @Column({ - name: 'revoked_by', - type: 'uuid', - nullable: true, - }) - revokedById: string | null; - - @Column({ - name: 'revoke_reason', - type: 'text', - nullable: true, - }) - revokeReason: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'user_id' }) - user: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'granted_by' }) - grantedBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'revoked_by' }) - revokedBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/admin/entities/index.ts b/projects/erp-construccion/backend/src/modules/admin/entities/index.ts deleted file mode 100644 index 9276d74f2..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/entities/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Admin Module - Entity Exports - * MAI-013: Administraci贸n & Seguridad - */ - -export * from './cost-center.entity'; -export * from './audit-log.entity'; -export * from './system-setting.entity'; -export * from './backup.entity'; -export * from './custom-permission.entity'; diff --git a/projects/erp-construccion/backend/src/modules/admin/entities/system-setting.entity.ts b/projects/erp-construccion/backend/src/modules/admin/entities/system-setting.entity.ts deleted file mode 100644 index 1a8d1600c..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/entities/system-setting.entity.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * SystemSetting Entity - * Configuraci贸n del sistema por tenant - * - * @module Admin - * @table admin.system_settings - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -export type SettingCategory = - | 'general' - | 'security' - | 'notifications' - | 'integrations' - | 'workflow' - | 'reports' - | 'backups' - | 'appearance'; - -export type SettingDataType = 'string' | 'number' | 'boolean' | 'json' | 'array'; - -@Entity({ schema: 'admin', name: 'system_settings' }) -@Index(['tenantId', 'key'], { unique: true }) -@Index(['tenantId']) -@Index(['category']) -@Index(['isPublic']) -export class SystemSetting { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ - type: 'varchar', - length: 100, - comment: 'Unique key for the setting', - }) - key: string; - - @Column({ - type: 'varchar', - length: 200, - }) - name: string; - - @Column({ - type: 'text', - nullable: true, - }) - description: string | null; - - @Column({ - type: 'varchar', - length: 30, - default: 'general', - }) - category: SettingCategory; - - @Column({ - name: 'data_type', - type: 'varchar', - length: 20, - default: 'string', - }) - dataType: SettingDataType; - - @Column({ - type: 'text', - comment: 'Current value (stored as string)', - }) - value: string; - - @Column({ - name: 'default_value', - type: 'text', - nullable: true, - comment: 'Default value for reset', - }) - defaultValue: string | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Validation rules (min, max, pattern, options, etc.)', - }) - validation: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Allowed options for select/enum types', - }) - options: Record[] | null; - - @Column({ - name: 'is_public', - type: 'boolean', - default: false, - comment: 'Can be read without authentication', - }) - isPublic: boolean; - - @Column({ - name: 'is_encrypted', - type: 'boolean', - default: false, - comment: 'Value is encrypted (for sensitive data)', - }) - isEncrypted: boolean; - - @Column({ - name: 'is_system', - type: 'boolean', - default: false, - comment: 'System setting cannot be deleted', - }) - isSystem: boolean; - - @Column({ - name: 'requires_restart', - type: 'boolean', - default: false, - comment: 'Requires app restart to take effect', - }) - requiresRestart: boolean; - - @Column({ - name: 'allowed_roles', - type: 'varchar', - array: true, - nullable: true, - comment: 'Roles that can modify this setting', - }) - allowedRoles: string[] | null; - - @Column({ - name: 'sort_order', - type: 'integer', - default: 0, - }) - sortOrder: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'updated_by' }) - updatedBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/admin/services/audit-log.service.ts b/projects/erp-construccion/backend/src/modules/admin/services/audit-log.service.ts deleted file mode 100644 index 2a1706cd7..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/services/audit-log.service.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * AuditLogService - Gesti贸n de Logs de Auditor铆a - * - * Registra y consulta eventos de auditor铆a para trazabilidad. - * - * @module Admin - */ - -import { Repository } from 'typeorm'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { AuditLog, AuditCategory, AuditAction, AuditSeverity } from '../entities/audit-log.entity'; - -export interface CreateAuditLogDto { - userId?: string; - category: AuditCategory; - action: AuditAction; - severity?: AuditSeverity; - entityType?: string; - entityId?: string; - entityName?: string; - description: string; - oldValues?: Record; - newValues?: Record; - changedFields?: string[]; - ipAddress?: string; - userAgent?: string; - requestId?: string; - sessionId?: string; - module?: string; - endpoint?: string; - httpMethod?: string; - responseStatus?: number; - durationMs?: number; - isSuccess?: boolean; - errorMessage?: string; - metadata?: Record; -} - -export interface AuditLogFilters { - userId?: string; - category?: AuditCategory; - action?: AuditAction; - severity?: AuditSeverity; - entityType?: string; - entityId?: string; - module?: string; - isSuccess?: boolean; - dateFrom?: Date; - dateTo?: Date; - ipAddress?: string; - search?: string; -} - -export class AuditLogService { - constructor(private readonly repository: Repository) {} - - /** - * Crear registro de auditor铆a - */ - async log(ctx: ServiceContext, data: CreateAuditLogDto): Promise { - // Determinar retenci贸n basada en severidad - let retentionDays = 90; // Default for operational logs - if (data.severity === 'critical' || data.category === 'critical_operation') { - retentionDays = 1825; // 5 years for critical - } else if (data.severity === 'high') { - retentionDays = 365; // 1 year for high severity - } - - const log = this.repository.create({ - tenantId: ctx.tenantId, - ...data, - severity: data.severity || 'medium', - isSuccess: data.isSuccess ?? true, - retentionDays, - }); - - return this.repository.save(log); - } - - /** - * Buscar logs con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: AuditLogFilters, - page = 1, - limit = 50 - ): Promise> { - const qb = this.repository - .createQueryBuilder('al') - .leftJoinAndSelect('al.user', 'u') - .where('al.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.userId) { - qb.andWhere('al.user_id = :userId', { userId: filters.userId }); - } - if (filters.category) { - qb.andWhere('al.category = :category', { category: filters.category }); - } - if (filters.action) { - qb.andWhere('al.action = :action', { action: filters.action }); - } - if (filters.severity) { - qb.andWhere('al.severity = :severity', { severity: filters.severity }); - } - if (filters.entityType) { - qb.andWhere('al.entity_type = :entityType', { entityType: filters.entityType }); - } - if (filters.entityId) { - qb.andWhere('al.entity_id = :entityId', { entityId: filters.entityId }); - } - if (filters.module) { - qb.andWhere('al.module = :module', { module: filters.module }); - } - if (filters.isSuccess !== undefined) { - qb.andWhere('al.is_success = :isSuccess', { isSuccess: filters.isSuccess }); - } - if (filters.dateFrom) { - qb.andWhere('al.created_at >= :dateFrom', { dateFrom: filters.dateFrom }); - } - if (filters.dateTo) { - qb.andWhere('al.created_at <= :dateTo', { dateTo: filters.dateTo }); - } - if (filters.ipAddress) { - qb.andWhere('al.ip_address = :ipAddress', { ipAddress: filters.ipAddress }); - } - if (filters.search) { - qb.andWhere( - '(al.description ILIKE :search OR al.entity_name ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - const skip = (page - 1) * limit; - qb.orderBy('al.created_at', 'DESC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Obtener logs por entidad - */ - async findByEntity( - ctx: ServiceContext, - entityType: string, - entityId: string, - page = 1, - limit = 20 - ): Promise> { - return this.findWithFilters(ctx, { entityType, entityId }, page, limit); - } - - /** - * Obtener logs por usuario - */ - async findByUser( - ctx: ServiceContext, - userId: string, - page = 1, - limit = 50 - ): Promise> { - return this.findWithFilters(ctx, { userId }, page, limit); - } - - /** - * Obtener logs cr铆ticos recientes - */ - async getCriticalLogs( - ctx: ServiceContext, - days = 7, - limit = 100 - ): Promise { - const dateFrom = new Date(); - dateFrom.setDate(dateFrom.getDate() - days); - - return this.repository - .createQueryBuilder('al') - .leftJoinAndSelect('al.user', 'u') - .where('al.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('al.severity IN (:...severities)', { severities: ['high', 'critical'] }) - .andWhere('al.created_at >= :dateFrom', { dateFrom }) - .orderBy('al.created_at', 'DESC') - .take(limit) - .getMany(); - } - - /** - * Obtener logs fallidos - */ - async getFailedLogs( - ctx: ServiceContext, - hours = 24, - limit = 100 - ): Promise { - const dateFrom = new Date(); - dateFrom.setHours(dateFrom.getHours() - hours); - - return this.repository - .createQueryBuilder('al') - .leftJoinAndSelect('al.user', 'u') - .where('al.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('al.is_success = false') - .andWhere('al.created_at >= :dateFrom', { dateFrom }) - .orderBy('al.created_at', 'DESC') - .take(limit) - .getMany(); - } - - /** - * Limpiar logs expirados - */ - async cleanupExpiredLogs(ctx: ServiceContext): Promise { - const result = await this.repository - .createQueryBuilder() - .delete() - .from(AuditLog) - .where('tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere("created_at < NOW() - (retention_days || ' days')::interval") - .execute(); - - return result.affected || 0; - } - - /** - * Obtener estad铆sticas - */ - async getStats(ctx: ServiceContext, days = 30): Promise { - const dateFrom = new Date(); - dateFrom.setDate(dateFrom.getDate() - days); - - const qb = this.repository - .createQueryBuilder('al') - .where('al.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('al.created_at >= :dateFrom', { dateFrom }); - - // Total count - const total = await qb.getCount(); - - // By category - const byCategory = await this.repository - .createQueryBuilder('al') - .select('al.category', 'category') - .addSelect('COUNT(*)', 'count') - .where('al.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('al.created_at >= :dateFrom', { dateFrom }) - .groupBy('al.category') - .getRawMany(); - - // By action - const byAction = await this.repository - .createQueryBuilder('al') - .select('al.action', 'action') - .addSelect('COUNT(*)', 'count') - .where('al.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('al.created_at >= :dateFrom', { dateFrom }) - .groupBy('al.action') - .getRawMany(); - - // Success/failure - const successCount = await this.repository - .createQueryBuilder('al') - .where('al.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('al.created_at >= :dateFrom', { dateFrom }) - .andWhere('al.is_success = true') - .getCount(); - - // By severity - const bySeverity = await this.repository - .createQueryBuilder('al') - .select('al.severity', 'severity') - .addSelect('COUNT(*)', 'count') - .where('al.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('al.created_at >= :dateFrom', { dateFrom }) - .groupBy('al.severity') - .getRawMany(); - - return { - period: { days, from: dateFrom, to: new Date() }, - total, - successCount, - failureCount: total - successCount, - successRate: total > 0 ? (successCount / total) * 100 : 0, - byCategory: byCategory.map((r) => ({ category: r.category, count: parseInt(r.count) })), - byAction: byAction.map((r) => ({ action: r.action, count: parseInt(r.count) })), - bySeverity: bySeverity.map((r) => ({ severity: r.severity, count: parseInt(r.count) })), - }; - } -} - -export interface AuditLogStats { - period: { days: number; from: Date; to: Date }; - total: number; - successCount: number; - failureCount: number; - successRate: number; - byCategory: { category: AuditCategory; count: number }[]; - byAction: { action: AuditAction; count: number }[]; - bySeverity: { severity: AuditSeverity; count: number }[]; -} diff --git a/projects/erp-construccion/backend/src/modules/admin/services/backup.service.ts b/projects/erp-construccion/backend/src/modules/admin/services/backup.service.ts deleted file mode 100644 index 745399e7b..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/services/backup.service.ts +++ /dev/null @@ -1,308 +0,0 @@ -/** - * BackupService - Gesti贸n de Backups - * - * Administra creaci贸n, verificaci贸n y restauraci贸n de backups. - * - * @module Admin - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Backup, BackupType, BackupStatus, BackupStorage } from '../entities/backup.entity'; - -export interface CreateBackupDto { - backupType: BackupType; - name: string; - description?: string; - storageLocation?: BackupStorage; - tablesIncluded?: string[]; - tablesExcluded?: string[]; - retentionPolicy?: string; - isScheduled?: boolean; - scheduleId?: string; - metadata?: Record; -} - -export interface BackupFilters { - backupType?: BackupType; - status?: BackupStatus; - storageLocation?: BackupStorage; - isScheduled?: boolean; - isVerified?: boolean; - dateFrom?: Date; - dateTo?: Date; -} - -export class BackupService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Buscar backups con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: BackupFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('b') - .leftJoinAndSelect('b.createdBy', 'cb') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.backupType) { - qb.andWhere('b.backup_type = :backupType', { backupType: filters.backupType }); - } - if (filters.status) { - qb.andWhere('b.status = :status', { status: filters.status }); - } - if (filters.storageLocation) { - qb.andWhere('b.storage_location = :storageLocation', { storageLocation: filters.storageLocation }); - } - if (filters.isScheduled !== undefined) { - qb.andWhere('b.is_scheduled = :isScheduled', { isScheduled: filters.isScheduled }); - } - if (filters.isVerified !== undefined) { - qb.andWhere('b.is_verified = :isVerified', { isVerified: filters.isVerified }); - } - if (filters.dateFrom) { - qb.andWhere('b.created_at >= :dateFrom', { dateFrom: filters.dateFrom }); - } - if (filters.dateTo) { - qb.andWhere('b.created_at <= :dateTo', { dateTo: filters.dateTo }); - } - - const skip = (page - 1) * limit; - qb.orderBy('b.created_at', 'DESC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Iniciar backup - */ - async initiateBackup(ctx: ServiceContext, data: CreateBackupDto): Promise { - // Calculate expiration based on retention policy - let expiresAt: Date | null = null; - if (data.retentionPolicy) { - expiresAt = this.calculateExpiration(data.retentionPolicy); - } - - const backup = await this.create(ctx, { - ...data, - status: 'pending' as BackupStatus, - isEncrypted: true, - isCompressed: true, - compressionType: 'gzip', - expiresAt, - }); - - // In production, this would trigger an async job - // For now, we simulate starting the backup - await this.startBackupProcess(ctx, backup.id); - - return this.findById(ctx, backup.id) as Promise; - } - - /** - * Proceso de backup (simulado) - */ - private async startBackupProcess(ctx: ServiceContext, backupId: string): Promise { - const startTime = Date.now(); - - await this.update(ctx, backupId, { - status: 'running' as BackupStatus, - startedAt: new Date(), - }); - - // Simulate backup process - // In production, this would be handled by a job queue - const duration = Date.now() - startTime; - - await this.update(ctx, backupId, { - status: 'completed' as BackupStatus, - completedAt: new Date(), - durationSeconds: Math.ceil(duration / 1000), - // These would be real values from the backup process - fileSize: 0, - rowCount: 0, - }); - } - - /** - * Calcular fecha de expiraci贸n - */ - private calculateExpiration(policy: string): Date | null { - const now = new Date(); - switch (policy) { - case 'daily': - now.setDate(now.getDate() + 7); // Keep 7 days - return now; - case 'weekly': - now.setDate(now.getDate() + 30); // Keep 30 days - return now; - case 'monthly': - now.setMonth(now.getMonth() + 12); // Keep 12 months - return now; - case 'yearly': - now.setFullYear(now.getFullYear() + 7); // Keep 7 years - return now; - case 'permanent': - return null; // Never expires - default: - now.setDate(now.getDate() + 30); // Default 30 days - return now; - } - } - - /** - * Marcar backup como verificado - */ - async markVerified(ctx: ServiceContext, id: string): Promise { - const backup = await this.findById(ctx, id); - if (!backup) { - return null; - } - - if (backup.status !== 'completed') { - throw new Error('Only completed backups can be verified'); - } - - return this.update(ctx, id, { - isVerified: true, - verifiedAt: new Date(), - verifiedById: ctx.userId, - }); - } - - /** - * Cancelar backup en progreso - */ - async cancelBackup(ctx: ServiceContext, id: string): Promise { - const backup = await this.findById(ctx, id); - if (!backup) { - return null; - } - - if (!['pending', 'running'].includes(backup.status)) { - throw new Error('Only pending or running backups can be cancelled'); - } - - return this.update(ctx, id, { - status: 'cancelled' as BackupStatus, - completedAt: new Date(), - }); - } - - /** - * Obtener 煤ltimo backup exitoso - */ - async getLastSuccessful(ctx: ServiceContext, backupType?: BackupType): Promise { - const qb = this.repository - .createQueryBuilder('b') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.status = :status', { status: 'completed' }); - - if (backupType) { - qb.andWhere('b.backup_type = :backupType', { backupType }); - } - - return qb.orderBy('b.completed_at', 'DESC').getOne(); - } - - /** - * Obtener backups pendientes de verificaci贸n - */ - async getPendingVerification(ctx: ServiceContext): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - status: 'completed' as BackupStatus, - isVerified: false, - } as any, - order: { completedAt: 'DESC' }, - }); - } - - /** - * Limpiar backups expirados - */ - async cleanupExpired(ctx: ServiceContext): Promise { - const result = await this.repository - .createQueryBuilder() - .update(Backup) - .set({ status: 'expired' as BackupStatus }) - .where('tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('expires_at < NOW()') - .andWhere('status = :status', { status: 'completed' }) - .execute(); - - return result.affected || 0; - } - - /** - * Obtener estad铆sticas - */ - async getStats(ctx: ServiceContext): Promise { - const all = await this.repository.find({ - where: { tenantId: ctx.tenantId } as any, - }); - - const byType = new Map(); - const byStatus = new Map(); - let totalSize = 0; - let verifiedCount = 0; - - for (const backup of all) { - byType.set(backup.backupType, (byType.get(backup.backupType) || 0) + 1); - byStatus.set(backup.status, (byStatus.get(backup.status) || 0) + 1); - totalSize += Number(backup.fileSize) || 0; - if (backup.isVerified) verifiedCount++; - } - - const lastBackup = await this.getLastSuccessful(ctx); - - return { - total: all.length, - totalSizeBytes: totalSize, - totalSizeHuman: this.formatBytes(totalSize), - verifiedCount, - byType: Array.from(byType.entries()).map(([type, count]) => ({ type, count })), - byStatus: Array.from(byStatus.entries()).map(([status, count]) => ({ status, count })), - lastBackupAt: lastBackup?.completedAt || null, - }; - } - - /** - * Formatear bytes a formato legible - */ - private formatBytes(bytes: number): string { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - } -} - -export interface BackupStats { - total: number; - totalSizeBytes: number; - totalSizeHuman: string; - verifiedCount: number; - byType: { type: BackupType; count: number }[]; - byStatus: { status: BackupStatus; count: number }[]; - lastBackupAt: Date | null; -} diff --git a/projects/erp-construccion/backend/src/modules/admin/services/cost-center.service.ts b/projects/erp-construccion/backend/src/modules/admin/services/cost-center.service.ts deleted file mode 100644 index ede0ae961..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/services/cost-center.service.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * CostCenterService - Gesti贸n de Centros de Costo - * - * Administra centros de costo jer谩rquicos para imputaci贸n de gastos. - * - * @module Admin - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { CostCenter, CostCenterType, CostCenterLevel } from '../entities/cost-center.entity'; - -export interface CreateCostCenterDto { - code: string; - name: string; - description?: string; - costCenterType?: CostCenterType; - level?: CostCenterLevel; - parentId?: string; - fraccionamientoId?: string; - responsibleId?: string; - budget?: number; - distributionPercentage?: number; - distributionBase?: string; - accountingCode?: string; - isBillable?: boolean; - metadata?: Record; -} - -export interface UpdateCostCenterDto extends Partial { - isActive?: boolean; - sortOrder?: number; -} - -export interface CostCenterFilters { - costCenterType?: CostCenterType; - level?: CostCenterLevel; - fraccionamientoId?: string; - parentId?: string; - responsibleId?: string; - isActive?: boolean; - search?: string; -} - -export class CostCenterService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Buscar centros de costo con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: CostCenterFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('cc') - .leftJoinAndSelect('cc.responsible', 'r') - .where('cc.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('cc.deleted_at IS NULL'); - - if (filters.costCenterType) { - qb.andWhere('cc.cost_center_type = :costCenterType', { costCenterType: filters.costCenterType }); - } - if (filters.level) { - qb.andWhere('cc.level = :level', { level: filters.level }); - } - if (filters.fraccionamientoId) { - qb.andWhere('cc.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId: filters.fraccionamientoId }); - } - if (filters.parentId !== undefined) { - if (filters.parentId === null) { - qb.andWhere('cc.parent_id IS NULL'); - } else { - qb.andWhere('cc.parent_id = :parentId', { parentId: filters.parentId }); - } - } - if (filters.responsibleId) { - qb.andWhere('cc.responsible_id = :responsibleId', { responsibleId: filters.responsibleId }); - } - if (filters.isActive !== undefined) { - qb.andWhere('cc.is_active = :isActive', { isActive: filters.isActive }); - } - if (filters.search) { - qb.andWhere('(cc.name ILIKE :search OR cc.code ILIKE :search OR cc.description ILIKE :search)', { - search: `%${filters.search}%`, - }); - } - - const skip = (page - 1) * limit; - qb.orderBy('cc.sort_order', 'ASC') - .addOrderBy('cc.code', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Buscar por c贸digo - */ - async findByCode(ctx: ServiceContext, code: string): Promise { - return this.repository.findOne({ - where: { - tenantId: ctx.tenantId, - code, - deletedAt: null, - } as any, - }); - } - - /** - * Obtener 谩rbol de centros de costo - */ - async getTree(ctx: ServiceContext, fraccionamientoId?: string): Promise { - const qb = this.repository - .createQueryBuilder('cc') - .where('cc.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('cc.deleted_at IS NULL') - .andWhere('cc.parent_id IS NULL'); - - if (fraccionamientoId) { - qb.andWhere('(cc.fraccionamiento_id = :fraccionamientoId OR cc.fraccionamiento_id IS NULL)', { - fraccionamientoId, - }); - } - - const roots = await qb.orderBy('cc.sort_order', 'ASC').getMany(); - - // Load children recursively - for (const root of roots) { - await this.loadChildren(ctx, root, fraccionamientoId); - } - - return roots; - } - - /** - * Cargar hijos recursivamente - */ - private async loadChildren( - ctx: ServiceContext, - parent: CostCenter, - fraccionamientoId?: string - ): Promise { - const qb = this.repository - .createQueryBuilder('cc') - .where('cc.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('cc.deleted_at IS NULL') - .andWhere('cc.parent_id = :parentId', { parentId: parent.id }); - - if (fraccionamientoId) { - qb.andWhere('(cc.fraccionamiento_id = :fraccionamientoId OR cc.fraccionamiento_id IS NULL)', { - fraccionamientoId, - }); - } - - parent.children = await qb.orderBy('cc.sort_order', 'ASC').getMany(); - - for (const child of parent.children) { - await this.loadChildren(ctx, child, fraccionamientoId); - } - } - - /** - * Obtener hijos directos - */ - async getChildren(ctx: ServiceContext, parentId: string): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - parentId, - deletedAt: null, - } as any, - order: { sortOrder: 'ASC', code: 'ASC' }, - }); - } - - /** - * Crear centro de costo - */ - async createCostCenter(ctx: ServiceContext, data: CreateCostCenterDto): Promise { - const existing = await this.findByCode(ctx, data.code); - if (existing) { - throw new Error(`Cost center with code ${data.code} already exists`); - } - - // Validate parent if provided - if (data.parentId) { - const parent = await this.findById(ctx, data.parentId); - if (!parent) { - throw new Error('Parent cost center not found'); - } - } - - return this.create(ctx, { - ...data, - isActive: true, - budgetConsumed: 0, - }); - } - - /** - * Actualizar presupuesto consumido - */ - async updateBudgetConsumed( - ctx: ServiceContext, - id: string, - amount: number - ): Promise { - const cc = await this.findById(ctx, id); - if (!cc) { - return null; - } - - return this.update(ctx, id, { - budgetConsumed: Number(cc.budgetConsumed) + amount, - }); - } - - /** - * Obtener resumen de presupuesto - */ - async getBudgetSummary( - ctx: ServiceContext, - fraccionamientoId?: string - ): Promise { - const qb = this.repository - .createQueryBuilder('cc') - .select([ - 'cc.cost_center_type as type', - 'SUM(cc.budget) as total_budget', - 'SUM(cc.budget_consumed) as total_consumed', - 'COUNT(*) as count', - ]) - .where('cc.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('cc.deleted_at IS NULL') - .andWhere('cc.is_active = true') - .groupBy('cc.cost_center_type'); - - if (fraccionamientoId) { - qb.andWhere('cc.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - const results = await qb.getRawMany(); - - let totalBudget = 0; - let totalConsumed = 0; - const byType: { type: CostCenterType; budget: number; consumed: number; count: number }[] = []; - - for (const r of results) { - const budget = parseFloat(r.total_budget || '0'); - const consumed = parseFloat(r.total_consumed || '0'); - totalBudget += budget; - totalConsumed += consumed; - byType.push({ - type: r.type, - budget, - consumed, - count: parseInt(r.count), - }); - } - - return { - totalBudget, - totalConsumed, - totalAvailable: totalBudget - totalConsumed, - utilizationPercentage: totalBudget > 0 ? (totalConsumed / totalBudget) * 100 : 0, - byType, - }; - } - - /** - * Obtener estad铆sticas - */ - async getStats(ctx: ServiceContext): Promise { - const all = await this.repository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - }); - - const byType = new Map(); - const byLevel = new Map(); - let activeCount = 0; - - for (const cc of all) { - byType.set(cc.costCenterType, (byType.get(cc.costCenterType) || 0) + 1); - byLevel.set(cc.level, (byLevel.get(cc.level) || 0) + 1); - if (cc.isActive) activeCount++; - } - - return { - total: all.length, - active: activeCount, - inactive: all.length - activeCount, - byType: Array.from(byType.entries()).map(([type, count]) => ({ type, count })), - byLevel: Array.from(byLevel.entries()).map(([level, count]) => ({ level, count })), - }; - } -} - -export interface CostCenterBudgetSummary { - totalBudget: number; - totalConsumed: number; - totalAvailable: number; - utilizationPercentage: number; - byType: { - type: CostCenterType; - budget: number; - consumed: number; - count: number; - }[]; -} - -export interface CostCenterStats { - total: number; - active: number; - inactive: number; - byType: { type: CostCenterType; count: number }[]; - byLevel: { level: CostCenterLevel; count: number }[]; -} diff --git a/projects/erp-construccion/backend/src/modules/admin/services/index.ts b/projects/erp-construccion/backend/src/modules/admin/services/index.ts deleted file mode 100644 index ade298138..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/services/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Admin Module - Service Exports - * MAI-013: Administraci贸n & Seguridad - */ - -export * from './cost-center.service'; -export * from './audit-log.service'; -export * from './system-setting.service'; -export * from './backup.service'; diff --git a/projects/erp-construccion/backend/src/modules/admin/services/system-setting.service.ts b/projects/erp-construccion/backend/src/modules/admin/services/system-setting.service.ts deleted file mode 100644 index 3daf8f639..000000000 --- a/projects/erp-construccion/backend/src/modules/admin/services/system-setting.service.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * SystemSettingService - Gesti贸n de Configuraci贸n del Sistema - * - * Administra configuraciones del sistema por tenant. - * - * @module Admin - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { SystemSetting, SettingCategory, SettingDataType } from '../entities/system-setting.entity'; - -export interface CreateSettingDto { - key: string; - name: string; - description?: string; - category?: SettingCategory; - dataType?: SettingDataType; - value: string; - defaultValue?: string; - validation?: Record; - options?: Record[]; - isPublic?: boolean; - isEncrypted?: boolean; - requiresRestart?: boolean; - allowedRoles?: string[]; -} - -export interface UpdateSettingDto { - name?: string; - description?: string; - value?: string; - validation?: Record; - options?: Record[]; - isPublic?: boolean; - allowedRoles?: string[]; - sortOrder?: number; -} - -export interface SettingFilters { - category?: SettingCategory; - isPublic?: boolean; - search?: string; -} - -export class SystemSettingService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Obtener valor de configuraci贸n - */ - async getValue(ctx: ServiceContext, key: string): Promise { - const setting = await this.findByKey(ctx, key); - if (!setting) { - return null; - } - - return this.parseValue(setting.value, setting.dataType); - } - - /** - * Obtener valor con default - */ - async getValueOrDefault(ctx: ServiceContext, key: string, defaultValue: any): Promise { - const value = await this.getValue(ctx, key); - return value !== null ? value : defaultValue; - } - - /** - * Buscar por key - */ - async findByKey(ctx: ServiceContext, key: string): Promise { - return this.repository.findOne({ - where: { - tenantId: ctx.tenantId, - key, - } as any, - }); - } - - /** - * Buscar configuraciones con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: SettingFilters, - page = 1, - limit = 50 - ): Promise> { - const qb = this.repository - .createQueryBuilder('ss') - .where('ss.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.category) { - qb.andWhere('ss.category = :category', { category: filters.category }); - } - if (filters.isPublic !== undefined) { - qb.andWhere('ss.is_public = :isPublic', { isPublic: filters.isPublic }); - } - if (filters.search) { - qb.andWhere( - '(ss.key ILIKE :search OR ss.name ILIKE :search OR ss.description ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - const skip = (page - 1) * limit; - qb.orderBy('ss.category', 'ASC') - .addOrderBy('ss.sort_order', 'ASC') - .addOrderBy('ss.key', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Obtener por categor铆a - */ - async findByCategory(ctx: ServiceContext, category: SettingCategory): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - category, - } as any, - order: { sortOrder: 'ASC', key: 'ASC' }, - }); - } - - /** - * Obtener configuraciones p煤blicas - */ - async getPublicSettings(ctx: ServiceContext): Promise> { - const settings = await this.repository.find({ - where: { - tenantId: ctx.tenantId, - isPublic: true, - } as any, - }); - - const result: Record = {}; - for (const setting of settings) { - result[setting.key] = this.parseValue(setting.value, setting.dataType); - } - - return result; - } - - /** - * Crear configuraci贸n - */ - async createSetting(ctx: ServiceContext, data: CreateSettingDto): Promise { - const existing = await this.findByKey(ctx, data.key); - if (existing) { - throw new Error(`Setting with key ${data.key} already exists`); - } - - // Validate value against type and validation rules - this.validateValue(data.value, data.dataType || 'string', data.validation); - - return this.create(ctx, { - ...data, - isSystem: false, - }); - } - - /** - * Actualizar valor - */ - async updateValue(ctx: ServiceContext, key: string, value: string): Promise { - const setting = await this.findByKey(ctx, key); - if (!setting) { - return null; - } - - // Validate value against type and validation rules - this.validateValue(value, setting.dataType, setting.validation); - - return this.update(ctx, setting.id, { value }); - } - - /** - * Restablecer a valor por defecto - */ - async resetToDefault(ctx: ServiceContext, key: string): Promise { - const setting = await this.findByKey(ctx, key); - if (!setting || !setting.defaultValue) { - return null; - } - - return this.update(ctx, setting.id, { value: setting.defaultValue }); - } - - /** - * Actualizar m煤ltiples configuraciones - */ - async updateMultiple( - ctx: ServiceContext, - settings: { key: string; value: string }[] - ): Promise<{ updated: number; errors: string[] }> { - let updated = 0; - const errors: string[] = []; - - for (const { key, value } of settings) { - try { - const result = await this.updateValue(ctx, key, value); - if (result) { - updated++; - } else { - errors.push(`Setting ${key} not found`); - } - } catch (error) { - errors.push(`${key}: ${error instanceof Error ? error.message : 'Unknown error'}`); - } - } - - return { updated, errors }; - } - - /** - * Parsear valor seg煤n tipo - */ - private parseValue(value: string, dataType: SettingDataType): any { - switch (dataType) { - case 'number': - return parseFloat(value); - case 'boolean': - return value === 'true' || value === '1'; - case 'json': - case 'array': - try { - return JSON.parse(value); - } catch { - return null; - } - default: - return value; - } - } - - /** - * Validar valor - */ - private validateValue( - value: string, - dataType: SettingDataType, - validation?: Record | null - ): void { - // Basic type validation - switch (dataType) { - case 'number': - if (isNaN(parseFloat(value))) { - throw new Error('Value must be a valid number'); - } - break; - case 'boolean': - if (!['true', 'false', '1', '0'].includes(value)) { - throw new Error('Value must be true/false or 1/0'); - } - break; - case 'json': - case 'array': - try { - JSON.parse(value); - } catch { - throw new Error('Value must be valid JSON'); - } - break; - } - - // Custom validation rules - if (validation) { - if (validation.min !== undefined && parseFloat(value) < validation.min) { - throw new Error(`Value must be at least ${validation.min}`); - } - if (validation.max !== undefined && parseFloat(value) > validation.max) { - throw new Error(`Value must be at most ${validation.max}`); - } - if (validation.pattern && !new RegExp(validation.pattern).test(value)) { - throw new Error(`Value does not match required pattern`); - } - if (validation.options && !validation.options.includes(value)) { - throw new Error(`Value must be one of: ${validation.options.join(', ')}`); - } - } - } - - /** - * Obtener estad铆sticas - */ - async getStats(ctx: ServiceContext): Promise { - const all = await this.repository.find({ - where: { tenantId: ctx.tenantId } as any, - }); - - const byCategory = new Map(); - let publicCount = 0; - let systemCount = 0; - let encryptedCount = 0; - - for (const setting of all) { - byCategory.set(setting.category, (byCategory.get(setting.category) || 0) + 1); - if (setting.isPublic) publicCount++; - if (setting.isSystem) systemCount++; - if (setting.isEncrypted) encryptedCount++; - } - - return { - total: all.length, - publicCount, - systemCount, - encryptedCount, - byCategory: Array.from(byCategory.entries()).map(([category, count]) => ({ category, count })), - }; - } -} - -export interface SettingStats { - total: number; - publicCount: number; - systemCount: number; - encryptedCount: number; - byCategory: { category: SettingCategory; count: number }[]; -} diff --git a/projects/erp-construccion/backend/src/modules/auth/controllers/auth.controller.ts b/projects/erp-construccion/backend/src/modules/auth/controllers/auth.controller.ts deleted file mode 100644 index 461cf028c..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/controllers/auth.controller.ts +++ /dev/null @@ -1,268 +0,0 @@ -/** - * AuthController - Controlador de Autenticaci贸n - * - * Endpoints REST para login, register, refresh y logout. - * Implementa validaci贸n de datos y manejo de errores. - * - * @module Auth - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { AuthService } from '../services/auth.service'; -import { AuthMiddleware } from '../middleware/auth.middleware'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../entities/refresh-token.entity'; -import { - LoginDto, - RegisterDto, - RefreshTokenDto, - ChangePasswordDto, -} from '../dto/auth.dto'; - -/** - * Crear router de autenticaci贸n - */ -export function createAuthController(dataSource: DataSource): Router { - const router = Router(); - - // Inicializar repositorios - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Inicializar servicio - const authService = new AuthService( - userRepository, - tenantRepository, - refreshTokenRepository as any - ); - - // Inicializar middleware - const authMiddleware = new AuthMiddleware(authService, dataSource); - - /** - * POST /auth/login - * Login de usuario - */ - router.post('/login', async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: LoginDto = req.body; - - if (!dto.email || !dto.password) { - res.status(400).json({ - error: 'Bad Request', - message: 'Email and password are required', - }); - return; - } - - const result = await authService.login(dto); - res.status(200).json({ success: true, data: result }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Invalid credentials') { - res.status(401).json({ error: 'Unauthorized', message: 'Invalid email or password' }); - return; - } - if (error.message === 'User is not active') { - res.status(403).json({ error: 'Forbidden', message: 'User account is disabled' }); - return; - } - if (error.message === 'No tenant specified' || error.message === 'Tenant not found or inactive') { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * POST /auth/register - * Registro de nuevo usuario - */ - router.post('/register', async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: RegisterDto = req.body; - - if (!dto.email || !dto.password || !dto.firstName || !dto.lastName || !dto.tenantId) { - res.status(400).json({ - error: 'Bad Request', - message: 'Email, password, firstName, lastName and tenantId are required', - }); - return; - } - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(dto.email)) { - res.status(400).json({ error: 'Bad Request', message: 'Invalid email format' }); - return; - } - - if (dto.password.length < 8) { - res.status(400).json({ error: 'Bad Request', message: 'Password must be at least 8 characters' }); - return; - } - - const result = await authService.register(dto); - res.status(201).json({ success: true, data: result }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Email already registered') { - res.status(409).json({ error: 'Conflict', message: 'Email is already registered' }); - return; - } - if (error.message === 'Tenant not found') { - res.status(400).json({ error: 'Bad Request', message: 'Invalid tenant ID' }); - return; - } - } - next(error); - } - }); - - /** - * POST /auth/refresh - * Renovar access token usando refresh token - */ - router.post('/refresh', async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: RefreshTokenDto = req.body; - - if (!dto.refreshToken) { - res.status(400).json({ error: 'Bad Request', message: 'Refresh token is required' }); - return; - } - - const result = await authService.refresh(dto); - res.status(200).json({ success: true, data: result }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Invalid refresh token' || error.message === 'Refresh token expired or revoked') { - res.status(401).json({ error: 'Unauthorized', message: error.message }); - return; - } - if (error.message === 'User not found or inactive') { - res.status(401).json({ error: 'Unauthorized', message: 'User account is disabled or deleted' }); - return; - } - } - next(error); - } - }); - - /** - * POST /auth/logout - * Cerrar sesi贸n (revocar refresh token) - */ - router.post('/logout', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { refreshToken } = req.body; - if (refreshToken) { - await authService.logout(refreshToken); - } - res.status(200).json({ success: true, message: 'Logged out successfully' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /auth/change-password - * Cambiar contrase帽a (requiere autenticaci贸n) - */ - router.post('/change-password', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: ChangePasswordDto = req.body; - const userId = req.user?.sub; - - if (!userId) { - res.status(401).json({ error: 'Unauthorized', message: 'User not authenticated' }); - return; - } - - if (!dto.currentPassword || !dto.newPassword) { - res.status(400).json({ error: 'Bad Request', message: 'Current password and new password are required' }); - return; - } - - if (dto.newPassword.length < 8) { - res.status(400).json({ error: 'Bad Request', message: 'New password must be at least 8 characters' }); - return; - } - - await authService.changePassword(userId, dto); - res.status(200).json({ success: true, message: 'Password changed successfully' }); - } catch (error) { - if (error instanceof Error && error.message === 'Current password is incorrect') { - res.status(400).json({ error: 'Bad Request', message: 'Current password is incorrect' }); - return; - } - next(error); - } - }); - - /** - * GET /auth/me - * Obtener informaci贸n del usuario autenticado - */ - router.get('/me', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const userId = req.user?.sub; - - if (!userId) { - res.status(401).json({ error: 'Unauthorized', message: 'User not authenticated' }); - return; - } - - const user = await userRepository.findOne({ - where: { id: userId } as any, - select: ['id', 'email', 'firstName', 'lastName', 'isActive', 'createdAt'], - }); - - if (!user) { - res.status(404).json({ error: 'Not Found', message: 'User not found' }); - return; - } - - res.status(200).json({ - success: true, - data: { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - roles: req.user?.roles || [], - tenantId: req.tenantId, - }, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /auth/verify - * Verificar si el token es v谩lido - */ - router.get('/verify', authMiddleware.authenticate, (req: Request, res: Response): void => { - res.status(200).json({ - success: true, - data: { - valid: true, - user: { - id: req.user?.sub, - email: req.user?.email, - roles: req.user?.roles, - }, - tenantId: req.tenantId, - }, - }); - }); - - return router; -} - -export default createAuthController; diff --git a/projects/erp-construccion/backend/src/modules/auth/controllers/index.ts b/projects/erp-construccion/backend/src/modules/auth/controllers/index.ts deleted file mode 100644 index 8884f4f5b..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/controllers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Auth Controllers - Export - */ - -export * from './auth.controller'; diff --git a/projects/erp-construccion/backend/src/modules/auth/dto/auth.dto.ts b/projects/erp-construccion/backend/src/modules/auth/dto/auth.dto.ts deleted file mode 100644 index 9fd85eb2c..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/dto/auth.dto.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Auth DTOs - Data Transfer Objects para autenticaci贸n - * - * @module Auth - */ - -export interface LoginDto { - email: string; - password: string; - tenantId?: string; -} - -export interface RegisterDto { - email: string; - password: string; - firstName: string; - lastName: string; - tenantId: string; -} - -export interface RefreshTokenDto { - refreshToken: string; -} - -export interface ChangePasswordDto { - currentPassword: string; - newPassword: string; -} - -export interface ResetPasswordRequestDto { - email: string; -} - -export interface ResetPasswordDto { - token: string; - newPassword: string; -} - -export interface TokenPayload { - sub: string; // userId - email: string; - tenantId: string; - roles: string[]; - type: 'access' | 'refresh'; - iat?: number; - exp?: number; -} - -export interface AuthResponse { - accessToken: string; - refreshToken: string; - expiresIn: number; - user: { - id: string; - email: string; - firstName: string; - lastName: string; - roles: string[]; - }; - tenant: { - id: string; - name: string; - }; -} - -export interface TokenValidationResult { - valid: boolean; - payload?: TokenPayload; - error?: string; -} diff --git a/projects/erp-construccion/backend/src/modules/auth/entities/index.ts b/projects/erp-construccion/backend/src/modules/auth/entities/index.ts deleted file mode 100644 index a0283133a..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/entities/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Auth Entities - Export - */ - -export { RefreshToken } from './refresh-token.entity'; -export { Role } from './role.entity'; -export { Permission } from './permission.entity'; -export { UserRole } from './user-role.entity'; diff --git a/projects/erp-construccion/backend/src/modules/auth/entities/permission.entity.ts b/projects/erp-construccion/backend/src/modules/auth/entities/permission.entity.ts deleted file mode 100644 index 8599af97d..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/entities/permission.entity.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Permission Entity - * Permisos granulares del sistema - * - * @module Auth - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, -} from 'typeorm'; - -@Entity({ schema: 'auth', name: 'permissions' }) -export class Permission { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 100, unique: true }) - code: string; - - @Column({ type: 'varchar', length: 200 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string; - - @Column({ type: 'varchar', length: 50 }) - module: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/auth/entities/refresh-token.entity.ts b/projects/erp-construccion/backend/src/modules/auth/entities/refresh-token.entity.ts deleted file mode 100644 index 1091227fc..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/entities/refresh-token.entity.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * RefreshToken Entity - * - * Almacena refresh tokens para autenticaci贸n JWT. - * Permite revocar tokens y gestionar sesiones. - * - * @module Auth - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; - -@Entity({ name: 'refresh_tokens', schema: 'auth' }) -@Index(['userId', 'revokedAt']) -@Index(['token']) -export class RefreshToken { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'user_id', type: 'uuid' }) - userId: string; - - @ManyToOne(() => User, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'user_id' }) - user: User; - - @Column({ type: 'text' }) - token: string; - - @Column({ name: 'expires_at', type: 'timestamptz' }) - expiresAt: Date; - - @Column({ name: 'revoked_at', type: 'timestamptz', nullable: true }) - revokedAt: Date | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'user_agent', type: 'varchar', length: 500, nullable: true }) - userAgent: string | null; - - @Column({ name: 'ip_address', type: 'varchar', length: 45, nullable: true }) - ipAddress: string | null; - - /** - * Verificar si el token est谩 expirado - */ - isExpired(): boolean { - return this.expiresAt < new Date(); - } - - /** - * Verificar si el token est谩 revocado - */ - isRevoked(): boolean { - return this.revokedAt !== null; - } - - /** - * Verificar si el token es v谩lido - */ - isValid(): boolean { - return !this.isExpired() && !this.isRevoked(); - } -} diff --git a/projects/erp-construccion/backend/src/modules/auth/entities/role.entity.ts b/projects/erp-construccion/backend/src/modules/auth/entities/role.entity.ts deleted file mode 100644 index a69a83a77..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/entities/role.entity.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Role Entity - * Roles del sistema para RBAC - * - * @module Auth - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToMany, - ManyToMany, - JoinTable, -} from 'typeorm'; -import { Permission } from './permission.entity'; -import { UserRole } from './user-role.entity'; - -@Entity({ schema: 'auth', name: 'roles' }) -export class Role { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 50, unique: true }) - code: string; - - @Column({ type: 'varchar', length: 100 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string; - - @Column({ name: 'is_system', type: 'boolean', default: false }) - isSystem: boolean; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToMany(() => Permission) - @JoinTable({ - name: 'role_permissions', - joinColumn: { name: 'role_id', referencedColumnName: 'id' }, - inverseJoinColumn: { name: 'permission_id', referencedColumnName: 'id' }, - }) - permissions: Permission[]; - - @OneToMany(() => UserRole, (userRole) => userRole.role) - userRoles: UserRole[]; -} diff --git a/projects/erp-construccion/backend/src/modules/auth/entities/user-role.entity.ts b/projects/erp-construccion/backend/src/modules/auth/entities/user-role.entity.ts deleted file mode 100644 index 9a6ea4b10..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/entities/user-role.entity.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * UserRole Entity - * Relaci贸n usuarios-roles con soporte multi-tenant - * - * @module Auth - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { Role } from './role.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; - -@Entity({ schema: 'auth', name: 'user_roles' }) -@Index(['userId', 'roleId', 'tenantId'], { unique: true }) -export class UserRole { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'user_id', type: 'uuid' }) - userId: string; - - @Column({ name: 'role_id', type: 'uuid' }) - roleId: string; - - @Column({ name: 'tenant_id', type: 'uuid', nullable: true }) - tenantId: string; - - @Column({ name: 'assigned_by', type: 'uuid', nullable: true }) - assignedBy: string; - - @CreateDateColumn({ name: 'assigned_at', type: 'timestamptz' }) - assignedAt: Date; - - // Relations - @ManyToOne(() => User, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'user_id' }) - user: User; - - @ManyToOne(() => Role, (role) => role.userRoles, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'role_id' }) - role: Role; - - @ManyToOne(() => Tenant, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} diff --git a/projects/erp-construccion/backend/src/modules/auth/index.ts b/projects/erp-construccion/backend/src/modules/auth/index.ts deleted file mode 100644 index 50b4d616b..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Auth Module - Main Exports - * - * M贸dulo de autenticaci贸n con JWT y refresh tokens. - * Implementa multi-tenancy con RLS. - * - * @module Auth - */ - -export * from './dto/auth.dto'; -export { RefreshToken } from './entities/refresh-token.entity'; -export { AuthService } from './services/auth.service'; -export { AuthMiddleware, createAuthMiddleware } from './middleware/auth.middleware'; -export { createAuthController } from './controllers/auth.controller'; diff --git a/projects/erp-construccion/backend/src/modules/auth/middleware/auth.middleware.ts b/projects/erp-construccion/backend/src/modules/auth/middleware/auth.middleware.ts deleted file mode 100644 index fd75f17d2..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/middleware/auth.middleware.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Auth Middleware - Middleware de Autenticaci贸n - * - * Middleware para Express que valida JWT y extrae informaci贸n del usuario. - * Configura el tenant_id para RLS en PostgreSQL. - * - * @module Auth - */ - -import { Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { AuthService } from '../services/auth.service'; -import { TokenPayload } from '../dto/auth.dto'; - -// Extender Request de Express con informaci贸n de autenticaci贸n -declare global { - namespace Express { - interface Request { - user?: TokenPayload; - tenantId?: string; - } - } -} - -export class AuthMiddleware { - constructor( - private readonly authService: AuthService, - private readonly dataSource: DataSource - ) {} - - /** - * Middleware de autenticaci贸n requerida - */ - authenticate = async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const token = this.extractToken(req); - - if (!token) { - res.status(401).json({ - error: 'Unauthorized', - message: 'No token provided', - }); - return; - } - - const validation = this.authService.validateAccessToken(token); - - if (!validation.valid || !validation.payload) { - res.status(401).json({ - error: 'Unauthorized', - message: validation.error || 'Invalid token', - }); - return; - } - - // Establecer informaci贸n en el request - req.user = validation.payload; - req.tenantId = validation.payload.tenantId; - - // Configurar tenant_id para RLS en PostgreSQL - await this.setTenantContext(validation.payload.tenantId); - - next(); - } catch (error) { - res.status(401).json({ - error: 'Unauthorized', - message: 'Authentication failed', - }); - } - }; - - /** - * Middleware de autenticaci贸n opcional - */ - optionalAuthenticate = async (req: Request, _res: Response, next: NextFunction): Promise => { - try { - const token = this.extractToken(req); - - if (token) { - const validation = this.authService.validateAccessToken(token); - - if (validation.valid && validation.payload) { - req.user = validation.payload; - req.tenantId = validation.payload.tenantId; - await this.setTenantContext(validation.payload.tenantId); - } - } - - next(); - } catch { - // Si hay error, continuar sin autenticaci贸n - next(); - } - }; - - /** - * Middleware de autorizaci贸n por roles - */ - authorize = (...allowedRoles: string[]) => { - return (req: Request, res: Response, next: NextFunction): void => { - if (!req.user) { - res.status(401).json({ - error: 'Unauthorized', - message: 'Authentication required', - }); - return; - } - - const hasRole = req.user.roles.some((role) => allowedRoles.includes(role)); - - if (!hasRole) { - res.status(403).json({ - error: 'Forbidden', - message: 'Insufficient permissions', - }); - return; - } - - next(); - }; - }; - - /** - * Middleware que requiere rol de admin - */ - requireAdmin = (req: Request, res: Response, next: NextFunction): void => { - return this.authorize('admin', 'super_admin')(req, res, next); - }; - - /** - * Middleware que requiere ser supervisor - */ - requireSupervisor = (req: Request, res: Response, next: NextFunction): void => { - return this.authorize('admin', 'super_admin', 'supervisor_obra', 'supervisor_hse')(req, res, next); - }; - - /** - * Extraer token del header Authorization - */ - private extractToken(req: Request): string | null { - const authHeader = req.headers.authorization; - - if (!authHeader) { - return null; - } - - // Bearer token - const [type, token] = authHeader.split(' '); - - if (type !== 'Bearer' || !token) { - return null; - } - - return token; - } - - /** - * Configurar contexto de tenant para RLS - */ - private async setTenantContext(tenantId: string): Promise { - try { - await this.dataSource.query(`SET app.current_tenant_id = '${tenantId}'`); - } catch (error) { - console.error('Error setting tenant context:', error); - throw new Error('Failed to set tenant context'); - } - } -} - -/** - * Factory para crear middleware de autenticaci贸n - */ -export function createAuthMiddleware( - authService: AuthService, - dataSource: DataSource -): AuthMiddleware { - return new AuthMiddleware(authService, dataSource); -} diff --git a/projects/erp-construccion/backend/src/modules/auth/services/auth.service.ts b/projects/erp-construccion/backend/src/modules/auth/services/auth.service.ts deleted file mode 100644 index 5803739d5..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/services/auth.service.ts +++ /dev/null @@ -1,370 +0,0 @@ -/** - * AuthService - Servicio de Autenticaci贸n - * - * Gestiona login, logout, refresh tokens y validaci贸n de JWT. - * Implementa patr贸n multi-tenant con verificaci贸n de tenant_id. - * - * @module Auth - */ - -import * as jwt from 'jsonwebtoken'; -import * as bcrypt from 'bcryptjs'; -import { Repository } from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { - LoginDto, - RegisterDto, - RefreshTokenDto, - ChangePasswordDto, - TokenPayload, - AuthResponse, - TokenValidationResult, -} from '../dto/auth.dto'; - -export interface RefreshToken { - id: string; - userId: string; - token: string; - expiresAt: Date; - revokedAt?: Date; -} - -export class AuthService { - private readonly jwtSecret: string; - private readonly jwtExpiresIn: string; - private readonly jwtRefreshExpiresIn: string; - - constructor( - private readonly userRepository: Repository, - private readonly tenantRepository: Repository, - private readonly refreshTokenRepository: Repository - ) { - this.jwtSecret = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production-minimum-32-chars'; - this.jwtExpiresIn = process.env.JWT_EXPIRES_IN || '1d'; - this.jwtRefreshExpiresIn = process.env.JWT_REFRESH_EXPIRES_IN || '7d'; - } - - /** - * Login de usuario - */ - async login(dto: LoginDto): Promise { - // Buscar usuario por email - const user = await this.userRepository.findOne({ - where: { email: dto.email, deletedAt: null } as any, - relations: ['userRoles', 'userRoles.role'], - }); - - if (!user) { - throw new Error('Invalid credentials'); - } - - // Verificar password - const isPasswordValid = await bcrypt.compare(dto.password, user.passwordHash); - if (!isPasswordValid) { - throw new Error('Invalid credentials'); - } - - // Verificar que el usuario est茅 activo - if (!user.isActive) { - throw new Error('User is not active'); - } - - // Obtener tenant - const tenantId = dto.tenantId || user.defaultTenantId; - if (!tenantId) { - throw new Error('No tenant specified'); - } - - const tenant = await this.tenantRepository.findOne({ - where: { id: tenantId, isActive: true, deletedAt: null } as any, - }); - - if (!tenant) { - throw new Error('Tenant not found or inactive'); - } - - // Obtener roles del usuario - const roles = user.userRoles?.map((ur) => ur.role.code) || []; - - // Generar tokens - const accessToken = this.generateAccessToken(user, tenantId, roles); - const refreshToken = await this.generateRefreshToken(user.id); - - // Actualizar 煤ltimo login - await this.userRepository.update(user.id, { lastLoginAt: new Date() }); - - return { - accessToken, - refreshToken, - expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn), - user: { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - roles, - }, - tenant: { - id: tenant.id, - name: tenant.name, - }, - }; - } - - /** - * Registro de usuario - */ - async register(dto: RegisterDto): Promise { - // Verificar si el email ya existe - const existingUser = await this.userRepository.findOne({ - where: { email: dto.email } as any, - }); - - if (existingUser) { - throw new Error('Email already registered'); - } - - // Verificar que el tenant existe - const tenant = await this.tenantRepository.findOne({ - where: { id: dto.tenantId, isActive: true } as any, - }); - - if (!tenant) { - throw new Error('Tenant not found'); - } - - // Hash del password - const passwordHash = await bcrypt.hash(dto.password, 12); - - // Crear usuario - const user = await this.userRepository.save( - this.userRepository.create({ - email: dto.email, - passwordHash, - firstName: dto.firstName, - lastName: dto.lastName, - defaultTenantId: dto.tenantId, - isActive: true, - }) - ); - - // Generar tokens (rol default: user) - const roles = ['user']; - const accessToken = this.generateAccessToken(user, dto.tenantId, roles); - const refreshToken = await this.generateRefreshToken(user.id); - - return { - accessToken, - refreshToken, - expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn), - user: { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - roles, - }, - tenant: { - id: tenant.id, - name: tenant.name, - }, - }; - } - - /** - * Refresh de token - */ - async refresh(dto: RefreshTokenDto): Promise { - // Validar refresh token - const validation = this.validateToken(dto.refreshToken, 'refresh'); - if (!validation.valid || !validation.payload) { - throw new Error('Invalid refresh token'); - } - - // Verificar que el token no est谩 revocado - const storedToken = await this.refreshTokenRepository.findOne({ - where: { token: dto.refreshToken, revokedAt: null } as any, - }); - - if (!storedToken || storedToken.expiresAt < new Date()) { - throw new Error('Refresh token expired or revoked'); - } - - // Obtener usuario - const user = await this.userRepository.findOne({ - where: { id: validation.payload.sub, deletedAt: null } as any, - relations: ['userRoles', 'userRoles.role'], - }); - - if (!user || !user.isActive) { - throw new Error('User not found or inactive'); - } - - // Obtener tenant - const tenant = await this.tenantRepository.findOne({ - where: { id: validation.payload.tenantId, isActive: true } as any, - }); - - if (!tenant) { - throw new Error('Tenant not found or inactive'); - } - - const roles = user.userRoles?.map((ur) => ur.role.code) || []; - - // Revocar token anterior - await this.refreshTokenRepository.update(storedToken.id, { revokedAt: new Date() }); - - // Generar nuevos tokens - const accessToken = this.generateAccessToken(user, tenant.id, roles); - const refreshToken = await this.generateRefreshToken(user.id); - - return { - accessToken, - refreshToken, - expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn), - user: { - id: user.id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - roles, - }, - tenant: { - id: tenant.id, - name: tenant.name, - }, - }; - } - - /** - * Logout - Revocar refresh token - */ - async logout(refreshToken: string): Promise { - await this.refreshTokenRepository.update( - { token: refreshToken } as any, - { revokedAt: new Date() } - ); - } - - /** - * Cambiar password - */ - async changePassword(userId: string, dto: ChangePasswordDto): Promise { - const user = await this.userRepository.findOne({ - where: { id: userId } as any, - }); - - if (!user) { - throw new Error('User not found'); - } - - const isCurrentValid = await bcrypt.compare(dto.currentPassword, user.passwordHash); - if (!isCurrentValid) { - throw new Error('Current password is incorrect'); - } - - const newPasswordHash = await bcrypt.hash(dto.newPassword, 12); - await this.userRepository.update(userId, { passwordHash: newPasswordHash }); - - // Revocar todos los refresh tokens del usuario - await this.refreshTokenRepository.update( - { userId } as any, - { revokedAt: new Date() } - ); - } - - /** - * Validar access token - */ - validateAccessToken(token: string): TokenValidationResult { - return this.validateToken(token, 'access'); - } - - /** - * Validar token - */ - private validateToken(token: string, expectedType: 'access' | 'refresh'): TokenValidationResult { - try { - const payload = jwt.verify(token, this.jwtSecret) as TokenPayload; - - if (payload.type !== expectedType) { - return { valid: false, error: 'Invalid token type' }; - } - - return { valid: true, payload }; - } catch (error) { - if (error instanceof jwt.TokenExpiredError) { - return { valid: false, error: 'Token expired' }; - } - if (error instanceof jwt.JsonWebTokenError) { - return { valid: false, error: 'Invalid token' }; - } - return { valid: false, error: 'Token validation failed' }; - } - } - - /** - * Generar access token - */ - private generateAccessToken(user: User, tenantId: string, roles: string[]): string { - const payload: TokenPayload = { - sub: user.id, - email: user.email, - tenantId, - roles, - type: 'access', - }; - - return jwt.sign(payload, this.jwtSecret, { - expiresIn: this.jwtExpiresIn as jwt.SignOptions['expiresIn'], - }); - } - - /** - * Generar refresh token - */ - private async generateRefreshToken(userId: string): Promise { - const payload: Partial = { - sub: userId, - type: 'refresh', - }; - - const token = jwt.sign(payload, this.jwtSecret, { - expiresIn: this.jwtRefreshExpiresIn as jwt.SignOptions['expiresIn'], - }); - - // Almacenar en DB - const expiresAt = new Date(); - expiresAt.setDate(expiresAt.getDate() + 7); // 7 d铆as - - await this.refreshTokenRepository.save( - this.refreshTokenRepository.create({ - userId, - token, - expiresAt, - }) - ); - - return token; - } - - /** - * Convertir expiresIn a segundos - */ - private getExpiresInSeconds(expiresIn: string): number { - const match = expiresIn.match(/^(\d+)([dhms])$/); - if (!match) return 86400; // default 1 d铆a - - const value = parseInt(match[1]); - const unit = match[2]; - - switch (unit) { - case 'd': return value * 86400; - case 'h': return value * 3600; - case 'm': return value * 60; - case 's': return value; - default: return 86400; - } - } -} diff --git a/projects/erp-construccion/backend/src/modules/auth/services/index.ts b/projects/erp-construccion/backend/src/modules/auth/services/index.ts deleted file mode 100644 index 7255de3ef..000000000 --- a/projects/erp-construccion/backend/src/modules/auth/services/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Auth Module - Service Exports - */ - -export * from './auth.service'; diff --git a/projects/erp-construccion/backend/src/modules/bidding/controllers/bid-analytics.controller.ts b/projects/erp-construccion/backend/src/modules/bidding/controllers/bid-analytics.controller.ts deleted file mode 100644 index 05b6c45f7..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/controllers/bid-analytics.controller.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * BidAnalyticsController - Controller de An谩lisis de Licitaciones - * - * Endpoints REST para dashboards y an谩lisis de preconstrucci贸n. - * - * @module Bidding - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { BidAnalyticsService } from '../services/bid-analytics.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Bid } from '../entities/bid.entity'; -import { Opportunity } from '../entities/opportunity.entity'; -import { BidCompetitor } from '../entities/bid-competitor.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createBidAnalyticsController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const bidRepository = dataSource.getRepository(Bid); - const opportunityRepository = dataSource.getRepository(Opportunity); - const competitorRepository = dataSource.getRepository(BidCompetitor); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const analyticsService = new BidAnalyticsService(bidRepository, opportunityRepository, competitorRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /bid-analytics/dashboard - */ - router.get('/dashboard', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dashboard = await analyticsService.getDashboard(getContext(req)); - res.status(200).json({ success: true, data: dashboard }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-analytics/pipeline-by-source - */ - router.get('/pipeline-by-source', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const data = await analyticsService.getPipelineBySource(getContext(req)); - res.status(200).json({ success: true, data }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-analytics/win-rate-by-type - */ - router.get('/win-rate-by-type', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const months = parseInt(req.query.months as string) || 12; - const data = await analyticsService.getWinRateByType(getContext(req), months); - res.status(200).json({ success: true, data }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-analytics/monthly-trend - */ - router.get('/monthly-trend', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const months = parseInt(req.query.months as string) || 12; - const data = await analyticsService.getMonthlyTrend(getContext(req), months); - res.status(200).json({ success: true, data }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-analytics/competitors - */ - router.get('/competitors', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const data = await analyticsService.getCompetitorAnalysis(getContext(req)); - res.status(200).json({ success: true, data }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-analytics/funnel - */ - router.get('/funnel', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const months = parseInt(req.query.months as string) || 12; - const data = await analyticsService.getFunnelAnalysis(getContext(req), months); - res.status(200).json({ success: true, data }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-analytics/cycle-time - */ - router.get('/cycle-time', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const months = parseInt(req.query.months as string) || 12; - const data = await analyticsService.getCycleTimeAnalysis(getContext(req), months); - res.status(200).json({ success: true, data }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createBidAnalyticsController; diff --git a/projects/erp-construccion/backend/src/modules/bidding/controllers/bid-budget.controller.ts b/projects/erp-construccion/backend/src/modules/bidding/controllers/bid-budget.controller.ts deleted file mode 100644 index b1227ff7d..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/controllers/bid-budget.controller.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * BidBudgetController - Controller de Presupuestos de Licitaci贸n - * - * Endpoints REST para gesti贸n de propuestas econ贸micas. - * - * @module Bidding - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { BidBudgetService, CreateBudgetItemDto, UpdateBudgetItemDto, BudgetFilters } from '../services/bid-budget.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { BidBudget } from '../entities/bid-budget.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createBidBudgetController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const budgetRepository = dataSource.getRepository(BidBudget); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const budgetService = new BidBudgetService(budgetRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /bid-budgets - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const bidId = req.query.bidId as string; - if (!bidId) { - res.status(400).json({ error: 'Bad Request', message: 'bidId is required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 100, 500); - - const filters: BudgetFilters = { bidId }; - if (req.query.itemType) filters.itemType = req.query.itemType as any; - if (req.query.status) filters.status = req.query.status as any; - if (req.query.parentId !== undefined) { - filters.parentId = req.query.parentId === 'null' ? null : req.query.parentId as string; - } - if (req.query.isSummary !== undefined) filters.isSummary = req.query.isSummary === 'true'; - - const result = await budgetService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-budgets/tree - */ - router.get('/tree', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const bidId = req.query.bidId as string; - if (!bidId) { - res.status(400).json({ error: 'Bad Request', message: 'bidId is required' }); - return; - } - - const tree = await budgetService.getTree(getContext(req), bidId); - res.status(200).json({ success: true, data: tree }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-budgets/summary - */ - router.get('/summary', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const bidId = req.query.bidId as string; - if (!bidId) { - res.status(400).json({ error: 'Bad Request', message: 'bidId is required' }); - return; - } - - const summary = await budgetService.getSummary(getContext(req), bidId); - res.status(200).json({ success: true, data: summary }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bid-budgets/:id - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const item = await budgetService.findById(getContext(req), req.params.id); - if (!item) { - res.status(404).json({ error: 'Not Found', message: 'Budget item not found' }); - return; - } - - res.status(200).json({ success: true, data: item }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bid-budgets - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente', 'costos'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateBudgetItemDto = req.body; - if (!dto.bidId || !dto.code || !dto.name || !dto.itemType) { - res.status(400).json({ - error: 'Bad Request', - message: 'bidId, code, name, and itemType are required', - }); - return; - } - - const item = await budgetService.create(getContext(req), dto); - res.status(201).json({ success: true, data: item }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /bid-budgets/:id - */ - router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente', 'costos'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateBudgetItemDto = req.body; - const item = await budgetService.update(getContext(req), req.params.id, dto); - - if (!item) { - res.status(404).json({ error: 'Not Found', message: 'Budget item not found' }); - return; - } - - res.status(200).json({ success: true, data: item }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bid-budgets/status - */ - router.post('/status', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { bidId, status } = req.body; - if (!bidId || !status) { - res.status(400).json({ error: 'Bad Request', message: 'bidId and status are required' }); - return; - } - - const updated = await budgetService.changeStatus(getContext(req), bidId, status); - res.status(200).json({ - success: true, - message: `Updated ${updated} budget items`, - data: { updated }, - }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /bid-budgets/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await budgetService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Budget item not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Budget item deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createBidBudgetController; diff --git a/projects/erp-construccion/backend/src/modules/bidding/controllers/bid.controller.ts b/projects/erp-construccion/backend/src/modules/bidding/controllers/bid.controller.ts deleted file mode 100644 index 6ab112264..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/controllers/bid.controller.ts +++ /dev/null @@ -1,370 +0,0 @@ -/** - * BidController - Controller de Licitaciones - * - * Endpoints REST para gesti贸n de licitaciones/propuestas. - * - * @module Bidding - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { BidService, CreateBidDto, UpdateBidDto, BidFilters } from '../services/bid.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Bid, BidStatus } from '../entities/bid.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createBidController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const bidRepository = dataSource.getRepository(Bid); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const bidService = new BidService(bidRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /bids - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: BidFilters = {}; - if (req.query.status) { - const statuses = (req.query.status as string).split(',') as BidStatus[]; - filters.status = statuses.length === 1 ? statuses[0] : statuses; - } - if (req.query.bidType) filters.bidType = req.query.bidType as any; - if (req.query.stage) filters.stage = req.query.stage as any; - if (req.query.opportunityId) filters.opportunityId = req.query.opportunityId as string; - if (req.query.bidManagerId) filters.bidManagerId = req.query.bidManagerId as string; - if (req.query.contractingEntity) filters.contractingEntity = req.query.contractingEntity as string; - if (req.query.deadlineFrom) filters.deadlineFrom = new Date(req.query.deadlineFrom as string); - if (req.query.deadlineTo) filters.deadlineTo = new Date(req.query.deadlineTo as string); - if (req.query.minBudget) filters.minBudget = parseFloat(req.query.minBudget as string); - if (req.query.maxBudget) filters.maxBudget = parseFloat(req.query.maxBudget as string); - if (req.query.search) filters.search = req.query.search as string; - - const result = await bidService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bids/upcoming-deadlines - */ - router.get('/upcoming-deadlines', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const days = parseInt(req.query.days as string) || 7; - const bids = await bidService.getUpcomingDeadlines(getContext(req), days); - res.status(200).json({ success: true, data: bids }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bids/stats - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const year = req.query.year ? parseInt(req.query.year as string) : undefined; - const stats = await bidService.getStats(getContext(req), year); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bids/:id - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const bid = await bidService.findById(getContext(req), req.params.id); - if (!bid) { - res.status(404).json({ error: 'Not Found', message: 'Bid not found' }); - return; - } - - res.status(200).json({ success: true, data: bid }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bids - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente', 'comercial'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateBidDto = req.body; - if (!dto.opportunityId || !dto.code || !dto.name || !dto.bidType || !dto.submissionDeadline) { - res.status(400).json({ - error: 'Bad Request', - message: 'opportunityId, code, name, bidType, and submissionDeadline are required', - }); - return; - } - - const bid = await bidService.create(getContext(req), dto); - res.status(201).json({ success: true, data: bid }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /bids/:id - */ - router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente', 'comercial'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateBidDto = req.body; - const bid = await bidService.update(getContext(req), req.params.id, dto); - - if (!bid) { - res.status(404).json({ error: 'Not Found', message: 'Bid not found' }); - return; - } - - res.status(200).json({ success: true, data: bid }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bids/:id/status - */ - router.post('/:id/status', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { status } = req.body; - if (!status) { - res.status(400).json({ error: 'Bad Request', message: 'status is required' }); - return; - } - - const bid = await bidService.changeStatus(getContext(req), req.params.id, status); - - if (!bid) { - res.status(404).json({ error: 'Not Found', message: 'Bid not found' }); - return; - } - - res.status(200).json({ success: true, data: bid }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bids/:id/stage - */ - router.post('/:id/stage', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { stage } = req.body; - if (!stage) { - res.status(400).json({ error: 'Bad Request', message: 'stage is required' }); - return; - } - - const bid = await bidService.changeStage(getContext(req), req.params.id, stage); - - if (!bid) { - res.status(404).json({ error: 'Not Found', message: 'Bid not found' }); - return; - } - - res.status(200).json({ success: true, data: bid }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bids/:id/submit - */ - router.post('/:id/submit', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { proposalAmount } = req.body; - if (!proposalAmount) { - res.status(400).json({ error: 'Bad Request', message: 'proposalAmount is required' }); - return; - } - - const bid = await bidService.submit(getContext(req), req.params.id, proposalAmount); - - if (!bid) { - res.status(404).json({ error: 'Not Found', message: 'Bid not found' }); - return; - } - - res.status(200).json({ success: true, data: bid }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bids/:id/result - */ - router.post('/:id/result', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { won, winnerName, winningAmount, rankingPosition, rejectionReason, lessonsLearned } = req.body; - if (won === undefined) { - res.status(400).json({ error: 'Bad Request', message: 'won is required' }); - return; - } - - const bid = await bidService.recordResult(getContext(req), req.params.id, won, { - winnerName, - winningAmount, - rankingPosition, - rejectionReason, - lessonsLearned, - }); - - if (!bid) { - res.status(404).json({ error: 'Not Found', message: 'Bid not found' }); - return; - } - - res.status(200).json({ success: true, data: bid }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bids/:id/convert - */ - router.post('/:id/convert', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { projectId } = req.body; - if (!projectId) { - res.status(400).json({ error: 'Bad Request', message: 'projectId is required' }); - return; - } - - const bid = await bidService.convertToProject(getContext(req), req.params.id, projectId); - - if (!bid) { - res.status(404).json({ error: 'Not Found', message: 'Bid not found or not awarded' }); - return; - } - - res.status(200).json({ success: true, data: bid }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /bids/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await bidService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Bid not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Bid deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createBidController; diff --git a/projects/erp-construccion/backend/src/modules/bidding/controllers/index.ts b/projects/erp-construccion/backend/src/modules/bidding/controllers/index.ts deleted file mode 100644 index f3eb096e7..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/controllers/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Bidding Controllers Index - * @module Bidding - */ - -export { createOpportunityController } from './opportunity.controller'; -export { createBidController } from './bid.controller'; -export { createBidBudgetController } from './bid-budget.controller'; -export { createBidAnalyticsController } from './bid-analytics.controller'; diff --git a/projects/erp-construccion/backend/src/modules/bidding/controllers/opportunity.controller.ts b/projects/erp-construccion/backend/src/modules/bidding/controllers/opportunity.controller.ts deleted file mode 100644 index ce35ed55f..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/controllers/opportunity.controller.ts +++ /dev/null @@ -1,266 +0,0 @@ -/** - * OpportunityController - Controller de Oportunidades - * - * Endpoints REST para gesti贸n del pipeline de oportunidades. - * - * @module Bidding - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { OpportunityService, CreateOpportunityDto, UpdateOpportunityDto, OpportunityFilters } from '../services/opportunity.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Opportunity, OpportunityStatus } from '../entities/opportunity.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createOpportunityController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const opportunityRepository = dataSource.getRepository(Opportunity); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const opportunityService = new OpportunityService(opportunityRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /opportunities - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: OpportunityFilters = {}; - if (req.query.status) { - const statuses = (req.query.status as string).split(',') as OpportunityStatus[]; - filters.status = statuses.length === 1 ? statuses[0] : statuses; - } - if (req.query.source) filters.source = req.query.source as any; - if (req.query.projectType) filters.projectType = req.query.projectType as any; - if (req.query.priority) filters.priority = req.query.priority as any; - if (req.query.assignedToId) filters.assignedToId = req.query.assignedToId as string; - if (req.query.clientName) filters.clientName = req.query.clientName as string; - if (req.query.state) filters.state = req.query.state as string; - if (req.query.dateFrom) filters.dateFrom = new Date(req.query.dateFrom as string); - if (req.query.dateTo) filters.dateTo = new Date(req.query.dateTo as string); - if (req.query.minValue) filters.minValue = parseFloat(req.query.minValue as string); - if (req.query.maxValue) filters.maxValue = parseFloat(req.query.maxValue as string); - if (req.query.search) filters.search = req.query.search as string; - - const result = await opportunityService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /opportunities/pipeline - */ - router.get('/pipeline', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const pipeline = await opportunityService.getPipeline(getContext(req)); - res.status(200).json({ success: true, data: pipeline }); - } catch (error) { - next(error); - } - }); - - /** - * GET /opportunities/upcoming-deadlines - */ - router.get('/upcoming-deadlines', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const days = parseInt(req.query.days as string) || 7; - const opportunities = await opportunityService.getUpcomingDeadlines(getContext(req), days); - res.status(200).json({ success: true, data: opportunities }); - } catch (error) { - next(error); - } - }); - - /** - * GET /opportunities/stats - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const year = req.query.year ? parseInt(req.query.year as string) : undefined; - const stats = await opportunityService.getStats(getContext(req), year); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /opportunities/:id - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const opportunity = await opportunityService.findById(getContext(req), req.params.id); - if (!opportunity) { - res.status(404).json({ error: 'Not Found', message: 'Opportunity not found' }); - return; - } - - res.status(200).json({ success: true, data: opportunity }); - } catch (error) { - next(error); - } - }); - - /** - * POST /opportunities - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente', 'comercial'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateOpportunityDto = req.body; - if (!dto.code || !dto.name || !dto.source || !dto.projectType || !dto.clientName || !dto.identificationDate) { - res.status(400).json({ - error: 'Bad Request', - message: 'code, name, source, projectType, clientName, and identificationDate are required', - }); - return; - } - - const opportunity = await opportunityService.create(getContext(req), dto); - res.status(201).json({ success: true, data: opportunity }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /opportunities/:id - */ - router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente', 'comercial'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateOpportunityDto = req.body; - const opportunity = await opportunityService.update(getContext(req), req.params.id, dto); - - if (!opportunity) { - res.status(404).json({ error: 'Not Found', message: 'Opportunity not found' }); - return; - } - - res.status(200).json({ success: true, data: opportunity }); - } catch (error) { - next(error); - } - }); - - /** - * POST /opportunities/:id/status - */ - router.post('/:id/status', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'gerente', 'comercial'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { status, reason } = req.body; - if (!status) { - res.status(400).json({ error: 'Bad Request', message: 'status is required' }); - return; - } - - const opportunity = await opportunityService.changeStatus(getContext(req), req.params.id, status, reason); - - if (!opportunity) { - res.status(404).json({ error: 'Not Found', message: 'Opportunity not found' }); - return; - } - - res.status(200).json({ success: true, data: opportunity }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /opportunities/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await opportunityService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Opportunity not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Opportunity deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createOpportunityController; diff --git a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-budget.entity.ts b/projects/erp-construccion/backend/src/modules/bidding/entities/bid-budget.entity.ts deleted file mode 100644 index 86e267562..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-budget.entity.ts +++ /dev/null @@ -1,256 +0,0 @@ -/** - * BidBudget Entity - Presupuesto de Licitaci贸n - * - * Desglose del presupuesto para la propuesta econ贸mica. - * - * @module Bidding - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Bid } from './bid.entity'; - -export type BudgetItemType = - | 'direct_cost' - | 'indirect_cost' - | 'labor' - | 'materials' - | 'equipment' - | 'subcontract' - | 'overhead' - | 'profit' - | 'contingency' - | 'financing' - | 'taxes' - | 'bonds' - | 'other'; - -export type BudgetStatus = 'draft' | 'calculated' | 'reviewed' | 'approved' | 'locked'; - -@Entity('bid_budget', { schema: 'bidding' }) -@Index(['tenantId', 'bidId']) -@Index(['tenantId', 'itemType']) -export class BidBudget { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Referencia a licitaci贸n - @Column({ name: 'bid_id', type: 'uuid' }) - bidId!: string; - - @ManyToOne(() => Bid, (bid) => bid.budgetItems) - @JoinColumn({ name: 'bid_id' }) - bid?: Bid; - - // Jerarqu铆a - @Column({ name: 'parent_id', type: 'uuid', nullable: true }) - parentId?: string; - - @Column({ name: 'sort_order', type: 'int', default: 0 }) - sortOrder!: number; - - @Column({ type: 'int', default: 0 }) - level!: number; - - @Column({ length: 50 }) - code!: string; - - // Informaci贸n del item - @Column({ length: 255 }) - name!: string; - - @Column({ type: 'text', nullable: true }) - description?: string; - - @Column({ - name: 'item_type', - type: 'enum', - enum: ['direct_cost', 'indirect_cost', 'labor', 'materials', 'equipment', 'subcontract', 'overhead', 'profit', 'contingency', 'financing', 'taxes', 'bonds', 'other'], - enumName: 'bid_budget_item_type', - }) - itemType!: BudgetItemType; - - @Column({ - type: 'enum', - enum: ['draft', 'calculated', 'reviewed', 'approved', 'locked'], - enumName: 'bid_budget_status', - default: 'draft', - }) - status!: BudgetStatus; - - // Unidad y cantidad - @Column({ length: 20, nullable: true }) - unit?: string; - - @Column({ - type: 'decimal', - precision: 18, - scale: 4, - default: 0, - }) - quantity!: number; - - // Precios - @Column({ - name: 'unit_price', - type: 'decimal', - precision: 18, - scale: 4, - default: 0, - }) - unitPrice!: number; - - @Column({ - name: 'total_amount', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - totalAmount!: number; - - // Desglose de costos directos - @Column({ - name: 'materials_cost', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - materialsCost?: number; - - @Column({ - name: 'labor_cost', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - laborCost?: number; - - @Column({ - name: 'equipment_cost', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - equipmentCost?: number; - - @Column({ - name: 'subcontract_cost', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - subcontractCost?: number; - - // Porcentajes - @Column({ - name: 'indirect_percentage', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - indirectPercentage?: number; - - @Column({ - name: 'profit_percentage', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - profitPercentage?: number; - - @Column({ - name: 'financing_percentage', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - financingPercentage?: number; - - // Comparaci贸n con base de licitaci贸n - @Column({ - name: 'base_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - baseAmount?: number; - - @Column({ - name: 'variance_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - varianceAmount?: number; - - @Column({ - name: 'variance_percentage', - type: 'decimal', - precision: 8, - scale: 2, - nullable: true, - }) - variancePercentage?: number; - - // Flags - @Column({ name: 'is_summary', type: 'boolean', default: false }) - isSummary!: boolean; - - @Column({ name: 'is_calculated', type: 'boolean', default: false }) - isCalculated!: boolean; - - @Column({ name: 'is_adjusted', type: 'boolean', default: false }) - isAdjusted!: boolean; - - @Column({ name: 'adjustment_reason', type: 'text', nullable: true }) - adjustmentReason?: string; - - // Referencia a concepto de cat谩logo - @Column({ name: 'catalog_concept_id', type: 'uuid', nullable: true }) - catalogConceptId?: string; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-calendar.entity.ts b/projects/erp-construccion/backend/src/modules/bidding/entities/bid-calendar.entity.ts deleted file mode 100644 index 0b0a02500..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-calendar.entity.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * BidCalendar Entity - Calendario de Licitaci贸n - * - * Eventos y fechas importantes del proceso de licitaci贸n. - * - * @module Bidding - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { Bid } from './bid.entity'; - -export type CalendarEventType = - | 'publication' - | 'site_visit' - | 'clarification_meeting' - | 'clarification_deadline' - | 'submission_deadline' - | 'opening' - | 'technical_evaluation' - | 'economic_evaluation' - | 'award_notification' - | 'contract_signing' - | 'kick_off' - | 'milestone' - | 'internal_review' - | 'team_meeting' - | 'reminder' - | 'other'; - -export type EventPriority = 'low' | 'medium' | 'high' | 'critical'; - -export type EventStatus = 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'postponed'; - -@Entity('bid_calendar', { schema: 'bidding' }) -@Index(['tenantId', 'bidId']) -@Index(['tenantId', 'eventDate']) -@Index(['tenantId', 'eventType']) -export class BidCalendar { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Referencia a licitaci贸n - @Column({ name: 'bid_id', type: 'uuid' }) - bidId!: string; - - @ManyToOne(() => Bid, (bid) => bid.calendarEvents) - @JoinColumn({ name: 'bid_id' }) - bid?: Bid; - - // Informaci贸n del evento - @Column({ length: 255 }) - title!: string; - - @Column({ type: 'text', nullable: true }) - description?: string; - - @Column({ - name: 'event_type', - type: 'enum', - enum: ['publication', 'site_visit', 'clarification_meeting', 'clarification_deadline', 'submission_deadline', 'opening', 'technical_evaluation', 'economic_evaluation', 'award_notification', 'contract_signing', 'kick_off', 'milestone', 'internal_review', 'team_meeting', 'reminder', 'other'], - enumName: 'calendar_event_type', - }) - eventType!: CalendarEventType; - - @Column({ - type: 'enum', - enum: ['low', 'medium', 'high', 'critical'], - enumName: 'event_priority', - default: 'medium', - }) - priority!: EventPriority; - - @Column({ - type: 'enum', - enum: ['scheduled', 'in_progress', 'completed', 'cancelled', 'postponed'], - enumName: 'event_status', - default: 'scheduled', - }) - status!: EventStatus; - - // Fechas y hora - @Column({ name: 'event_date', type: 'timestamptz' }) - eventDate!: Date; - - @Column({ name: 'end_date', type: 'timestamptz', nullable: true }) - endDate?: Date; - - @Column({ name: 'is_all_day', type: 'boolean', default: false }) - isAllDay!: boolean; - - @Column({ name: 'timezone', length: 50, default: 'America/Mexico_City' }) - timezone!: string; - - // Ubicaci贸n - @Column({ length: 255, nullable: true }) - location?: string; - - @Column({ name: 'is_virtual', type: 'boolean', default: false }) - isVirtual!: boolean; - - @Column({ name: 'meeting_link', length: 500, nullable: true }) - meetingLink?: string; - - // Recordatorios - @Column({ name: 'reminder_minutes', type: 'int', array: true, nullable: true }) - reminderMinutes?: number[]; - - @Column({ name: 'reminder_sent', type: 'boolean', default: false }) - reminderSent!: boolean; - - @Column({ name: 'last_reminder_at', type: 'timestamptz', nullable: true }) - lastReminderAt?: Date; - - // Asignaci贸n - @Column({ name: 'assigned_to_id', type: 'uuid', nullable: true }) - assignedToId?: string; - - @ManyToOne(() => User, { nullable: true }) - @JoinColumn({ name: 'assigned_to_id' }) - assignedTo?: User; - - @Column({ name: 'attendees', type: 'uuid', array: true, nullable: true }) - attendees?: string[]; - - // Resultado del evento - @Column({ name: 'outcome', type: 'text', nullable: true }) - outcome?: string; - - @Column({ name: 'action_items', type: 'jsonb', nullable: true }) - actionItems?: Record[]; - - // Recurrencia - @Column({ name: 'is_recurring', type: 'boolean', default: false }) - isRecurring!: boolean; - - @Column({ name: 'recurrence_rule', length: 255, nullable: true }) - recurrenceRule?: string; - - @Column({ name: 'parent_event_id', type: 'uuid', nullable: true }) - parentEventId?: string; - - // Flags - @Column({ name: 'is_mandatory', type: 'boolean', default: false }) - isMandatory!: boolean; - - @Column({ name: 'is_external', type: 'boolean', default: false }) - isExternal!: boolean; - - @Column({ name: 'requires_preparation', type: 'boolean', default: false }) - requiresPreparation!: boolean; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-competitor.entity.ts b/projects/erp-construccion/backend/src/modules/bidding/entities/bid-competitor.entity.ts deleted file mode 100644 index 9779d7ff8..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-competitor.entity.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * BidCompetitor Entity - Competidores en Licitaci贸n - * - * Informaci贸n de competidores en el proceso de licitaci贸n. - * - * @module Bidding - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Bid } from './bid.entity'; - -export type CompetitorStatus = - | 'identified' - | 'registered' - | 'qualified' - | 'disqualified' - | 'withdrew' - | 'submitted' - | 'winner' - | 'loser'; - -export type ThreatLevel = 'low' | 'medium' | 'high' | 'critical'; - -@Entity('bid_competitors', { schema: 'bidding' }) -@Index(['tenantId', 'bidId']) -export class BidCompetitor { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Referencia a licitaci贸n - @Column({ name: 'bid_id', type: 'uuid' }) - bidId!: string; - - @ManyToOne(() => Bid, (bid) => bid.competitors) - @JoinColumn({ name: 'bid_id' }) - bid?: Bid; - - // Informaci贸n del competidor - @Column({ name: 'company_name', length: 255 }) - companyName!: string; - - @Column({ name: 'trade_name', length: 255, nullable: true }) - tradeName?: string; - - @Column({ name: 'rfc', length: 13, nullable: true }) - rfc?: string; - - @Column({ name: 'contact_name', length: 255, nullable: true }) - contactName?: string; - - @Column({ name: 'contact_email', length: 255, nullable: true }) - contactEmail?: string; - - @Column({ name: 'contact_phone', length: 50, nullable: true }) - contactPhone?: string; - - @Column({ length: 255, nullable: true }) - website?: string; - - // Estado y an谩lisis - @Column({ - type: 'enum', - enum: ['identified', 'registered', 'qualified', 'disqualified', 'withdrew', 'submitted', 'winner', 'loser'], - enumName: 'competitor_status', - default: 'identified', - }) - status!: CompetitorStatus; - - @Column({ - name: 'threat_level', - type: 'enum', - enum: ['low', 'medium', 'high', 'critical'], - enumName: 'competitor_threat_level', - default: 'medium', - }) - threatLevel!: ThreatLevel; - - // Capacidades conocidas - @Column({ - name: 'estimated_annual_revenue', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - estimatedAnnualRevenue?: number; - - @Column({ name: 'employee_count', type: 'int', nullable: true }) - employeeCount?: number; - - @Column({ name: 'years_in_business', type: 'int', nullable: true }) - yearsInBusiness?: number; - - @Column({ name: 'certifications', type: 'text', array: true, nullable: true }) - certifications?: string[]; - - @Column({ name: 'specializations', type: 'text', array: true, nullable: true }) - specializations?: string[]; - - // Hist贸rico de competencia - @Column({ name: 'previous_encounters', type: 'int', default: 0 }) - previousEncounters!: number; - - @Column({ name: 'wins_against', type: 'int', default: 0 }) - winsAgainst!: number; - - @Column({ name: 'losses_against', type: 'int', default: 0 }) - lossesAgainst!: number; - - // Informaci贸n de propuesta (si es p煤blica) - @Column({ - name: 'proposed_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - proposedAmount?: number; - - @Column({ - name: 'technical_score', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - technicalScore?: number; - - @Column({ - name: 'economic_score', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - economicScore?: number; - - @Column({ - name: 'final_score', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - finalScore?: number; - - @Column({ name: 'ranking_position', type: 'int', nullable: true }) - rankingPosition?: number; - - // Fortalezas y debilidades - @Column({ type: 'text', array: true, nullable: true }) - strengths?: string[]; - - @Column({ type: 'text', array: true, nullable: true }) - weaknesses?: string[]; - - // An谩lisis FODA resumido - @Column({ name: 'competitive_advantage', type: 'text', nullable: true }) - competitiveAdvantage?: string; - - @Column({ name: 'vulnerability', type: 'text', nullable: true }) - vulnerability?: string; - - // Raz贸n de descalificaci贸n/retiro - @Column({ name: 'disqualification_reason', type: 'text', nullable: true }) - disqualificationReason?: string; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-document.entity.ts b/projects/erp-construccion/backend/src/modules/bidding/entities/bid-document.entity.ts deleted file mode 100644 index e7f340c80..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-document.entity.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * BidDocument Entity - Documentos de Licitaci贸n - * - * Almacena documentos asociados a una licitaci贸n. - * - * @module Bidding - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { Bid } from './bid.entity'; - -export type DocumentCategory = - | 'tender_bases' - | 'clarifications' - | 'annexes' - | 'technical_proposal' - | 'economic_proposal' - | 'legal_documents' - | 'experience_certificates' - | 'financial_statements' - | 'bonds' - | 'contracts' - | 'correspondence' - | 'meeting_minutes' - | 'other'; - -export type DocumentStatus = 'draft' | 'pending_review' | 'approved' | 'rejected' | 'submitted' | 'archived'; - -@Entity('bid_documents', { schema: 'bidding' }) -@Index(['tenantId', 'bidId']) -@Index(['tenantId', 'category']) -export class BidDocument { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Referencia a licitaci贸n - @Column({ name: 'bid_id', type: 'uuid' }) - bidId!: string; - - @ManyToOne(() => Bid, (bid) => bid.documents) - @JoinColumn({ name: 'bid_id' }) - bid?: Bid; - - // Informaci贸n del documento - @Column({ length: 255 }) - name!: string; - - @Column({ type: 'text', nullable: true }) - description?: string; - - @Column({ - type: 'enum', - enum: ['tender_bases', 'clarifications', 'annexes', 'technical_proposal', 'economic_proposal', 'legal_documents', 'experience_certificates', 'financial_statements', 'bonds', 'contracts', 'correspondence', 'meeting_minutes', 'other'], - enumName: 'bid_document_category', - }) - category!: DocumentCategory; - - @Column({ - type: 'enum', - enum: ['draft', 'pending_review', 'approved', 'rejected', 'submitted', 'archived'], - enumName: 'bid_document_status', - default: 'draft', - }) - status!: DocumentStatus; - - // Archivo - @Column({ name: 'file_path', length: 500 }) - filePath!: string; - - @Column({ name: 'file_name', length: 255 }) - fileName!: string; - - @Column({ name: 'file_type', length: 100 }) - fileType!: string; - - @Column({ name: 'file_size', type: 'bigint' }) - fileSize!: number; - - @Column({ name: 'mime_type', length: 100, nullable: true }) - mimeType?: string; - - // Versi贸n - @Column({ type: 'int', default: 1 }) - version!: number; - - @Column({ name: 'is_current_version', type: 'boolean', default: true }) - isCurrentVersion!: boolean; - - @Column({ name: 'previous_version_id', type: 'uuid', nullable: true }) - previousVersionId?: string; - - // Metadatos de revisi贸n - @Column({ name: 'reviewed_by_id', type: 'uuid', nullable: true }) - reviewedById?: string; - - @ManyToOne(() => User, { nullable: true }) - @JoinColumn({ name: 'reviewed_by_id' }) - reviewedBy?: User; - - @Column({ name: 'reviewed_at', type: 'timestamptz', nullable: true }) - reviewedAt?: Date; - - @Column({ name: 'review_comments', type: 'text', nullable: true }) - reviewComments?: string; - - // Flags - @Column({ name: 'is_required', type: 'boolean', default: false }) - isRequired!: boolean; - - @Column({ name: 'is_confidential', type: 'boolean', default: false }) - isConfidential!: boolean; - - @Column({ name: 'is_submitted', type: 'boolean', default: false }) - isSubmitted!: boolean; - - @Column({ name: 'submitted_at', type: 'timestamptz', nullable: true }) - submittedAt?: Date; - - // Fecha de vencimiento (para documentos con vigencia) - @Column({ name: 'expiry_date', type: 'date', nullable: true }) - expiryDate?: Date; - - // Hash para verificaci贸n de integridad - @Column({ name: 'file_hash', length: 128, nullable: true }) - fileHash?: string; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'uploaded_by_id', type: 'uuid', nullable: true }) - uploadedById?: string; - - @ManyToOne(() => User, { nullable: true }) - @JoinColumn({ name: 'uploaded_by_id' }) - uploadedBy?: User; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-team.entity.ts b/projects/erp-construccion/backend/src/modules/bidding/entities/bid-team.entity.ts deleted file mode 100644 index d802f37c4..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/entities/bid-team.entity.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * BidTeam Entity - Equipo de Licitaci贸n - * - * Miembros del equipo asignados a una licitaci贸n. - * - * @module Bidding - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { Bid } from './bid.entity'; - -export type TeamRole = - | 'bid_manager' - | 'technical_lead' - | 'cost_engineer' - | 'legal_advisor' - | 'commercial_manager' - | 'project_manager' - | 'quality_manager' - | 'hse_manager' - | 'procurement_lead' - | 'design_lead' - | 'reviewer' - | 'contributor' - | 'support'; - -export type MemberStatus = 'active' | 'inactive' | 'pending' | 'removed'; - -@Entity('bid_team', { schema: 'bidding' }) -@Index(['tenantId', 'bidId']) -@Index(['tenantId', 'userId']) -export class BidTeam { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Referencia a licitaci贸n - @Column({ name: 'bid_id', type: 'uuid' }) - bidId!: string; - - @ManyToOne(() => Bid, (bid) => bid.teamMembers) - @JoinColumn({ name: 'bid_id' }) - bid?: Bid; - - // Referencia a usuario - @Column({ name: 'user_id', type: 'uuid' }) - userId!: string; - - @ManyToOne(() => User) - @JoinColumn({ name: 'user_id' }) - user?: User; - - // Rol y responsabilidades - @Column({ - type: 'enum', - enum: ['bid_manager', 'technical_lead', 'cost_engineer', 'legal_advisor', 'commercial_manager', 'project_manager', 'quality_manager', 'hse_manager', 'procurement_lead', 'design_lead', 'reviewer', 'contributor', 'support'], - enumName: 'bid_team_role', - }) - role!: TeamRole; - - @Column({ - type: 'enum', - enum: ['active', 'inactive', 'pending', 'removed'], - enumName: 'bid_team_status', - default: 'active', - }) - status!: MemberStatus; - - @Column({ type: 'text', array: true, nullable: true }) - responsibilities?: string[]; - - // Dedicaci贸n - @Column({ - name: 'allocation_percentage', - type: 'decimal', - precision: 5, - scale: 2, - default: 100, - }) - allocationPercentage!: number; - - @Column({ name: 'estimated_hours', type: 'decimal', precision: 8, scale: 2, nullable: true }) - estimatedHours?: number; - - @Column({ name: 'actual_hours', type: 'decimal', precision: 8, scale: 2, default: 0 }) - actualHours!: number; - - // Fechas de participaci贸n - @Column({ name: 'start_date', type: 'date' }) - startDate!: Date; - - @Column({ name: 'end_date', type: 'date', nullable: true }) - endDate?: Date; - - // Permisos espec铆ficos - @Column({ name: 'can_edit_technical', type: 'boolean', default: false }) - canEditTechnical!: boolean; - - @Column({ name: 'can_edit_economic', type: 'boolean', default: false }) - canEditEconomic!: boolean; - - @Column({ name: 'can_approve', type: 'boolean', default: false }) - canApprove!: boolean; - - @Column({ name: 'can_submit', type: 'boolean', default: false }) - canSubmit!: boolean; - - // Notificaciones - @Column({ name: 'receive_notifications', type: 'boolean', default: true }) - receiveNotifications!: boolean; - - @Column({ name: 'notification_preferences', type: 'jsonb', nullable: true }) - notificationPreferences?: Record; - - // Evaluaci贸n de participaci贸n - @Column({ - name: 'performance_rating', - type: 'decimal', - precision: 3, - scale: 2, - nullable: true, - }) - performanceRating?: number; - - @Column({ name: 'performance_notes', type: 'text', nullable: true }) - performanceNotes?: string; - - // Informaci贸n de contacto externa (si no es empleado) - @Column({ name: 'is_external', type: 'boolean', default: false }) - isExternal!: boolean; - - @Column({ name: 'external_company', length: 255, nullable: true }) - externalCompany?: string; - - @Column({ name: 'external_email', length: 255, nullable: true }) - externalEmail?: string; - - @Column({ name: 'external_phone', length: 50, nullable: true }) - externalPhone?: string; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/entities/bid.entity.ts b/projects/erp-construccion/backend/src/modules/bidding/entities/bid.entity.ts deleted file mode 100644 index 7712ea8f5..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/entities/bid.entity.ts +++ /dev/null @@ -1,311 +0,0 @@ -/** - * Bid Entity - Licitaciones/Propuestas - * - * Representa una licitaci贸n o propuesta formal vinculada a una oportunidad. - * - * @module Bidding - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { Opportunity } from './opportunity.entity'; -import { BidDocument } from './bid-document.entity'; -import { BidCalendar } from './bid-calendar.entity'; -import { BidBudget } from './bid-budget.entity'; -import { BidCompetitor } from './bid-competitor.entity'; -import { BidTeam } from './bid-team.entity'; - -export type BidType = 'public' | 'private' | 'invitation' | 'direct_award' | 'framework_agreement'; - -export type BidStatus = - | 'draft' - | 'preparation' - | 'review' - | 'approved' - | 'submitted' - | 'clarification' - | 'evaluation' - | 'awarded' - | 'rejected' - | 'cancelled' - | 'withdrawn'; - -export type BidStage = - | 'initial' - | 'technical_proposal' - | 'economic_proposal' - | 'final_submission' - | 'post_submission'; - -@Entity('bids', { schema: 'bidding' }) -@Index(['tenantId', 'status']) -@Index(['tenantId', 'bidType']) -@Index(['tenantId', 'opportunityId']) -export class Bid { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Referencia a oportunidad - @Column({ name: 'opportunity_id', type: 'uuid' }) - opportunityId!: string; - - @ManyToOne(() => Opportunity, (opp) => opp.bids) - @JoinColumn({ name: 'opportunity_id' }) - opportunity?: Opportunity; - - // Informaci贸n b谩sica - @Column({ length: 100 }) - code!: string; - - @Column({ length: 500 }) - name!: string; - - @Column({ type: 'text', nullable: true }) - description?: string; - - @Column({ - name: 'bid_type', - type: 'enum', - enum: ['public', 'private', 'invitation', 'direct_award', 'framework_agreement'], - enumName: 'bid_type', - }) - bidType!: BidType; - - @Column({ - type: 'enum', - enum: ['draft', 'preparation', 'review', 'approved', 'submitted', 'clarification', 'evaluation', 'awarded', 'rejected', 'cancelled', 'withdrawn'], - enumName: 'bid_status', - default: 'draft', - }) - status!: BidStatus; - - @Column({ - type: 'enum', - enum: ['initial', 'technical_proposal', 'economic_proposal', 'final_submission', 'post_submission'], - enumName: 'bid_stage', - default: 'initial', - }) - stage!: BidStage; - - // Referencia de convocatoria - @Column({ name: 'tender_number', length: 100, nullable: true }) - tenderNumber?: string; - - @Column({ name: 'tender_name', length: 500, nullable: true }) - tenderName?: string; - - @Column({ name: 'contracting_entity', length: 255, nullable: true }) - contractingEntity?: string; - - // Fechas clave - @Column({ name: 'publication_date', type: 'date', nullable: true }) - publicationDate?: Date; - - @Column({ name: 'site_visit_date', type: 'timestamptz', nullable: true }) - siteVisitDate?: Date; - - @Column({ name: 'clarification_deadline', type: 'timestamptz', nullable: true }) - clarificationDeadline?: Date; - - @Column({ name: 'submission_deadline', type: 'timestamptz' }) - submissionDeadline!: Date; - - @Column({ name: 'opening_date', type: 'timestamptz', nullable: true }) - openingDate?: Date; - - @Column({ name: 'award_date', type: 'date', nullable: true }) - awardDate?: Date; - - @Column({ name: 'contract_signing_date', type: 'date', nullable: true }) - contractSigningDate?: Date; - - // Montos - @Column({ - name: 'base_budget', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - baseBudget?: number; - - @Column({ - name: 'our_proposal_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - ourProposalAmount?: number; - - @Column({ - name: 'winning_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - winningAmount?: number; - - @Column({ name: 'currency', length: 3, default: 'MXN' }) - currency!: string; - - // Propuesta t茅cnica - @Column({ - name: 'technical_score', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - technicalScore?: number; - - @Column({ - name: 'technical_weight', - type: 'decimal', - precision: 5, - scale: 2, - default: 50, - }) - technicalWeight!: number; - - // Propuesta econ贸mica - @Column({ - name: 'economic_score', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - economicScore?: number; - - @Column({ - name: 'economic_weight', - type: 'decimal', - precision: 5, - scale: 2, - default: 50, - }) - economicWeight!: number; - - // Puntuaci贸n final - @Column({ - name: 'final_score', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - finalScore?: number; - - @Column({ name: 'ranking_position', type: 'int', nullable: true }) - rankingPosition?: number; - - // Garant铆as - @Column({ - name: 'bid_bond_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - bidBondAmount?: number; - - @Column({ name: 'bid_bond_number', length: 100, nullable: true }) - bidBondNumber?: string; - - @Column({ name: 'bid_bond_expiry', type: 'date', nullable: true }) - bidBondExpiry?: Date; - - // Asignaci贸n - @Column({ name: 'bid_manager_id', type: 'uuid', nullable: true }) - bidManagerId?: string; - - @ManyToOne(() => User, { nullable: true }) - @JoinColumn({ name: 'bid_manager_id' }) - bidManager?: User; - - // Resultado - @Column({ name: 'winner_name', length: 255, nullable: true }) - winnerName?: string; - - @Column({ name: 'rejection_reason', type: 'text', nullable: true }) - rejectionReason?: string; - - @Column({ name: 'lessons_learned', type: 'text', nullable: true }) - lessonsLearned?: string; - - // Progreso - @Column({ - name: 'completion_percentage', - type: 'decimal', - precision: 5, - scale: 2, - default: 0, - }) - completionPercentage!: number; - - // Checklist de documentos - @Column({ name: 'checklist', type: 'jsonb', nullable: true }) - checklist?: Record; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Relaciones - @OneToMany(() => BidDocument, (doc) => doc.bid) - documents?: BidDocument[]; - - @OneToMany(() => BidCalendar, (event) => event.bid) - calendarEvents?: BidCalendar[]; - - @OneToMany(() => BidBudget, (budget) => budget.bid) - budgetItems?: BidBudget[]; - - @OneToMany(() => BidCompetitor, (comp) => comp.bid) - competitors?: BidCompetitor[]; - - @OneToMany(() => BidTeam, (team) => team.bid) - teamMembers?: BidTeam[]; - - // Conversi贸n a proyecto - @Column({ name: 'converted_to_project_id', type: 'uuid', nullable: true }) - convertedToProjectId?: string; - - @Column({ name: 'converted_at', type: 'timestamptz', nullable: true }) - convertedAt?: Date; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/entities/index.ts b/projects/erp-construccion/backend/src/modules/bidding/entities/index.ts deleted file mode 100644 index dee4637c9..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/entities/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Bidding Entities Index - * @module Bidding - */ - -export { Opportunity, OpportunitySource, OpportunityStatus, OpportunityPriority, ProjectType } from './opportunity.entity'; -export { Bid, BidType, BidStatus, BidStage } from './bid.entity'; -export { BidDocument, DocumentCategory, DocumentStatus } from './bid-document.entity'; -export { BidCalendar, CalendarEventType, EventPriority, EventStatus } from './bid-calendar.entity'; -export { BidBudget, BudgetItemType, BudgetStatus } from './bid-budget.entity'; -export { BidCompetitor, CompetitorStatus, ThreatLevel } from './bid-competitor.entity'; -export { BidTeam, TeamRole, MemberStatus } from './bid-team.entity'; diff --git a/projects/erp-construccion/backend/src/modules/bidding/entities/opportunity.entity.ts b/projects/erp-construccion/backend/src/modules/bidding/entities/opportunity.entity.ts deleted file mode 100644 index b1b88130c..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/entities/opportunity.entity.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Opportunity Entity - Oportunidades de Negocio - * - * Representa oportunidades de licitaci贸n/proyecto en el pipeline comercial. - * - * @module Bidding - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { Bid } from './bid.entity'; - -export type OpportunitySource = - | 'portal_compranet' - | 'portal_state' - | 'direct_invitation' - | 'referral' - | 'public_notice' - | 'networking' - | 'repeat_client' - | 'cold_call' - | 'website' - | 'other'; - -export type OpportunityStatus = - | 'identified' - | 'qualified' - | 'pursuing' - | 'bid_submitted' - | 'won' - | 'lost' - | 'cancelled' - | 'on_hold'; - -export type OpportunityPriority = 'low' | 'medium' | 'high' | 'critical'; - -export type ProjectType = - | 'residential' - | 'commercial' - | 'industrial' - | 'infrastructure' - | 'institutional' - | 'mixed_use' - | 'renovation' - | 'maintenance'; - -@Entity('opportunities', { schema: 'bidding' }) -@Index(['tenantId', 'status']) -@Index(['tenantId', 'source']) -@Index(['tenantId', 'assignedToId']) -export class Opportunity { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Informaci贸n b谩sica - @Column({ length: 100 }) - code!: string; - - @Column({ length: 500 }) - name!: string; - - @Column({ type: 'text', nullable: true }) - description?: string; - - @Column({ - type: 'enum', - enum: ['portal_compranet', 'portal_state', 'direct_invitation', 'referral', 'public_notice', 'networking', 'repeat_client', 'cold_call', 'website', 'other'], - enumName: 'opportunity_source', - }) - source!: OpportunitySource; - - @Column({ - type: 'enum', - enum: ['identified', 'qualified', 'pursuing', 'bid_submitted', 'won', 'lost', 'cancelled', 'on_hold'], - enumName: 'opportunity_status', - default: 'identified', - }) - status!: OpportunityStatus; - - @Column({ - type: 'enum', - enum: ['low', 'medium', 'high', 'critical'], - enumName: 'opportunity_priority', - default: 'medium', - }) - priority!: OpportunityPriority; - - @Column({ - name: 'project_type', - type: 'enum', - enum: ['residential', 'commercial', 'industrial', 'infrastructure', 'institutional', 'mixed_use', 'renovation', 'maintenance'], - enumName: 'project_type', - }) - projectType!: ProjectType; - - // Cliente/Convocante - @Column({ name: 'client_name', length: 255 }) - clientName!: string; - - @Column({ name: 'client_contact', length: 255, nullable: true }) - clientContact?: string; - - @Column({ name: 'client_email', length: 255, nullable: true }) - clientEmail?: string; - - @Column({ name: 'client_phone', length: 50, nullable: true }) - clientPhone?: string; - - @Column({ name: 'client_type', length: 50, nullable: true }) - clientType?: string; // 'gobierno_federal', 'gobierno_estatal', 'privado', etc. - - // Ubicaci贸n - @Column({ length: 255, nullable: true }) - location?: string; - - @Column({ length: 100, nullable: true }) - state?: string; - - @Column({ length: 100, nullable: true }) - city?: string; - - // Montos estimados - @Column({ - name: 'estimated_value', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - estimatedValue?: number; - - @Column({ name: 'currency', length: 3, default: 'MXN' }) - currency!: string; - - @Column({ - name: 'construction_area_m2', - type: 'decimal', - precision: 12, - scale: 2, - nullable: true, - }) - constructionAreaM2?: number; - - @Column({ - name: 'land_area_m2', - type: 'decimal', - precision: 12, - scale: 2, - nullable: true, - }) - landAreaM2?: number; - - // Fechas clave - @Column({ name: 'identification_date', type: 'date' }) - identificationDate!: Date; - - @Column({ name: 'deadline_date', type: 'timestamptz', nullable: true }) - deadlineDate?: Date; - - @Column({ name: 'expected_award_date', type: 'date', nullable: true }) - expectedAwardDate?: Date; - - @Column({ name: 'expected_start_date', type: 'date', nullable: true }) - expectedStartDate?: Date; - - @Column({ name: 'expected_duration_months', type: 'int', nullable: true }) - expectedDurationMonths?: number; - - // Probabilidad y an谩lisis - @Column({ - name: 'win_probability', - type: 'decimal', - precision: 5, - scale: 2, - default: 0, - }) - winProbability!: number; - - @Column({ - name: 'weighted_value', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - weightedValue?: number; - - // Requisitos - @Column({ name: 'requires_bond', type: 'boolean', default: false }) - requiresBond!: boolean; - - @Column({ name: 'requires_experience', type: 'boolean', default: false }) - requiresExperience!: boolean; - - @Column({ - name: 'minimum_experience_years', - type: 'int', - nullable: true, - }) - minimumExperienceYears?: number; - - @Column({ - name: 'minimum_capital', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - minimumCapital?: number; - - @Column({ - name: 'required_certifications', - type: 'text', - array: true, - nullable: true, - }) - requiredCertifications?: string[]; - - // Asignaci贸n - @Column({ name: 'assigned_to_id', type: 'uuid', nullable: true }) - assignedToId?: string; - - @ManyToOne(() => User, { nullable: true }) - @JoinColumn({ name: 'assigned_to_id' }) - assignedTo?: User; - - // Raz贸n de resultado - @Column({ name: 'loss_reason', type: 'text', nullable: true }) - lossReason?: string; - - @Column({ name: 'win_factors', type: 'text', nullable: true }) - winFactors?: string; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ name: 'source_url', length: 500, nullable: true }) - sourceUrl?: string; - - @Column({ name: 'source_reference', length: 255, nullable: true }) - sourceReference?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Relaciones - @OneToMany(() => Bid, (bid) => bid.opportunity) - bids?: Bid[]; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/services/bid-analytics.service.ts b/projects/erp-construccion/backend/src/modules/bidding/services/bid-analytics.service.ts deleted file mode 100644 index 8d443f96b..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/services/bid-analytics.service.ts +++ /dev/null @@ -1,385 +0,0 @@ -/** - * BidAnalyticsService - An谩lisis y Reportes de Licitaciones - * - * Estad铆sticas, tendencias y an谩lisis de competitividad. - * - * @module Bidding - */ - -import { Repository } from 'typeorm'; -import { ServiceContext } from '../../../shared/services/base.service'; -import { Bid, BidStatus, BidType } from '../entities/bid.entity'; -import { Opportunity, OpportunitySource, OpportunityStatus } from '../entities/opportunity.entity'; -import { BidCompetitor } from '../entities/bid-competitor.entity'; - -export class BidAnalyticsService { - constructor( - private readonly bidRepository: Repository, - private readonly opportunityRepository: Repository, - private readonly competitorRepository: Repository - ) {} - - /** - * Dashboard general de licitaciones - */ - async getDashboard(ctx: ServiceContext): Promise { - const now = new Date(); - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - - // Oportunidades activas - const activeOpportunities = await this.opportunityRepository.count({ - where: { - tenantId: ctx.tenantId, - deletedAt: undefined, - status: 'pursuing' as OpportunityStatus, - }, - }); - - // Licitaciones activas - const activeBids = await this.bidRepository.count({ - where: { - tenantId: ctx.tenantId, - deletedAt: undefined, - status: 'preparation' as BidStatus, - }, - }); - - // Valor del pipeline - const pipelineValue = await this.opportunityRepository - .createQueryBuilder('o') - .select('SUM(o.weighted_value)', 'value') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.status NOT IN (:...closedStatuses)', { closedStatuses: ['won', 'lost', 'cancelled'] }) - .getRawOne(); - - // Pr贸ximas fechas l铆mite - const upcomingDeadlines = await this.bidRepository - .createQueryBuilder('b') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.status IN (:...activeStatuses)', { activeStatuses: ['draft', 'preparation', 'review', 'approved'] }) - .andWhere('b.submission_deadline >= :now', { now }) - .orderBy('b.submission_deadline', 'ASC') - .take(5) - .getMany(); - - // Win rate 煤ltimos 12 meses - const yearAgo = new Date(); - yearAgo.setFullYear(yearAgo.getFullYear() - 1); - - const winRateStats = await this.bidRepository - .createQueryBuilder('b') - .select('b.status', 'status') - .addSelect('COUNT(*)', 'count') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.status IN (:...closedStatuses)', { closedStatuses: ['awarded', 'rejected'] }) - .andWhere('b.award_date >= :yearAgo', { yearAgo }) - .groupBy('b.status') - .getRawMany(); - - const awarded = winRateStats.find((s) => s.status === 'awarded')?.count || 0; - const rejected = winRateStats.find((s) => s.status === 'rejected')?.count || 0; - const totalClosed = parseInt(awarded) + parseInt(rejected); - const winRate = totalClosed > 0 ? (parseInt(awarded) / totalClosed) * 100 : 0; - - // Valor ganado este a帽o - const startOfYear = new Date(now.getFullYear(), 0, 1); - const wonValue = await this.bidRepository - .createQueryBuilder('b') - .select('SUM(b.winning_amount)', 'value') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.status = :status', { status: 'awarded' }) - .andWhere('b.award_date >= :startOfYear', { startOfYear }) - .getRawOne(); - - return { - activeOpportunities, - activeBids, - pipelineValue: parseFloat(pipelineValue?.value) || 0, - upcomingDeadlines: upcomingDeadlines.map((b) => ({ - id: b.id, - name: b.name, - deadline: b.submissionDeadline, - status: b.status, - })), - winRate, - wonValueYTD: parseFloat(wonValue?.value) || 0, - }; - } - - /** - * An谩lisis de pipeline por fuente - */ - async getPipelineBySource(ctx: ServiceContext): Promise { - const result = await this.opportunityRepository - .createQueryBuilder('o') - .select('o.source', 'source') - .addSelect('COUNT(*)', 'count') - .addSelect('SUM(o.estimated_value)', 'totalValue') - .addSelect('SUM(o.weighted_value)', 'weightedValue') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.status NOT IN (:...closedStatuses)', { closedStatuses: ['won', 'lost', 'cancelled'] }) - .groupBy('o.source') - .orderBy('SUM(o.weighted_value)', 'DESC') - .getRawMany(); - - return result.map((r) => ({ - source: r.source as OpportunitySource, - count: parseInt(r.count), - totalValue: parseFloat(r.totalValue) || 0, - weightedValue: parseFloat(r.weightedValue) || 0, - })); - } - - /** - * An谩lisis de win rate por tipo de licitaci贸n - */ - async getWinRateByType(ctx: ServiceContext, months = 12): Promise { - const fromDate = new Date(); - fromDate.setMonth(fromDate.getMonth() - months); - - const result = await this.bidRepository - .createQueryBuilder('b') - .select('b.bid_type', 'bidType') - .addSelect('COUNT(*) FILTER (WHERE b.status = \'awarded\')', 'won') - .addSelect('COUNT(*) FILTER (WHERE b.status IN (\'awarded\', \'rejected\'))', 'total') - .addSelect('SUM(CASE WHEN b.status = \'awarded\' THEN b.winning_amount ELSE 0 END)', 'wonValue') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.award_date >= :fromDate', { fromDate }) - .groupBy('b.bid_type') - .getRawMany(); - - return result.map((r) => ({ - bidType: r.bidType as BidType, - won: parseInt(r.won) || 0, - total: parseInt(r.total) || 0, - winRate: parseInt(r.total) > 0 ? (parseInt(r.won) / parseInt(r.total)) * 100 : 0, - wonValue: parseFloat(r.wonValue) || 0, - })); - } - - /** - * Tendencia mensual de oportunidades - */ - async getMonthlyTrend(ctx: ServiceContext, months = 12): Promise { - const fromDate = new Date(); - fromDate.setMonth(fromDate.getMonth() - months); - - const result = await this.opportunityRepository - .createQueryBuilder('o') - .select("TO_CHAR(o.identification_date, 'YYYY-MM')", 'month') - .addSelect('COUNT(*)', 'identified') - .addSelect('COUNT(*) FILTER (WHERE o.status = \'won\')', 'won') - .addSelect('COUNT(*) FILTER (WHERE o.status = \'lost\')', 'lost') - .addSelect('SUM(CASE WHEN o.status = \'won\' THEN o.estimated_value ELSE 0 END)', 'wonValue') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.identification_date >= :fromDate', { fromDate }) - .groupBy("TO_CHAR(o.identification_date, 'YYYY-MM')") - .orderBy("TO_CHAR(o.identification_date, 'YYYY-MM')", 'ASC') - .getRawMany(); - - return result.map((r) => ({ - month: r.month, - identified: parseInt(r.identified) || 0, - won: parseInt(r.won) || 0, - lost: parseInt(r.lost) || 0, - wonValue: parseFloat(r.wonValue) || 0, - })); - } - - /** - * An谩lisis de competidores - */ - async getCompetitorAnalysis(ctx: ServiceContext): Promise { - const result = await this.competitorRepository - .createQueryBuilder('c') - .select('c.company_name', 'companyName') - .addSelect('COUNT(*)', 'encounters') - .addSelect('SUM(CASE WHEN c.status = \'winner\' THEN 1 ELSE 0 END)', 'theirWins') - .addSelect('SUM(CASE WHEN c.status = \'loser\' THEN 1 ELSE 0 END)', 'ourWins') - .addSelect('AVG(c.proposed_amount)', 'avgProposedAmount') - .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('c.deleted_at IS NULL') - .groupBy('c.company_name') - .having('COUNT(*) >= 2') - .orderBy('COUNT(*)', 'DESC') - .take(20) - .getRawMany(); - - return result.map((r) => ({ - companyName: r.companyName, - encounters: parseInt(r.encounters) || 0, - theirWins: parseInt(r.theirWins) || 0, - ourWins: parseInt(r.ourWins) || 0, - winRateAgainst: parseInt(r.encounters) > 0 - ? (parseInt(r.ourWins) / parseInt(r.encounters)) * 100 - : 0, - avgProposedAmount: parseFloat(r.avgProposedAmount) || 0, - })); - } - - /** - * An谩lisis de conversi贸n del funnel - */ - async getFunnelAnalysis(ctx: ServiceContext, months = 12): Promise { - const fromDate = new Date(); - fromDate.setMonth(fromDate.getMonth() - months); - - const baseQuery = this.opportunityRepository - .createQueryBuilder('o') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.identification_date >= :fromDate', { fromDate }); - - const identified = await baseQuery.clone().getCount(); - - const qualified = await baseQuery.clone() - .andWhere('o.status NOT IN (:...earlyStatuses)', { earlyStatuses: ['identified'] }) - .getCount(); - - const pursuing = await baseQuery.clone() - .andWhere('o.status IN (:...pursuitStatuses)', { pursuitStatuses: ['pursuing', 'bid_submitted', 'won', 'lost'] }) - .getCount(); - - const bidSubmitted = await baseQuery.clone() - .andWhere('o.status IN (:...submittedStatuses)', { submittedStatuses: ['bid_submitted', 'won', 'lost'] }) - .getCount(); - - const won = await baseQuery.clone() - .andWhere('o.status = :status', { status: 'won' }) - .getCount(); - - return { - identified, - qualified, - pursuing, - bidSubmitted, - won, - conversionRates: { - identifiedToQualified: identified > 0 ? (qualified / identified) * 100 : 0, - qualifiedToPursuing: qualified > 0 ? (pursuing / qualified) * 100 : 0, - pursuingToSubmitted: pursuing > 0 ? (bidSubmitted / pursuing) * 100 : 0, - submittedToWon: bidSubmitted > 0 ? (won / bidSubmitted) * 100 : 0, - overallConversion: identified > 0 ? (won / identified) * 100 : 0, - }, - }; - } - - /** - * An谩lisis de tiempos de ciclo - */ - async getCycleTimeAnalysis(ctx: ServiceContext, months = 12): Promise { - const fromDate = new Date(); - fromDate.setMonth(fromDate.getMonth() - months); - - const result = await this.opportunityRepository - .createQueryBuilder('o') - .select('AVG(EXTRACT(DAY FROM (o.updated_at - o.identification_date)))', 'avgDays') - .addSelect('MIN(EXTRACT(DAY FROM (o.updated_at - o.identification_date)))', 'minDays') - .addSelect('MAX(EXTRACT(DAY FROM (o.updated_at - o.identification_date)))', 'maxDays') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.status IN (:...closedStatuses)', { closedStatuses: ['won', 'lost'] }) - .andWhere('o.identification_date >= :fromDate', { fromDate }) - .getRawOne(); - - const byOutcome = await this.opportunityRepository - .createQueryBuilder('o') - .select('o.status', 'outcome') - .addSelect('AVG(EXTRACT(DAY FROM (o.updated_at - o.identification_date)))', 'avgDays') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.status IN (:...closedStatuses)', { closedStatuses: ['won', 'lost'] }) - .andWhere('o.identification_date >= :fromDate', { fromDate }) - .groupBy('o.status') - .getRawMany(); - - return { - overall: { - avgDays: Math.round(parseFloat(result?.avgDays) || 0), - minDays: Math.round(parseFloat(result?.minDays) || 0), - maxDays: Math.round(parseFloat(result?.maxDays) || 0), - }, - byOutcome: byOutcome.map((r) => ({ - outcome: r.outcome as 'won' | 'lost', - avgDays: Math.round(parseFloat(r.avgDays) || 0), - })), - }; - } -} - -// Types -export interface BidDashboard { - activeOpportunities: number; - activeBids: number; - pipelineValue: number; - upcomingDeadlines: { id: string; name: string; deadline: Date; status: BidStatus }[]; - winRate: number; - wonValueYTD: number; -} - -export interface PipelineBySource { - source: OpportunitySource; - count: number; - totalValue: number; - weightedValue: number; -} - -export interface WinRateByType { - bidType: BidType; - won: number; - total: number; - winRate: number; - wonValue: number; -} - -export interface MonthlyTrend { - month: string; - identified: number; - won: number; - lost: number; - wonValue: number; -} - -export interface CompetitorAnalysis { - companyName: string; - encounters: number; - theirWins: number; - ourWins: number; - winRateAgainst: number; - avgProposedAmount: number; -} - -export interface FunnelAnalysis { - identified: number; - qualified: number; - pursuing: number; - bidSubmitted: number; - won: number; - conversionRates: { - identifiedToQualified: number; - qualifiedToPursuing: number; - pursuingToSubmitted: number; - submittedToWon: number; - overallConversion: number; - }; -} - -export interface CycleTimeAnalysis { - overall: { - avgDays: number; - minDays: number; - maxDays: number; - }; - byOutcome: { - outcome: 'won' | 'lost'; - avgDays: number; - }[]; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/services/bid-budget.service.ts b/projects/erp-construccion/backend/src/modules/bidding/services/bid-budget.service.ts deleted file mode 100644 index f156b7068..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/services/bid-budget.service.ts +++ /dev/null @@ -1,388 +0,0 @@ -/** - * BidBudgetService - Gesti贸n de Presupuestos de Licitaci贸n - * - * CRUD y c谩lculos para propuestas econ贸micas. - * - * @module Bidding - */ - -import { Repository } from 'typeorm'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { BidBudget, BudgetItemType, BudgetStatus } from '../entities/bid-budget.entity'; - -export interface CreateBudgetItemDto { - bidId: string; - parentId?: string; - code: string; - name: string; - description?: string; - itemType: BudgetItemType; - unit?: string; - quantity?: number; - unitPrice?: number; - materialsCost?: number; - laborCost?: number; - equipmentCost?: number; - subcontractCost?: number; - indirectPercentage?: number; - profitPercentage?: number; - financingPercentage?: number; - baseAmount?: number; - catalogConceptId?: string; - notes?: string; - metadata?: Record; -} - -export interface UpdateBudgetItemDto extends Partial { - status?: BudgetStatus; - adjustmentReason?: string; -} - -export interface BudgetFilters { - bidId: string; - itemType?: BudgetItemType; - status?: BudgetStatus; - parentId?: string | null; - isSummary?: boolean; -} - -export class BidBudgetService { - constructor(private readonly repository: Repository) {} - - /** - * Crear item de presupuesto - */ - async create(ctx: ServiceContext, data: CreateBudgetItemDto): Promise { - // Calcular nivel jer谩rquico - let level = 0; - if (data.parentId) { - const parent = await this.repository.findOne({ - where: { id: data.parentId, tenantId: ctx.tenantId }, - }); - if (parent) { - level = parent.level + 1; - } - } - - // Calcular orden - const lastItem = await this.repository - .createQueryBuilder('bb') - .where('bb.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('bb.bid_id = :bidId', { bidId: data.bidId }) - .andWhere(data.parentId ? 'bb.parent_id = :parentId' : 'bb.parent_id IS NULL', { parentId: data.parentId }) - .orderBy('bb.sort_order', 'DESC') - .getOne(); - - const sortOrder = lastItem ? lastItem.sortOrder + 1 : 0; - - // Calcular totales - const quantity = data.quantity || 0; - const unitPrice = data.unitPrice || 0; - const totalAmount = quantity * unitPrice; - - // Calcular varianza si hay base - let varianceAmount = null; - let variancePercentage = null; - if (data.baseAmount !== undefined && data.baseAmount > 0) { - varianceAmount = totalAmount - data.baseAmount; - variancePercentage = (varianceAmount / data.baseAmount) * 100; - } - - const item = this.repository.create({ - tenantId: ctx.tenantId, - bidId: data.bidId, - parentId: data.parentId, - code: data.code, - name: data.name, - description: data.description, - itemType: data.itemType, - unit: data.unit, - quantity: data.quantity || 0, - unitPrice: data.unitPrice || 0, - materialsCost: data.materialsCost, - laborCost: data.laborCost, - equipmentCost: data.equipmentCost, - subcontractCost: data.subcontractCost, - indirectPercentage: data.indirectPercentage, - profitPercentage: data.profitPercentage, - financingPercentage: data.financingPercentage, - baseAmount: data.baseAmount, - catalogConceptId: data.catalogConceptId, - notes: data.notes, - metadata: data.metadata, - level, - sortOrder, - totalAmount, - varianceAmount: varianceAmount ?? undefined, - variancePercentage: variancePercentage ?? undefined, - status: 'draft', - isSummary: false, - isCalculated: true, - createdBy: ctx.userId, - updatedBy: ctx.userId, - }); - - const saved = await this.repository.save(item); - - // Recalcular padres - if (data.parentId) { - await this.recalculateParent(ctx, data.parentId); - } - - return saved; - } - - /** - * Buscar por ID - */ - async findById(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { id, tenantId: ctx.tenantId, deletedAt: undefined }, - }); - } - - /** - * Buscar items de un presupuesto - */ - async findByBid(ctx: ServiceContext, bidId: string): Promise { - return this.repository.find({ - where: { bidId, tenantId: ctx.tenantId, deletedAt: undefined }, - order: { sortOrder: 'ASC' }, - }); - } - - /** - * Buscar items con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: BudgetFilters, - page = 1, - limit = 100 - ): Promise> { - const qb = this.repository - .createQueryBuilder('bb') - .where('bb.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('bb.bid_id = :bidId', { bidId: filters.bidId }) - .andWhere('bb.deleted_at IS NULL'); - - if (filters.itemType) { - qb.andWhere('bb.item_type = :itemType', { itemType: filters.itemType }); - } - if (filters.status) { - qb.andWhere('bb.status = :status', { status: filters.status }); - } - if (filters.parentId !== undefined) { - if (filters.parentId === null) { - qb.andWhere('bb.parent_id IS NULL'); - } else { - qb.andWhere('bb.parent_id = :parentId', { parentId: filters.parentId }); - } - } - if (filters.isSummary !== undefined) { - qb.andWhere('bb.is_summary = :isSummary', { isSummary: filters.isSummary }); - } - - const skip = (page - 1) * limit; - qb.orderBy('bb.sort_order', 'ASC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Obtener 谩rbol jer谩rquico - */ - async getTree(ctx: ServiceContext, bidId: string): Promise { - const items = await this.findByBid(ctx, bidId); - return this.buildTree(items); - } - - private buildTree(items: BidBudget[], parentId: string | null = null): (BidBudget & { children?: BidBudget[] })[] { - return items - .filter((item) => item.parentId === parentId) - .map((item) => ({ - ...item, - children: this.buildTree(items, item.id), - })); - } - - /** - * Actualizar item - */ - async update(ctx: ServiceContext, id: string, data: UpdateBudgetItemDto): Promise { - const item = await this.findById(ctx, id); - if (!item) return null; - - // Recalcular totales si cambian cantidad o precio - const quantity = data.quantity ?? item.quantity; - const unitPrice = data.unitPrice ?? item.unitPrice; - const totalAmount = quantity * unitPrice; - - // Recalcular varianza - const baseAmount = data.baseAmount ?? item.baseAmount; - let varianceAmount = item.varianceAmount; - let variancePercentage = item.variancePercentage; - if (baseAmount !== undefined && baseAmount > 0) { - varianceAmount = totalAmount - baseAmount; - variancePercentage = (varianceAmount / baseAmount) * 100; - } - - // Marcar como ajustado si hay raz贸n - const isAdjusted = data.adjustmentReason ? true : item.isAdjusted; - - Object.assign(item, { - ...data, - totalAmount, - varianceAmount, - variancePercentage, - isAdjusted, - isCalculated: true, - updatedBy: ctx.userId, - }); - - const saved = await this.repository.save(item); - - // Recalcular padres - if (item.parentId) { - await this.recalculateParent(ctx, item.parentId); - } - - return saved; - } - - /** - * Recalcular item padre - */ - private async recalculateParent(ctx: ServiceContext, parentId: string): Promise { - const parent = await this.findById(ctx, parentId); - if (!parent) return; - - const children = await this.repository.find({ - where: { parentId, tenantId: ctx.tenantId, deletedAt: undefined }, - }); - - const totalAmount = children.reduce((sum, child) => sum + (Number(child.totalAmount) || 0), 0); - - parent.totalAmount = totalAmount; - parent.isSummary = children.length > 0; - parent.isCalculated = true; - parent.updatedBy = ctx.userId; - - // Recalcular varianza - if (parent.baseAmount !== undefined && parent.baseAmount > 0) { - parent.varianceAmount = totalAmount - Number(parent.baseAmount); - parent.variancePercentage = (parent.varianceAmount / Number(parent.baseAmount)) * 100; - } - - await this.repository.save(parent); - - // Recursivamente actualizar ancestros - if (parent.parentId) { - await this.recalculateParent(ctx, parent.parentId); - } - } - - /** - * Obtener resumen de presupuesto - */ - async getSummary(ctx: ServiceContext, bidId: string): Promise { - const items = await this.findByBid(ctx, bidId); - - const directCosts = items - .filter((i) => i.itemType === 'direct_cost' || i.itemType === 'labor' || i.itemType === 'materials' || i.itemType === 'equipment' || i.itemType === 'subcontract') - .reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); - - const indirectCosts = items - .filter((i) => i.itemType === 'indirect_cost' || i.itemType === 'overhead') - .reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); - - const profit = items - .filter((i) => i.itemType === 'profit') - .reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); - - const financing = items - .filter((i) => i.itemType === 'financing') - .reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); - - const taxes = items - .filter((i) => i.itemType === 'taxes') - .reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); - - const bonds = items - .filter((i) => i.itemType === 'bonds') - .reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); - - const contingency = items - .filter((i) => i.itemType === 'contingency') - .reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); - - const subtotal = directCosts + indirectCosts + profit + financing + contingency + bonds; - const total = subtotal + taxes; - - const baseTotal = items.reduce((sum, i) => sum + (Number(i.baseAmount) || 0), 0); - - return { - directCosts, - indirectCosts, - profit, - financing, - taxes, - bonds, - contingency, - subtotal, - total, - baseTotal, - variance: total - baseTotal, - variancePercentage: baseTotal > 0 ? ((total - baseTotal) / baseTotal) * 100 : 0, - itemCount: items.length, - }; - } - - /** - * Cambiar estado del presupuesto - */ - async changeStatus(ctx: ServiceContext, bidId: string, status: BudgetStatus): Promise { - const result = await this.repository.update( - { bidId, tenantId: ctx.tenantId, deletedAt: undefined }, - { status, updatedBy: ctx.userId } - ); - return result.affected || 0; - } - - /** - * Soft delete - */ - async softDelete(ctx: ServiceContext, id: string): Promise { - const result = await this.repository.update( - { id, tenantId: ctx.tenantId }, - { deletedAt: new Date(), updatedBy: ctx.userId } - ); - return (result.affected || 0) > 0; - } -} - -export interface BudgetSummary { - directCosts: number; - indirectCosts: number; - profit: number; - financing: number; - taxes: number; - bonds: number; - contingency: number; - subtotal: number; - total: number; - baseTotal: number; - variance: number; - variancePercentage: number; - itemCount: number; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/services/bid.service.ts b/projects/erp-construccion/backend/src/modules/bidding/services/bid.service.ts deleted file mode 100644 index 13e14699a..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/services/bid.service.ts +++ /dev/null @@ -1,384 +0,0 @@ -/** - * BidService - Gesti贸n de Licitaciones - * - * CRUD y l贸gica de negocio para licitaciones/propuestas. - * - * @module Bidding - */ - -import { Repository, In, Between } from 'typeorm'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Bid, BidType, BidStatus, BidStage } from '../entities/bid.entity'; - -export interface CreateBidDto { - opportunityId: string; - code: string; - name: string; - description?: string; - bidType: BidType; - tenderNumber?: string; - tenderName?: string; - contractingEntity?: string; - publicationDate?: Date; - siteVisitDate?: Date; - clarificationDeadline?: Date; - submissionDeadline: Date; - openingDate?: Date; - baseBudget?: number; - currency?: string; - technicalWeight?: number; - economicWeight?: number; - bidBondAmount?: number; - bidManagerId?: string; - notes?: string; - metadata?: Record; -} - -export interface UpdateBidDto extends Partial { - status?: BidStatus; - stage?: BidStage; - ourProposalAmount?: number; - technicalScore?: number; - economicScore?: number; - finalScore?: number; - rankingPosition?: number; - bidBondNumber?: string; - bidBondExpiry?: Date; - awardDate?: Date; - contractSigningDate?: Date; - winnerName?: string; - winningAmount?: number; - rejectionReason?: string; - lessonsLearned?: string; - completionPercentage?: number; - checklist?: Record; -} - -export interface BidFilters { - status?: BidStatus | BidStatus[]; - bidType?: BidType; - stage?: BidStage; - opportunityId?: string; - bidManagerId?: string; - contractingEntity?: string; - deadlineFrom?: Date; - deadlineTo?: Date; - minBudget?: number; - maxBudget?: number; - search?: string; -} - -export class BidService { - constructor(private readonly repository: Repository) {} - - /** - * Crear licitaci贸n - */ - async create(ctx: ServiceContext, data: CreateBidDto): Promise { - const bid = this.repository.create({ - tenantId: ctx.tenantId, - ...data, - status: 'draft', - stage: 'initial', - completionPercentage: 0, - createdBy: ctx.userId, - updatedBy: ctx.userId, - }); - - return this.repository.save(bid); - } - - /** - * Buscar por ID - */ - async findById(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { id, tenantId: ctx.tenantId, deletedAt: undefined }, - relations: ['opportunity', 'bidManager', 'documents', 'calendarEvents', 'teamMembers'], - }); - } - - /** - * Buscar con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: BidFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('b') - .leftJoinAndSelect('b.opportunity', 'o') - .leftJoinAndSelect('b.bidManager', 'm') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL'); - - if (filters.status) { - if (Array.isArray(filters.status)) { - qb.andWhere('b.status IN (:...statuses)', { statuses: filters.status }); - } else { - qb.andWhere('b.status = :status', { status: filters.status }); - } - } - if (filters.bidType) { - qb.andWhere('b.bid_type = :bidType', { bidType: filters.bidType }); - } - if (filters.stage) { - qb.andWhere('b.stage = :stage', { stage: filters.stage }); - } - if (filters.opportunityId) { - qb.andWhere('b.opportunity_id = :opportunityId', { opportunityId: filters.opportunityId }); - } - if (filters.bidManagerId) { - qb.andWhere('b.bid_manager_id = :bidManagerId', { bidManagerId: filters.bidManagerId }); - } - if (filters.contractingEntity) { - qb.andWhere('b.contracting_entity ILIKE :entity', { entity: `%${filters.contractingEntity}%` }); - } - if (filters.deadlineFrom) { - qb.andWhere('b.submission_deadline >= :deadlineFrom', { deadlineFrom: filters.deadlineFrom }); - } - if (filters.deadlineTo) { - qb.andWhere('b.submission_deadline <= :deadlineTo', { deadlineTo: filters.deadlineTo }); - } - if (filters.minBudget !== undefined) { - qb.andWhere('b.base_budget >= :minBudget', { minBudget: filters.minBudget }); - } - if (filters.maxBudget !== undefined) { - qb.andWhere('b.base_budget <= :maxBudget', { maxBudget: filters.maxBudget }); - } - if (filters.search) { - qb.andWhere( - '(b.name ILIKE :search OR b.code ILIKE :search OR b.tender_number ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - const skip = (page - 1) * limit; - qb.orderBy('b.submission_deadline', 'ASC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Actualizar licitaci贸n - */ - async update(ctx: ServiceContext, id: string, data: UpdateBidDto): Promise { - const bid = await this.findById(ctx, id); - if (!bid) return null; - - // Calcular puntuaci贸n final si hay scores - let finalScore = data.finalScore ?? bid.finalScore; - const techScore = data.technicalScore ?? bid.technicalScore; - const econScore = data.economicScore ?? bid.economicScore; - const techWeight = data.technicalWeight ?? bid.technicalWeight; - const econWeight = data.economicWeight ?? bid.economicWeight; - - if (techScore !== undefined && econScore !== undefined) { - finalScore = (techScore * techWeight / 100) + (econScore * econWeight / 100); - } - - Object.assign(bid, { - ...data, - finalScore, - updatedBy: ctx.userId, - }); - - return this.repository.save(bid); - } - - /** - * Cambiar estado - */ - async changeStatus(ctx: ServiceContext, id: string, status: BidStatus): Promise { - const bid = await this.findById(ctx, id); - if (!bid) return null; - - bid.status = status; - bid.updatedBy = ctx.userId; - - return this.repository.save(bid); - } - - /** - * Cambiar etapa - */ - async changeStage(ctx: ServiceContext, id: string, stage: BidStage): Promise { - const bid = await this.findById(ctx, id); - if (!bid) return null; - - bid.stage = stage; - bid.updatedBy = ctx.userId; - - return this.repository.save(bid); - } - - /** - * Marcar como presentada - */ - async submit(ctx: ServiceContext, id: string, proposalAmount: number): Promise { - const bid = await this.findById(ctx, id); - if (!bid) return null; - - bid.status = 'submitted'; - bid.stage = 'post_submission'; - bid.ourProposalAmount = proposalAmount; - bid.updatedBy = ctx.userId; - - return this.repository.save(bid); - } - - /** - * Registrar resultado - */ - async recordResult( - ctx: ServiceContext, - id: string, - won: boolean, - details: { - winnerName?: string; - winningAmount?: number; - rankingPosition?: number; - rejectionReason?: string; - lessonsLearned?: string; - } - ): Promise { - const bid = await this.findById(ctx, id); - if (!bid) return null; - - bid.status = won ? 'awarded' : 'rejected'; - bid.awardDate = new Date(); - Object.assign(bid, details); - bid.updatedBy = ctx.userId; - - return this.repository.save(bid); - } - - /** - * Convertir a proyecto - */ - async convertToProject(ctx: ServiceContext, id: string, projectId: string): Promise { - const bid = await this.findById(ctx, id); - if (!bid || bid.status !== 'awarded') return null; - - bid.convertedToProjectId = projectId; - bid.convertedAt = new Date(); - bid.updatedBy = ctx.userId; - - return this.repository.save(bid); - } - - /** - * Obtener pr贸ximas fechas l铆mite - */ - async getUpcomingDeadlines(ctx: ServiceContext, days = 7): Promise { - const now = new Date(); - const future = new Date(); - future.setDate(future.getDate() + days); - - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: undefined, - status: In(['draft', 'preparation', 'review', 'approved']), - submissionDeadline: Between(now, future), - }, - relations: ['opportunity', 'bidManager'], - order: { submissionDeadline: 'ASC' }, - }); - } - - /** - * Obtener estad铆sticas - */ - async getStats(ctx: ServiceContext, year?: number): Promise { - const currentYear = year || new Date().getFullYear(); - const startDate = new Date(currentYear, 0, 1); - const endDate = new Date(currentYear, 11, 31); - - const total = await this.repository - .createQueryBuilder('b') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.created_at BETWEEN :startDate AND :endDate', { startDate, endDate }) - .getCount(); - - const byStatus = await this.repository - .createQueryBuilder('b') - .select('b.status', 'status') - .addSelect('COUNT(*)', 'count') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.created_at BETWEEN :startDate AND :endDate', { startDate, endDate }) - .groupBy('b.status') - .getRawMany(); - - const byType = await this.repository - .createQueryBuilder('b') - .select('b.bid_type', 'bidType') - .addSelect('COUNT(*)', 'count') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.created_at BETWEEN :startDate AND :endDate', { startDate, endDate }) - .groupBy('b.bid_type') - .getRawMany(); - - const valueStats = await this.repository - .createQueryBuilder('b') - .select('SUM(b.base_budget)', 'totalBudget') - .addSelect('SUM(b.our_proposal_amount)', 'totalProposed') - .addSelect('SUM(CASE WHEN b.status = \'awarded\' THEN b.winning_amount ELSE 0 END)', 'totalWon') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.created_at BETWEEN :startDate AND :endDate', { startDate, endDate }) - .getRawOne(); - - const awardedCount = byStatus.find((s) => s.status === 'awarded')?.count || 0; - const rejectedCount = byStatus.find((s) => s.status === 'rejected')?.count || 0; - const closedCount = parseInt(awardedCount) + parseInt(rejectedCount); - - return { - year: currentYear, - total, - byStatus: byStatus.map((r) => ({ status: r.status, count: parseInt(r.count) })), - byType: byType.map((r) => ({ bidType: r.bidType, count: parseInt(r.count) })), - totalBudget: parseFloat(valueStats?.totalBudget) || 0, - totalProposed: parseFloat(valueStats?.totalProposed) || 0, - totalWon: parseFloat(valueStats?.totalWon) || 0, - winRate: closedCount > 0 ? (parseInt(awardedCount) / closedCount) * 100 : 0, - }; - } - - /** - * Soft delete - */ - async softDelete(ctx: ServiceContext, id: string): Promise { - const result = await this.repository.update( - { id, tenantId: ctx.tenantId }, - { deletedAt: new Date(), updatedBy: ctx.userId } - ); - return (result.affected || 0) > 0; - } -} - -export interface BidStats { - year: number; - total: number; - byStatus: { status: BidStatus; count: number }[]; - byType: { bidType: BidType; count: number }[]; - totalBudget: number; - totalProposed: number; - totalWon: number; - winRate: number; -} diff --git a/projects/erp-construccion/backend/src/modules/bidding/services/index.ts b/projects/erp-construccion/backend/src/modules/bidding/services/index.ts deleted file mode 100644 index 12cc58745..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/services/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Bidding Services Index - * @module Bidding - */ - -export { OpportunityService, CreateOpportunityDto, UpdateOpportunityDto, OpportunityFilters, PipelineData, OpportunityStats } from './opportunity.service'; -export { BidService, CreateBidDto, UpdateBidDto, BidFilters, BidStats } from './bid.service'; -export { BidBudgetService, CreateBudgetItemDto, UpdateBudgetItemDto, BudgetFilters, BudgetSummary } from './bid-budget.service'; -export { BidAnalyticsService, BidDashboard, PipelineBySource, WinRateByType, MonthlyTrend, CompetitorAnalysis, FunnelAnalysis, CycleTimeAnalysis } from './bid-analytics.service'; diff --git a/projects/erp-construccion/backend/src/modules/bidding/services/opportunity.service.ts b/projects/erp-construccion/backend/src/modules/bidding/services/opportunity.service.ts deleted file mode 100644 index e67f9ffc5..000000000 --- a/projects/erp-construccion/backend/src/modules/bidding/services/opportunity.service.ts +++ /dev/null @@ -1,392 +0,0 @@ -/** - * OpportunityService - Gesti贸n de Oportunidades de Negocio - * - * CRUD y l贸gica de negocio para el pipeline de oportunidades. - * - * @module Bidding - */ - -import { Repository, In, Between } from 'typeorm'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Opportunity, OpportunitySource, OpportunityStatus, OpportunityPriority, ProjectType } from '../entities/opportunity.entity'; - -export interface CreateOpportunityDto { - code: string; - name: string; - description?: string; - source: OpportunitySource; - projectType: ProjectType; - clientName: string; - clientContact?: string; - clientEmail?: string; - clientPhone?: string; - clientType?: string; - location?: string; - state?: string; - city?: string; - estimatedValue?: number; - currency?: string; - constructionAreaM2?: number; - landAreaM2?: number; - identificationDate: Date; - deadlineDate?: Date; - expectedAwardDate?: Date; - expectedStartDate?: Date; - expectedDurationMonths?: number; - winProbability?: number; - requiresBond?: boolean; - requiresExperience?: boolean; - minimumExperienceYears?: number; - minimumCapital?: number; - requiredCertifications?: string[]; - assignedToId?: string; - sourceUrl?: string; - sourceReference?: string; - notes?: string; - metadata?: Record; -} - -export interface UpdateOpportunityDto extends Partial { - status?: OpportunityStatus; - priority?: OpportunityPriority; - lossReason?: string; - winFactors?: string; -} - -export interface OpportunityFilters { - status?: OpportunityStatus | OpportunityStatus[]; - source?: OpportunitySource; - projectType?: ProjectType; - priority?: OpportunityPriority; - assignedToId?: string; - clientName?: string; - state?: string; - dateFrom?: Date; - dateTo?: Date; - minValue?: number; - maxValue?: number; - search?: string; -} - -export class OpportunityService { - constructor(private readonly repository: Repository) {} - - /** - * Crear oportunidad - */ - async create(ctx: ServiceContext, data: CreateOpportunityDto): Promise { - const weightedValue = data.estimatedValue && data.winProbability - ? data.estimatedValue * (data.winProbability / 100) - : undefined; - - const opportunity = this.repository.create({ - tenantId: ctx.tenantId, - code: data.code, - name: data.name, - description: data.description, - source: data.source, - projectType: data.projectType, - clientName: data.clientName, - clientContact: data.clientContact, - clientEmail: data.clientEmail, - clientPhone: data.clientPhone, - clientType: data.clientType, - location: data.location, - state: data.state, - city: data.city, - estimatedValue: data.estimatedValue, - currency: data.currency || 'MXN', - constructionAreaM2: data.constructionAreaM2, - landAreaM2: data.landAreaM2, - identificationDate: data.identificationDate, - deadlineDate: data.deadlineDate, - expectedAwardDate: data.expectedAwardDate, - expectedStartDate: data.expectedStartDate, - expectedDurationMonths: data.expectedDurationMonths, - winProbability: data.winProbability || 0, - requiresBond: data.requiresBond || false, - requiresExperience: data.requiresExperience || false, - minimumExperienceYears: data.minimumExperienceYears, - minimumCapital: data.minimumCapital, - requiredCertifications: data.requiredCertifications, - assignedToId: data.assignedToId, - sourceUrl: data.sourceUrl, - sourceReference: data.sourceReference, - notes: data.notes, - metadata: data.metadata, - status: 'identified', - priority: 'medium', - weightedValue, - createdBy: ctx.userId, - updatedBy: ctx.userId, - }); - - return this.repository.save(opportunity); - } - - /** - * Buscar por ID - */ - async findById(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { id, tenantId: ctx.tenantId, deletedAt: undefined }, - relations: ['assignedTo', 'bids'], - }); - } - - /** - * Buscar con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: OpportunityFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('o') - .leftJoinAndSelect('o.assignedTo', 'u') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL'); - - if (filters.status) { - if (Array.isArray(filters.status)) { - qb.andWhere('o.status IN (:...statuses)', { statuses: filters.status }); - } else { - qb.andWhere('o.status = :status', { status: filters.status }); - } - } - if (filters.source) { - qb.andWhere('o.source = :source', { source: filters.source }); - } - if (filters.projectType) { - qb.andWhere('o.project_type = :projectType', { projectType: filters.projectType }); - } - if (filters.priority) { - qb.andWhere('o.priority = :priority', { priority: filters.priority }); - } - if (filters.assignedToId) { - qb.andWhere('o.assigned_to_id = :assignedToId', { assignedToId: filters.assignedToId }); - } - if (filters.clientName) { - qb.andWhere('o.client_name ILIKE :clientName', { clientName: `%${filters.clientName}%` }); - } - if (filters.state) { - qb.andWhere('o.state = :state', { state: filters.state }); - } - if (filters.dateFrom) { - qb.andWhere('o.identification_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - if (filters.dateTo) { - qb.andWhere('o.identification_date <= :dateTo', { dateTo: filters.dateTo }); - } - if (filters.minValue !== undefined) { - qb.andWhere('o.estimated_value >= :minValue', { minValue: filters.minValue }); - } - if (filters.maxValue !== undefined) { - qb.andWhere('o.estimated_value <= :maxValue', { maxValue: filters.maxValue }); - } - if (filters.search) { - qb.andWhere( - '(o.name ILIKE :search OR o.code ILIKE :search OR o.client_name ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - const skip = (page - 1) * limit; - qb.orderBy('o.created_at', 'DESC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Actualizar oportunidad - */ - async update(ctx: ServiceContext, id: string, data: UpdateOpportunityDto): Promise { - const opportunity = await this.findById(ctx, id); - if (!opportunity) return null; - - // Recalcular weighted value si cambian los factores - let weightedValue = opportunity.weightedValue; - const estimatedValue = data.estimatedValue ?? opportunity.estimatedValue; - const winProbability = data.winProbability ?? opportunity.winProbability; - if (estimatedValue && winProbability) { - weightedValue = estimatedValue * (winProbability / 100); - } - - Object.assign(opportunity, { - ...data, - weightedValue, - updatedBy: ctx.userId, - }); - - return this.repository.save(opportunity); - } - - /** - * Cambiar estado - */ - async changeStatus( - ctx: ServiceContext, - id: string, - status: OpportunityStatus, - reason?: string - ): Promise { - const opportunity = await this.findById(ctx, id); - if (!opportunity) return null; - - opportunity.status = status; - if (status === 'lost' && reason) { - opportunity.lossReason = reason; - } else if (status === 'won' && reason) { - opportunity.winFactors = reason; - } - opportunity.updatedBy = ctx.userId; - - return this.repository.save(opportunity); - } - - /** - * Obtener pipeline (agrupado por status) - */ - async getPipeline(ctx: ServiceContext): Promise { - const result = await this.repository - .createQueryBuilder('o') - .select('o.status', 'status') - .addSelect('COUNT(*)', 'count') - .addSelect('SUM(o.estimated_value)', 'totalValue') - .addSelect('SUM(o.weighted_value)', 'weightedValue') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .groupBy('o.status') - .getRawMany(); - - return result.map((r) => ({ - status: r.status as OpportunityStatus, - count: parseInt(r.count), - totalValue: parseFloat(r.totalValue) || 0, - weightedValue: parseFloat(r.weightedValue) || 0, - })); - } - - /** - * Obtener oportunidades pr贸ximas a vencer - */ - async getUpcomingDeadlines(ctx: ServiceContext, days = 7): Promise { - const now = new Date(); - const future = new Date(); - future.setDate(future.getDate() + days); - - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: undefined, - status: In(['identified', 'qualified', 'pursuing']), - deadlineDate: Between(now, future), - }, - relations: ['assignedTo'], - order: { deadlineDate: 'ASC' }, - }); - } - - /** - * Obtener estad铆sticas - */ - async getStats(ctx: ServiceContext, year?: number): Promise { - const currentYear = year || new Date().getFullYear(); - const startDate = new Date(currentYear, 0, 1); - const endDate = new Date(currentYear, 11, 31); - - const baseQuery = this.repository - .createQueryBuilder('o') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.identification_date BETWEEN :startDate AND :endDate', { startDate, endDate }); - - const total = await baseQuery.getCount(); - - const byStatus = await this.repository - .createQueryBuilder('o') - .select('o.status', 'status') - .addSelect('COUNT(*)', 'count') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.identification_date BETWEEN :startDate AND :endDate', { startDate, endDate }) - .groupBy('o.status') - .getRawMany(); - - const bySource = await this.repository - .createQueryBuilder('o') - .select('o.source', 'source') - .addSelect('COUNT(*)', 'count') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.identification_date BETWEEN :startDate AND :endDate', { startDate, endDate }) - .groupBy('o.source') - .getRawMany(); - - const valueStats = await this.repository - .createQueryBuilder('o') - .select('SUM(o.estimated_value)', 'totalValue') - .addSelect('SUM(o.weighted_value)', 'weightedValue') - .addSelect('AVG(o.estimated_value)', 'avgValue') - .where('o.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('o.deleted_at IS NULL') - .andWhere('o.identification_date BETWEEN :startDate AND :endDate', { startDate, endDate }) - .getRawOne(); - - const wonCount = byStatus.find((s) => s.status === 'won')?.count || 0; - const lostCount = byStatus.find((s) => s.status === 'lost')?.count || 0; - const closedCount = parseInt(wonCount) + parseInt(lostCount); - - return { - year: currentYear, - total, - byStatus: byStatus.map((r) => ({ status: r.status, count: parseInt(r.count) })), - bySource: bySource.map((r) => ({ source: r.source, count: parseInt(r.count) })), - totalValue: parseFloat(valueStats?.totalValue) || 0, - weightedValue: parseFloat(valueStats?.weightedValue) || 0, - avgValue: parseFloat(valueStats?.avgValue) || 0, - winRate: closedCount > 0 ? (parseInt(wonCount) / closedCount) * 100 : 0, - }; - } - - /** - * Soft delete - */ - async softDelete(ctx: ServiceContext, id: string): Promise { - const result = await this.repository.update( - { id, tenantId: ctx.tenantId }, - { deletedAt: new Date(), updatedBy: ctx.userId } - ); - return (result.affected || 0) > 0; - } -} - -export interface PipelineData { - status: OpportunityStatus; - count: number; - totalValue: number; - weightedValue: number; -} - -export interface OpportunityStats { - year: number; - total: number; - byStatus: { status: OpportunityStatus; count: number }[]; - bySource: { source: OpportunitySource; count: number }[]; - totalValue: number; - weightedValue: number; - avgValue: number; - winRate: number; -} diff --git a/projects/erp-construccion/backend/src/modules/budgets/controllers/concepto.controller.ts b/projects/erp-construccion/backend/src/modules/budgets/controllers/concepto.controller.ts deleted file mode 100644 index bbd80e90d..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/controllers/concepto.controller.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * ConceptoController - Controller de conceptos de obra - * - * Endpoints REST para gesti贸n del cat谩logo de conceptos. - * - * @module Budgets - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { ConceptoService, CreateConceptoDto, UpdateConceptoDto } from '../services/concepto.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Concepto } from '../entities/concepto.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de conceptos - */ -export function createConceptoController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const conceptoRepository = dataSource.getRepository(Concepto); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const conceptoService = new ConceptoService(conceptoRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /conceptos - * Listar conceptos ra铆z - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 50, 100); - - const result = await conceptoService.findRootConceptos(getContext(req), page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /conceptos/search - * Buscar conceptos por c贸digo o nombre - */ - router.get('/search', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const term = req.query.q as string; - if (!term || term.length < 2) { - res.status(400).json({ error: 'Bad Request', message: 'Search term must be at least 2 characters' }); - return; - } - - const limit = Math.min(parseInt(req.query.limit as string) || 20, 50); - const conceptos = await conceptoService.search(getContext(req), term, limit); - - res.status(200).json({ success: true, data: conceptos }); - } catch (error) { - next(error); - } - }); - - /** - * GET /conceptos/tree - * Obtener 谩rbol de conceptos - */ - router.get('/tree', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const rootId = req.query.rootId as string; - const tree = await conceptoService.getConceptoTree(getContext(req), rootId); - - res.status(200).json({ success: true, data: tree }); - } catch (error) { - next(error); - } - }); - - /** - * GET /conceptos/:id - * Obtener concepto por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const concepto = await conceptoService.findById(getContext(req), req.params.id); - if (!concepto) { - res.status(404).json({ error: 'Not Found', message: 'Concept not found' }); - return; - } - - res.status(200).json({ success: true, data: concepto }); - } catch (error) { - next(error); - } - }); - - /** - * GET /conceptos/:id/children - * Obtener hijos de un concepto - */ - router.get('/:id/children', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const children = await conceptoService.findChildren(getContext(req), req.params.id); - res.status(200).json({ success: true, data: children }); - } catch (error) { - next(error); - } - }); - - /** - * POST /conceptos - * Crear concepto - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateConceptoDto = req.body; - - if (!dto.code || !dto.name) { - res.status(400).json({ error: 'Bad Request', message: 'code and name are required' }); - return; - } - - // Verificar c贸digo 煤nico - const exists = await conceptoService.codeExists(getContext(req), dto.code); - if (exists) { - res.status(409).json({ error: 'Conflict', message: 'Concept code already exists' }); - return; - } - - const concepto = await conceptoService.createConcepto(getContext(req), dto); - res.status(201).json({ success: true, data: concepto }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /conceptos/:id - * Actualizar concepto - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateConceptoDto = req.body; - const concepto = await conceptoService.update(getContext(req), req.params.id, dto); - - if (!concepto) { - res.status(404).json({ error: 'Not Found', message: 'Concept not found' }); - return; - } - - res.status(200).json({ success: true, data: concepto }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /conceptos/:id - * Eliminar concepto (soft delete) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await conceptoService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Concept not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Concept deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createConceptoController; diff --git a/projects/erp-construccion/backend/src/modules/budgets/controllers/index.ts b/projects/erp-construccion/backend/src/modules/budgets/controllers/index.ts deleted file mode 100644 index 1e73b394a..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/controllers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Budgets Controllers Index - * @module Budgets - */ - -export { createConceptoController } from './concepto.controller'; -export { createPresupuestoController } from './presupuesto.controller'; diff --git a/projects/erp-construccion/backend/src/modules/budgets/controllers/presupuesto.controller.ts b/projects/erp-construccion/backend/src/modules/budgets/controllers/presupuesto.controller.ts deleted file mode 100644 index d56c1a065..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/controllers/presupuesto.controller.ts +++ /dev/null @@ -1,287 +0,0 @@ -/** - * PresupuestoController - Controller de presupuestos - * - * Endpoints REST para gesti贸n de presupuestos de obra. - * - * @module Budgets - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { PresupuestoService, CreatePresupuestoDto, AddPartidaDto, UpdatePartidaDto } from '../services/presupuesto.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Presupuesto } from '../entities/presupuesto.entity'; -import { PresupuestoPartida } from '../entities/presupuesto-partida.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de presupuestos - */ -export function createPresupuestoController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const presupuestoRepository = dataSource.getRepository(Presupuesto); - const partidaRepository = dataSource.getRepository(PresupuestoPartida); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const presupuestoService = new PresupuestoService(presupuestoRepository, partidaRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /presupuestos - * Listar presupuestos - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - const fraccionamientoId = req.query.fraccionamientoId as string; - - let result; - if (fraccionamientoId) { - result = await presupuestoService.findByFraccionamiento(getContext(req), fraccionamientoId, page, limit); - } else { - result = await presupuestoService.findAll(getContext(req), { page, limit }); - } - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /presupuestos/:id - * Obtener presupuesto por ID con sus partidas - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const presupuesto = await presupuestoService.findWithPartidas(getContext(req), req.params.id); - if (!presupuesto) { - res.status(404).json({ error: 'Not Found', message: 'Budget not found' }); - return; - } - - res.status(200).json({ success: true, data: presupuesto }); - } catch (error) { - next(error); - } - }); - - /** - * POST /presupuestos - * Crear presupuesto - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreatePresupuestoDto = req.body; - - if (!dto.code || !dto.name) { - res.status(400).json({ error: 'Bad Request', message: 'code and name are required' }); - return; - } - - const presupuesto = await presupuestoService.createPresupuesto(getContext(req), dto); - res.status(201).json({ success: true, data: presupuesto }); - } catch (error) { - next(error); - } - }); - - /** - * POST /presupuestos/:id/partidas - * Agregar partida al presupuesto - */ - router.post('/:id/partidas', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: AddPartidaDto = req.body; - - if (!dto.conceptoId || dto.quantity === undefined || dto.unitPrice === undefined) { - res.status(400).json({ error: 'Bad Request', message: 'conceptoId, quantity and unitPrice are required' }); - return; - } - - const partida = await presupuestoService.addPartida(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: partida }); - } catch (error) { - if (error instanceof Error && error.message === 'Presupuesto not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /presupuestos/:id/partidas/:partidaId - * Actualizar partida - */ - router.patch('/:id/partidas/:partidaId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdatePartidaDto = req.body; - const partida = await presupuestoService.updatePartida(getContext(req), req.params.partidaId, dto); - - if (!partida) { - res.status(404).json({ error: 'Not Found', message: 'Budget item not found' }); - return; - } - - res.status(200).json({ success: true, data: partida }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /presupuestos/:id/partidas/:partidaId - * Eliminar partida - */ - router.delete('/:id/partidas/:partidaId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await presupuestoService.removePartida(getContext(req), req.params.partidaId); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Budget item not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Budget item deleted' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /presupuestos/:id/version - * Crear nueva versi贸n del presupuesto - */ - router.post('/:id/version', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const newVersion = await presupuestoService.createNewVersion(getContext(req), req.params.id); - res.status(201).json({ success: true, data: newVersion }); - } catch (error) { - if (error instanceof Error && error.message === 'Presupuesto not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /presupuestos/:id/approve - * Aprobar presupuesto - */ - router.post('/:id/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const presupuesto = await presupuestoService.approve(getContext(req), req.params.id); - if (!presupuesto) { - res.status(404).json({ error: 'Not Found', message: 'Budget not found' }); - return; - } - - res.status(200).json({ success: true, data: presupuesto, message: 'Budget approved' }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /presupuestos/:id - * Eliminar presupuesto (soft delete) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await presupuestoService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Budget not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Budget deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createPresupuestoController; diff --git a/projects/erp-construccion/backend/src/modules/budgets/entities/concepto.entity.ts b/projects/erp-construccion/backend/src/modules/budgets/entities/concepto.entity.ts deleted file mode 100644 index da83fc8a9..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/entities/concepto.entity.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Concepto Entity - * Catalogo de conceptos de obra (estructura jerarquica) - * - * @module Budgets - * @table construction.conceptos - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -@Entity({ schema: 'construction', name: 'conceptos' }) -@Index(['tenantId', 'code'], { unique: true }) -@Index(['tenantId']) -@Index(['parentId']) -@Index(['code']) -export class Concepto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'parent_id', type: 'uuid', nullable: true }) - parentId: string | null; - - @Column({ type: 'varchar', length: 50 }) - code: string; - - @Column({ type: 'varchar', length: 255 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string | null; - - @Column({ name: 'unit_id', type: 'uuid', nullable: true }) - unitId: string | null; - - @Column({ name: 'unit_price', type: 'decimal', precision: 12, scale: 4, nullable: true }) - unitPrice: number | null; - - @Column({ name: 'is_composite', type: 'boolean', default: false }) - isComposite: boolean; - - @Column({ type: 'integer', default: 0 }) - level: number; - - @Column({ type: 'varchar', length: 500, nullable: true }) - path: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Concepto, (c) => c.children, { nullable: true }) - @JoinColumn({ name: 'parent_id' }) - parent: Concepto | null; - - @OneToMany(() => Concepto, (c) => c.parent) - children: Concepto[]; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'updated_by' }) - updatedBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/budgets/entities/index.ts b/projects/erp-construccion/backend/src/modules/budgets/entities/index.ts deleted file mode 100644 index 94a23afe3..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/entities/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Budgets Module - Entity Exports - * MAI-003: Presupuestos - */ - -export * from './concepto.entity'; -export * from './presupuesto.entity'; -export * from './presupuesto-partida.entity'; diff --git a/projects/erp-construccion/backend/src/modules/budgets/entities/presupuesto-partida.entity.ts b/projects/erp-construccion/backend/src/modules/budgets/entities/presupuesto-partida.entity.ts deleted file mode 100644 index aa926fba4..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/entities/presupuesto-partida.entity.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * PresupuestoPartida Entity - * Lineas/partidas de un presupuesto - * - * @module Budgets - * @table construction.presupuesto_partidas - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Presupuesto } from './presupuesto.entity'; -import { Concepto } from './concepto.entity'; - -@Entity({ schema: 'construction', name: 'presupuesto_partidas' }) -@Index(['presupuestoId', 'conceptoId'], { unique: true }) -@Index(['tenantId']) -export class PresupuestoPartida { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'presupuesto_id', type: 'uuid' }) - presupuestoId: string; - - @Column({ name: 'concepto_id', type: 'uuid' }) - conceptoId: string; - - @Column({ type: 'integer', default: 0 }) - sequence: number; - - @Column({ type: 'decimal', precision: 12, scale: 4, default: 0 }) - quantity: number; - - @Column({ name: 'unit_price', type: 'decimal', precision: 12, scale: 4, default: 0 }) - unitPrice: number; - - // Columna calculada (GENERATED ALWAYS AS) - solo lectura - @Column({ - name: 'total_amount', - type: 'decimal', - precision: 14, - scale: 2, - insert: false, - update: false, - }) - totalAmount: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Presupuesto, (p) => p.partidas) - @JoinColumn({ name: 'presupuesto_id' }) - presupuesto: Presupuesto; - - @ManyToOne(() => Concepto) - @JoinColumn({ name: 'concepto_id' }) - concepto: Concepto; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/budgets/entities/presupuesto.entity.ts b/projects/erp-construccion/backend/src/modules/budgets/entities/presupuesto.entity.ts deleted file mode 100644 index a4da148af..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/entities/presupuesto.entity.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Presupuesto Entity - * Presupuestos de obra por prototipo o fraccionamiento - * - * @module Budgets - * @table construction.presupuestos - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { PresupuestoPartida } from './presupuesto-partida.entity'; - -@Entity({ schema: 'construction', name: 'presupuestos' }) -@Index(['tenantId', 'code', 'version'], { unique: true }) -@Index(['tenantId']) -@Index(['fraccionamientoId']) -export class Presupuesto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid', nullable: true }) - fraccionamientoId: string | null; - - @Column({ name: 'prototipo_id', type: 'uuid', nullable: true }) - prototipoId: string | null; - - @Column({ type: 'varchar', length: 30 }) - code: string; - - @Column({ type: 'varchar', length: 255 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string | null; - - @Column({ type: 'integer', default: 1 }) - version: number; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @Column({ name: 'total_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - totalAmount: number; - - @Column({ name: 'currency_id', type: 'uuid', nullable: true }) - currencyId: string | null; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date | null; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento, { nullable: true }) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User | null; - - @OneToMany(() => PresupuestoPartida, (p) => p.presupuesto) - partidas: PresupuestoPartida[]; -} diff --git a/projects/erp-construccion/backend/src/modules/budgets/services/concepto.service.ts b/projects/erp-construccion/backend/src/modules/budgets/services/concepto.service.ts deleted file mode 100644 index 0108de494..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/services/concepto.service.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * ConceptoService - Catalogo de Conceptos de Obra - * - * Gestiona el cat谩logo jer谩rquico de conceptos de obra. - * Los conceptos pueden tener estructura padre-hijo (niveles). - * - * @module Budgets - */ - -import { Repository, IsNull } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Concepto } from '../entities/concepto.entity'; - -export interface CreateConceptoDto { - code: string; - name: string; - description?: string; - parentId?: string; - unitId?: string; - unitPrice?: number; - isComposite?: boolean; -} - -export interface UpdateConceptoDto { - name?: string; - description?: string; - unitId?: string; - unitPrice?: number; - isComposite?: boolean; -} - -export class ConceptoService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Crear un nuevo concepto con c谩lculo autom谩tico de nivel y path - */ - async createConcepto( - ctx: ServiceContext, - data: CreateConceptoDto - ): Promise { - let level = 0; - let path = data.code; - - if (data.parentId) { - const parent = await this.findById(ctx, data.parentId); - if (parent) { - level = parent.level + 1; - path = `${parent.path}/${data.code}`; - } - } - - return this.create(ctx, { - ...data, - level, - path, - }); - } - - /** - * Obtener conceptos ra铆z (sin padre) - */ - async findRootConceptos( - ctx: ServiceContext, - page = 1, - limit = 50 - ): Promise> { - return this.findAll(ctx, { - page, - limit, - where: { parentId: IsNull() } as any, - }); - } - - /** - * Obtener hijos de un concepto - */ - async findChildren( - ctx: ServiceContext, - parentId: string - ): Promise { - return this.find(ctx, { - where: { parentId } as any, - order: { code: 'ASC' }, - }); - } - - /** - * Obtener 谩rbol completo de conceptos - */ - async getConceptoTree( - ctx: ServiceContext, - rootId?: string - ): Promise { - const where = rootId - ? { parentId: rootId } - : { parentId: IsNull() }; - - const roots = await this.find(ctx, { - where: where as any, - order: { code: 'ASC' }, - }); - - return this.buildTree(ctx, roots); - } - - private async buildTree( - ctx: ServiceContext, - conceptos: Concepto[] - ): Promise { - const tree: ConceptoNode[] = []; - - for (const concepto of conceptos) { - const children = await this.findChildren(ctx, concepto.id); - const childNodes = children.length > 0 - ? await this.buildTree(ctx, children) - : []; - - tree.push({ - ...concepto, - children: childNodes, - }); - } - - return tree; - } - - /** - * Buscar conceptos por c贸digo o nombre - */ - async search( - ctx: ServiceContext, - term: string, - limit = 20 - ): Promise { - return this.repository - .createQueryBuilder('c') - .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('c.deleted_at IS NULL') - .andWhere('(c.code ILIKE :term OR c.name ILIKE :term)', { - term: `%${term}%`, - }) - .orderBy('c.code', 'ASC') - .take(limit) - .getMany(); - } - - /** - * Verificar si un c贸digo ya existe - */ - async codeExists(ctx: ServiceContext, code: string): Promise { - return this.exists(ctx, { code } as any); - } -} - -interface ConceptoNode extends Concepto { - children: ConceptoNode[]; -} diff --git a/projects/erp-construccion/backend/src/modules/budgets/services/index.ts b/projects/erp-construccion/backend/src/modules/budgets/services/index.ts deleted file mode 100644 index f7e3fe4c7..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/services/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Budgets Module - Service Exports - */ - -export * from './concepto.service'; -export * from './presupuesto.service'; diff --git a/projects/erp-construccion/backend/src/modules/budgets/services/presupuesto.service.ts b/projects/erp-construccion/backend/src/modules/budgets/services/presupuesto.service.ts deleted file mode 100644 index d3879be88..000000000 --- a/projects/erp-construccion/backend/src/modules/budgets/services/presupuesto.service.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * PresupuestoService - Gesti贸n de Presupuestos de Obra - * - * Gestiona presupuestos de obra con sus partidas. - * Soporta versionamiento y aprobaci贸n. - * - * @module Budgets - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Presupuesto } from '../entities/presupuesto.entity'; -import { PresupuestoPartida } from '../entities/presupuesto-partida.entity'; - -export interface CreatePresupuestoDto { - code: string; - name: string; - description?: string; - fraccionamientoId?: string; - prototipoId?: string; - currencyId?: string; -} - -export interface AddPartidaDto { - conceptoId: string; - quantity: number; - unitPrice: number; - sequence?: number; -} - -export interface UpdatePartidaDto { - quantity?: number; - unitPrice?: number; - sequence?: number; -} - -export class PresupuestoService extends BaseService { - constructor( - repository: Repository, - private readonly partidaRepository: Repository - ) { - super(repository); - } - - /** - * Crear nuevo presupuesto - */ - async createPresupuesto( - ctx: ServiceContext, - data: CreatePresupuestoDto - ): Promise { - return this.create(ctx, { - ...data, - version: 1, - isActive: true, - totalAmount: 0, - }); - } - - /** - * Obtener presupuestos por fraccionamiento - */ - async findByFraccionamiento( - ctx: ServiceContext, - fraccionamientoId: string, - page = 1, - limit = 20 - ): Promise> { - return this.findAll(ctx, { - page, - limit, - where: { fraccionamientoId, isActive: true } as any, - }); - } - - /** - * Obtener presupuesto con sus partidas - */ - async findWithPartidas( - ctx: ServiceContext, - id: string - ): Promise { - return this.repository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - relations: ['partidas', 'partidas.concepto'], - }); - } - - /** - * Agregar partida al presupuesto - */ - async addPartida( - ctx: ServiceContext, - presupuestoId: string, - data: AddPartidaDto - ): Promise { - const presupuesto = await this.findById(ctx, presupuestoId); - if (!presupuesto) { - throw new Error('Presupuesto not found'); - } - - const partida = this.partidaRepository.create({ - tenantId: ctx.tenantId, - presupuestoId, - conceptoId: data.conceptoId, - quantity: data.quantity, - unitPrice: data.unitPrice, - sequence: data.sequence || 0, - createdById: ctx.userId, - }); - - const savedPartida = await this.partidaRepository.save(partida); - await this.recalculateTotal(ctx, presupuestoId); - - return savedPartida; - } - - /** - * Actualizar partida - */ - async updatePartida( - ctx: ServiceContext, - partidaId: string, - data: UpdatePartidaDto - ): Promise { - const partida = await this.partidaRepository.findOne({ - where: { - id: partidaId, - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - }); - - if (!partida) { - return null; - } - - const updated = this.partidaRepository.merge(partida, { - ...data, - updatedById: ctx.userId, - }); - - const saved = await this.partidaRepository.save(updated); - await this.recalculateTotal(ctx, partida.presupuestoId); - - return saved; - } - - /** - * Eliminar partida - */ - async removePartida(ctx: ServiceContext, partidaId: string): Promise { - const partida = await this.partidaRepository.findOne({ - where: { - id: partidaId, - tenantId: ctx.tenantId, - } as any, - }); - - if (!partida) { - return false; - } - - await this.partidaRepository.update( - { id: partidaId }, - { - deletedAt: new Date(), - deletedById: ctx.userId, - } - ); - - await this.recalculateTotal(ctx, partida.presupuestoId); - return true; - } - - /** - * Recalcular total del presupuesto - */ - async recalculateTotal(ctx: ServiceContext, presupuestoId: string): Promise { - const result = await this.partidaRepository - .createQueryBuilder('p') - .select('SUM(p.quantity * p.unit_price)', 'total') - .where('p.presupuesto_id = :presupuestoId', { presupuestoId }) - .andWhere('p.deleted_at IS NULL') - .getRawOne(); - - const total = parseFloat(result?.total || '0'); - - await this.repository.update( - { id: presupuestoId }, - { totalAmount: total, updatedById: ctx.userId } - ); - } - - /** - * Crear nueva versi贸n del presupuesto - */ - async createNewVersion( - ctx: ServiceContext, - presupuestoId: string - ): Promise { - const original = await this.findWithPartidas(ctx, presupuestoId); - if (!original) { - throw new Error('Presupuesto not found'); - } - - // Desactivar versi贸n anterior - await this.repository.update( - { id: presupuestoId }, - { isActive: false, updatedById: ctx.userId } - ); - - // Crear nueva versi贸n - const newVersion = await this.create(ctx, { - code: original.code, - name: original.name, - description: original.description, - fraccionamientoId: original.fraccionamientoId, - prototipoId: original.prototipoId, - currencyId: original.currencyId, - version: original.version + 1, - isActive: true, - totalAmount: original.totalAmount, - }); - - // Copiar partidas - for (const partida of original.partidas) { - await this.partidaRepository.save( - this.partidaRepository.create({ - tenantId: ctx.tenantId, - presupuestoId: newVersion.id, - conceptoId: partida.conceptoId, - quantity: partida.quantity, - unitPrice: partida.unitPrice, - sequence: partida.sequence, - createdById: ctx.userId, - }) - ); - } - - return newVersion; - } - - /** - * Aprobar presupuesto - */ - async approve(ctx: ServiceContext, presupuestoId: string): Promise { - const presupuesto = await this.findById(ctx, presupuestoId); - if (!presupuesto) { - return null; - } - - return this.update(ctx, presupuestoId, { - approvedAt: new Date(), - approvedById: ctx.userId, - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/construction/controllers/etapa.controller.ts b/projects/erp-construccion/backend/src/modules/construction/controllers/etapa.controller.ts deleted file mode 100644 index b8a812573..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/controllers/etapa.controller.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * EtapaController - Controller de etapas - * - * Endpoints REST para gesti贸n de etapas de fraccionamientos. - * - * @module Construction - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { EtapaService, CreateEtapaDto, UpdateEtapaDto } from '../services/etapa.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Etapa } from '../entities/etapa.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; - -/** - * Crear router de etapas - */ -export function createEtapaController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const etapaRepository = dataSource.getRepository(Etapa); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const etapaService = new EtapaService(etapaRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - /** - * GET /etapas - * Listar etapas - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - const search = req.query.search as string; - const status = req.query.status as string; - const fraccionamientoId = req.query.fraccionamientoId as string; - - const result = await etapaService.findAll({ tenantId, page, limit, search, status, fraccionamientoId }); - - res.status(200).json({ - success: true, - data: result.items, - pagination: { - page, - limit, - total: result.total, - totalPages: Math.ceil(result.total / limit), - }, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /etapas/:id - * Obtener etapa por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const etapa = await etapaService.findById(req.params.id, tenantId); - if (!etapa) { - res.status(404).json({ error: 'Not Found', message: 'Stage not found' }); - return; - } - - res.status(200).json({ success: true, data: etapa }); - } catch (error) { - next(error); - } - }); - - /** - * POST /etapas - * Crear etapa - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateEtapaDto = req.body; - - if (!dto.fraccionamientoId || !dto.code || !dto.name) { - res.status(400).json({ error: 'Bad Request', message: 'fraccionamientoId, code and name are required' }); - return; - } - - const etapa = await etapaService.create(tenantId, dto, req.user?.sub); - res.status(201).json({ success: true, data: etapa }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /etapas/:id - * Actualizar etapa - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateEtapaDto = req.body; - const etapa = await etapaService.update(req.params.id, tenantId, dto, req.user?.sub); - res.status(200).json({ success: true, data: etapa }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Stage not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * DELETE /etapas/:id - * Eliminar etapa - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - await etapaService.delete(req.params.id, tenantId, req.user?.sub); - res.status(200).json({ success: true, message: 'Stage deleted' }); - } catch (error) { - if (error instanceof Error && error.message === 'Stage not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - return router; -} - -export default createEtapaController; diff --git a/projects/erp-construccion/backend/src/modules/construction/controllers/fraccionamiento.controller.ts b/projects/erp-construccion/backend/src/modules/construction/controllers/fraccionamiento.controller.ts deleted file mode 100644 index adb73a4f9..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/controllers/fraccionamiento.controller.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Fraccionamiento Controller - * API endpoints para gesti贸n de fraccionamientos/obras - * - * @module Construction - * @prefix /api/v1/fraccionamientos - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { - FraccionamientoService, - CreateFraccionamientoDto, - UpdateFraccionamientoDto -} from '../services/fraccionamiento.service'; - -const router = Router(); -const fraccionamientoService = new FraccionamientoService(); - -/** - * GET /api/v1/fraccionamientos - * Lista todos los fraccionamientos del tenant - */ -router.get('/', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const { proyectoId, estado } = req.query; - - const fraccionamientos = await fraccionamientoService.findAll({ - tenantId, - proyectoId: proyectoId as string, - estado: estado as any, - }); - - return res.json({ - success: true, - data: fraccionamientos, - count: fraccionamientos.length, - }); - } catch (error) { - return next(error); - } -}); - -/** - * GET /api/v1/fraccionamientos/:id - * Obtiene un fraccionamiento por ID - */ -router.get('/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const fraccionamiento = await fraccionamientoService.findById(req.params.id, tenantId); - if (!fraccionamiento) { - return res.status(404).json({ error: 'Fraccionamiento no encontrado' }); - } - - return res.json({ success: true, data: fraccionamiento }); - } catch (error) { - return next(error); - } -}); - -/** - * POST /api/v1/fraccionamientos - * Crea un nuevo fraccionamiento - */ -router.post('/', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const data: CreateFraccionamientoDto = { - ...req.body, - tenantId, - createdById: (req as any).user?.id, - }; - - // Validate required fields - if (!data.codigo || !data.nombre || !data.proyectoId) { - return res.status(400).json({ - error: 'codigo, nombre y proyectoId son requeridos' - }); - } - - // Check if codigo already exists - const existing = await fraccionamientoService.findByCodigo(data.codigo, tenantId); - if (existing) { - return res.status(409).json({ error: 'Ya existe un fraccionamiento con ese c贸digo' }); - } - - const fraccionamiento = await fraccionamientoService.create(data); - return res.status(201).json({ success: true, data: fraccionamiento }); - } catch (error) { - return next(error); - } -}); - -/** - * PATCH /api/v1/fraccionamientos/:id - * Actualiza un fraccionamiento - */ -router.patch('/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const data: UpdateFraccionamientoDto = req.body; - const fraccionamiento = await fraccionamientoService.update( - req.params.id, - tenantId, - data - ); - - if (!fraccionamiento) { - return res.status(404).json({ error: 'Fraccionamiento no encontrado' }); - } - - return res.json({ success: true, data: fraccionamiento }); - } catch (error) { - return next(error); - } -}); - -/** - * DELETE /api/v1/fraccionamientos/:id - * Elimina un fraccionamiento - */ -router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const deleted = await fraccionamientoService.delete(req.params.id, tenantId); - if (!deleted) { - return res.status(404).json({ error: 'Fraccionamiento no encontrado' }); - } - - return res.json({ success: true, message: 'Fraccionamiento eliminado' }); - } catch (error) { - return next(error); - } -}); - -export default router; diff --git a/projects/erp-construccion/backend/src/modules/construction/controllers/index.ts b/projects/erp-construccion/backend/src/modules/construction/controllers/index.ts deleted file mode 100644 index 624f31833..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/controllers/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Construction Controllers Index - * @module Construction - */ - -export { default as proyectoController } from './proyecto.controller'; -export { default as fraccionamientoController } from './fraccionamiento.controller'; -export { createEtapaController } from './etapa.controller'; -export { createManzanaController } from './manzana.controller'; -export { createLoteController } from './lote.controller'; -export { createPrototipoController } from './prototipo.controller'; diff --git a/projects/erp-construccion/backend/src/modules/construction/controllers/lote.controller.ts b/projects/erp-construccion/backend/src/modules/construction/controllers/lote.controller.ts deleted file mode 100644 index 2749045bf..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/controllers/lote.controller.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * LoteController - Controller de lotes - * - * Endpoints REST para gesti贸n de lotes/terrenos. - * - * @module Construction - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { LoteService, CreateLoteDto, UpdateLoteDto } from '../services/lote.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Lote } from '../entities/lote.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; - -/** - * Crear router de lotes - */ -export function createLoteController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const loteRepository = dataSource.getRepository(Lote); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const loteService = new LoteService(loteRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - /** - * GET /lotes - * Listar lotes - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - const search = req.query.search as string; - const status = req.query.status as string; - const manzanaId = req.query.manzanaId as string; - const prototipoId = req.query.prototipoId as string; - - const result = await loteService.findAll({ tenantId, page, limit, search, status, manzanaId, prototipoId }); - - res.status(200).json({ - success: true, - data: result.items, - pagination: { - page, - limit, - total: result.total, - totalPages: Math.ceil(result.total / limit), - }, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /lotes/stats - * Estad铆sticas de lotes por estado - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const manzanaId = req.query.manzanaId as string; - const stats = await loteService.getStatsByStatus(tenantId, manzanaId); - - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /lotes/:id - * Obtener lote por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const lote = await loteService.findById(req.params.id, tenantId); - if (!lote) { - res.status(404).json({ error: 'Not Found', message: 'Lot not found' }); - return; - } - - res.status(200).json({ success: true, data: lote }); - } catch (error) { - next(error); - } - }); - - /** - * POST /lotes - * Crear lote - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateLoteDto = req.body; - - if (!dto.manzanaId || !dto.code) { - res.status(400).json({ error: 'Bad Request', message: 'manzanaId and code are required' }); - return; - } - - const lote = await loteService.create(tenantId, dto, req.user?.sub); - res.status(201).json({ success: true, data: lote }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /lotes/:id - * Actualizar lote - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateLoteDto = req.body; - const lote = await loteService.update(req.params.id, tenantId, dto, req.user?.sub); - res.status(200).json({ success: true, data: lote }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Lot not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * PATCH /lotes/:id/prototipo - * Asignar prototipo a lote - */ - router.patch('/:id/prototipo', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { prototipoId } = req.body; - if (!prototipoId) { - res.status(400).json({ error: 'Bad Request', message: 'prototipoId is required' }); - return; - } - - const lote = await loteService.assignPrototipo(req.params.id, tenantId, prototipoId, req.user?.sub); - res.status(200).json({ success: true, data: lote }); - } catch (error) { - if (error instanceof Error && error.message === 'Lot not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /lotes/:id/status - * Cambiar estado del lote - */ - router.patch('/:id/status', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'finance'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { status } = req.body; - if (!status) { - res.status(400).json({ error: 'Bad Request', message: 'status is required' }); - return; - } - - const validStatuses = ['available', 'reserved', 'sold', 'blocked', 'in_construction']; - if (!validStatuses.includes(status)) { - res.status(400).json({ error: 'Bad Request', message: `Invalid status. Must be one of: ${validStatuses.join(', ')}` }); - return; - } - - const lote = await loteService.changeStatus(req.params.id, tenantId, status, req.user?.sub); - res.status(200).json({ success: true, data: lote }); - } catch (error) { - if (error instanceof Error && error.message === 'Lot not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * DELETE /lotes/:id - * Eliminar lote - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - await loteService.delete(req.params.id, tenantId, req.user?.sub); - res.status(200).json({ success: true, message: 'Lot deleted' }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Lot not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message === 'Cannot delete a sold lot') { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - return router; -} - -export default createLoteController; diff --git a/projects/erp-construccion/backend/src/modules/construction/controllers/manzana.controller.ts b/projects/erp-construccion/backend/src/modules/construction/controllers/manzana.controller.ts deleted file mode 100644 index c5287e3da..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/controllers/manzana.controller.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * ManzanaController - Controller de manzanas - * - * Endpoints REST para gesti贸n de manzanas (bloques). - * - * @module Construction - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { ManzanaService, CreateManzanaDto, UpdateManzanaDto } from '../services/manzana.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Manzana } from '../entities/manzana.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; - -/** - * Crear router de manzanas - */ -export function createManzanaController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const manzanaRepository = dataSource.getRepository(Manzana); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const manzanaService = new ManzanaService(manzanaRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - /** - * GET /manzanas - * Listar manzanas - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - const search = req.query.search as string; - const etapaId = req.query.etapaId as string; - - const result = await manzanaService.findAll({ tenantId, page, limit, search, etapaId }); - - res.status(200).json({ - success: true, - data: result.items, - pagination: { - page, - limit, - total: result.total, - totalPages: Math.ceil(result.total / limit), - }, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /manzanas/:id - * Obtener manzana por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const manzana = await manzanaService.findById(req.params.id, tenantId); - if (!manzana) { - res.status(404).json({ error: 'Not Found', message: 'Block not found' }); - return; - } - - res.status(200).json({ success: true, data: manzana }); - } catch (error) { - next(error); - } - }); - - /** - * POST /manzanas - * Crear manzana - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateManzanaDto = req.body; - - if (!dto.etapaId || !dto.code) { - res.status(400).json({ error: 'Bad Request', message: 'etapaId and code are required' }); - return; - } - - const manzana = await manzanaService.create(tenantId, dto, req.user?.sub); - res.status(201).json({ success: true, data: manzana }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /manzanas/:id - * Actualizar manzana - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateManzanaDto = req.body; - const manzana = await manzanaService.update(req.params.id, tenantId, dto, req.user?.sub); - res.status(200).json({ success: true, data: manzana }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Block not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * DELETE /manzanas/:id - * Eliminar manzana - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - await manzanaService.delete(req.params.id, tenantId, req.user?.sub); - res.status(200).json({ success: true, message: 'Block deleted' }); - } catch (error) { - if (error instanceof Error && error.message === 'Block not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - return router; -} - -export default createManzanaController; diff --git a/projects/erp-construccion/backend/src/modules/construction/controllers/prototipo.controller.ts b/projects/erp-construccion/backend/src/modules/construction/controllers/prototipo.controller.ts deleted file mode 100644 index eb5efcfd6..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/controllers/prototipo.controller.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * PrototipoController - Controller de prototipos - * - * Endpoints REST para gesti贸n de prototipos de vivienda. - * - * @module Construction - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { PrototipoService, CreatePrototipoDto, UpdatePrototipoDto } from '../services/prototipo.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Prototipo } from '../entities/prototipo.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; - -/** - * Crear router de prototipos - */ -export function createPrototipoController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const prototipoRepository = dataSource.getRepository(Prototipo); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const prototipoService = new PrototipoService(prototipoRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - /** - * GET /prototipos - * Listar prototipos - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - const search = req.query.search as string; - const type = req.query.type as string; - const isActive = req.query.isActive === 'true' ? true : req.query.isActive === 'false' ? false : undefined; - - const result = await prototipoService.findAll({ tenantId, page, limit, search, type, isActive }); - - res.status(200).json({ - success: true, - data: result.items, - pagination: { - page, - limit, - total: result.total, - totalPages: Math.ceil(result.total / limit), - }, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /prototipos/:id - * Obtener prototipo por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const prototipo = await prototipoService.findById(req.params.id, tenantId); - if (!prototipo) { - res.status(404).json({ error: 'Not Found', message: 'Prototype not found' }); - return; - } - - res.status(200).json({ success: true, data: prototipo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /prototipos - * Crear prototipo - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreatePrototipoDto = req.body; - - if (!dto.code || !dto.name) { - res.status(400).json({ error: 'Bad Request', message: 'Code and name are required' }); - return; - } - - const prototipo = await prototipoService.create(tenantId, dto, req.user?.sub); - res.status(201).json({ success: true, data: prototipo }); - } catch (error) { - if (error instanceof Error && error.message === 'Prototype code already exists') { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /prototipos/:id - * Actualizar prototipo - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdatePrototipoDto = req.body; - const prototipo = await prototipoService.update(req.params.id, tenantId, dto, req.user?.sub); - res.status(200).json({ success: true, data: prototipo }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Prototype not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message === 'Prototype code already exists') { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * DELETE /prototipos/:id - * Eliminar prototipo - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - await prototipoService.delete(req.params.id, tenantId, req.user?.sub); - res.status(200).json({ success: true, message: 'Prototype deleted' }); - } catch (error) { - if (error instanceof Error && error.message === 'Prototype not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - return router; -} - -export default createPrototipoController; diff --git a/projects/erp-construccion/backend/src/modules/construction/controllers/proyecto.controller.ts b/projects/erp-construccion/backend/src/modules/construction/controllers/proyecto.controller.ts deleted file mode 100644 index d87ee2c61..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/controllers/proyecto.controller.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Proyecto Controller - * API endpoints para gesti贸n de proyectos - * - * @module Construction - * @prefix /api/v1/proyectos - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { ProyectoService, CreateProyectoDto, UpdateProyectoDto } from '../services/proyecto.service'; - -const router = Router(); -const proyectoService = new ProyectoService(); - -/** - * GET /api/v1/proyectos - * Lista todos los proyectos del tenant - */ -router.get('/', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const { estadoProyecto, ciudad } = req.query; - - const proyectos = await proyectoService.findAll({ - tenantId, - estadoProyecto: estadoProyecto as any, - ciudad: ciudad as string, - }); - - return res.json({ - success: true, - data: proyectos, - count: proyectos.length, - }); - } catch (error) { - return next(error); - } -}); - -/** - * GET /api/v1/proyectos/statistics - * Estad铆sticas de proyectos - */ -router.get('/statistics', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const stats = await proyectoService.getStatistics(tenantId); - return res.json({ success: true, data: stats }); - } catch (error) { - return next(error); - } -}); - -/** - * GET /api/v1/proyectos/:id - * Obtiene un proyecto por ID - */ -router.get('/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const proyecto = await proyectoService.findById(req.params.id, tenantId); - if (!proyecto) { - return res.status(404).json({ error: 'Proyecto no encontrado' }); - } - - return res.json({ success: true, data: proyecto }); - } catch (error) { - return next(error); - } -}); - -/** - * POST /api/v1/proyectos - * Crea un nuevo proyecto - */ -router.post('/', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const data: CreateProyectoDto = { - ...req.body, - tenantId, - createdById: (req as any).user?.id, - }; - - // Validate required fields - if (!data.codigo || !data.nombre) { - return res.status(400).json({ error: 'codigo y nombre son requeridos' }); - } - - // Check if codigo already exists - const existing = await proyectoService.findByCodigo(data.codigo, tenantId); - if (existing) { - return res.status(409).json({ error: 'Ya existe un proyecto con ese c贸digo' }); - } - - const proyecto = await proyectoService.create(data); - return res.status(201).json({ success: true, data: proyecto }); - } catch (error) { - return next(error); - } -}); - -/** - * PATCH /api/v1/proyectos/:id - * Actualiza un proyecto - */ -router.patch('/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const data: UpdateProyectoDto = req.body; - const proyecto = await proyectoService.update(req.params.id, tenantId, data); - - if (!proyecto) { - return res.status(404).json({ error: 'Proyecto no encontrado' }); - } - - return res.json({ success: true, data: proyecto }); - } catch (error) { - return next(error); - } -}); - -/** - * DELETE /api/v1/proyectos/:id - * Elimina un proyecto - */ -router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => { - try { - const tenantId = req.headers['x-tenant-id'] as string; - if (!tenantId) { - return res.status(400).json({ error: 'X-Tenant-Id header required' }); - } - - const deleted = await proyectoService.delete(req.params.id, tenantId); - if (!deleted) { - return res.status(404).json({ error: 'Proyecto no encontrado' }); - } - - return res.json({ success: true, message: 'Proyecto eliminado' }); - } catch (error) { - return next(error); - } -}); - -export default router; diff --git a/projects/erp-construccion/backend/src/modules/construction/entities/etapa.entity.ts b/projects/erp-construccion/backend/src/modules/construction/entities/etapa.entity.ts deleted file mode 100644 index bc37c7a61..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/entities/etapa.entity.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Etapa Entity - * Etapas/Fases de un fraccionamiento - * - * @module Construction - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Fraccionamiento } from './fraccionamiento.entity'; -import { Manzana } from './manzana.entity'; - -@Entity({ schema: 'construction', name: 'etapas' }) -@Index(['fraccionamientoId', 'code'], { unique: true }) -export class Etapa { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ type: 'varchar', length: 20 }) - code: string; - - @Column({ type: 'varchar', length: 100 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string; - - @Column({ type: 'integer', default: 1 }) - sequence: number; - - @Column({ name: 'total_lots', type: 'integer', default: 0 }) - totalLots: number; - - @Column({ type: 'varchar', length: 50, default: 'draft' }) - status: string; - - @Column({ name: 'start_date', type: 'date', nullable: true }) - startDate: Date; - - @Column({ name: 'expected_end_date', type: 'date', nullable: true }) - expectedEndDate: Date; - - @Column({ name: 'actual_end_date', type: 'date', nullable: true }) - actualEndDate: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - // Relations - @ManyToOne(() => Fraccionamiento, (f) => f.etapas, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @OneToMany(() => Manzana, (m) => m.etapa) - manzanas: Manzana[]; -} diff --git a/projects/erp-construccion/backend/src/modules/construction/entities/fraccionamiento.entity.ts b/projects/erp-construccion/backend/src/modules/construction/entities/fraccionamiento.entity.ts deleted file mode 100644 index cb66c7d4a..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/entities/fraccionamiento.entity.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Fraccionamiento Entity - * Obras/fraccionamientos dentro de un proyecto - * - * @module Construction - * @table construction.fraccionamientos - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Proyecto } from './proyecto.entity'; -import { Etapa } from './etapa.entity'; - -export type EstadoFraccionamiento = 'activo' | 'pausado' | 'completado' | 'cancelado'; - -@Entity({ schema: 'construction', name: 'fraccionamientos' }) -@Index(['tenantId', 'codigo'], { unique: true }) -export class Fraccionamiento { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'proyecto_id', type: 'uuid' }) - proyectoId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ type: 'text', nullable: true }) - direccion: string; - - // PostGIS geometry - stored as GeoJSON for TypeORM compatibility - @Column({ - name: 'ubicacion_geo', - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true - }) - ubicacionGeo: string; - - @Column({ name: 'fecha_inicio', type: 'date', nullable: true }) - fechaInicio: Date; - - @Column({ name: 'fecha_fin_estimada', type: 'date', nullable: true }) - fechaFinEstimada: Date; - - @Column({ type: 'varchar', length: 20, default: 'activo' }) - estado: EstadoFraccionamiento; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Proyecto, (p) => p.fraccionamientos) - @JoinColumn({ name: 'proyecto_id' }) - proyecto: Proyecto; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => Etapa, (e) => e.fraccionamiento) - etapas: Etapa[]; -} diff --git a/projects/erp-construccion/backend/src/modules/construction/entities/index.ts b/projects/erp-construccion/backend/src/modules/construction/entities/index.ts deleted file mode 100644 index 6bfb8b8a6..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/entities/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Construction Entities Index - * @module Construction - */ - -export { Proyecto } from './proyecto.entity'; -export { Fraccionamiento } from './fraccionamiento.entity'; -export { Etapa } from './etapa.entity'; -export { Manzana } from './manzana.entity'; -export { Lote } from './lote.entity'; -export { Prototipo } from './prototipo.entity'; diff --git a/projects/erp-construccion/backend/src/modules/construction/entities/lote.entity.ts b/projects/erp-construccion/backend/src/modules/construction/entities/lote.entity.ts deleted file mode 100644 index 9da49c0ba..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/entities/lote.entity.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Lote Entity - * Lotes/Terrenos individuales - * - * @module Construction - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Manzana } from './manzana.entity'; -import { Prototipo } from './prototipo.entity'; - -@Entity({ schema: 'construction', name: 'lotes' }) -@Index(['manzanaId', 'code'], { unique: true }) -export class Lote { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'manzana_id', type: 'uuid' }) - manzanaId: string; - - @Column({ name: 'prototipo_id', type: 'uuid', nullable: true }) - prototipoId: string; - - @Column({ type: 'varchar', length: 30 }) - code: string; - - @Column({ name: 'official_number', type: 'varchar', length: 50, nullable: true }) - officialNumber: string; - - @Column({ name: 'area_m2', type: 'decimal', precision: 10, scale: 2, nullable: true }) - areaM2: number; - - @Column({ name: 'front_m', type: 'decimal', precision: 8, scale: 2, nullable: true }) - frontM: number; - - @Column({ name: 'depth_m', type: 'decimal', precision: 8, scale: 2, nullable: true }) - depthM: number; - - @Column({ type: 'varchar', length: 50, default: 'available' }) - status: string; - - @Column({ name: 'price_base', type: 'decimal', precision: 14, scale: 2, nullable: true }) - priceBase: number; - - @Column({ name: 'price_final', type: 'decimal', precision: 14, scale: 2, nullable: true }) - priceFinal: number; - - @Column({ name: 'buyer_id', type: 'uuid', nullable: true }) - buyerId: string; - - @Column({ name: 'sale_date', type: 'date', nullable: true }) - saleDate: Date; - - @Column({ name: 'delivery_date', type: 'date', nullable: true }) - deliveryDate: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - // Relations - @ManyToOne(() => Manzana, (m) => m.lotes, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'manzana_id' }) - manzana: Manzana; - - @ManyToOne(() => Prototipo) - @JoinColumn({ name: 'prototipo_id' }) - prototipo: Prototipo; -} diff --git a/projects/erp-construccion/backend/src/modules/construction/entities/manzana.entity.ts b/projects/erp-construccion/backend/src/modules/construction/entities/manzana.entity.ts deleted file mode 100644 index a20613de6..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/entities/manzana.entity.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Manzana Entity - * Manzanas (bloques) dentro de una etapa - * - * @module Construction - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Etapa } from './etapa.entity'; -import { Lote } from './lote.entity'; - -@Entity({ schema: 'construction', name: 'manzanas' }) -@Index(['etapaId', 'code'], { unique: true }) -export class Manzana { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'etapa_id', type: 'uuid' }) - etapaId: string; - - @Column({ type: 'varchar', length: 20 }) - code: string; - - @Column({ type: 'varchar', length: 100, nullable: true }) - name: string; - - @Column({ name: 'total_lots', type: 'integer', default: 0 }) - totalLots: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - // Relations - @ManyToOne(() => Etapa, (e) => e.manzanas, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'etapa_id' }) - etapa: Etapa; - - @OneToMany(() => Lote, (l) => l.manzana) - lotes: Lote[]; -} diff --git a/projects/erp-construccion/backend/src/modules/construction/entities/prototipo.entity.ts b/projects/erp-construccion/backend/src/modules/construction/entities/prototipo.entity.ts deleted file mode 100644 index cffc3ad1d..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/entities/prototipo.entity.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Prototipo Entity - * Prototipos de vivienda (modelos) - * - * @module Construction - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - Index, -} from 'typeorm'; - -@Entity({ schema: 'construction', name: 'prototipos' }) -@Index(['tenantId', 'code'], { unique: true }) -export class Prototipo { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - code: string; - - @Column({ type: 'varchar', length: 100 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string; - - @Column({ type: 'varchar', length: 50, default: 'horizontal' }) - type: string; - - @Column({ name: 'area_construction_m2', type: 'decimal', precision: 10, scale: 2, nullable: true }) - areaConstructionM2: number; - - @Column({ name: 'area_terrain_m2', type: 'decimal', precision: 10, scale: 2, nullable: true }) - areaTerrainM2: number; - - @Column({ type: 'integer', default: 0 }) - bedrooms: number; - - @Column({ type: 'decimal', precision: 3, scale: 1, default: 0 }) - bathrooms: number; - - @Column({ name: 'parking_spaces', type: 'integer', default: 0 }) - parkingSpaces: number; - - @Column({ type: 'integer', default: 1 }) - floors: number; - - @Column({ name: 'base_price', type: 'decimal', precision: 14, scale: 2, nullable: true }) - basePrice: number; - - @Column({ name: 'blueprint_url', type: 'varchar', length: 500, nullable: true }) - blueprintUrl: string; - - @Column({ name: 'render_url', type: 'varchar', length: 500, nullable: true }) - renderUrl: string; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @Column({ type: 'jsonb', default: {} }) - metadata: Record; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/construction/entities/proyecto.entity.ts b/projects/erp-construccion/backend/src/modules/construction/entities/proyecto.entity.ts deleted file mode 100644 index 95964d43b..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/entities/proyecto.entity.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Proyecto Entity - * Proyectos de desarrollo inmobiliario - * - * @module Construction - * @table construction.proyectos - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from './fraccionamiento.entity'; - -export type EstadoProyecto = 'activo' | 'pausado' | 'completado' | 'cancelado'; - -@Entity({ schema: 'construction', name: 'proyectos' }) -@Index(['tenantId', 'codigo'], { unique: true }) -export class Proyecto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ type: 'text', nullable: true }) - direccion: string; - - @Column({ type: 'varchar', length: 100, nullable: true }) - ciudad: string; - - @Column({ type: 'varchar', length: 100, nullable: true }) - estado: string; - - @Column({ name: 'fecha_inicio', type: 'date', nullable: true }) - fechaInicio: Date; - - @Column({ name: 'fecha_fin_estimada', type: 'date', nullable: true }) - fechaFinEstimada: Date; - - @Column({ - name: 'estado_proyecto', - type: 'varchar', - length: 20, - default: 'activo' - }) - estadoProyecto: EstadoProyecto; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => Fraccionamiento, (f) => f.proyecto) - fraccionamientos: Fraccionamiento[]; -} diff --git a/projects/erp-construccion/backend/src/modules/construction/services/etapa.service.ts b/projects/erp-construccion/backend/src/modules/construction/services/etapa.service.ts deleted file mode 100644 index e43e4927d..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/services/etapa.service.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * EtapaService - Gesti贸n de etapas de fraccionamientos - * - * CRUD de etapas con soporte multi-tenant. - * - * @module Construction - */ - -import { Repository, IsNull } from 'typeorm'; -import { Etapa } from '../entities/etapa.entity'; - -export interface CreateEtapaDto { - fraccionamientoId: string; - code: string; - name: string; - description?: string; - sequence?: number; - totalLots?: number; - status?: string; - startDate?: Date; - expectedEndDate?: Date; -} - -export interface UpdateEtapaDto extends Partial { - actualEndDate?: Date; -} - -export interface EtapaListOptions { - tenantId: string; - fraccionamientoId?: string; - page?: number; - limit?: number; - search?: string; - status?: string; -} - -export class EtapaService { - constructor(private readonly repository: Repository) {} - - /** - * Listar etapas - */ - async findAll(options: EtapaListOptions): Promise<{ items: Etapa[]; total: number }> { - const { tenantId, fraccionamientoId, page = 1, limit = 20, search, status } = options; - - const query = this.repository - .createQueryBuilder('e') - .where('e.tenant_id = :tenantId', { tenantId }) - .andWhere('e.deleted_at IS NULL'); - - if (fraccionamientoId) { - query.andWhere('e.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - if (search) { - query.andWhere('(e.code ILIKE :search OR e.name ILIKE :search)', { search: `%${search}%` }); - } - - if (status) { - query.andWhere('e.status = :status', { status }); - } - - const total = await query.getCount(); - const items = await query - .skip((page - 1) * limit) - .take(limit) - .orderBy('e.sequence', 'ASC') - .addOrderBy('e.name', 'ASC') - .getMany(); - - return { items, total }; - } - - /** - * Obtener etapa por ID - */ - async findById(id: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { id, tenantId, deletedAt: IsNull() } as any, - relations: ['manzanas'], - }); - } - - /** - * Obtener etapa por c贸digo dentro de un fraccionamiento - */ - async findByCode(code: string, fraccionamientoId: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { code, fraccionamientoId, tenantId, deletedAt: IsNull() } as any, - }); - } - - /** - * Crear etapa - */ - async create(tenantId: string, dto: CreateEtapaDto, createdBy?: string): Promise { - const existing = await this.findByCode(dto.code, dto.fraccionamientoId, tenantId); - if (existing) { - throw new Error('Stage code already exists in this fraccionamiento'); - } - - return this.repository.save( - this.repository.create({ - tenantId, - ...dto, - createdBy, - status: dto.status || 'draft', - }) - ); - } - - /** - * Actualizar etapa - */ - async update(id: string, tenantId: string, dto: UpdateEtapaDto, updatedBy?: string): Promise { - const etapa = await this.findById(id, tenantId); - if (!etapa) { - throw new Error('Stage not found'); - } - - // Verificar c贸digo 煤nico si se est谩 cambiando - if (dto.code && dto.code !== etapa.code) { - const existing = await this.findByCode(dto.code, etapa.fraccionamientoId, tenantId); - if (existing) { - throw new Error('Stage code already exists in this fraccionamiento'); - } - } - - await this.repository.update(id, { - ...dto, - updatedBy, - updatedAt: new Date(), - }); - - return this.findById(id, tenantId) as Promise; - } - - /** - * Eliminar etapa (soft delete) - */ - async delete(id: string, tenantId: string, _deletedBy?: string): Promise { - const etapa = await this.findById(id, tenantId); - if (!etapa) { - throw new Error('Stage not found'); - } - - // TODO: Verificar si tiene manzanas antes de eliminar - - await this.repository.update(id, { - deletedAt: new Date(), - }); - } - - /** - * Obtener etapas por fraccionamiento - */ - async findByFraccionamiento(fraccionamientoId: string, tenantId: string): Promise { - return this.repository.find({ - where: { fraccionamientoId, tenantId, deletedAt: IsNull() } as any, - order: { sequence: 'ASC' }, - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/construction/services/fraccionamiento.service.ts b/projects/erp-construccion/backend/src/modules/construction/services/fraccionamiento.service.ts deleted file mode 100644 index 604b234a1..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/services/fraccionamiento.service.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Fraccionamiento Service - * Servicio para gesti贸n de fraccionamientos/obras - * - * @module Construction - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { AppDataSource } from '../../../shared/database/typeorm.config'; -import { Fraccionamiento, EstadoFraccionamiento } from '../entities/fraccionamiento.entity'; - -export interface CreateFraccionamientoDto { - tenantId: string; - proyectoId: string; - codigo: string; - nombre: string; - descripcion?: string; - direccion?: string; - ubicacionGeo?: string; - fechaInicio?: Date; - fechaFinEstimada?: Date; - createdById?: string; -} - -export interface UpdateFraccionamientoDto { - nombre?: string; - descripcion?: string; - direccion?: string; - ubicacionGeo?: string; - fechaInicio?: Date; - fechaFinEstimada?: Date; - estado?: EstadoFraccionamiento; -} - -export interface FraccionamientoFilters { - tenantId: string; - proyectoId?: string; - estado?: EstadoFraccionamiento; -} - -export class FraccionamientoService { - private repository: Repository; - - constructor() { - this.repository = AppDataSource.getRepository(Fraccionamiento); - } - - async findAll(filters: FraccionamientoFilters): Promise { - const where: FindOptionsWhere = { - tenantId: filters.tenantId, - }; - - if (filters.proyectoId) { - where.proyectoId = filters.proyectoId; - } - - if (filters.estado) { - where.estado = filters.estado; - } - - return this.repository.find({ - where, - relations: ['proyecto'], - order: { createdAt: 'DESC' }, - }); - } - - async findById(id: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { id, tenantId }, - relations: ['proyecto', 'createdBy'], - }); - } - - async findByCodigo(codigo: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { codigo, tenantId }, - }); - } - - async findByProyecto(proyectoId: string, tenantId: string): Promise { - return this.repository.find({ - where: { proyectoId, tenantId }, - order: { codigo: 'ASC' }, - }); - } - - async create(data: CreateFraccionamientoDto): Promise { - const fraccionamiento = this.repository.create(data); - return this.repository.save(fraccionamiento); - } - - async update( - id: string, - tenantId: string, - data: UpdateFraccionamientoDto - ): Promise { - const fraccionamiento = await this.findById(id, tenantId); - if (!fraccionamiento) { - return null; - } - - Object.assign(fraccionamiento, data); - return this.repository.save(fraccionamiento); - } - - async delete(id: string, tenantId: string): Promise { - const result = await this.repository.delete({ id, tenantId }); - return result.affected ? result.affected > 0 : false; - } - - async countByProyecto(proyectoId: string, tenantId: string): Promise { - return this.repository.count({ - where: { proyectoId, tenantId }, - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/construction/services/index.ts b/projects/erp-construccion/backend/src/modules/construction/services/index.ts deleted file mode 100644 index 743750ae4..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/services/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Construction Services Index - * @module Construction - */ - -export * from './proyecto.service'; -export * from './fraccionamiento.service'; -export * from './etapa.service'; -export * from './manzana.service'; -export * from './lote.service'; -export * from './prototipo.service'; diff --git a/projects/erp-construccion/backend/src/modules/construction/services/lote.service.ts b/projects/erp-construccion/backend/src/modules/construction/services/lote.service.ts deleted file mode 100644 index 50beb8a30..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/services/lote.service.ts +++ /dev/null @@ -1,230 +0,0 @@ -/** - * LoteService - Gesti贸n de lotes/terrenos - * - * CRUD de lotes con soporte multi-tenant. - * - * @module Construction - */ - -import { Repository, IsNull } from 'typeorm'; -import { Lote } from '../entities/lote.entity'; - -export interface CreateLoteDto { - manzanaId: string; - prototipoId?: string; - code: string; - officialNumber?: string; - areaM2?: number; - frontM?: number; - depthM?: number; - status?: string; - priceBase?: number; - priceFinal?: number; -} - -export interface UpdateLoteDto extends Partial { - buyerId?: string; - saleDate?: Date; - deliveryDate?: Date; -} - -export interface LoteListOptions { - tenantId: string; - manzanaId?: string; - prototipoId?: string; - page?: number; - limit?: number; - search?: string; - status?: string; -} - -export class LoteService { - constructor(private readonly repository: Repository) {} - - /** - * Listar lotes - */ - async findAll(options: LoteListOptions): Promise<{ items: Lote[]; total: number }> { - const { tenantId, manzanaId, prototipoId, page = 1, limit = 20, search, status } = options; - - const query = this.repository - .createQueryBuilder('l') - .where('l.tenant_id = :tenantId', { tenantId }) - .andWhere('l.deleted_at IS NULL'); - - if (manzanaId) { - query.andWhere('l.manzana_id = :manzanaId', { manzanaId }); - } - - if (prototipoId) { - query.andWhere('l.prototipo_id = :prototipoId', { prototipoId }); - } - - if (search) { - query.andWhere('(l.code ILIKE :search OR l.official_number ILIKE :search)', { search: `%${search}%` }); - } - - if (status) { - query.andWhere('l.status = :status', { status }); - } - - const total = await query.getCount(); - const items = await query - .leftJoinAndSelect('l.prototipo', 'prototipo') - .skip((page - 1) * limit) - .take(limit) - .orderBy('l.code', 'ASC') - .getMany(); - - return { items, total }; - } - - /** - * Obtener lote por ID - */ - async findById(id: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { id, tenantId, deletedAt: IsNull() } as any, - relations: ['prototipo', 'manzana'], - }); - } - - /** - * Obtener lote por c贸digo dentro de una manzana - */ - async findByCode(code: string, manzanaId: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { code, manzanaId, tenantId, deletedAt: IsNull() } as any, - }); - } - - /** - * Crear lote - */ - async create(tenantId: string, dto: CreateLoteDto, createdBy?: string): Promise { - const existing = await this.findByCode(dto.code, dto.manzanaId, tenantId); - if (existing) { - throw new Error('Lot code already exists in this block'); - } - - return this.repository.save( - this.repository.create({ - tenantId, - ...dto, - createdBy, - status: dto.status || 'available', - }) - ); - } - - /** - * Actualizar lote - */ - async update(id: string, tenantId: string, dto: UpdateLoteDto, updatedBy?: string): Promise { - const lote = await this.findById(id, tenantId); - if (!lote) { - throw new Error('Lot not found'); - } - - // Verificar c贸digo 煤nico si se est谩 cambiando - if (dto.code && dto.code !== lote.code) { - const existing = await this.findByCode(dto.code, lote.manzanaId, tenantId); - if (existing) { - throw new Error('Lot code already exists in this block'); - } - } - - await this.repository.update(id, { - ...dto, - updatedBy, - updatedAt: new Date(), - }); - - return this.findById(id, tenantId) as Promise; - } - - /** - * Eliminar lote (soft delete) - */ - async delete(id: string, tenantId: string, _deletedBy?: string): Promise { - const lote = await this.findById(id, tenantId); - if (!lote) { - throw new Error('Lot not found'); - } - - // Verificar que no est茅 vendido - if (lote.status === 'sold') { - throw new Error('Cannot delete a sold lot'); - } - - await this.repository.update(id, { - deletedAt: new Date(), - }); - } - - /** - * Obtener lotes por manzana - */ - async findByManzana(manzanaId: string, tenantId: string): Promise { - return this.repository.find({ - where: { manzanaId, tenantId, deletedAt: IsNull() } as any, - order: { code: 'ASC' }, - relations: ['prototipo'], - }); - } - - /** - * Asignar prototipo a lote - */ - async assignPrototipo(id: string, tenantId: string, prototipoId: string, updatedBy?: string): Promise { - const lote = await this.findById(id, tenantId); - if (!lote) { - throw new Error('Lot not found'); - } - - await this.repository.update(id, { - prototipoId, - updatedBy, - updatedAt: new Date(), - }); - - return this.findById(id, tenantId) as Promise; - } - - /** - * Cambiar estado del lote - */ - async changeStatus(id: string, tenantId: string, status: string, updatedBy?: string): Promise { - const lote = await this.findById(id, tenantId); - if (!lote) { - throw new Error('Lot not found'); - } - - await this.repository.update(id, { - status, - updatedBy, - updatedAt: new Date(), - }); - - return this.findById(id, tenantId) as Promise; - } - - /** - * Obtener estad铆sticas de lotes por estado - */ - async getStatsByStatus(tenantId: string, manzanaId?: string): Promise<{ status: string; count: number }[]> { - const query = this.repository - .createQueryBuilder('l') - .select('l.status', 'status') - .addSelect('COUNT(*)', 'count') - .where('l.tenant_id = :tenantId', { tenantId }) - .andWhere('l.deleted_at IS NULL') - .groupBy('l.status'); - - if (manzanaId) { - query.andWhere('l.manzana_id = :manzanaId', { manzanaId }); - } - - return query.getRawMany(); - } -} diff --git a/projects/erp-construccion/backend/src/modules/construction/services/manzana.service.ts b/projects/erp-construccion/backend/src/modules/construction/services/manzana.service.ts deleted file mode 100644 index 3b963075c..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/services/manzana.service.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * ManzanaService - Gesti贸n de manzanas (bloques) - * - * CRUD de manzanas con soporte multi-tenant. - * - * @module Construction - */ - -import { Repository, IsNull } from 'typeorm'; -import { Manzana } from '../entities/manzana.entity'; - -export interface CreateManzanaDto { - etapaId: string; - code: string; - name?: string; - totalLots?: number; -} - -export interface UpdateManzanaDto extends Partial {} - -export interface ManzanaListOptions { - tenantId: string; - etapaId?: string; - page?: number; - limit?: number; - search?: string; -} - -export class ManzanaService { - constructor(private readonly repository: Repository) {} - - /** - * Listar manzanas - */ - async findAll(options: ManzanaListOptions): Promise<{ items: Manzana[]; total: number }> { - const { tenantId, etapaId, page = 1, limit = 20, search } = options; - - const query = this.repository - .createQueryBuilder('m') - .where('m.tenant_id = :tenantId', { tenantId }) - .andWhere('m.deleted_at IS NULL'); - - if (etapaId) { - query.andWhere('m.etapa_id = :etapaId', { etapaId }); - } - - if (search) { - query.andWhere('(m.code ILIKE :search OR m.name ILIKE :search)', { search: `%${search}%` }); - } - - const total = await query.getCount(); - const items = await query - .skip((page - 1) * limit) - .take(limit) - .orderBy('m.code', 'ASC') - .getMany(); - - return { items, total }; - } - - /** - * Obtener manzana por ID - */ - async findById(id: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { id, tenantId, deletedAt: IsNull() } as any, - relations: ['lotes'], - }); - } - - /** - * Obtener manzana por c贸digo dentro de una etapa - */ - async findByCode(code: string, etapaId: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { code, etapaId, tenantId, deletedAt: IsNull() } as any, - }); - } - - /** - * Crear manzana - */ - async create(tenantId: string, dto: CreateManzanaDto, createdBy?: string): Promise { - const existing = await this.findByCode(dto.code, dto.etapaId, tenantId); - if (existing) { - throw new Error('Block code already exists in this stage'); - } - - return this.repository.save( - this.repository.create({ - tenantId, - ...dto, - createdBy, - }) - ); - } - - /** - * Actualizar manzana - */ - async update(id: string, tenantId: string, dto: UpdateManzanaDto, updatedBy?: string): Promise { - const manzana = await this.findById(id, tenantId); - if (!manzana) { - throw new Error('Block not found'); - } - - // Verificar c贸digo 煤nico si se est谩 cambiando - if (dto.code && dto.code !== manzana.code) { - const existing = await this.findByCode(dto.code, manzana.etapaId, tenantId); - if (existing) { - throw new Error('Block code already exists in this stage'); - } - } - - await this.repository.update(id, { - ...dto, - updatedBy, - updatedAt: new Date(), - }); - - return this.findById(id, tenantId) as Promise; - } - - /** - * Eliminar manzana (soft delete) - */ - async delete(id: string, tenantId: string, _deletedBy?: string): Promise { - const manzana = await this.findById(id, tenantId); - if (!manzana) { - throw new Error('Block not found'); - } - - // TODO: Verificar si tiene lotes antes de eliminar - - await this.repository.update(id, { - deletedAt: new Date(), - }); - } - - /** - * Obtener manzanas por etapa - */ - async findByEtapa(etapaId: string, tenantId: string): Promise { - return this.repository.find({ - where: { etapaId, tenantId, deletedAt: IsNull() } as any, - order: { code: 'ASC' }, - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/construction/services/prototipo.service.ts b/projects/erp-construccion/backend/src/modules/construction/services/prototipo.service.ts deleted file mode 100644 index 39cf51fe0..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/services/prototipo.service.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * PrototipoService - Gesti贸n de prototipos de vivienda - * - * CRUD de prototipos con soporte multi-tenant. - * - * @module Construction - */ - -import { Repository, IsNull } from 'typeorm'; -import { Prototipo } from '../entities/prototipo.entity'; - -export interface CreatePrototipoDto { - code: string; - name: string; - description?: string; - type?: string; - areaConstructionM2?: number; - areaTerrainM2?: number; - bedrooms?: number; - bathrooms?: number; - parkingSpaces?: number; - floors?: number; - basePrice?: number; - blueprintUrl?: string; - renderUrl?: string; - metadata?: Record; -} - -export interface UpdatePrototipoDto extends Partial { - isActive?: boolean; -} - -export interface PrototipoListOptions { - tenantId: string; - page?: number; - limit?: number; - search?: string; - type?: string; - isActive?: boolean; -} - -export class PrototipoService { - constructor(private readonly repository: Repository) {} - - /** - * Listar prototipos - */ - async findAll(options: PrototipoListOptions): Promise<{ items: Prototipo[]; total: number }> { - const { tenantId, page = 1, limit = 20, search, type, isActive } = options; - - const query = this.repository - .createQueryBuilder('p') - .where('p.tenant_id = :tenantId', { tenantId }) - .andWhere('p.deleted_at IS NULL'); - - if (search) { - query.andWhere('(p.code ILIKE :search OR p.name ILIKE :search)', { search: `%${search}%` }); - } - - if (type) { - query.andWhere('p.type = :type', { type }); - } - - if (isActive !== undefined) { - query.andWhere('p.is_active = :isActive', { isActive }); - } - - const total = await query.getCount(); - const items = await query - .skip((page - 1) * limit) - .take(limit) - .orderBy('p.name', 'ASC') - .getMany(); - - return { items, total }; - } - - /** - * Obtener prototipo por ID - */ - async findById(id: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { id, tenantId, deletedAt: IsNull() } as any, - }); - } - - /** - * Obtener prototipo por c贸digo - */ - async findByCode(code: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { code, tenantId, deletedAt: IsNull() } as any, - }); - } - - /** - * Crear prototipo - */ - async create(tenantId: string, dto: CreatePrototipoDto, createdBy?: string): Promise { - const existing = await this.findByCode(dto.code, tenantId); - if (existing) { - throw new Error('Prototype code already exists'); - } - - return this.repository.save( - this.repository.create({ - tenantId, - ...dto, - createdBy, - isActive: true, - }) - ); - } - - /** - * Actualizar prototipo - */ - async update(id: string, tenantId: string, dto: UpdatePrototipoDto, updatedBy?: string): Promise { - const prototipo = await this.findById(id, tenantId); - if (!prototipo) { - throw new Error('Prototype not found'); - } - - // Verificar c贸digo 煤nico si se est谩 cambiando - if (dto.code && dto.code !== prototipo.code) { - const existing = await this.findByCode(dto.code, tenantId); - if (existing) { - throw new Error('Prototype code already exists'); - } - } - - // Exclude metadata from spread to avoid TypeORM type issues - const { metadata, ...updateData } = dto; - await this.repository.update(id, { - ...updateData, - ...(metadata && { metadata: metadata as any }), - updatedBy, - updatedAt: new Date(), - }); - - return this.findById(id, tenantId) as Promise; - } - - /** - * Eliminar prototipo (soft delete) - */ - async delete(id: string, tenantId: string, _deletedBy?: string): Promise { - const prototipo = await this.findById(id, tenantId); - if (!prototipo) { - throw new Error('Prototype not found'); - } - - // TODO: Verificar si est谩 asignado a lotes antes de eliminar - - await this.repository.update(id, { - deletedAt: new Date(), - isActive: false, - }); - } - - /** - * Activar/Desactivar prototipo - */ - async setActive(id: string, tenantId: string, isActive: boolean): Promise { - const prototipo = await this.findById(id, tenantId); - if (!prototipo) { - throw new Error('Prototype not found'); - } - - await this.repository.update(id, { isActive }); - return this.findById(id, tenantId) as Promise; - } -} diff --git a/projects/erp-construccion/backend/src/modules/construction/services/proyecto.service.ts b/projects/erp-construccion/backend/src/modules/construction/services/proyecto.service.ts deleted file mode 100644 index ae55f2d3d..000000000 --- a/projects/erp-construccion/backend/src/modules/construction/services/proyecto.service.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Proyecto Service - * Servicio para gesti贸n de proyectos de construcci贸n - * - * @module Construction - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { AppDataSource } from '../../../shared/database/typeorm.config'; -import { Proyecto, EstadoProyecto } from '../entities/proyecto.entity'; - -export interface CreateProyectoDto { - tenantId: string; - codigo: string; - nombre: string; - descripcion?: string; - direccion?: string; - ciudad?: string; - estado?: string; - fechaInicio?: Date; - fechaFinEstimada?: Date; - createdById?: string; -} - -export interface UpdateProyectoDto { - nombre?: string; - descripcion?: string; - direccion?: string; - ciudad?: string; - estado?: string; - fechaInicio?: Date; - fechaFinEstimada?: Date; - estadoProyecto?: EstadoProyecto; -} - -export interface ProyectoFilters { - tenantId: string; - estadoProyecto?: EstadoProyecto; - ciudad?: string; -} - -export class ProyectoService { - private repository: Repository; - - constructor() { - this.repository = AppDataSource.getRepository(Proyecto); - } - - async findAll(filters: ProyectoFilters): Promise { - const where: FindOptionsWhere = { - tenantId: filters.tenantId, - }; - - if (filters.estadoProyecto) { - where.estadoProyecto = filters.estadoProyecto; - } - - if (filters.ciudad) { - where.ciudad = filters.ciudad; - } - - return this.repository.find({ - where, - relations: ['fraccionamientos'], - order: { createdAt: 'DESC' }, - }); - } - - async findById(id: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { id, tenantId }, - relations: ['fraccionamientos', 'createdBy'], - }); - } - - async findByCodigo(codigo: string, tenantId: string): Promise { - return this.repository.findOne({ - where: { codigo, tenantId }, - }); - } - - async create(data: CreateProyectoDto): Promise { - const proyecto = this.repository.create(data); - return this.repository.save(proyecto); - } - - async update(id: string, tenantId: string, data: UpdateProyectoDto): Promise { - const proyecto = await this.findById(id, tenantId); - if (!proyecto) { - return null; - } - - Object.assign(proyecto, data); - return this.repository.save(proyecto); - } - - async delete(id: string, tenantId: string): Promise { - const result = await this.repository.delete({ id, tenantId }); - return result.affected ? result.affected > 0 : false; - } - - async getStatistics(tenantId: string): Promise<{ - total: number; - activos: number; - completados: number; - pausados: number; - }> { - const proyectos = await this.repository.find({ where: { tenantId } }); - - return { - total: proyectos.length, - activos: proyectos.filter(p => p.estadoProyecto === 'activo').length, - completados: proyectos.filter(p => p.estadoProyecto === 'completado').length, - pausados: proyectos.filter(p => p.estadoProyecto === 'pausado').length, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/contracts/controllers/contract.controller.ts b/projects/erp-construccion/backend/src/modules/contracts/controllers/contract.controller.ts deleted file mode 100644 index ed8bd4992..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/controllers/contract.controller.ts +++ /dev/null @@ -1,415 +0,0 @@ -/** - * ContractController - REST API for contracts - * - * Endpoints para gesti贸n de contratos. - * - * @module Contracts - * @routes /api/contracts - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { ContractService, ContractFilters } from '../services/contract.service'; -import { Contract } from '../entities/contract.entity'; -import { ContractAddendum } from '../entities/contract-addendum.entity'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createContractController(dataSource: DataSource): Router { - const router = Router(); - - // Repositories - const contractRepo = dataSource.getRepository(Contract); - const addendumRepo = dataSource.getRepository(ContractAddendum); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Services - const service = new ContractService(contractRepo, addendumRepo); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper for service context - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /api/contracts - * List contracts with filters - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: ContractFilters = {}; - if (req.query.projectId) filters.projectId = req.query.projectId as string; - if (req.query.fraccionamientoId) filters.fraccionamientoId = req.query.fraccionamientoId as string; - if (req.query.contractType) filters.contractType = req.query.contractType as ContractFilters['contractType']; - if (req.query.subcontractorId) filters.subcontractorId = req.query.subcontractorId as string; - if (req.query.status) filters.status = req.query.status as ContractFilters['status']; - if (req.query.expiringInDays) filters.expiringInDays = parseInt(req.query.expiringInDays as string); - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/contracts/expiring - * Get contracts expiring soon - */ - router.get('/expiring', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const days = parseInt(req.query.days as string) || 30; - const contracts = await service.getExpiringContracts(getContext(req), days); - res.status(200).json({ success: true, data: contracts }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/contracts/:id - * Get contract with details - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.findWithDetails(getContext(req), req.params.id); - if (!contract) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(200).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts - * Create new contract - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'legal', 'contracts'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.create(getContext(req), { - projectId: req.body.projectId, - fraccionamientoId: req.body.fraccionamientoId, - contractType: req.body.contractType, - clientContractType: req.body.clientContractType, - name: req.body.name, - description: req.body.description, - clientName: req.body.clientName, - clientRfc: req.body.clientRfc, - clientAddress: req.body.clientAddress, - subcontractorId: req.body.subcontractorId, - specialty: req.body.specialty, - startDate: new Date(req.body.startDate), - endDate: new Date(req.body.endDate), - contractAmount: req.body.contractAmount, - currency: req.body.currency, - paymentTerms: req.body.paymentTerms, - retentionPercentage: req.body.retentionPercentage, - advancePercentage: req.body.advancePercentage, - notes: req.body.notes, - }); - - res.status(201).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts/:id/submit - * Submit contract for review - */ - router.post('/:id/submit', authMiddleware.authenticate, authMiddleware.authorize('admin', 'legal', 'contracts'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.submitForReview(getContext(req), req.params.id); - if (!contract) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(200).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts/:id/approve-legal - * Legal approval - */ - router.post('/:id/approve-legal', authMiddleware.authenticate, authMiddleware.authorize('admin', 'legal'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.approveLegal(getContext(req), req.params.id); - if (!contract) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(200).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts/:id/approve - * Final approval - */ - router.post('/:id/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.approve(getContext(req), req.params.id); - if (!contract) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(200).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts/:id/activate - * Activate signed contract - */ - router.post('/:id/activate', authMiddleware.authenticate, authMiddleware.authorize('admin', 'legal', 'contracts'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.activate(getContext(req), req.params.id, req.body.signedDocumentUrl); - if (!contract) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(200).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts/:id/complete - * Mark contract as completed - */ - router.post('/:id/complete', authMiddleware.authenticate, authMiddleware.authorize('admin', 'contracts'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.complete(getContext(req), req.params.id); - if (!contract) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(200).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts/:id/terminate - * Terminate contract - */ - router.post('/:id/terminate', authMiddleware.authenticate, authMiddleware.authorize('admin', 'legal', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.terminate(getContext(req), req.params.id, req.body.reason); - if (!contract) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(200).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /api/contracts/:id/progress - * Update contract progress - */ - router.put('/:id/progress', authMiddleware.authenticate, authMiddleware.authorize('admin', 'contracts', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const contract = await service.updateProgress( - getContext(req), - req.params.id, - req.body.progressPercentage, - req.body.invoicedAmount, - req.body.paidAmount - ); - - if (!contract) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(200).json({ success: true, data: contract }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts/:id/addendums - * Create contract addendum - */ - router.post('/:id/addendums', authMiddleware.authenticate, authMiddleware.authorize('admin', 'legal', 'contracts'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const addendum = await service.createAddendum(getContext(req), req.params.id, { - addendumType: req.body.addendumType, - title: req.body.title, - description: req.body.description, - effectiveDate: new Date(req.body.effectiveDate), - newEndDate: req.body.newEndDate ? new Date(req.body.newEndDate) : undefined, - amountChange: req.body.amountChange, - scopeChanges: req.body.scopeChanges, - notes: req.body.notes, - }); - - res.status(201).json({ success: true, data: addendum }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/contracts/addendums/:addendumId/approve - * Approve addendum - */ - router.post('/addendums/:addendumId/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'legal', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const addendum = await service.approveAddendum(getContext(req), req.params.addendumId); - if (!addendum) { - res.status(404).json({ error: 'Not Found', message: 'Addendum not found' }); - return; - } - - res.status(200).json({ success: true, data: addendum }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /api/contracts/:id - * Soft delete contract - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await service.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Contract not found' }); - return; - } - - res.status(204).send(); - } catch (error) { - next(error); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/contracts/controllers/index.ts b/projects/erp-construccion/backend/src/modules/contracts/controllers/index.ts deleted file mode 100644 index b9de7e85e..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/controllers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Contracts Controllers Index - * @module Contracts - */ - -export * from './contract.controller'; -export * from './subcontractor.controller'; diff --git a/projects/erp-construccion/backend/src/modules/contracts/controllers/subcontractor.controller.ts b/projects/erp-construccion/backend/src/modules/contracts/controllers/subcontractor.controller.ts deleted file mode 100644 index a29365fe8..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/controllers/subcontractor.controller.ts +++ /dev/null @@ -1,257 +0,0 @@ -/** - * SubcontractorController - REST API for subcontractors - * - * Endpoints para gesti贸n de subcontratistas. - * - * @module Contracts - * @routes /api/subcontractors - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { SubcontractorService, SubcontractorFilters } from '../services/subcontractor.service'; -import { Subcontractor } from '../entities/subcontractor.entity'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createSubcontractorController(dataSource: DataSource): Router { - const router = Router(); - - // Repositories - const subcontractorRepo = dataSource.getRepository(Subcontractor); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Services - const service = new SubcontractorService(subcontractorRepo); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper for service context - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /api/subcontractors - * List subcontractors with filters - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: SubcontractorFilters = {}; - if (req.query.specialty) filters.specialty = req.query.specialty as SubcontractorFilters['specialty']; - if (req.query.status) filters.status = req.query.status as SubcontractorFilters['status']; - if (req.query.search) filters.search = req.query.search as string; - if (req.query.minRating) filters.minRating = parseFloat(req.query.minRating as string); - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/subcontractors/specialty/:specialty - * Get subcontractors by specialty - */ - router.get('/specialty/:specialty', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const subcontractors = await service.getBySpecialty(getContext(req), req.params.specialty as any); - res.status(200).json({ success: true, data: subcontractors }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/subcontractors/:id - * Get subcontractor by ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const subcontractor = await service.findById(getContext(req), req.params.id); - if (!subcontractor) { - res.status(404).json({ error: 'Not Found', message: 'Subcontractor not found' }); - return; - } - - res.status(200).json({ success: true, data: subcontractor }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/subcontractors - * Create new subcontractor - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'contracts'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const subcontractor = await service.create(getContext(req), req.body); - res.status(201).json({ success: true, data: subcontractor }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /api/subcontractors/:id - * Update subcontractor - */ - router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'contracts'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const subcontractor = await service.update(getContext(req), req.params.id, req.body); - if (!subcontractor) { - res.status(404).json({ error: 'Not Found', message: 'Subcontractor not found' }); - return; - } - - res.status(200).json({ success: true, data: subcontractor }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/subcontractors/:id/rate - * Rate subcontractor - */ - router.post('/:id/rate', authMiddleware.authenticate, authMiddleware.authorize('admin', 'contracts', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const subcontractor = await service.updateRating(getContext(req), req.params.id, req.body.rating); - if (!subcontractor) { - res.status(404).json({ error: 'Not Found', message: 'Subcontractor not found' }); - return; - } - - res.status(200).json({ success: true, data: subcontractor }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/subcontractors/:id/deactivate - * Deactivate subcontractor - */ - router.post('/:id/deactivate', authMiddleware.authenticate, authMiddleware.authorize('admin', 'contracts'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const subcontractor = await service.deactivate(getContext(req), req.params.id); - if (!subcontractor) { - res.status(404).json({ error: 'Not Found', message: 'Subcontractor not found' }); - return; - } - - res.status(200).json({ success: true, data: subcontractor }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/subcontractors/:id/blacklist - * Blacklist subcontractor - */ - router.post('/:id/blacklist', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const subcontractor = await service.blacklist(getContext(req), req.params.id, req.body.reason); - if (!subcontractor) { - res.status(404).json({ error: 'Not Found', message: 'Subcontractor not found' }); - return; - } - - res.status(200).json({ success: true, data: subcontractor }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /api/subcontractors/:id - * Soft delete subcontractor - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await service.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Subcontractor not found' }); - return; - } - - res.status(204).send(); - } catch (error) { - next(error); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/contracts/entities/contract-addendum.entity.ts b/projects/erp-construccion/backend/src/modules/contracts/entities/contract-addendum.entity.ts deleted file mode 100644 index fd3cdd97f..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/entities/contract-addendum.entity.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * ContractAddendum Entity - * Addendas y modificaciones a contratos - * - * @module Contracts - * @table contracts.contract_addendums - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Contract } from './contract.entity'; - -export type AddendumType = 'extension' | 'amount_increase' | 'amount_decrease' | 'scope_change' | 'termination' | 'other'; -export type AddendumStatus = 'draft' | 'review' | 'approved' | 'rejected'; - -@Entity({ schema: 'contracts', name: 'contract_addendums' }) -@Index(['tenantId', 'addendumNumber'], { unique: true }) -export class ContractAddendum { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'contract_id', type: 'uuid' }) - contractId: string; - - @Column({ name: 'addendum_number', type: 'varchar', length: 50 }) - addendumNumber: string; - - @Column({ name: 'addendum_type', type: 'varchar', length: 30 }) - addendumType: AddendumType; - - @Column({ type: 'varchar', length: 255 }) - title: string; - - @Column({ type: 'text' }) - description: string; - - @Column({ name: 'effective_date', type: 'date' }) - effectiveDate: Date; - - // Changes - @Column({ name: 'new_end_date', type: 'date', nullable: true }) - newEndDate: Date; - - @Column({ name: 'amount_change', type: 'decimal', precision: 16, scale: 2, default: 0 }) - amountChange: number; - - @Column({ name: 'new_contract_amount', type: 'decimal', precision: 16, scale: 2, nullable: true }) - newContractAmount: number; - - @Column({ name: 'scope_changes', type: 'text', nullable: true }) - scopeChanges: string; - - // Status - @Column({ type: 'varchar', length: 20, default: 'draft' }) - status: AddendumStatus; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string; - - @Column({ name: 'rejection_reason', type: 'text', nullable: true }) - rejectionReason: string; - - // Document - @Column({ name: 'document_url', type: 'varchar', length: 500, nullable: true }) - documentUrl: string; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Contract, (c) => c.addendums, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'contract_id' }) - contract: Contract; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/contracts/entities/contract.entity.ts b/projects/erp-construccion/backend/src/modules/contracts/entities/contract.entity.ts deleted file mode 100644 index b416f0332..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/entities/contract.entity.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Contract Entity - * Contratos con clientes y subcontratistas - * - * @module Contracts - * @table contracts.contracts - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { ContractAddendum } from './contract-addendum.entity'; - -export type ContractType = 'client' | 'subcontractor'; -export type ContractStatus = 'draft' | 'review' | 'approved' | 'active' | 'completed' | 'terminated'; -export type ClientContractType = 'desarrollo' | 'llave_en_mano' | 'administracion'; - -@Entity({ schema: 'contracts', name: 'contracts' }) -@Index(['tenantId', 'contractNumber'], { unique: true }) -export class Contract { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'project_id', type: 'uuid', nullable: true }) - projectId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid', nullable: true }) - fraccionamientoId: string; - - @Column({ name: 'contract_number', type: 'varchar', length: 50 }) - contractNumber: string; - - @Column({ name: 'contract_type', type: 'varchar', length: 20 }) - contractType: ContractType; - - @Column({ name: 'client_contract_type', type: 'varchar', length: 30, nullable: true }) - clientContractType: ClientContractType; - - @Column({ type: 'varchar', length: 255 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string; - - // Client info (for client contracts) - @Column({ name: 'client_name', type: 'varchar', length: 255, nullable: true }) - clientName: string; - - @Column({ name: 'client_rfc', type: 'varchar', length: 13, nullable: true }) - clientRfc: string; - - @Column({ name: 'client_address', type: 'text', nullable: true }) - clientAddress: string; - - // Subcontractor info (for subcontractor contracts) - @Column({ name: 'subcontractor_id', type: 'uuid', nullable: true }) - subcontractorId: string; - - @Column({ name: 'specialty', type: 'varchar', length: 50, nullable: true }) - specialty: string; - - // Contract terms - @Column({ name: 'start_date', type: 'date' }) - startDate: Date; - - @Column({ name: 'end_date', type: 'date' }) - endDate: Date; - - @Column({ name: 'contract_amount', type: 'decimal', precision: 16, scale: 2 }) - contractAmount: number; - - @Column({ type: 'varchar', length: 3, default: 'MXN' }) - currency: string; - - @Column({ name: 'payment_terms', type: 'text', nullable: true }) - paymentTerms: string; - - @Column({ name: 'retention_percentage', type: 'decimal', precision: 5, scale: 2, default: 5 }) - retentionPercentage: number; - - @Column({ name: 'advance_percentage', type: 'decimal', precision: 5, scale: 2, default: 0 }) - advancePercentage: number; - - // Status and workflow - @Column({ type: 'varchar', length: 20, default: 'draft' }) - status: ContractStatus; - - @Column({ name: 'submitted_at', type: 'timestamptz', nullable: true }) - submittedAt: Date; - - @Column({ name: 'submitted_by', type: 'uuid', nullable: true }) - submittedById: string; - - @Column({ name: 'legal_approved_at', type: 'timestamptz', nullable: true }) - legalApprovedAt: Date; - - @Column({ name: 'legal_approved_by', type: 'uuid', nullable: true }) - legalApprovedById: string; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string; - - @Column({ name: 'signed_at', type: 'timestamptz', nullable: true }) - signedAt: Date; - - @Column({ name: 'terminated_at', type: 'timestamptz', nullable: true }) - terminatedAt: Date; - - @Column({ name: 'termination_reason', type: 'text', nullable: true }) - terminationReason: string; - - // Documents - @Column({ name: 'document_url', type: 'varchar', length: 500, nullable: true }) - documentUrl: string; - - @Column({ name: 'signed_document_url', type: 'varchar', length: 500, nullable: true }) - signedDocumentUrl: string; - - // Progress tracking - @Column({ name: 'progress_percentage', type: 'decimal', precision: 5, scale: 2, default: 0 }) - progressPercentage: number; - - @Column({ name: 'invoiced_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - invoicedAmount: number; - - @Column({ name: 'paid_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - paidAmount: number; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Computed properties - get remainingAmount(): number { - return Number(this.contractAmount) - Number(this.invoicedAmount); - } - - get isExpiring(): boolean { - const thirtyDaysFromNow = new Date(); - thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30); - return this.endDate <= thirtyDaysFromNow && this.status === 'active'; - } - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User; - - @OneToMany(() => ContractAddendum, (a) => a.contract) - addendums: ContractAddendum[]; -} diff --git a/projects/erp-construccion/backend/src/modules/contracts/entities/index.ts b/projects/erp-construccion/backend/src/modules/contracts/entities/index.ts deleted file mode 100644 index 289d6282e..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/entities/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Contracts Entities Index - * @module Contracts - * - * Gesti贸n de contratos y subcontratos (MAI-012) - */ - -export * from './contract.entity'; -export * from './subcontractor.entity'; -export * from './contract-addendum.entity'; diff --git a/projects/erp-construccion/backend/src/modules/contracts/entities/subcontractor.entity.ts b/projects/erp-construccion/backend/src/modules/contracts/entities/subcontractor.entity.ts deleted file mode 100644 index 89dabf34f..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/entities/subcontractor.entity.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Subcontractor Entity - * Cat谩logo de subcontratistas - * - * @module Contracts - * @table contracts.subcontractors - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -export type SubcontractorSpecialty = 'cimentacion' | 'estructura' | 'instalaciones_electricas' | 'instalaciones_hidraulicas' | 'acabados' | 'urbanizacion' | 'carpinteria' | 'herreria' | 'otros'; -export type SubcontractorStatus = 'active' | 'inactive' | 'blacklisted'; - -@Entity({ schema: 'contracts', name: 'subcontractors' }) -@Index(['tenantId', 'code'], { unique: true }) -@Index(['tenantId', 'rfc'], { unique: true }) -export class Subcontractor { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 30 }) - code: string; - - @Column({ name: 'business_name', type: 'varchar', length: 255 }) - businessName: string; - - @Column({ name: 'trade_name', type: 'varchar', length: 255, nullable: true }) - tradeName: string; - - @Column({ type: 'varchar', length: 13 }) - rfc: string; - - @Column({ type: 'text', nullable: true }) - address: string; - - @Column({ type: 'varchar', length: 20, nullable: true }) - phone: string; - - @Column({ type: 'varchar', length: 255, nullable: true }) - email: string; - - @Column({ name: 'contact_name', type: 'varchar', length: 200, nullable: true }) - contactName: string; - - @Column({ name: 'contact_phone', type: 'varchar', length: 20, nullable: true }) - contactPhone: string; - - @Column({ name: 'primary_specialty', type: 'varchar', length: 50 }) - primarySpecialty: SubcontractorSpecialty; - - @Column({ name: 'secondary_specialties', type: 'simple-array', nullable: true }) - secondarySpecialties: string[]; - - @Column({ type: 'varchar', length: 20, default: 'active' }) - status: SubcontractorStatus; - - // Performance tracking - @Column({ name: 'total_contracts', type: 'integer', default: 0 }) - totalContracts: number; - - @Column({ name: 'completed_contracts', type: 'integer', default: 0 }) - completedContracts: number; - - @Column({ name: 'average_rating', type: 'decimal', precision: 3, scale: 2, default: 0 }) - averageRating: number; - - @Column({ name: 'total_incidents', type: 'integer', default: 0 }) - totalIncidents: number; - - // Financial info - @Column({ name: 'bank_name', type: 'varchar', length: 100, nullable: true }) - bankName: string; - - @Column({ name: 'bank_account', type: 'varchar', length: 30, nullable: true }) - bankAccount: string; - - @Column({ type: 'varchar', length: 18, nullable: true }) - clabe: string; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/contracts/services/contract.service.ts b/projects/erp-construccion/backend/src/modules/contracts/services/contract.service.ts deleted file mode 100644 index 1a197aa67..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/services/contract.service.ts +++ /dev/null @@ -1,422 +0,0 @@ -/** - * ContractService - Servicio de gesti贸n de contratos - * - * Gesti贸n de contratos con workflow de aprobaci贸n. - * - * @module Contracts - */ - -import { Repository, FindOptionsWhere, LessThan } from 'typeorm'; -import { Contract, ContractStatus, ContractType } from '../entities/contract.entity'; -import { ContractAddendum } from '../entities/contract-addendum.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateContractDto { - projectId?: string; - fraccionamientoId?: string; - contractType: ContractType; - clientContractType?: string; - name: string; - description?: string; - clientName?: string; - clientRfc?: string; - clientAddress?: string; - subcontractorId?: string; - specialty?: string; - startDate: Date; - endDate: Date; - contractAmount: number; - currency?: string; - paymentTerms?: string; - retentionPercentage?: number; - advancePercentage?: number; - notes?: string; -} - -export interface CreateAddendumDto { - addendumType: string; - title: string; - description: string; - effectiveDate: Date; - newEndDate?: Date; - amountChange?: number; - scopeChanges?: string; - notes?: string; -} - -export interface ContractFilters { - projectId?: string; - fraccionamientoId?: string; - contractType?: ContractType; - subcontractorId?: string; - status?: ContractStatus; - expiringInDays?: number; -} - -export class ContractService { - constructor( - private readonly contractRepository: Repository, - private readonly addendumRepository: Repository - ) {} - - private generateContractNumber(type: ContractType): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - const prefix = type === 'client' ? 'CTR' : 'SUB'; - return `${prefix}-${year}${month}-${random}`; - } - - private generateAddendumNumber(contractNumber: string, sequence: number): string { - return `${contractNumber}-ADD${sequence.toString().padStart(2, '0')}`; - } - - async findWithFilters( - ctx: ServiceContext, - filters: ContractFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.contractRepository - .createQueryBuilder('c') - .leftJoinAndSelect('c.createdBy', 'createdBy') - .leftJoinAndSelect('c.approvedBy', 'approvedBy') - .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('c.deleted_at IS NULL'); - - if (filters.projectId) { - queryBuilder.andWhere('c.project_id = :projectId', { projectId: filters.projectId }); - } - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('c.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.contractType) { - queryBuilder.andWhere('c.contract_type = :contractType', { contractType: filters.contractType }); - } - - if (filters.subcontractorId) { - queryBuilder.andWhere('c.subcontractor_id = :subcontractorId', { - subcontractorId: filters.subcontractorId, - }); - } - - if (filters.status) { - queryBuilder.andWhere('c.status = :status', { status: filters.status }); - } - - if (filters.expiringInDays) { - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + filters.expiringInDays); - queryBuilder.andWhere('c.end_date <= :futureDate', { futureDate }); - queryBuilder.andWhere('c.end_date >= :today', { today: new Date() }); - queryBuilder.andWhere('c.status = :activeStatus', { activeStatus: 'active' }); - } - - queryBuilder - .orderBy('c.created_at', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.contractRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - } - - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.contractRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - relations: ['createdBy', 'approvedBy', 'addendums'], - }); - } - - async create(ctx: ServiceContext, dto: CreateContractDto): Promise { - const contract = this.contractRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - contractNumber: this.generateContractNumber(dto.contractType), - projectId: dto.projectId, - fraccionamientoId: dto.fraccionamientoId, - contractType: dto.contractType, - clientContractType: dto.clientContractType as any, - name: dto.name, - description: dto.description, - clientName: dto.clientName, - clientRfc: dto.clientRfc?.toUpperCase(), - clientAddress: dto.clientAddress, - subcontractorId: dto.subcontractorId, - specialty: dto.specialty, - startDate: dto.startDate, - endDate: dto.endDate, - contractAmount: dto.contractAmount, - currency: dto.currency || 'MXN', - paymentTerms: dto.paymentTerms, - retentionPercentage: dto.retentionPercentage || 5, - advancePercentage: dto.advancePercentage || 0, - notes: dto.notes, - status: 'draft', - }); - - return this.contractRepository.save(contract); - } - - async submitForReview(ctx: ServiceContext, id: string): Promise { - const contract = await this.findById(ctx, id); - if (!contract) { - return null; - } - - if (contract.status !== 'draft') { - throw new Error('Can only submit draft contracts for review'); - } - - contract.status = 'review'; - contract.submittedAt = new Date(); - contract.submittedById = ctx.userId || ''; - contract.updatedById = ctx.userId || ''; - - return this.contractRepository.save(contract); - } - - async approveLegal(ctx: ServiceContext, id: string): Promise { - const contract = await this.findById(ctx, id); - if (!contract) { - return null; - } - - if (contract.status !== 'review') { - throw new Error('Can only approve contracts in review'); - } - - contract.legalApprovedAt = new Date(); - contract.legalApprovedById = ctx.userId || ''; - contract.updatedById = ctx.userId || ''; - - return this.contractRepository.save(contract); - } - - async approve(ctx: ServiceContext, id: string): Promise { - const contract = await this.findById(ctx, id); - if (!contract) { - return null; - } - - if (contract.status !== 'review') { - throw new Error('Can only approve contracts in review'); - } - - contract.status = 'approved'; - contract.approvedAt = new Date(); - contract.approvedById = ctx.userId || ''; - contract.updatedById = ctx.userId || ''; - - return this.contractRepository.save(contract); - } - - async activate(ctx: ServiceContext, id: string, signedDocumentUrl?: string): Promise { - const contract = await this.findById(ctx, id); - if (!contract) { - return null; - } - - if (contract.status !== 'approved') { - throw new Error('Can only activate approved contracts'); - } - - contract.status = 'active'; - contract.signedAt = new Date(); - if (signedDocumentUrl) { - contract.signedDocumentUrl = signedDocumentUrl; - } - contract.updatedById = ctx.userId || ''; - - return this.contractRepository.save(contract); - } - - async complete(ctx: ServiceContext, id: string): Promise { - const contract = await this.findById(ctx, id); - if (!contract) { - return null; - } - - if (contract.status !== 'active') { - throw new Error('Can only complete active contracts'); - } - - contract.status = 'completed'; - contract.updatedById = ctx.userId || ''; - - return this.contractRepository.save(contract); - } - - async terminate(ctx: ServiceContext, id: string, reason: string): Promise { - const contract = await this.findById(ctx, id); - if (!contract) { - return null; - } - - if (contract.status !== 'active') { - throw new Error('Can only terminate active contracts'); - } - - contract.status = 'terminated'; - contract.terminatedAt = new Date(); - contract.terminationReason = reason; - contract.updatedById = ctx.userId || ''; - - return this.contractRepository.save(contract); - } - - async updateProgress( - ctx: ServiceContext, - id: string, - progressPercentage: number, - invoicedAmount?: number, - paidAmount?: number - ): Promise { - const contract = await this.findById(ctx, id); - if (!contract) { - return null; - } - - contract.progressPercentage = progressPercentage; - if (invoicedAmount !== undefined) { - contract.invoicedAmount = invoicedAmount; - } - if (paidAmount !== undefined) { - contract.paidAmount = paidAmount; - } - contract.updatedById = ctx.userId || ''; - - return this.contractRepository.save(contract); - } - - async createAddendum(ctx: ServiceContext, contractId: string, dto: CreateAddendumDto): Promise { - const contract = await this.findWithDetails(ctx, contractId); - if (!contract) { - throw new Error('Contract not found'); - } - - if (contract.status !== 'active') { - throw new Error('Can only add addendums to active contracts'); - } - - const sequence = (contract.addendums?.length || 0) + 1; - - const addendum = this.addendumRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - contractId, - addendumNumber: this.generateAddendumNumber(contract.contractNumber, sequence), - addendumType: dto.addendumType as any, - title: dto.title, - description: dto.description, - effectiveDate: dto.effectiveDate, - newEndDate: dto.newEndDate, - amountChange: dto.amountChange || 0, - newContractAmount: dto.amountChange - ? Number(contract.contractAmount) + Number(dto.amountChange) - : undefined, - scopeChanges: dto.scopeChanges, - notes: dto.notes, - status: 'draft', - }); - - return this.addendumRepository.save(addendum); - } - - async approveAddendum(ctx: ServiceContext, addendumId: string): Promise { - const addendum = await this.addendumRepository.findOne({ - where: { - id: addendumId, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - relations: ['contract'], - }); - - if (!addendum) { - return null; - } - - if (addendum.status !== 'draft' && addendum.status !== 'review') { - throw new Error('Can only approve draft or review addendums'); - } - - addendum.status = 'approved'; - addendum.approvedAt = new Date(); - addendum.approvedById = ctx.userId || ''; - addendum.updatedById = ctx.userId || ''; - - // Apply changes to contract - if (addendum.newEndDate) { - addendum.contract.endDate = addendum.newEndDate; - } - if (addendum.newContractAmount) { - addendum.contract.contractAmount = addendum.newContractAmount; - } - await this.contractRepository.save(addendum.contract); - - return this.addendumRepository.save(addendum); - } - - async softDelete(ctx: ServiceContext, id: string): Promise { - const contract = await this.findById(ctx, id); - if (!contract) { - return false; - } - - if (contract.status === 'active') { - throw new Error('Cannot delete active contracts'); - } - - await this.contractRepository.update( - { id, tenantId: ctx.tenantId } as FindOptionsWhere, - { deletedAt: new Date(), deletedById: ctx.userId || '' } - ); - - return true; - } - - async getExpiringContracts(ctx: ServiceContext, days: number = 30): Promise { - const futureDate = new Date(); - futureDate.setDate(futureDate.getDate() + days); - - return this.contractRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'active' as ContractStatus, - endDate: LessThan(futureDate), - deletedAt: null, - } as unknown as FindOptionsWhere, - order: { endDate: 'ASC' }, - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/contracts/services/index.ts b/projects/erp-construccion/backend/src/modules/contracts/services/index.ts deleted file mode 100644 index 1905ca0b9..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/services/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Contracts Services Index - * @module Contracts - */ - -export * from './contract.service'; -export * from './subcontractor.service'; diff --git a/projects/erp-construccion/backend/src/modules/contracts/services/subcontractor.service.ts b/projects/erp-construccion/backend/src/modules/contracts/services/subcontractor.service.ts deleted file mode 100644 index eea8d86f4..000000000 --- a/projects/erp-construccion/backend/src/modules/contracts/services/subcontractor.service.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * SubcontractorService - Servicio de gesti贸n de subcontratistas - * - * Cat谩logo de subcontratistas con evaluaciones. - * - * @module Contracts - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { Subcontractor, SubcontractorStatus, SubcontractorSpecialty } from '../entities/subcontractor.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateSubcontractorDto { - businessName: string; - tradeName?: string; - rfc: string; - address?: string; - phone?: string; - email?: string; - contactName?: string; - contactPhone?: string; - primarySpecialty: SubcontractorSpecialty; - secondarySpecialties?: string[]; - bankName?: string; - bankAccount?: string; - clabe?: string; - notes?: string; -} - -export interface UpdateSubcontractorDto { - tradeName?: string; - address?: string; - phone?: string; - email?: string; - contactName?: string; - contactPhone?: string; - secondarySpecialties?: string[]; - bankName?: string; - bankAccount?: string; - clabe?: string; - notes?: string; -} - -export interface SubcontractorFilters { - specialty?: SubcontractorSpecialty; - status?: SubcontractorStatus; - search?: string; - minRating?: number; -} - -export class SubcontractorService { - constructor(private readonly subcontractorRepository: Repository) {} - - private generateCode(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - return `SC-${year}-${random}`; - } - - async findWithFilters( - ctx: ServiceContext, - filters: SubcontractorFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.subcontractorRepository - .createQueryBuilder('sc') - .leftJoinAndSelect('sc.createdBy', 'createdBy') - .where('sc.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('sc.deleted_at IS NULL'); - - if (filters.specialty) { - queryBuilder.andWhere('sc.primary_specialty = :specialty', { specialty: filters.specialty }); - } - - if (filters.status) { - queryBuilder.andWhere('sc.status = :status', { status: filters.status }); - } - - if (filters.search) { - queryBuilder.andWhere( - '(sc.business_name ILIKE :search OR sc.trade_name ILIKE :search OR sc.rfc ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - if (filters.minRating !== undefined) { - queryBuilder.andWhere('sc.average_rating >= :minRating', { minRating: filters.minRating }); - } - - queryBuilder - .orderBy('sc.business_name', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.subcontractorRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - } - - async findByRfc(ctx: ServiceContext, rfc: string): Promise { - return this.subcontractorRepository.findOne({ - where: { - rfc: rfc.toUpperCase(), - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - } - - async create(ctx: ServiceContext, dto: CreateSubcontractorDto): Promise { - // Check for existing RFC - const existing = await this.findByRfc(ctx, dto.rfc); - if (existing) { - throw new Error('A subcontractor with this RFC already exists'); - } - - const subcontractor = this.subcontractorRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - code: this.generateCode(), - businessName: dto.businessName, - tradeName: dto.tradeName, - rfc: dto.rfc.toUpperCase(), - address: dto.address, - phone: dto.phone, - email: dto.email, - contactName: dto.contactName, - contactPhone: dto.contactPhone, - primarySpecialty: dto.primarySpecialty, - secondarySpecialties: dto.secondarySpecialties, - bankName: dto.bankName, - bankAccount: dto.bankAccount, - clabe: dto.clabe, - notes: dto.notes, - status: 'active', - }); - - return this.subcontractorRepository.save(subcontractor); - } - - async update(ctx: ServiceContext, id: string, dto: UpdateSubcontractorDto): Promise { - const subcontractor = await this.findById(ctx, id); - if (!subcontractor) { - return null; - } - - Object.assign(subcontractor, { - ...dto, - updatedById: ctx.userId || '', - }); - - return this.subcontractorRepository.save(subcontractor); - } - - async updateRating(ctx: ServiceContext, id: string, rating: number): Promise { - const subcontractor = await this.findById(ctx, id); - if (!subcontractor) { - return null; - } - - // Calculate new average rating - const totalRatings = subcontractor.completedContracts; - const currentTotal = subcontractor.averageRating * totalRatings; - const newTotal = currentTotal + rating; - subcontractor.averageRating = newTotal / (totalRatings + 1); - subcontractor.updatedById = ctx.userId || ''; - - return this.subcontractorRepository.save(subcontractor); - } - - async incrementContracts(ctx: ServiceContext, id: string, completed: boolean = false): Promise { - const subcontractor = await this.findById(ctx, id); - if (!subcontractor) { - return null; - } - - subcontractor.totalContracts += 1; - if (completed) { - subcontractor.completedContracts += 1; - } - subcontractor.updatedById = ctx.userId || ''; - - return this.subcontractorRepository.save(subcontractor); - } - - async incrementIncidents(ctx: ServiceContext, id: string): Promise { - const subcontractor = await this.findById(ctx, id); - if (!subcontractor) { - return null; - } - - subcontractor.totalIncidents += 1; - subcontractor.updatedById = ctx.userId || ''; - - return this.subcontractorRepository.save(subcontractor); - } - - async deactivate(ctx: ServiceContext, id: string): Promise { - const subcontractor = await this.findById(ctx, id); - if (!subcontractor) { - return null; - } - - subcontractor.status = 'inactive'; - subcontractor.updatedById = ctx.userId || ''; - - return this.subcontractorRepository.save(subcontractor); - } - - async blacklist(ctx: ServiceContext, id: string, reason: string): Promise { - const subcontractor = await this.findById(ctx, id); - if (!subcontractor) { - return null; - } - - subcontractor.status = 'blacklisted'; - subcontractor.notes = `${subcontractor.notes || ''}\n[BLACKLISTED] ${reason}`; - subcontractor.updatedById = ctx.userId || ''; - - return this.subcontractorRepository.save(subcontractor); - } - - async softDelete(ctx: ServiceContext, id: string): Promise { - const subcontractor = await this.findById(ctx, id); - if (!subcontractor) { - return false; - } - - await this.subcontractorRepository.update( - { id, tenantId: ctx.tenantId } as FindOptionsWhere, - { deletedAt: new Date(), deletedById: ctx.userId || '' } - ); - - return true; - } - - async getBySpecialty(ctx: ServiceContext, specialty: SubcontractorSpecialty): Promise { - return this.subcontractorRepository.find({ - where: { - tenantId: ctx.tenantId, - primarySpecialty: specialty, - status: 'active' as SubcontractorStatus, - deletedAt: null, - } as unknown as FindOptionsWhere, - order: { averageRating: 'DESC' }, - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/core/entities/index.ts b/projects/erp-construccion/backend/src/modules/core/entities/index.ts deleted file mode 100644 index e828c0e99..000000000 --- a/projects/erp-construccion/backend/src/modules/core/entities/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Core Entities Index - */ - -export { Tenant } from './tenant.entity'; -export { User } from './user.entity'; diff --git a/projects/erp-construccion/backend/src/modules/core/entities/tenant.entity.ts b/projects/erp-construccion/backend/src/modules/core/entities/tenant.entity.ts deleted file mode 100644 index ccb8d0eb0..000000000 --- a/projects/erp-construccion/backend/src/modules/core/entities/tenant.entity.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Tenant Entity - * Entidad para multi-tenancy - * - * @module Core - * @table core.tenants - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToMany, - Index, -} from 'typeorm'; -import { User } from './user.entity'; - -@Entity({ schema: 'auth', name: 'tenants' }) -export class Tenant { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 50, unique: true }) - @Index() - code: string; - - @Column({ type: 'varchar', length: 200 }) - name: string; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @Column({ type: 'jsonb', default: {} }) - settings: Record; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - // Relations - @OneToMany(() => User, (user) => user.tenant) - users: User[]; -} diff --git a/projects/erp-construccion/backend/src/modules/core/entities/user.entity.ts b/projects/erp-construccion/backend/src/modules/core/entities/user.entity.ts deleted file mode 100644 index 9ebe84379..000000000 --- a/projects/erp-construccion/backend/src/modules/core/entities/user.entity.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * User Entity - * Entidad de usuarios del sistema - * - * @module Core - * @table core.users - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from './tenant.entity'; - -@Entity({ schema: 'auth', name: 'users' }) -@Index(['tenantId', 'email'], { unique: true }) -export class User { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid', nullable: true }) - tenantId: string; - - @Column({ type: 'varchar', length: 255 }) - email: string; - - @Column({ type: 'varchar', length: 100, nullable: true }) - username: string; - - @Column({ name: 'password_hash', type: 'varchar', length: 255, select: false }) - passwordHash: string; - - @Column({ name: 'first_name', type: 'varchar', length: 100, nullable: true }) - firstName: string; - - @Column({ name: 'last_name', type: 'varchar', length: 100, nullable: true }) - lastName: string; - - @Column({ type: 'varchar', array: true, default: ['viewer'] }) - roles: string[]; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @Column({ name: 'last_login_at', type: 'timestamptz', nullable: true }) - lastLoginAt: Date; - - @Column({ name: 'default_tenant_id', type: 'uuid', nullable: true }) - defaultTenantId: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - // Placeholder para relaci贸n de roles (se implementar谩 en ST-004) - userRoles?: { role: { code: string } }[]; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant, (tenant) => tenant.users) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - // Computed property - get fullName(): string { - return [this.firstName, this.lastName].filter(Boolean).join(' ') || this.email; - } -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/controllers/anticipo.controller.ts b/projects/erp-construccion/backend/src/modules/estimates/controllers/anticipo.controller.ts deleted file mode 100644 index b599d83f5..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/controllers/anticipo.controller.ts +++ /dev/null @@ -1,324 +0,0 @@ -/** - * AnticipoController - Controller de anticipos de obra - * - * Endpoints REST para gesti贸n de anticipos de contratos de construcci贸n. - * - * @module Estimates - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { AnticipoService, CreateAnticipoDto, AnticipoFilters } from '../services/anticipo.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Anticipo } from '../entities/anticipo.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createAnticipoController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const anticipoRepository = dataSource.getRepository(Anticipo); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const anticipoService = new AnticipoService(anticipoRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /anticipos - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: AnticipoFilters = {}; - if (req.query.contratoId) filters.contratoId = req.query.contratoId as string; - if (req.query.advanceType) filters.advanceType = req.query.advanceType as any; - if (req.query.isFullyAmortized !== undefined) { - filters.isFullyAmortized = req.query.isFullyAmortized === 'true'; - } - if (req.query.startDate) filters.startDate = new Date(req.query.startDate as string); - if (req.query.endDate) filters.endDate = new Date(req.query.endDate as string); - - const result = await anticipoService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /anticipos/stats - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: AnticipoFilters = {}; - if (req.query.contratoId) filters.contratoId = req.query.contratoId as string; - if (req.query.advanceType) filters.advanceType = req.query.advanceType as any; - - const stats = await anticipoService.getStats(getContext(req), filters); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /anticipos/contrato/:contratoId - */ - router.get('/contrato/:contratoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await anticipoService.findByContrato( - getContext(req), - req.params.contratoId, - page, - limit - ); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /anticipos/:id - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const anticipo = await anticipoService.findById(getContext(req), req.params.id); - if (!anticipo) { - res.status(404).json({ error: 'Not Found', message: 'Anticipo not found' }); - return; - } - - res.status(200).json({ success: true, data: anticipo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /anticipos - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateAnticipoDto = req.body; - - if (!dto.contratoId || !dto.advanceType || !dto.advanceNumber) { - res.status(400).json({ - error: 'Bad Request', - message: 'contratoId, advanceType, and advanceNumber are required', - }); - return; - } - - const anticipo = await anticipoService.createAnticipo(getContext(req), dto); - res.status(201).json({ success: true, data: anticipo }); - } catch (error) { - if (error instanceof Error && error.message.includes('no coincide')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /anticipos/:id/approve - */ - router.post('/:id/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const approvedById = req.user?.sub; - if (!approvedById) { - res.status(400).json({ error: 'Bad Request', message: 'User ID required' }); - return; - } - - const anticipo = await anticipoService.approveAnticipo( - getContext(req), - req.params.id, - approvedById, - req.body.notes - ); - - res.status(200).json({ success: true, data: anticipo }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Anticipo no encontrado') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('aprobado')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * POST /anticipos/:id/pay - */ - router.post('/:id/pay', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'accountant'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { paymentReference, paidAt } = req.body; - if (!paymentReference) { - res.status(400).json({ error: 'Bad Request', message: 'paymentReference is required' }); - return; - } - - const anticipo = await anticipoService.markPaid( - getContext(req), - req.params.id, - paymentReference, - paidAt ? new Date(paidAt) : undefined - ); - - res.status(200).json({ success: true, data: anticipo }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Anticipo no encontrado') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('aprobado') || error.message.includes('pagado')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * POST /anticipos/:id/amortize - */ - router.post('/:id/amortize', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { amount } = req.body; - if (amount === undefined || amount <= 0) { - res.status(400).json({ error: 'Bad Request', message: 'Valid amount is required' }); - return; - } - - const anticipo = await anticipoService.updateAmortization( - getContext(req), - req.params.id, - amount - ); - - res.status(200).json({ success: true, data: anticipo }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Anticipo no encontrado') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('amortizado') || error.message.includes('excede')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * DELETE /anticipos/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await anticipoService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Anticipo not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Anticipo deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createAnticipoController; diff --git a/projects/erp-construccion/backend/src/modules/estimates/controllers/estimacion.controller.ts b/projects/erp-construccion/backend/src/modules/estimates/controllers/estimacion.controller.ts deleted file mode 100644 index dd59cf7a6..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/controllers/estimacion.controller.ts +++ /dev/null @@ -1,408 +0,0 @@ -/** - * EstimacionController - Controller de estimaciones de obra - * - * Endpoints REST para gesti贸n de estimaciones peri贸dicas. - * Incluye workflow de aprobaci贸n: draft -> submitted -> reviewed -> approved -> invoiced -> paid - * - * @module Estimates - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - EstimacionService, - CreateEstimacionDto, - AddConceptoDto, - AddGeneradorDto, - EstimacionFilters, -} from '../services/estimacion.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Estimacion } from '../entities/estimacion.entity'; -import { EstimacionConcepto } from '../entities/estimacion-concepto.entity'; -import { Generador } from '../entities/generador.entity'; -import { EstimacionWorkflow } from '../entities/estimacion-workflow.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de estimaciones - */ -export function createEstimacionController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const estimacionRepository = dataSource.getRepository(Estimacion); - const conceptoRepository = dataSource.getRepository(EstimacionConcepto); - const generadorRepository = dataSource.getRepository(Generador); - const workflowRepository = dataSource.getRepository(EstimacionWorkflow); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const estimacionService = new EstimacionService( - estimacionRepository, - conceptoRepository, - generadorRepository, - workflowRepository, - dataSource - ); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /estimaciones - * Listar estimaciones con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: EstimacionFilters = { - contratoId: req.query.contratoId as string, - fraccionamientoId: req.query.fraccionamientoId as string, - status: req.query.status as any, - periodFrom: req.query.periodFrom ? new Date(req.query.periodFrom as string) : undefined, - periodTo: req.query.periodTo ? new Date(req.query.periodTo as string) : undefined, - }; - - const result = await estimacionService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /estimaciones/summary/:contratoId - * Obtener resumen de estimaciones por contrato - */ - router.get('/summary/:contratoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const summary = await estimacionService.getContractSummary(getContext(req), req.params.contratoId); - res.status(200).json({ success: true, data: summary }); - } catch (error) { - next(error); - } - }); - - /** - * GET /estimaciones/:id - * Obtener estimaci贸n con detalles completos - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const estimacion = await estimacionService.findWithDetails(getContext(req), req.params.id); - if (!estimacion) { - res.status(404).json({ error: 'Not Found', message: 'Estimate not found' }); - return; - } - - res.status(200).json({ success: true, data: estimacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /estimaciones - * Crear estimaci贸n - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateEstimacionDto = req.body; - - if (!dto.contratoId || !dto.fraccionamientoId || !dto.periodStart || !dto.periodEnd) { - res.status(400).json({ error: 'Bad Request', message: 'contratoId, fraccionamientoId, periodStart and periodEnd are required' }); - return; - } - - const estimacion = await estimacionService.createEstimacion(getContext(req), dto); - res.status(201).json({ success: true, data: estimacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /estimaciones/:id/conceptos - * Agregar concepto a estimaci贸n - */ - router.post('/:id/conceptos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: AddConceptoDto = req.body; - - if (!dto.conceptoId || dto.quantityCurrent === undefined || dto.unitPrice === undefined) { - res.status(400).json({ error: 'Bad Request', message: 'conceptoId, quantityCurrent and unitPrice are required' }); - return; - } - - const concepto = await estimacionService.addConcepto(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: concepto }); - } catch (error) { - if (error instanceof Error && error.message.includes('non-draft')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /estimaciones/conceptos/:conceptoId/generadores - * Agregar generador a concepto de estimaci贸n - */ - router.post('/conceptos/:conceptoId/generadores', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: AddGeneradorDto = req.body; - - if (!dto.generatorNumber || dto.quantity === undefined) { - res.status(400).json({ error: 'Bad Request', message: 'generatorNumber and quantity are required' }); - return; - } - - const generador = await estimacionService.addGenerador(getContext(req), req.params.conceptoId, dto); - res.status(201).json({ success: true, data: generador }); - } catch (error) { - if (error instanceof Error && error.message === 'Concepto not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /estimaciones/:id/submit - * Enviar estimaci贸n para revisi贸n - */ - router.post('/:id/submit', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const estimacion = await estimacionService.submit(getContext(req), req.params.id); - if (!estimacion) { - res.status(400).json({ error: 'Bad Request', message: 'Cannot submit this estimate' }); - return; - } - - res.status(200).json({ success: true, data: estimacion, message: 'Estimate submitted for review' }); - } catch (error) { - if (error instanceof Error && error.message.includes('Invalid status')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /estimaciones/:id/review - * Revisar estimaci贸n - */ - router.post('/:id/review', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const estimacion = await estimacionService.review(getContext(req), req.params.id); - if (!estimacion) { - res.status(400).json({ error: 'Bad Request', message: 'Cannot review this estimate' }); - return; - } - - res.status(200).json({ success: true, data: estimacion, message: 'Estimate reviewed' }); - } catch (error) { - if (error instanceof Error && error.message.includes('Invalid status')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /estimaciones/:id/approve - * Aprobar estimaci贸n - */ - router.post('/:id/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const estimacion = await estimacionService.approve(getContext(req), req.params.id); - if (!estimacion) { - res.status(400).json({ error: 'Bad Request', message: 'Cannot approve this estimate' }); - return; - } - - res.status(200).json({ success: true, data: estimacion, message: 'Estimate approved' }); - } catch (error) { - if (error instanceof Error && error.message.includes('Invalid status')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /estimaciones/:id/reject - * Rechazar estimaci贸n - */ - router.post('/:id/reject', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { reason } = req.body; - if (!reason) { - res.status(400).json({ error: 'Bad Request', message: 'reason is required' }); - return; - } - - const estimacion = await estimacionService.reject(getContext(req), req.params.id, reason); - if (!estimacion) { - res.status(400).json({ error: 'Bad Request', message: 'Cannot reject this estimate' }); - return; - } - - res.status(200).json({ success: true, data: estimacion, message: 'Estimate rejected' }); - } catch (error) { - if (error instanceof Error && error.message.includes('Invalid status')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /estimaciones/:id/recalculate - * Recalcular totales de estimaci贸n - */ - router.post('/:id/recalculate', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - await estimacionService.recalculateTotals(getContext(req), req.params.id); - const estimacion = await estimacionService.findWithDetails(getContext(req), req.params.id); - - res.status(200).json({ success: true, data: estimacion, message: 'Totals recalculated' }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /estimaciones/:id - * Eliminar estimaci贸n (solo draft) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const estimacion = await estimacionService.findById(getContext(req), req.params.id); - if (!estimacion) { - res.status(404).json({ error: 'Not Found', message: 'Estimate not found' }); - return; - } - - if (estimacion.status !== 'draft') { - res.status(400).json({ error: 'Bad Request', message: 'Only draft estimates can be deleted' }); - return; - } - - const deleted = await estimacionService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Estimate not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Estimate deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createEstimacionController; diff --git a/projects/erp-construccion/backend/src/modules/estimates/controllers/fondo-garantia.controller.ts b/projects/erp-construccion/backend/src/modules/estimates/controllers/fondo-garantia.controller.ts deleted file mode 100644 index b0f98f45c..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/controllers/fondo-garantia.controller.ts +++ /dev/null @@ -1,299 +0,0 @@ -/** - * FondoGarantiaController - Controller de fondos de garant铆a - * - * Endpoints REST para gesti贸n de fondos de garant铆a de contratos. - * - * @module Estimates - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { FondoGarantiaService, CreateFondoGarantiaDto, ReleaseFondoDto } from '../services/fondo-garantia.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { FondoGarantia } from '../entities/fondo-garantia.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createFondoGarantiaController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const fondoGarantiaRepository = dataSource.getRepository(FondoGarantia); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const fondoGarantiaService = new FondoGarantiaService(fondoGarantiaRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /fondos-garantia - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await fondoGarantiaService.findAll(getContext(req), { page, limit }); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /fondos-garantia/stats - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await fondoGarantiaService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /fondos-garantia/contrato/:contratoId - */ - router.get('/contrato/:contratoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fondo = await fondoGarantiaService.findByContrato(getContext(req), req.params.contratoId); - if (!fondo) { - res.status(404).json({ error: 'Not Found', message: 'Guarantee fund not found' }); - return; - } - - res.status(200).json({ success: true, data: fondo }); - } catch (error) { - next(error); - } - }); - - /** - * GET /fondos-garantia/:id - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fondo = await fondoGarantiaService.findById(getContext(req), req.params.id); - if (!fondo) { - res.status(404).json({ error: 'Not Found', message: 'Guarantee fund not found' }); - return; - } - - res.status(200).json({ success: true, data: fondo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /fondos-garantia - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateFondoGarantiaDto = req.body; - if (!dto.contratoId) { - res.status(400).json({ error: 'Bad Request', message: 'contratoId is required' }); - return; - } - - const fondo = await fondoGarantiaService.createOrUpdate(getContext(req), dto); - res.status(201).json({ success: true, data: fondo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /fondos-garantia/:contratoId/accumulate - */ - router.post('/:contratoId/accumulate', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { amount } = req.body; - if (amount === undefined || amount <= 0) { - res.status(400).json({ error: 'Bad Request', message: 'Valid amount is required' }); - return; - } - - const fondo = await fondoGarantiaService.addAccumulation( - getContext(req), - req.params.contratoId, - amount - ); - - res.status(200).json({ success: true, data: fondo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /fondos-garantia/:contratoId/release - */ - router.post('/:contratoId/release', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const releasedById = req.user?.sub; - if (!releasedById) { - res.status(400).json({ error: 'Bad Request', message: 'User ID required' }); - return; - } - - const { amount, releaseDate, notes } = req.body; - if (amount === undefined || amount <= 0) { - res.status(400).json({ error: 'Bad Request', message: 'Valid amount is required' }); - return; - } - - const dto: ReleaseFondoDto = { - amount, - releasedById, - releaseDate: releaseDate ? new Date(releaseDate) : undefined, - notes, - }; - - const fondo = await fondoGarantiaService.releasePartial( - getContext(req), - req.params.contratoId, - dto - ); - - res.status(200).json({ success: true, data: fondo }); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('no encontrado')) { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('excede')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * POST /fondos-garantia/:contratoId/release-full - */ - router.post('/:contratoId/release-full', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const releasedById = req.user?.sub; - if (!releasedById) { - res.status(400).json({ error: 'Bad Request', message: 'User ID required' }); - return; - } - - const { releaseDate } = req.body; - - const fondo = await fondoGarantiaService.releaseFull( - getContext(req), - req.params.contratoId, - releasedById, - releaseDate ? new Date(releaseDate) : undefined - ); - - res.status(200).json({ success: true, data: fondo }); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('no encontrado')) { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('pendiente')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * DELETE /fondos-garantia/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await fondoGarantiaService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Guarantee fund not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Guarantee fund deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createFondoGarantiaController; diff --git a/projects/erp-construccion/backend/src/modules/estimates/controllers/index.ts b/projects/erp-construccion/backend/src/modules/estimates/controllers/index.ts deleted file mode 100644 index 888567b18..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/controllers/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Estimates Controllers Index - * @module Estimates - */ - -export { createEstimacionController } from './estimacion.controller'; -export { createAnticipoController } from './anticipo.controller'; -export { createFondoGarantiaController } from './fondo-garantia.controller'; -export { createRetencionController } from './retencion.controller'; diff --git a/projects/erp-construccion/backend/src/modules/estimates/controllers/retencion.controller.ts b/projects/erp-construccion/backend/src/modules/estimates/controllers/retencion.controller.ts deleted file mode 100644 index b82e07636..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/controllers/retencion.controller.ts +++ /dev/null @@ -1,241 +0,0 @@ -/** - * RetencionController - Controller de retenciones - * - * Endpoints REST para gesti贸n de retenciones de estimaciones. - * - * @module Estimates - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { RetencionService, CreateRetencionDto, ReleaseRetencionDto, RetencionFilters } from '../services/retencion.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Retencion } from '../entities/retencion.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createRetencionController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const retencionRepository = dataSource.getRepository(Retencion); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const retencionService = new RetencionService(retencionRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /retenciones - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: RetencionFilters = {}; - if (req.query.estimacionId) filters.estimacionId = req.query.estimacionId as string; - if (req.query.contratoId) filters.contratoId = req.query.contratoId as string; - if (req.query.retentionType) filters.retentionType = req.query.retentionType as any; - if (req.query.isReleased !== undefined) { - filters.isReleased = req.query.isReleased === 'true'; - } - if (req.query.dateFrom) filters.dateFrom = new Date(req.query.dateFrom as string); - if (req.query.dateTo) filters.dateTo = new Date(req.query.dateTo as string); - - const result = await retencionService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /retenciones/stats - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: RetencionFilters = {}; - if (req.query.contratoId) filters.contratoId = req.query.contratoId as string; - - const stats = await retencionService.getStats(getContext(req), filters); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /retenciones/contrato/:contratoId/totals - */ - router.get('/contrato/:contratoId/totals', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const totals = await retencionService.getTotalByContrato(getContext(req), req.params.contratoId); - res.status(200).json({ success: true, data: totals }); - } catch (error) { - next(error); - } - }); - - /** - * GET /retenciones/estimacion/:estimacionId - */ - router.get('/estimacion/:estimacionId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const retenciones = await retencionService.findByEstimacion(getContext(req), req.params.estimacionId); - res.status(200).json({ success: true, data: retenciones }); - } catch (error) { - next(error); - } - }); - - /** - * GET /retenciones/:id - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const retencion = await retencionService.findById(getContext(req), req.params.id); - if (!retencion) { - res.status(404).json({ error: 'Not Found', message: 'Retention not found' }); - return; - } - - res.status(200).json({ success: true, data: retencion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /retenciones - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateRetencionDto = req.body; - if (!dto.estimacionId || !dto.retentionType || !dto.description || dto.amount === undefined) { - res.status(400).json({ - error: 'Bad Request', - message: 'estimacionId, retentionType, description, and amount are required', - }); - return; - } - - const retencion = await retencionService.createRetencion(getContext(req), dto); - res.status(201).json({ success: true, data: retencion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /retenciones/:id/release - */ - router.post('/:id/release', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: ReleaseRetencionDto = req.body; - if (dto.releasedAmount === undefined || dto.releasedAmount <= 0) { - res.status(400).json({ error: 'Bad Request', message: 'Valid releasedAmount is required' }); - return; - } - - const retencion = await retencionService.releaseRetencion(getContext(req), req.params.id, dto); - if (!retencion) { - res.status(404).json({ error: 'Not Found', message: 'Retention not found' }); - return; - } - - res.status(200).json({ success: true, data: retencion }); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('already released') || error.message.includes('exceed')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * DELETE /retenciones/:id - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await retencionService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Retention not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Retention deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createRetencionController; diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/amortizacion.entity.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/amortizacion.entity.ts deleted file mode 100644 index d413c5f30..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/amortizacion.entity.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Amortizacion Entity - * Amortizaciones de anticipos por estimacion - * - * @module Estimates - * @table estimates.amortizaciones - * @ddl schemas/04-estimates-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Anticipo } from './anticipo.entity'; -import { Estimacion } from './estimacion.entity'; - -@Entity({ schema: 'estimates', name: 'amortizaciones' }) -@Index(['anticipoId', 'estimacionId'], { unique: true }) -@Index(['tenantId']) -@Index(['anticipoId']) -@Index(['estimacionId']) -export class Amortizacion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'anticipo_id', type: 'uuid' }) - anticipoId: string; - - @Column({ name: 'estimacion_id', type: 'uuid' }) - estimacionId: string; - - @Column({ type: 'decimal', precision: 16, scale: 2 }) - amount: number; - - @Column({ name: 'amortization_date', type: 'date' }) - amortizationDate: Date; - - @Column({ type: 'text', nullable: true }) - notes: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Anticipo, (a) => a.amortizaciones) - @JoinColumn({ name: 'anticipo_id' }) - anticipo: Anticipo; - - @ManyToOne(() => Estimacion, (e) => e.amortizaciones) - @JoinColumn({ name: 'estimacion_id' }) - estimacion: Estimacion; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/anticipo.entity.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/anticipo.entity.ts deleted file mode 100644 index 0938f0a26..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/anticipo.entity.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Anticipo Entity - * Anticipos otorgados a subcontratistas - * - * @module Estimates - * @table estimates.anticipos - * @ddl schemas/04-estimates-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Amortizacion } from './amortizacion.entity'; - -export type AdvanceType = 'initial' | 'progress' | 'materials'; - -@Entity({ schema: 'estimates', name: 'anticipos' }) -@Index(['tenantId', 'advanceNumber'], { unique: true }) -@Index(['tenantId']) -@Index(['contratoId']) -@Index(['advanceType']) -export class Anticipo { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'contrato_id', type: 'uuid' }) - contratoId: string; - - @Column({ - name: 'advance_type', - type: 'enum', - enum: ['initial', 'progress', 'materials'], - enumName: 'estimates.advance_type', - default: 'initial', - }) - advanceType: AdvanceType; - - @Column({ name: 'advance_number', type: 'varchar', length: 30 }) - advanceNumber: string; - - @Column({ name: 'advance_date', type: 'date' }) - advanceDate: Date; - - @Column({ name: 'gross_amount', type: 'decimal', precision: 16, scale: 2 }) - grossAmount: number; - - @Column({ name: 'tax_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - taxAmount: number; - - @Column({ name: 'net_amount', type: 'decimal', precision: 16, scale: 2 }) - netAmount: number; - - @Column({ name: 'amortization_percentage', type: 'decimal', precision: 5, scale: 2, default: 0 }) - amortizationPercentage: number; - - @Column({ name: 'amortized_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - amortizedAmount: number; - - // Columna calculada (GENERATED ALWAYS AS) - solo lectura - @Column({ - name: 'pending_amount', - type: 'decimal', - precision: 16, - scale: 2, - insert: false, - update: false, - }) - pendingAmount: number; - - @Column({ name: 'is_fully_amortized', type: 'boolean', default: false }) - isFullyAmortized: boolean; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date | null; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string | null; - - @Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) - paidAt: Date | null; - - @Column({ name: 'payment_reference', type: 'varchar', length: 100, nullable: true }) - paymentReference: string | null; - - @Column({ type: 'text', nullable: true }) - notes: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @OneToMany(() => Amortizacion, (a) => a.anticipo) - amortizaciones: Amortizacion[]; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion-concepto.entity.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion-concepto.entity.ts deleted file mode 100644 index 0d2aaa707..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion-concepto.entity.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * EstimacionConcepto Entity - * Lineas de concepto por estimacion - * - * @module Estimates - * @table estimates.estimacion_conceptos - * @ddl schemas/04-estimates-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Concepto } from '../../budgets/entities/concepto.entity'; -import { Estimacion } from './estimacion.entity'; -import { Generador } from './generador.entity'; - -@Entity({ schema: 'estimates', name: 'estimacion_conceptos' }) -@Index(['estimacionId', 'conceptoId'], { unique: true }) -@Index(['tenantId']) -@Index(['estimacionId']) -@Index(['conceptoId']) -export class EstimacionConcepto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'estimacion_id', type: 'uuid' }) - estimacionId: string; - - @Column({ name: 'concepto_id', type: 'uuid' }) - conceptoId: string; - - @Column({ name: 'contrato_partida_id', type: 'uuid', nullable: true }) - contratoPartidaId: string | null; - - @Column({ name: 'quantity_contract', type: 'decimal', precision: 12, scale: 4, default: 0 }) - quantityContract: number; - - @Column({ name: 'quantity_previous', type: 'decimal', precision: 12, scale: 4, default: 0 }) - quantityPrevious: number; - - @Column({ name: 'quantity_current', type: 'decimal', precision: 12, scale: 4, default: 0 }) - quantityCurrent: number; - - // Columna calculada (GENERATED ALWAYS AS) - solo lectura - @Column({ - name: 'quantity_accumulated', - type: 'decimal', - precision: 12, - scale: 4, - insert: false, - update: false, - }) - quantityAccumulated: number; - - @Column({ name: 'unit_price', type: 'decimal', precision: 12, scale: 4, default: 0 }) - unitPrice: number; - - // Columna calculada (GENERATED ALWAYS AS) - solo lectura - @Column({ - name: 'amount_current', - type: 'decimal', - precision: 14, - scale: 2, - insert: false, - update: false, - }) - amountCurrent: number; - - @Column({ type: 'text', nullable: true }) - notes: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Estimacion, (e) => e.conceptos) - @JoinColumn({ name: 'estimacion_id' }) - estimacion: Estimacion; - - @ManyToOne(() => Concepto) - @JoinColumn({ name: 'concepto_id' }) - concepto: Concepto; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @OneToMany(() => Generador, (g) => g.estimacionConcepto) - generadores: Generador[]; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion-workflow.entity.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion-workflow.entity.ts deleted file mode 100644 index 20c59f37b..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion-workflow.entity.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * EstimacionWorkflow Entity - * Historial de workflow de estimaciones - * - * @module Estimates - * @table estimates.estimacion_workflow - * @ddl schemas/04-estimates-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Estimacion, EstimateStatus } from './estimacion.entity'; - -@Entity({ schema: 'estimates', name: 'estimacion_workflow' }) -@Index(['tenantId']) -@Index(['estimacionId']) -export class EstimacionWorkflow { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'estimacion_id', type: 'uuid' }) - estimacionId: string; - - @Column({ - name: 'from_status', - type: 'enum', - enum: ['draft', 'submitted', 'reviewed', 'approved', 'invoiced', 'paid', 'rejected', 'cancelled'], - enumName: 'estimates.estimate_status', - nullable: true, - }) - fromStatus: EstimateStatus | null; - - @Column({ - name: 'to_status', - type: 'enum', - enum: ['draft', 'submitted', 'reviewed', 'approved', 'invoiced', 'paid', 'rejected', 'cancelled'], - enumName: 'estimates.estimate_status', - }) - toStatus: EstimateStatus; - - @Column({ type: 'varchar', length: 50 }) - action: string; - - @Column({ type: 'text', nullable: true }) - comments: string | null; - - @Column({ name: 'performed_by', type: 'uuid' }) - performedById: string; - - @Column({ name: 'performed_at', type: 'timestamptz', default: () => 'NOW()' }) - performedAt: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Estimacion, (e) => e.workflow) - @JoinColumn({ name: 'estimacion_id' }) - estimacion: Estimacion; - - @ManyToOne(() => User) - @JoinColumn({ name: 'performed_by' }) - performedBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion.entity.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion.entity.ts deleted file mode 100644 index 132353fb9..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/estimacion.entity.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Estimacion Entity - * Estimaciones de obra periodicas para subcontratistas - * - * @module Estimates - * @table estimates.estimaciones - * @ddl schemas/04-estimates-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, - Check, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { EstimacionConcepto } from './estimacion-concepto.entity'; -import { Amortizacion } from './amortizacion.entity'; -import { Retencion } from './retencion.entity'; -import { EstimacionWorkflow } from './estimacion-workflow.entity'; - -export type EstimateStatus = 'draft' | 'submitted' | 'reviewed' | 'approved' | 'invoiced' | 'paid' | 'rejected' | 'cancelled'; - -@Entity({ schema: 'estimates', name: 'estimaciones' }) -@Index(['tenantId', 'estimateNumber'], { unique: true }) -@Index(['contratoId', 'sequenceNumber'], { unique: true }) -@Index(['tenantId']) -@Index(['contratoId']) -@Index(['fraccionamientoId']) -@Index(['status']) -@Index(['periodStart', 'periodEnd']) -@Check(`"period_end" >= "period_start"`) -export class Estimacion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'contrato_id', type: 'uuid' }) - contratoId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'estimate_number', type: 'varchar', length: 30 }) - estimateNumber: string; - - @Column({ name: 'period_start', type: 'date' }) - periodStart: Date; - - @Column({ name: 'period_end', type: 'date' }) - periodEnd: Date; - - @Column({ name: 'sequence_number', type: 'integer' }) - sequenceNumber: number; - - @Column({ - type: 'enum', - enum: ['draft', 'submitted', 'reviewed', 'approved', 'invoiced', 'paid', 'rejected', 'cancelled'], - enumName: 'estimates.estimate_status', - default: 'draft', - }) - status: EstimateStatus; - - @Column({ type: 'decimal', precision: 16, scale: 2, default: 0 }) - subtotal: number; - - @Column({ name: 'advance_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - advanceAmount: number; - - @Column({ name: 'retention_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - retentionAmount: number; - - @Column({ name: 'tax_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - taxAmount: number; - - @Column({ name: 'total_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - totalAmount: number; - - @Column({ name: 'submitted_at', type: 'timestamptz', nullable: true }) - submittedAt: Date | null; - - @Column({ name: 'submitted_by', type: 'uuid', nullable: true }) - submittedById: string | null; - - @Column({ name: 'reviewed_at', type: 'timestamptz', nullable: true }) - reviewedAt: Date | null; - - @Column({ name: 'reviewed_by', type: 'uuid', nullable: true }) - reviewedById: string | null; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date | null; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string | null; - - @Column({ name: 'invoice_id', type: 'uuid', nullable: true }) - invoiceId: string | null; - - @Column({ name: 'invoiced_at', type: 'timestamptz', nullable: true }) - invoicedAt: Date | null; - - @Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) - paidAt: Date | null; - - @Column({ type: 'text', nullable: true }) - notes: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => User) - @JoinColumn({ name: 'submitted_by' }) - submittedBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'reviewed_by' }) - reviewedBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @OneToMany(() => EstimacionConcepto, (c) => c.estimacion) - conceptos: EstimacionConcepto[]; - - @OneToMany(() => Amortizacion, (a) => a.estimacion) - amortizaciones: Amortizacion[]; - - @OneToMany(() => Retencion, (r) => r.estimacion) - retenciones: Retencion[]; - - @OneToMany(() => EstimacionWorkflow, (w) => w.estimacion) - workflow: EstimacionWorkflow[]; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/fondo-garantia.entity.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/fondo-garantia.entity.ts deleted file mode 100644 index 31d2739de..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/fondo-garantia.entity.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * FondoGarantia Entity - * Fondo de garantia acumulado por contrato - * - * @module Estimates - * @table estimates.fondo_garantia - * @ddl schemas/04-estimates-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -@Entity({ schema: 'estimates', name: 'fondo_garantia' }) -@Index(['contratoId'], { unique: true }) -@Index(['tenantId']) -export class FondoGarantia { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'contrato_id', type: 'uuid' }) - contratoId: string; - - @Column({ name: 'accumulated_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - accumulatedAmount: number; - - @Column({ name: 'released_amount', type: 'decimal', precision: 16, scale: 2, default: 0 }) - releasedAmount: number; - - // Columna calculada (GENERATED ALWAYS AS) - solo lectura - @Column({ - name: 'pending_amount', - type: 'decimal', - precision: 16, - scale: 2, - insert: false, - update: false, - }) - pendingAmount: number; - - @Column({ name: 'release_date', type: 'date', nullable: true }) - releaseDate: Date | null; - - @Column({ name: 'released_at', type: 'timestamptz', nullable: true }) - releasedAt: Date | null; - - @Column({ name: 'released_by', type: 'uuid', nullable: true }) - releasedById: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'released_by' }) - releasedBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/generador.entity.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/generador.entity.ts deleted file mode 100644 index 5ca305f6d..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/generador.entity.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Generador Entity - * Numeros generadores (soporte de cantidades para estimaciones) - * - * @module Estimates - * @table estimates.generadores - * @ddl schemas/04-estimates-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { EstimacionConcepto } from './estimacion-concepto.entity'; - -export type GeneratorStatus = 'draft' | 'in_progress' | 'completed' | 'approved'; - -@Entity({ schema: 'estimates', name: 'generadores' }) -@Index(['tenantId']) -@Index(['estimacionConceptoId']) -@Index(['status']) -export class Generador { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'estimacion_concepto_id', type: 'uuid' }) - estimacionConceptoId: string; - - @Column({ name: 'generator_number', type: 'varchar', length: 30 }) - generatorNumber: string; - - @Column({ type: 'text', nullable: true }) - description: string | null; - - @Column({ - type: 'enum', - enum: ['draft', 'in_progress', 'completed', 'approved'], - enumName: 'estimates.generator_status', - default: 'draft', - }) - status: GeneratorStatus; - - @Column({ name: 'lote_id', type: 'uuid', nullable: true }) - loteId: string | null; - - @Column({ name: 'departamento_id', type: 'uuid', nullable: true }) - departamentoId: string | null; - - @Column({ name: 'location_description', type: 'varchar', length: 255, nullable: true }) - locationDescription: string | null; - - @Column({ type: 'decimal', precision: 12, scale: 4, default: 0 }) - quantity: number; - - @Column({ type: 'text', nullable: true }) - formula: string | null; - - @Column({ name: 'photo_url', type: 'varchar', length: 500, nullable: true }) - photoUrl: string | null; - - @Column({ name: 'sketch_url', type: 'varchar', length: 500, nullable: true }) - sketchUrl: string | null; - - @Column({ name: 'captured_by', type: 'uuid' }) - capturedById: string; - - @Column({ name: 'captured_at', type: 'timestamptz', default: () => 'NOW()' }) - capturedAt: Date; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string | null; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => EstimacionConcepto, (ec) => ec.generadores) - @JoinColumn({ name: 'estimacion_concepto_id' }) - estimacionConcepto: EstimacionConcepto; - - @ManyToOne(() => User) - @JoinColumn({ name: 'captured_by' }) - capturedBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/index.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/index.ts deleted file mode 100644 index 76096aaf0..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Estimates Module - Entity Exports - * MAI-008: Estimaciones y Facturacion - */ - -export * from './estimacion.entity'; -export * from './estimacion-concepto.entity'; -export * from './generador.entity'; -export * from './anticipo.entity'; -export * from './amortizacion.entity'; -export * from './retencion.entity'; -export * from './fondo-garantia.entity'; -export * from './estimacion-workflow.entity'; diff --git a/projects/erp-construccion/backend/src/modules/estimates/entities/retencion.entity.ts b/projects/erp-construccion/backend/src/modules/estimates/entities/retencion.entity.ts deleted file mode 100644 index 1bfc70c99..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/entities/retencion.entity.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Retencion Entity - * Retenciones aplicadas a estimaciones - * - * @module Estimates - * @table estimates.retenciones - * @ddl schemas/04-estimates-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Estimacion } from './estimacion.entity'; - -export type RetentionType = 'guarantee' | 'tax' | 'penalty' | 'other'; - -@Entity({ schema: 'estimates', name: 'retenciones' }) -@Index(['tenantId']) -@Index(['estimacionId']) -@Index(['retentionType']) -export class Retencion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'estimacion_id', type: 'uuid' }) - estimacionId: string; - - @Column({ - name: 'retention_type', - type: 'enum', - enum: ['guarantee', 'tax', 'penalty', 'other'], - enumName: 'estimates.retention_type', - }) - retentionType: RetentionType; - - @Column({ type: 'varchar', length: 255 }) - description: string; - - @Column({ type: 'decimal', precision: 5, scale: 2, nullable: true }) - percentage: number | null; - - @Column({ type: 'decimal', precision: 16, scale: 2 }) - amount: number; - - @Column({ name: 'release_date', type: 'date', nullable: true }) - releaseDate: Date | null; - - @Column({ name: 'released_at', type: 'timestamptz', nullable: true }) - releasedAt: Date | null; - - @Column({ name: 'released_amount', type: 'decimal', precision: 16, scale: 2, nullable: true }) - releasedAmount: number | null; - - @Column({ type: 'text', nullable: true }) - notes: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Estimacion, (e) => e.retenciones) - @JoinColumn({ name: 'estimacion_id' }) - estimacion: Estimacion; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/services/anticipo.service.ts b/projects/erp-construccion/backend/src/modules/estimates/services/anticipo.service.ts deleted file mode 100644 index 691eb0d84..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/services/anticipo.service.ts +++ /dev/null @@ -1,351 +0,0 @@ -/** - * AnticipoService - Gesti贸n de Anticipos de Obra - * - * Gestiona anticipos otorgados a subcontratistas: inicial, avance, materiales. - * - * @module Estimates - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Anticipo, AdvanceType } from '../entities/anticipo.entity'; - -export interface CreateAnticipoDto { - contratoId: string; - advanceType: AdvanceType; - advanceNumber: string; - advanceDate: Date; - grossAmount: number; - taxAmount: number; - netAmount: number; - amortizationPercentage?: number; - notes?: string; -} - -export interface AnticipoFilters { - contratoId?: string; - advanceType?: AdvanceType; - isFullyAmortized?: boolean; - startDate?: Date; - endDate?: Date; - approvedById?: string; -} - -export interface AnticipoStats { - totalAnticipated: number; - totalAmortized: number; - pendingAmortization: number; - totalApproved: number; - totalPending: number; - totalPaid: number; - byType: { - type: AdvanceType; - count: number; - totalAmount: number; - }[]; -} - -export class AnticipoService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Busca anticipos por contrato - */ - async findByContrato( - ctx: ServiceContext, - contratoId: string, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.contrato_id = :contratoId', { contratoId }) - .andWhere('a.deleted_at IS NULL'); - - const skip = (page - 1) * limit; - qb.orderBy('a.advance_date', 'DESC') - .addOrderBy('a.advance_number', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Crea un nuevo anticipo - */ - async createAnticipo( - ctx: ServiceContext, - dto: CreateAnticipoDto - ): Promise { - // Validar que el monto neto sea correcto - const expectedNet = dto.grossAmount - dto.taxAmount; - if (Math.abs(expectedNet - dto.netAmount) > 0.01) { - throw new Error('El monto neto no coincide con el c谩lculo (bruto - impuestos)'); - } - - return this.create(ctx, { - ...dto, - amortizedAmount: 0, - isFullyAmortized: false, - }); - } - - /** - * Aprueba un anticipo - */ - async approveAnticipo( - ctx: ServiceContext, - id: string, - approvedById: string, - notes?: string - ): Promise { - const anticipo = await this.findById(ctx, id); - if (!anticipo) { - throw new Error('Anticipo no encontrado'); - } - - if (anticipo.approvedAt) { - throw new Error('El anticipo ya est谩 aprobado'); - } - - const updateData: Partial = { - approvedAt: new Date(), - approvedById, - }; - - if (notes) { - updateData.notes = anticipo.notes - ? `${anticipo.notes}\n\nAprobaci贸n: ${notes}` - : `Aprobaci贸n: ${notes}`; - } - - const updated = await this.update(ctx, id, updateData); - if (!updated) { - throw new Error('Error al aprobar anticipo'); - } - - return updated; - } - - /** - * Marca un anticipo como pagado - */ - async markPaid( - ctx: ServiceContext, - id: string, - paymentReference: string, - paidAt?: Date - ): Promise { - const anticipo = await this.findById(ctx, id); - if (!anticipo) { - throw new Error('Anticipo no encontrado'); - } - - if (!anticipo.approvedAt) { - throw new Error('El anticipo debe estar aprobado antes de marcarse como pagado'); - } - - if (anticipo.paidAt) { - throw new Error('El anticipo ya est谩 marcado como pagado'); - } - - const updated = await this.update(ctx, id, { - paidAt: paidAt || new Date(), - paymentReference, - }); - - if (!updated) { - throw new Error('Error al marcar anticipo como pagado'); - } - - return updated; - } - - /** - * Obtiene estad铆sticas de anticipos - */ - async getStats( - ctx: ServiceContext, - filters?: AnticipoFilters - ): Promise { - const qb = this.repository - .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.deleted_at IS NULL'); - - if (filters?.contratoId) { - qb.andWhere('a.contrato_id = :contratoId', { contratoId: filters.contratoId }); - } - if (filters?.advanceType) { - qb.andWhere('a.advance_type = :advanceType', { advanceType: filters.advanceType }); - } - if (filters?.isFullyAmortized !== undefined) { - qb.andWhere('a.is_fully_amortized = :isFullyAmortized', { - isFullyAmortized: filters.isFullyAmortized, - }); - } - if (filters?.startDate) { - qb.andWhere('a.advance_date >= :startDate', { startDate: filters.startDate }); - } - if (filters?.endDate) { - qb.andWhere('a.advance_date <= :endDate', { endDate: filters.endDate }); - } - if (filters?.approvedById) { - qb.andWhere('a.approved_by = :approvedById', { approvedById: filters.approvedById }); - } - - const anticipos = await qb.getMany(); - - const stats: AnticipoStats = { - totalAnticipated: 0, - totalAmortized: 0, - pendingAmortization: 0, - totalApproved: 0, - totalPending: 0, - totalPaid: 0, - byType: [], - }; - - const typeStats = new Map(); - - anticipos.forEach((anticipo) => { - const netAmount = Number(anticipo.netAmount) || 0; - const amortizedAmount = Number(anticipo.amortizedAmount) || 0; - - stats.totalAnticipated += netAmount; - stats.totalAmortized += amortizedAmount; - stats.pendingAmortization += netAmount - amortizedAmount; - - if (anticipo.approvedAt) { - stats.totalApproved += netAmount; - } else { - stats.totalPending += netAmount; - } - - if (anticipo.paidAt) { - stats.totalPaid += netAmount; - } - - const existing = typeStats.get(anticipo.advanceType) || { count: 0, totalAmount: 0 }; - typeStats.set(anticipo.advanceType, { - count: existing.count + 1, - totalAmount: existing.totalAmount + netAmount, - }); - }); - - stats.byType = Array.from(typeStats.entries()).map(([type, data]) => ({ - type, - count: data.count, - totalAmount: data.totalAmount, - })); - - return stats; - } - - /** - * Busca anticipos con filtros y paginaci贸n - */ - async findWithFilters( - ctx: ServiceContext, - filters: AnticipoFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.deleted_at IS NULL'); - - if (filters.contratoId) { - qb.andWhere('a.contrato_id = :contratoId', { contratoId: filters.contratoId }); - } - if (filters.advanceType) { - qb.andWhere('a.advance_type = :advanceType', { advanceType: filters.advanceType }); - } - if (filters.isFullyAmortized !== undefined) { - qb.andWhere('a.is_fully_amortized = :isFullyAmortized', { - isFullyAmortized: filters.isFullyAmortized, - }); - } - if (filters.startDate) { - qb.andWhere('a.advance_date >= :startDate', { startDate: filters.startDate }); - } - if (filters.endDate) { - qb.andWhere('a.advance_date <= :endDate', { endDate: filters.endDate }); - } - if (filters.approvedById) { - qb.andWhere('a.approved_by = :approvedById', { approvedById: filters.approvedById }); - } - - const skip = (page - 1) * limit; - qb.orderBy('a.advance_date', 'DESC') - .addOrderBy('a.advance_number', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Actualiza el monto amortizado de un anticipo - */ - async updateAmortization( - ctx: ServiceContext, - id: string, - amountToAmortize: number - ): Promise { - const anticipo = await this.findById(ctx, id); - if (!anticipo) { - throw new Error('Anticipo no encontrado'); - } - - if (anticipo.isFullyAmortized) { - throw new Error('El anticipo ya est谩 completamente amortizado'); - } - - const currentAmortized = Number(anticipo.amortizedAmount) || 0; - const netAmount = Number(anticipo.netAmount) || 0; - const newAmortizedAmount = currentAmortized + amountToAmortize; - - if (newAmortizedAmount > netAmount) { - throw new Error('El monto a amortizar excede el saldo del anticipo'); - } - - const isFullyAmortized = Math.abs(newAmortizedAmount - netAmount) < 0.01; - - const updated = await this.update(ctx, id, { - amortizedAmount: newAmortizedAmount, - isFullyAmortized, - }); - - if (!updated) { - throw new Error('Error al actualizar amortizaci贸n'); - } - - return updated; - } -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/services/estimacion.service.ts b/projects/erp-construccion/backend/src/modules/estimates/services/estimacion.service.ts deleted file mode 100644 index 0512294bf..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/services/estimacion.service.ts +++ /dev/null @@ -1,424 +0,0 @@ -/** - * EstimacionService - Gesti贸n de Estimaciones de Obra - * - * Gestiona estimaciones peri贸dicas con workflow de aprobaci贸n. - * Incluye c谩lculo de anticipos, retenciones e IVA. - * - * @module Estimates - */ - -import { Repository, DataSource } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Estimacion, EstimateStatus } from '../entities/estimacion.entity'; -import { EstimacionConcepto } from '../entities/estimacion-concepto.entity'; -import { Generador } from '../entities/generador.entity'; -import { EstimacionWorkflow } from '../entities/estimacion-workflow.entity'; - -export interface CreateEstimacionDto { - contratoId: string; - fraccionamientoId: string; - periodStart: Date; - periodEnd: Date; - notes?: string; -} - -export interface AddConceptoDto { - conceptoId: string; - contratoPartidaId?: string; - quantityContract?: number; - quantityPrevious?: number; - quantityCurrent: number; - unitPrice: number; - notes?: string; -} - -export interface AddGeneradorDto { - generatorNumber: string; - description?: string; - loteId?: string; - departamentoId?: string; - locationDescription?: string; - quantity: number; - formula?: string; - photoUrl?: string; - sketchUrl?: string; -} - -export interface EstimacionFilters { - contratoId?: string; - fraccionamientoId?: string; - status?: EstimateStatus; - periodFrom?: Date; - periodTo?: Date; -} - -export class EstimacionService extends BaseService { - constructor( - repository: Repository, - private readonly conceptoRepository: Repository, - private readonly generadorRepository: Repository, - private readonly workflowRepository: Repository, - private readonly dataSource: DataSource - ) { - super(repository); - } - - /** - * Crear nueva estimaci贸n - */ - async createEstimacion( - ctx: ServiceContext, - data: CreateEstimacionDto - ): Promise { - const sequenceNumber = await this.getNextSequenceNumber(ctx, data.contratoId); - const estimateNumber = await this.generateEstimateNumber(ctx, data.contratoId, sequenceNumber); - - const estimacion = await this.create(ctx, { - ...data, - estimateNumber, - sequenceNumber, - status: 'draft', - }); - - // Registrar en workflow - await this.addWorkflowEntry(ctx, estimacion.id, null, 'draft', 'create', 'Estimaci贸n creada'); - - return estimacion; - } - - /** - * Obtener siguiente n煤mero de secuencia - */ - private async getNextSequenceNumber( - ctx: ServiceContext, - contratoId: string - ): Promise { - const result = await this.repository - .createQueryBuilder('e') - .select('MAX(e.sequence_number)', 'maxNumber') - .where('e.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('e.contrato_id = :contratoId', { contratoId }) - .getRawOne(); - - return (result?.maxNumber || 0) + 1; - } - - /** - * Generar n煤mero de estimaci贸n - */ - private async generateEstimateNumber( - _ctx: ServiceContext, - contratoId: string, - sequenceNumber: number - ): Promise { - const year = new Date().getFullYear(); - return `EST-${year}-${contratoId.substring(0, 8).toUpperCase()}-${sequenceNumber.toString().padStart(3, '0')}`; - } - - /** - * Obtener estimaciones por contrato - */ - async findByContrato( - ctx: ServiceContext, - contratoId: string, - page = 1, - limit = 20 - ): Promise> { - return this.findAll(ctx, { - page, - limit, - where: { contratoId } as any, - }); - } - - /** - * Obtener estimaciones con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: EstimacionFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('e') - .where('e.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('e.deleted_at IS NULL'); - - if (filters.contratoId) { - qb.andWhere('e.contrato_id = :contratoId', { contratoId: filters.contratoId }); - } - if (filters.fraccionamientoId) { - qb.andWhere('e.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId: filters.fraccionamientoId }); - } - if (filters.status) { - qb.andWhere('e.status = :status', { status: filters.status }); - } - if (filters.periodFrom) { - qb.andWhere('e.period_start >= :periodFrom', { periodFrom: filters.periodFrom }); - } - if (filters.periodTo) { - qb.andWhere('e.period_end <= :periodTo', { periodTo: filters.periodTo }); - } - - const skip = (page - 1) * limit; - qb.orderBy('e.sequence_number', 'DESC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Obtener estimaci贸n con detalles completos - */ - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - relations: [ - 'conceptos', - 'conceptos.concepto', - 'conceptos.generadores', - 'amortizaciones', - 'retenciones', - 'workflow', - ], - }); - } - - /** - * Agregar concepto a estimaci贸n - */ - async addConcepto( - ctx: ServiceContext, - estimacionId: string, - data: AddConceptoDto - ): Promise { - const estimacion = await this.findById(ctx, estimacionId); - if (!estimacion || estimacion.status !== 'draft') { - throw new Error('Cannot modify non-draft estimation'); - } - - const concepto = this.conceptoRepository.create({ - tenantId: ctx.tenantId, - estimacionId, - conceptoId: data.conceptoId, - contratoPartidaId: data.contratoPartidaId, - quantityContract: data.quantityContract || 0, - quantityPrevious: data.quantityPrevious || 0, - quantityCurrent: data.quantityCurrent, - unitPrice: data.unitPrice, - notes: data.notes, - createdById: ctx.userId, - }); - - const savedConcepto = await this.conceptoRepository.save(concepto); - await this.recalculateTotals(ctx, estimacionId); - - return savedConcepto; - } - - /** - * Agregar generador a concepto de estimaci贸n - */ - async addGenerador( - ctx: ServiceContext, - estimacionConceptoId: string, - data: AddGeneradorDto - ): Promise { - const concepto = await this.conceptoRepository.findOne({ - where: { id: estimacionConceptoId, tenantId: ctx.tenantId } as any, - relations: ['estimacion'], - }); - - if (!concepto) { - throw new Error('Concepto not found'); - } - - const generador = this.generadorRepository.create({ - tenantId: ctx.tenantId, - estimacionConceptoId, - generatorNumber: data.generatorNumber, - description: data.description, - loteId: data.loteId, - departamentoId: data.departamentoId, - locationDescription: data.locationDescription, - quantity: data.quantity, - formula: data.formula, - photoUrl: data.photoUrl, - sketchUrl: data.sketchUrl, - status: 'draft', - capturedById: ctx.userId, - createdById: ctx.userId, - }); - - return this.generadorRepository.save(generador); - } - - /** - * Recalcular totales de estimaci贸n - */ - async recalculateTotals(_ctx: ServiceContext, estimacionId: string): Promise { - // Ejecutar funci贸n de PostgreSQL - await this.dataSource.query('SELECT estimates.calculate_estimate_totals($1)', [estimacionId]); - } - - /** - * Cambiar estado de estimaci贸n - */ - async changeStatus( - ctx: ServiceContext, - estimacionId: string, - newStatus: EstimateStatus, - action: string, - comments?: string - ): Promise { - const estimacion = await this.findById(ctx, estimacionId); - if (!estimacion) { - return null; - } - - const validTransitions: Record = { - draft: ['submitted'], - submitted: ['reviewed', 'rejected'], - reviewed: ['approved', 'rejected'], - approved: ['invoiced'], - invoiced: ['paid'], - paid: [], - rejected: ['draft'], - cancelled: [], - }; - - if (!validTransitions[estimacion.status]?.includes(newStatus)) { - throw new Error(`Invalid status transition from ${estimacion.status} to ${newStatus}`); - } - - const updateData: Partial = { status: newStatus }; - - switch (newStatus) { - case 'submitted': - updateData.submittedAt = new Date(); - updateData.submittedById = ctx.userId; - break; - case 'reviewed': - updateData.reviewedAt = new Date(); - updateData.reviewedById = ctx.userId; - break; - case 'approved': - updateData.approvedAt = new Date(); - updateData.approvedById = ctx.userId; - break; - case 'invoiced': - updateData.invoicedAt = new Date(); - break; - case 'paid': - updateData.paidAt = new Date(); - break; - } - - const updated = await this.update(ctx, estimacionId, updateData); - - // Registrar en workflow - await this.addWorkflowEntry(ctx, estimacionId, estimacion.status, newStatus, action, comments); - - return updated; - } - - /** - * Agregar entrada al workflow - */ - private async addWorkflowEntry( - ctx: ServiceContext, - estimacionId: string, - fromStatus: EstimateStatus | null, - toStatus: EstimateStatus, - action: string, - comments?: string - ): Promise { - await this.workflowRepository.save( - this.workflowRepository.create({ - tenantId: ctx.tenantId, - estimacionId, - fromStatus, - toStatus, - action, - comments, - performedById: ctx.userId, - createdById: ctx.userId, - }) - ); - } - - /** - * Enviar estimaci贸n para revisi贸n - */ - async submit(ctx: ServiceContext, estimacionId: string): Promise { - return this.changeStatus(ctx, estimacionId, 'submitted', 'submit', 'Enviada para revisi贸n'); - } - - /** - * Revisar estimaci贸n - */ - async review(ctx: ServiceContext, estimacionId: string): Promise { - return this.changeStatus(ctx, estimacionId, 'reviewed', 'review', 'Revisi贸n completada'); - } - - /** - * Aprobar estimaci贸n - */ - async approve(ctx: ServiceContext, estimacionId: string): Promise { - return this.changeStatus(ctx, estimacionId, 'approved', 'approve', 'Aprobada'); - } - - /** - * Rechazar estimaci贸n - */ - async reject( - ctx: ServiceContext, - estimacionId: string, - reason: string - ): Promise { - return this.changeStatus(ctx, estimacionId, 'rejected', 'reject', reason); - } - - /** - * Obtener resumen de estimaciones por contrato - */ - async getContractSummary(ctx: ServiceContext, contratoId: string): Promise { - const result = await this.repository - .createQueryBuilder('e') - .select([ - 'COUNT(*) as total_estimates', - 'SUM(CASE WHEN e.status = \'approved\' THEN e.total_amount ELSE 0 END) as total_approved', - 'SUM(CASE WHEN e.status = \'paid\' THEN e.total_amount ELSE 0 END) as total_paid', - ]) - .where('e.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('e.contrato_id = :contratoId', { contratoId }) - .andWhere('e.deleted_at IS NULL') - .getRawOne(); - - return { - totalEstimates: parseInt(result?.total_estimates || '0'), - totalApproved: parseFloat(result?.total_approved || '0'), - totalPaid: parseFloat(result?.total_paid || '0'), - }; - } -} - -interface ContractEstimateSummary { - totalEstimates: number; - totalApproved: number; - totalPaid: number; -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/services/fondo-garantia.service.ts b/projects/erp-construccion/backend/src/modules/estimates/services/fondo-garantia.service.ts deleted file mode 100644 index 87ca697d7..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/services/fondo-garantia.service.ts +++ /dev/null @@ -1,275 +0,0 @@ -/** - * FondoGarantiaService - Gesti贸n de Fondos de Garant铆a - * - * Gestiona fondos de garant铆a acumulados por contrato: retenci贸n y liberaci贸n. - * - * @module Estimates - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { FondoGarantia } from '../entities/fondo-garantia.entity'; - -export interface CreateFondoGarantiaDto { - contratoId: string; - accumulatedAmount?: number; - releasedAmount?: number; - releaseDate?: Date; -} - -export interface ReleaseFondoDto { - amount: number; - releasedById: string; - releaseDate?: Date; - notes?: string; -} - -export interface FondoGarantiaFilters { - contratoId?: string; - hasPending?: boolean; - fullyReleased?: boolean; -} - -export interface FondoGarantiaStats { - totalAccumulated: number; - totalReleased: number; - totalPending: number; - fondosCount: number; - fondosPendingRelease: number; - fondosFullyReleased: number; -} - -export class FondoGarantiaService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Encuentra fondo de garant铆a por contrato - */ - async findByContrato( - ctx: ServiceContext, - contratoId: string - ): Promise { - return this.repository.findOne({ - where: { - tenantId: ctx.tenantId, - contratoId, - deletedAt: null, - } as any, - }); - } - - /** - * Crea o actualiza un fondo de garant铆a - */ - async createOrUpdate( - ctx: ServiceContext, - dto: CreateFondoGarantiaDto - ): Promise { - const existing = await this.findByContrato(ctx, dto.contratoId); - - if (existing) { - const updated = await this.update(ctx, existing.id, { - accumulatedAmount: dto.accumulatedAmount ?? existing.accumulatedAmount, - releasedAmount: dto.releasedAmount ?? existing.releasedAmount, - releaseDate: dto.releaseDate ?? existing.releaseDate, - }); - return updated!; - } - - return this.create(ctx, { - contratoId: dto.contratoId, - accumulatedAmount: dto.accumulatedAmount ?? 0, - releasedAmount: dto.releasedAmount ?? 0, - releaseDate: dto.releaseDate ?? null, - }); - } - - /** - * A帽ade monto al fondo acumulado - */ - async addAccumulation( - ctx: ServiceContext, - contratoId: string, - amount: number - ): Promise { - let fondo = await this.findByContrato(ctx, contratoId); - - if (!fondo) { - fondo = await this.create(ctx, { - contratoId, - accumulatedAmount: amount, - releasedAmount: 0, - }); - return fondo; - } - - const currentAccumulated = Number(fondo.accumulatedAmount) || 0; - const updated = await this.update(ctx, fondo.id, { - accumulatedAmount: currentAccumulated + amount, - }); - - return updated!; - } - - /** - * Libera parcialmente el fondo de garant铆a - */ - async releasePartial( - ctx: ServiceContext, - contratoId: string, - dto: ReleaseFondoDto - ): Promise { - const fondo = await this.findByContrato(ctx, contratoId); - - if (!fondo) { - throw new Error('Fondo de garant铆a no encontrado'); - } - - const accumulated = Number(fondo.accumulatedAmount) || 0; - const released = Number(fondo.releasedAmount) || 0; - const pending = accumulated - released; - - if (dto.amount > pending) { - throw new Error(`Monto a liberar (${dto.amount}) excede el monto pendiente (${pending})`); - } - - const updated = await this.update(ctx, fondo.id, { - releasedAmount: released + dto.amount, - releasedAt: dto.releaseDate || new Date(), - releasedById: dto.releasedById, - }); - - return updated!; - } - - /** - * Libera completamente el fondo de garant铆a - */ - async releaseFull( - ctx: ServiceContext, - contratoId: string, - releasedById: string, - releaseDate?: Date - ): Promise { - const fondo = await this.findByContrato(ctx, contratoId); - - if (!fondo) { - throw new Error('Fondo de garant铆a no encontrado'); - } - - const accumulated = Number(fondo.accumulatedAmount) || 0; - const released = Number(fondo.releasedAmount) || 0; - const pending = accumulated - released; - - if (pending <= 0) { - throw new Error('No hay monto pendiente para liberar'); - } - - const updated = await this.update(ctx, fondo.id, { - releasedAmount: accumulated, - releasedAt: releaseDate || new Date(), - releasedById, - }); - - return updated!; - } - - /** - * Obtiene estad铆sticas de fondos de garant铆a - */ - async getStats(ctx: ServiceContext): Promise { - const fondos = await this.repository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - }); - - const stats: FondoGarantiaStats = { - totalAccumulated: 0, - totalReleased: 0, - totalPending: 0, - fondosCount: fondos.length, - fondosPendingRelease: 0, - fondosFullyReleased: 0, - }; - - fondos.forEach((fondo) => { - const accumulated = Number(fondo.accumulatedAmount) || 0; - const released = Number(fondo.releasedAmount) || 0; - const pending = accumulated - released; - - stats.totalAccumulated += accumulated; - stats.totalReleased += released; - stats.totalPending += pending; - - if (pending > 0) { - stats.fondosPendingRelease++; - } else if (released > 0 && pending === 0) { - stats.fondosFullyReleased++; - } - }); - - return stats; - } - - /** - * Encuentra fondos con filtros y paginaci贸n - */ - async findWithFilters( - ctx: ServiceContext, - filters: FondoGarantiaFilters = {}, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('f') - .where('f.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('f.deleted_at IS NULL'); - - if (filters.contratoId) { - qb.andWhere('f.contrato_id = :contratoId', { contratoId: filters.contratoId }); - } - - const skip = (page - 1) * limit; - qb.orderBy('f.created_at', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await qb.getManyAndCount(); - - // Aplicar filtros post-query si es necesario - let filteredData = data; - - if (filters.hasPending !== undefined) { - filteredData = filteredData.filter((fondo) => { - const pending = (Number(fondo.accumulatedAmount) || 0) - (Number(fondo.releasedAmount) || 0); - return filters.hasPending ? pending > 0 : pending === 0; - }); - } - - if (filters.fullyReleased !== undefined) { - filteredData = filteredData.filter((fondo) => { - const accumulated = Number(fondo.accumulatedAmount) || 0; - const released = Number(fondo.releasedAmount) || 0; - return filters.fullyReleased - ? released > 0 && released >= accumulated - : released < accumulated; - }); - } - - return { - data: filteredData, - meta: { - total: filteredData.length !== data.length ? filteredData.length : total, - page, - limit, - totalPages: Math.ceil( - (filteredData.length !== data.length ? filteredData.length : total) / limit - ), - }, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/estimates/services/index.ts b/projects/erp-construccion/backend/src/modules/estimates/services/index.ts deleted file mode 100644 index 0e9df8d2e..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/services/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Estimates Module - Service Exports - * MAI-008: Estimaciones y Facturaci贸n - */ - -export * from './estimacion.service'; -export * from './anticipo.service'; -export * from './fondo-garantia.service'; -export * from './retencion.service'; diff --git a/projects/erp-construccion/backend/src/modules/estimates/services/retencion.service.ts b/projects/erp-construccion/backend/src/modules/estimates/services/retencion.service.ts deleted file mode 100644 index cb4ec6dbf..000000000 --- a/projects/erp-construccion/backend/src/modules/estimates/services/retencion.service.ts +++ /dev/null @@ -1,258 +0,0 @@ -/** - * RetencionService - Gesti贸n de Retenciones de Estimaciones - * - * Gestiona retenciones aplicadas a estimaciones: garant铆a, impuestos, penalizaciones. - * - * @module Estimates - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Retencion, RetentionType } from '../entities/retencion.entity'; - -export interface CreateRetencionDto { - estimacionId: string; - retentionType: RetentionType; - description: string; - percentage?: number; - amount: number; - releaseDate?: Date; - notes?: string; -} - -export interface ReleaseRetencionDto { - releasedAmount: number; - notes?: string; -} - -export interface RetencionFilters { - estimacionId?: string; - contratoId?: string; - retentionType?: RetentionType; - isReleased?: boolean; - dateFrom?: Date; - dateTo?: Date; -} - -export class RetencionService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Obtener retenciones por estimaci贸n - */ - async findByEstimacion( - ctx: ServiceContext, - estimacionId: string - ): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - estimacionId, - deletedAt: null, - } as any, - order: { createdAt: 'DESC' }, - }); - } - - /** - * Crear retenci贸n - */ - async createRetencion( - ctx: ServiceContext, - data: CreateRetencionDto - ): Promise { - return this.create(ctx, { - ...data, - releasedAmount: null, - releasedAt: null, - }); - } - - /** - * Obtener retenciones con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: RetencionFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('r') - .leftJoin('r.estimacion', 'e') - .where('r.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('r.deleted_at IS NULL'); - - if (filters.estimacionId) { - qb.andWhere('r.estimacion_id = :estimacionId', { estimacionId: filters.estimacionId }); - } - if (filters.contratoId) { - qb.andWhere('e.contrato_id = :contratoId', { contratoId: filters.contratoId }); - } - if (filters.retentionType) { - qb.andWhere('r.retention_type = :retentionType', { retentionType: filters.retentionType }); - } - if (filters.isReleased !== undefined) { - if (filters.isReleased) { - qb.andWhere('r.released_at IS NOT NULL'); - } else { - qb.andWhere('r.released_at IS NULL'); - } - } - if (filters.dateFrom) { - qb.andWhere('r.created_at >= :dateFrom', { dateFrom: filters.dateFrom }); - } - if (filters.dateTo) { - qb.andWhere('r.created_at <= :dateTo', { dateTo: filters.dateTo }); - } - - const skip = (page - 1) * limit; - qb.orderBy('r.created_at', 'DESC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Liberar retenci贸n - */ - async releaseRetencion( - ctx: ServiceContext, - id: string, - data: ReleaseRetencionDto - ): Promise { - const retencion = await this.findById(ctx, id); - if (!retencion) { - return null; - } - - if (retencion.releasedAt) { - throw new Error('Retention already released'); - } - - if (data.releasedAmount > retencion.amount) { - throw new Error('Released amount cannot exceed retention amount'); - } - - return this.update(ctx, id, { - releasedAmount: data.releasedAmount, - releasedAt: new Date(), - notes: data.notes ? `${retencion.notes || ''}\nLiberaci贸n: ${data.notes}` : retencion.notes, - }); - } - - /** - * Obtener totales por contrato - */ - async getTotalByContrato(ctx: ServiceContext, contratoId: string): Promise { - const result = await this.repository - .createQueryBuilder('r') - .leftJoin('r.estimacion', 'e') - .select([ - 'SUM(r.amount) as total_retained', - 'SUM(COALESCE(r.released_amount, 0)) as total_released', - ]) - .where('r.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('e.contrato_id = :contratoId', { contratoId }) - .andWhere('r.deleted_at IS NULL') - .getRawOne(); - - const totalRetained = parseFloat(result?.total_retained || '0'); - const totalReleased = parseFloat(result?.total_released || '0'); - - return { - totalRetained, - totalReleased, - totalPending: totalRetained - totalReleased, - }; - } - - /** - * Estad铆sticas de retenciones - */ - async getStats(ctx: ServiceContext, filters?: RetencionFilters): Promise { - const qb = this.repository - .createQueryBuilder('r') - .where('r.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('r.deleted_at IS NULL'); - - if (filters?.contratoId) { - qb.leftJoin('r.estimacion', 'e') - .andWhere('e.contrato_id = :contratoId', { contratoId: filters.contratoId }); - } - - const retenciones = await qb.getMany(); - - const byType = new Map(); - - let totalRetained = 0; - let totalReleased = 0; - let pendingCount = 0; - let releasedCount = 0; - - retenciones.forEach((r) => { - totalRetained += r.amount; - totalReleased += r.releasedAmount || 0; - - if (r.releasedAt) { - releasedCount++; - } else { - pendingCount++; - } - - const existing = byType.get(r.retentionType) || { count: 0, amount: 0, released: 0 }; - byType.set(r.retentionType, { - count: existing.count + 1, - amount: existing.amount + r.amount, - released: existing.released + (r.releasedAmount || 0), - }); - }); - - return { - totalRetained, - totalReleased, - totalPending: totalRetained - totalReleased, - totalCount: retenciones.length, - pendingCount, - releasedCount, - byType: Array.from(byType.entries()).map(([type, data]) => ({ - type, - ...data, - pending: data.amount - data.released, - })), - }; - } -} - -export interface RetencionTotals { - totalRetained: number; - totalReleased: number; - totalPending: number; -} - -export interface RetencionStats { - totalRetained: number; - totalReleased: number; - totalPending: number; - totalCount: number; - pendingCount: number; - releasedCount: number; - byType: { - type: RetentionType; - count: number; - amount: number; - released: number; - pending: number; - }[]; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/controllers/accounting.controller.ts b/projects/erp-construccion/backend/src/modules/finance/controllers/accounting.controller.ts deleted file mode 100644 index 4608a84f8..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/controllers/accounting.controller.ts +++ /dev/null @@ -1,385 +0,0 @@ -/** - * AccountingController - Controlador de Contabilidad - * - * Endpoints para cat谩logo de cuentas y p贸lizas. - * - * @module Finance - */ - -import { Router, Request, Response } from 'express'; -import { DataSource } from 'typeorm'; -import { AccountingService } from '../services'; - -export function createAccountingController(dataSource: DataSource): Router { - const router = Router(); - const service = new AccountingService(dataSource); - - // ==================== CAT脕LOGO DE CUENTAS ==================== - - /** - * GET /accounts - * Lista cuentas contables con filtros y paginaci贸n - */ - router.get('/accounts', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - page: parseInt(req.query.page as string) || 1, - limit: parseInt(req.query.limit as string) || 50, - accountType: req.query.accountType as any, - status: req.query.status as any, - parentId: req.query.parentId as string, - search: req.query.search as string, - acceptsMovements: req.query.acceptsMovements === 'true' ? true : req.query.acceptsMovements === 'false' ? false : undefined, - }; - - const result = await service.findAllAccounts(ctx, options); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /accounts/tree - * Obtiene 谩rbol jer谩rquico de cuentas - */ - router.get('/accounts/tree', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const tree = await service.getAccountTree(ctx); - res.json(tree); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /accounts/:id - * Obtiene una cuenta por ID - */ - router.get('/accounts/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const account = await service.findAccountById(ctx, req.params.id); - if (!account) { - return res.status(404).json({ error: 'Cuenta no encontrada' }); - } - - res.json(account); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /accounts/code/:code - * Obtiene una cuenta por c贸digo - */ - router.get('/accounts/code/:code', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const account = await service.findAccountByCode(ctx, req.params.code); - if (!account) { - return res.status(404).json({ error: 'Cuenta no encontrada' }); - } - - res.json(account); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * POST /accounts - * Crea una nueva cuenta - */ - router.post('/accounts', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const account = await service.createAccount(ctx, req.body); - res.status(201).json(account); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * PUT /accounts/:id - * Actualiza una cuenta - */ - router.put('/accounts/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const account = await service.updateAccount(ctx, req.params.id, req.body); - res.json(account); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * DELETE /accounts/:id - * Elimina una cuenta (soft delete) - */ - router.delete('/accounts/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - await service.deleteAccount(ctx, req.params.id); - res.status(204).send(); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - // ==================== P脫LIZAS CONTABLES ==================== - - /** - * GET /entries - * Lista p贸lizas con filtros y paginaci贸n - */ - router.get('/entries', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - page: parseInt(req.query.page as string) || 1, - limit: parseInt(req.query.limit as string) || 20, - entryType: req.query.entryType as any, - status: req.query.status as any, - startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined, - endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined, - projectId: req.query.projectId as string, - fiscalYear: req.query.fiscalYear ? parseInt(req.query.fiscalYear as string) : undefined, - fiscalPeriod: req.query.fiscalPeriod ? parseInt(req.query.fiscalPeriod as string) : undefined, - search: req.query.search as string, - }; - - const result = await service.findAllEntries(ctx, options); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /entries/:id - * Obtiene una p贸liza por ID - */ - router.get('/entries/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const entry = await service.findEntryById(ctx, req.params.id); - if (!entry) { - return res.status(404).json({ error: 'P贸liza no encontrada' }); - } - - res.json(entry); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * POST /entries - * Crea una nueva p贸liza - */ - router.post('/entries', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const entry = await service.createEntry(ctx, req.body); - res.status(201).json(entry); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /entries/:id/submit - * Env铆a p贸liza a aprobaci贸n - */ - router.post('/entries/:id/submit', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const entry = await service.submitForApproval(ctx, req.params.id); - res.json(entry); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /entries/:id/approve - * Aprueba una p贸liza - */ - router.post('/entries/:id/approve', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const entry = await service.approveEntry(ctx, req.params.id); - res.json(entry); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /entries/:id/post - * Contabiliza una p贸liza - */ - router.post('/entries/:id/post', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const entry = await service.postEntry(ctx, req.params.id); - res.json(entry); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /entries/:id/cancel - * Cancela una p贸liza - */ - router.post('/entries/:id/cancel', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { reason } = req.body; - if (!reason) { - return res.status(400).json({ error: 'Se requiere motivo de cancelaci贸n' }); - } - - const entry = await service.cancelEntry(ctx, req.params.id, reason); - res.json(entry); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /entries/:id/reverse - * Reversa una p贸liza - */ - router.post('/entries/:id/reverse', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { reason } = req.body; - if (!reason) { - return res.status(400).json({ error: 'Se requiere motivo de reverso' }); - } - - const entry = await service.reverseEntry(ctx, req.params.id, reason); - res.json(entry); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - // ==================== REPORTES ==================== - - /** - * GET /reports/trial-balance - * Obtiene balanza de comprobaci贸n - */ - router.get('/reports/trial-balance', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const fiscalYear = parseInt(req.query.fiscalYear as string) || new Date().getFullYear(); - const fiscalPeriod = req.query.fiscalPeriod ? parseInt(req.query.fiscalPeriod as string) : undefined; - - const result = await service.getTrialBalance(ctx, fiscalYear, fiscalPeriod); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /reports/account-ledger/:accountId - * Obtiene mayor de una cuenta - */ - router.get('/reports/account-ledger/:accountId', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const startDate = new Date(req.query.startDate as string); - const endDate = new Date(req.query.endDate as string); - - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const result = await service.getAccountLedger(ctx, req.params.accountId, startDate, endDate); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/controllers/ap.controller.ts b/projects/erp-construccion/backend/src/modules/finance/controllers/ap.controller.ts deleted file mode 100644 index e7356aa69..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/controllers/ap.controller.ts +++ /dev/null @@ -1,264 +0,0 @@ -/** - * APController - Controlador de Cuentas por Pagar - * - * Endpoints para cuentas por pagar y pagos a proveedores. - * - * @module Finance - */ - -import { Router, Request, Response } from 'express'; -import { DataSource } from 'typeorm'; -import { APService } from '../services'; - -export function createAPController(dataSource: DataSource): Router { - const router = Router(); - const service = new APService(dataSource); - - // ==================== CUENTAS POR PAGAR ==================== - - /** - * GET / - * Lista cuentas por pagar con filtros - */ - router.get('/', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - page: parseInt(req.query.page as string) || 1, - limit: parseInt(req.query.limit as string) || 20, - status: req.query.status as any, - partnerId: req.query.partnerId as string, - projectId: req.query.projectId as string, - startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined, - endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined, - overdue: req.query.overdue === 'true', - search: req.query.search as string, - }; - - const result = await service.findAll(ctx, options); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /dashboard - * Obtiene estad铆sticas del dashboard - */ - router.get('/dashboard', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const stats = await service.getDashboardStats(ctx); - res.json(stats); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /aging - * Obtiene reporte de antig眉edad - */ - router.get('/aging', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - partnerId: req.query.partnerId as string, - projectId: req.query.projectId as string, - asOfDate: req.query.asOfDate ? new Date(req.query.asOfDate as string) : undefined, - }; - - const report = await service.getAgingReport(ctx, options); - res.json(report); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /payment-schedule - * Obtiene calendario de pagos - */ - router.get('/payment-schedule', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const startDate = new Date(req.query.startDate as string); - const endDate = new Date(req.query.endDate as string); - - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - partnerId: req.query.partnerId as string, - projectId: req.query.projectId as string, - }; - - const schedule = await service.getPaymentSchedule(ctx, startDate, endDate, options); - res.json(schedule); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /:id - * Obtiene una cuenta por pagar por ID - */ - router.get('/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const ap = await service.findById(ctx, req.params.id); - if (!ap) { - return res.status(404).json({ error: 'Cuenta por pagar no encontrada' }); - } - - res.json(ap); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * POST / - * Crea una nueva cuenta por pagar - */ - router.post('/', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const ap = await service.create(ctx, req.body); - res.status(201).json(ap); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * PUT /:id - * Actualiza una cuenta por pagar - */ - router.put('/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const ap = await service.update(ctx, req.params.id, req.body); - res.json(ap); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /:id/cancel - * Cancela una cuenta por pagar - */ - router.post('/:id/cancel', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { reason } = req.body; - if (!reason) { - return res.status(400).json({ error: 'Se requiere motivo de cancelaci贸n' }); - } - - const ap = await service.cancel(ctx, req.params.id, reason); - res.json(ap); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - // ==================== PAGOS ==================== - - /** - * POST /payments - * Registra un pago a proveedores - */ - router.post('/payments', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const payment = await service.createPayment(ctx, req.body); - res.status(201).json(payment); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /payments/:id/confirm - * Confirma un pago - */ - router.post('/payments/:id/confirm', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const payment = await service.confirmPayment(ctx, req.params.id); - res.json(payment); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /payments/:id/cancel - * Cancela un pago - */ - router.post('/payments/:id/cancel', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { reason } = req.body; - if (!reason) { - return res.status(400).json({ error: 'Se requiere motivo de cancelaci贸n' }); - } - - const payment = await service.cancelPayment(ctx, req.params.id, reason); - res.json(payment); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/controllers/ar.controller.ts b/projects/erp-construccion/backend/src/modules/finance/controllers/ar.controller.ts deleted file mode 100644 index 367980002..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/controllers/ar.controller.ts +++ /dev/null @@ -1,310 +0,0 @@ -/** - * ARController - Controlador de Cuentas por Cobrar - * - * Endpoints para cuentas por cobrar y cobranza. - * - * @module Finance - */ - -import { Router, Request, Response } from 'express'; -import { DataSource } from 'typeorm'; -import { ARService } from '../services'; - -export function createARController(dataSource: DataSource): Router { - const router = Router(); - const service = new ARService(dataSource); - - // ==================== CUENTAS POR COBRAR ==================== - - /** - * GET / - * Lista cuentas por cobrar con filtros - */ - router.get('/', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - page: parseInt(req.query.page as string) || 1, - limit: parseInt(req.query.limit as string) || 20, - status: req.query.status as any, - partnerId: req.query.partnerId as string, - projectId: req.query.projectId as string, - startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined, - endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined, - overdue: req.query.overdue === 'true', - search: req.query.search as string, - }; - - const result = await service.findAll(ctx, options); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /dashboard - * Obtiene estad铆sticas del dashboard - */ - router.get('/dashboard', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const stats = await service.getDashboardStats(ctx); - res.json(stats); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /aging - * Obtiene reporte de antig眉edad - */ - router.get('/aging', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - partnerId: req.query.partnerId as string, - projectId: req.query.projectId as string, - asOfDate: req.query.asOfDate ? new Date(req.query.asOfDate as string) : undefined, - }; - - const report = await service.getAgingReport(ctx, options); - res.json(report); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /collection-forecast - * Obtiene pron贸stico de cobranza - */ - router.get('/collection-forecast', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const startDate = new Date(req.query.startDate as string); - const endDate = new Date(req.query.endDate as string); - - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - partnerId: req.query.partnerId as string, - projectId: req.query.projectId as string, - }; - - const forecast = await service.getCollectionForecast(ctx, startDate, endDate, options); - res.json(forecast); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /:id - * Obtiene una cuenta por cobrar por ID - */ - router.get('/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const ar = await service.findById(ctx, req.params.id); - if (!ar) { - return res.status(404).json({ error: 'Cuenta por cobrar no encontrada' }); - } - - res.json(ar); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * POST / - * Crea una nueva cuenta por cobrar - */ - router.post('/', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const ar = await service.create(ctx, req.body); - res.status(201).json(ar); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * PUT /:id - * Actualiza una cuenta por cobrar - */ - router.put('/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const ar = await service.update(ctx, req.params.id, req.body); - res.json(ar); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /:id/cancel - * Cancela una cuenta por cobrar - */ - router.post('/:id/cancel', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { reason } = req.body; - if (!reason) { - return res.status(400).json({ error: 'Se requiere motivo de cancelaci贸n' }); - } - - const ar = await service.cancel(ctx, req.params.id, reason); - res.json(ar); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /:id/write-off - * Castiga una cuenta por cobrar - */ - router.post('/:id/write-off', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { reason } = req.body; - if (!reason) { - return res.status(400).json({ error: 'Se requiere motivo de castigo' }); - } - - const ar = await service.writeOff(ctx, req.params.id, reason); - res.json(ar); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /:id/collection-attempt - * Registra intento de cobranza - */ - router.post('/:id/collection-attempt', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { notes } = req.body; - if (!notes) { - return res.status(400).json({ error: 'Se requieren notas de la gesti贸n' }); - } - - const ar = await service.recordCollectionAttempt(ctx, req.params.id, notes); - res.json(ar); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - // ==================== COBROS ==================== - - /** - * POST /collections - * Registra un cobro - */ - router.post('/collections', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const collection = await service.createCollection(ctx, req.body); - res.status(201).json(collection); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /collections/:id/confirm - * Confirma un cobro - */ - router.post('/collections/:id/confirm', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const collection = await service.confirmCollection(ctx, req.params.id); - res.json(collection); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /collections/:id/cancel - * Cancela un cobro - */ - router.post('/collections/:id/cancel', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { reason } = req.body; - if (!reason) { - return res.status(400).json({ error: 'Se requiere motivo de cancelaci贸n' }); - } - - const collection = await service.cancelCollection(ctx, req.params.id, reason); - res.json(collection); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/controllers/bank-reconciliation.controller.ts b/projects/erp-construccion/backend/src/modules/finance/controllers/bank-reconciliation.controller.ts deleted file mode 100644 index ec4ad7570..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/controllers/bank-reconciliation.controller.ts +++ /dev/null @@ -1,434 +0,0 @@ -/** - * BankReconciliationController - Controlador de Conciliaci贸n Bancaria - * - * Endpoints para cuentas bancarias, movimientos y conciliaci贸n. - * - * @module Finance - */ - -import { Router, Request, Response } from 'express'; -import { DataSource } from 'typeorm'; -import { BankReconciliationService } from '../services'; - -export function createBankReconciliationController(dataSource: DataSource): Router { - const router = Router(); - const service = new BankReconciliationService(dataSource); - - // ==================== CUENTAS BANCARIAS ==================== - - /** - * GET /accounts - * Lista cuentas bancarias - */ - router.get('/accounts', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - page: parseInt(req.query.page as string) || 1, - limit: parseInt(req.query.limit as string) || 20, - accountType: req.query.accountType as any, - status: req.query.status as any, - projectId: req.query.projectId as string, - search: req.query.search as string, - }; - - const result = await service.findAllAccounts(ctx, options); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /accounts/summary - * Obtiene resumen de saldos - */ - router.get('/accounts/summary', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const summary = await service.getBankAccountSummary(ctx); - res.json(summary); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /accounts/:id - * Obtiene una cuenta bancaria por ID - */ - router.get('/accounts/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const account = await service.findAccountById(ctx, req.params.id); - if (!account) { - return res.status(404).json({ error: 'Cuenta bancaria no encontrada' }); - } - - res.json(account); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * POST /accounts - * Crea una nueva cuenta bancaria - */ - router.post('/accounts', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const account = await service.createAccount(ctx, req.body); - res.status(201).json(account); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * PUT /accounts/:id - * Actualiza una cuenta bancaria - */ - router.put('/accounts/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const account = await service.updateAccount(ctx, req.params.id, req.body); - res.json(account); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * PUT /accounts/:id/balance - * Actualiza saldo de cuenta - */ - router.put('/accounts/:id/balance', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { currentBalance, availableBalance } = req.body; - const account = await service.updateAccountBalance( - ctx, - req.params.id, - currentBalance, - availableBalance - ); - res.json(account); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - // ==================== MOVIMIENTOS ==================== - - /** - * GET /movements - * Lista movimientos bancarios - */ - router.get('/movements', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - page: parseInt(req.query.page as string) || 1, - limit: parseInt(req.query.limit as string) || 50, - bankAccountId: req.query.bankAccountId as string, - movementType: req.query.movementType as any, - status: req.query.status as any, - startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined, - endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined, - search: req.query.search as string, - }; - - const result = await service.findAllMovements(ctx, options); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /movements/:id - * Obtiene un movimiento por ID - */ - router.get('/movements/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const movement = await service.findMovementById(ctx, req.params.id); - if (!movement) { - return res.status(404).json({ error: 'Movimiento no encontrado' }); - } - - res.json(movement); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * POST /movements - * Crea un nuevo movimiento - */ - router.post('/movements', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const movement = await service.createMovement(ctx, req.body); - res.status(201).json(movement); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /movements/import - * Importa estado de cuenta - */ - router.post('/movements/import', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const result = await service.importBankStatement(ctx, req.body); - res.status(201).json(result); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /movements/:id/match - * Asocia movimiento con pago/cobro - */ - router.post('/movements/:id/match', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const movement = await service.matchMovement(ctx, req.params.id, req.body); - res.json(movement); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /movements/:id/ignore - * Marca movimiento como ignorado - */ - router.post('/movements/:id/ignore', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const movement = await service.ignoreMovement(ctx, req.params.id); - res.json(movement); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - // ==================== CONCILIACI脫N ==================== - - /** - * GET /reconciliations - * Lista conciliaciones - */ - router.get('/reconciliations', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - page: parseInt(req.query.page as string) || 1, - limit: parseInt(req.query.limit as string) || 20, - bankAccountId: req.query.bankAccountId as string, - status: req.query.status as any, - }; - - const result = await service.findAllReconciliations(ctx, options); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /reconciliations/status - * Obtiene estado general de conciliaciones - */ - router.get('/reconciliations/status', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const status = await service.getReconciliationStatus(ctx); - res.json(status); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /reconciliations/:id - * Obtiene una conciliaci贸n por ID - */ - router.get('/reconciliations/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const reconciliation = await service.findReconciliationById(ctx, req.params.id); - if (!reconciliation) { - return res.status(404).json({ error: 'Conciliaci贸n no encontrada' }); - } - - res.json(reconciliation); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * POST /reconciliations - * Crea una nueva conciliaci贸n - */ - router.post('/reconciliations', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const reconciliation = await service.createReconciliation(ctx, req.body); - res.status(201).json(reconciliation); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /reconciliations/:id/start - * Inicia proceso de conciliaci贸n - */ - router.post('/reconciliations/:id/start', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const reconciliation = await service.startReconciliation(ctx, req.params.id); - res.json(reconciliation); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /reconciliations/:id/reconcile-movement - * Concilia un movimiento - */ - router.post('/reconciliations/:id/reconcile-movement', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { movementId } = req.body; - if (!movementId) { - return res.status(400).json({ error: 'Se requiere ID del movimiento' }); - } - - const reconciliation = await service.reconcileMovement(ctx, req.params.id, movementId); - res.json(reconciliation); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /reconciliations/:id/complete - * Completa una conciliaci贸n - */ - router.post('/reconciliations/:id/complete', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const reconciliation = await service.completeReconciliation(ctx, req.params.id); - res.json(reconciliation); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /reconciliations/:id/approve - * Aprueba una conciliaci贸n - */ - router.post('/reconciliations/:id/approve', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const reconciliation = await service.approveReconciliation(ctx, req.params.id); - res.json(reconciliation); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/controllers/cash-flow.controller.ts b/projects/erp-construccion/backend/src/modules/finance/controllers/cash-flow.controller.ts deleted file mode 100644 index 5b72ab04a..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/controllers/cash-flow.controller.ts +++ /dev/null @@ -1,295 +0,0 @@ -/** - * CashFlowController - Controlador de Flujo de Efectivo - * - * Endpoints para proyecciones y an谩lisis de flujo de efectivo. - * - * @module Finance - */ - -import { Router, Request, Response } from 'express'; -import { DataSource } from 'typeorm'; -import { CashFlowService } from '../services'; - -export function createCashFlowController(dataSource: DataSource): Router { - const router = Router(); - const service = new CashFlowService(dataSource); - - // ==================== PROYECCIONES ==================== - - /** - * GET / - * Lista proyecciones con filtros - */ - router.get('/', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const options = { - page: parseInt(req.query.page as string) || 1, - limit: parseInt(req.query.limit as string) || 20, - flowType: req.query.flowType as any, - periodType: req.query.periodType as any, - projectId: req.query.projectId as string, - fiscalYear: req.query.fiscalYear ? parseInt(req.query.fiscalYear as string) : undefined, - startDate: req.query.startDate ? new Date(req.query.startDate as string) : undefined, - endDate: req.query.endDate ? new Date(req.query.endDate as string) : undefined, - }; - - const result = await service.findAll(ctx, options); - res.json(result); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /dashboard - * Obtiene datos del dashboard - */ - router.get('/dashboard', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const data = await service.getDashboardData(ctx); - res.json(data); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /summary - * Obtiene resumen de flujo de efectivo - */ - router.get('/summary', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const startDate = new Date(req.query.startDate as string); - const endDate = new Date(req.query.endDate as string); - - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - periodType: req.query.periodType as any, - flowType: req.query.flowType as any, - projectId: req.query.projectId as string, - }; - - const summary = await service.getCashFlowSummary(ctx, startDate, endDate, options); - res.json(summary); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /variance - * Obtiene an谩lisis de varianza - */ - router.get('/variance', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const fiscalYear = parseInt(req.query.fiscalYear as string) || new Date().getFullYear(); - const options = { - projectId: req.query.projectId as string, - }; - - const analysis = await service.getVarianceAnalysis(ctx, fiscalYear, options); - res.json(analysis); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /:id - * Obtiene una proyecci贸n por ID - */ - router.get('/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const projection = await service.findById(ctx, req.params.id); - if (!projection) { - return res.status(404).json({ error: 'Proyecci贸n no encontrada' }); - } - - res.json(projection); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * POST / - * Crea una nueva proyecci贸n - */ - router.post('/', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const projection = await service.create(ctx, req.body); - res.status(201).json(projection); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * PUT /:id - * Actualiza una proyecci贸n - */ - router.put('/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const projection = await service.update(ctx, req.params.id, req.body); - res.json(projection); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /:id/lock - * Bloquea una proyecci贸n - */ - router.post('/:id/lock', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const projection = await service.lock(ctx, req.params.id); - res.json(projection); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * DELETE /:id - * Elimina una proyecci贸n - */ - router.delete('/:id', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - await service.delete(ctx, req.params.id); - res.status(204).send(); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - // ==================== GENERACI脫N AUTOM脕TICA ==================== - - /** - * POST /generate - * Genera proyecci贸n autom谩tica para un periodo - */ - router.post('/generate', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { periodStart, periodEnd, periodType, projectId, projectCode } = req.body; - - if (!periodStart || !periodEnd) { - return res.status(400).json({ error: 'Se requieren fechas de inicio y fin' }); - } - - const projection = await service.generateProjection( - ctx, - new Date(periodStart), - new Date(periodEnd), - { periodType, projectId, projectCode } - ); - - res.status(201).json(projection); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /generate-multiple - * Genera m煤ltiples proyecciones (ej: 4 semanas) - */ - router.post('/generate-multiple', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { startDate, weeks, projectId, projectCode } = req.body; - - if (!startDate || !weeks) { - return res.status(400).json({ error: 'Se requiere fecha de inicio y n煤mero de semanas' }); - } - - const projections = await service.generateMultiplePeriods( - ctx, - new Date(startDate), - parseInt(weeks), - { projectId, projectCode } - ); - - res.status(201).json(projections); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - /** - * POST /:id/compare - * Crea comparaci贸n proyectado vs real - */ - router.post('/:id/compare', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const comparison = await service.createComparison(ctx, req.params.id); - res.status(201).json(comparison); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/controllers/index.ts b/projects/erp-construccion/backend/src/modules/finance/controllers/index.ts deleted file mode 100644 index c4f6d86f1..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/controllers/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Finance Controllers Index - * @module Finance - */ - -export { createAccountingController } from './accounting.controller'; -export { createAPController } from './ap.controller'; -export { createARController } from './ar.controller'; -export { createCashFlowController } from './cash-flow.controller'; -export { createBankReconciliationController } from './bank-reconciliation.controller'; -export { createReportsController } from './reports.controller'; diff --git a/projects/erp-construccion/backend/src/modules/finance/controllers/reports.controller.ts b/projects/erp-construccion/backend/src/modules/finance/controllers/reports.controller.ts deleted file mode 100644 index 8208235dc..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/controllers/reports.controller.ts +++ /dev/null @@ -1,410 +0,0 @@ -/** - * ReportsController - Controlador de Reportes Financieros - * - * Endpoints para estados financieros y exportaci贸n. - * - * @module Finance - */ - -import { Router, Request, Response } from 'express'; -import { DataSource } from 'typeorm'; -import { FinancialReportsService, ERPIntegrationService } from '../services'; - -export function createReportsController(dataSource: DataSource): Router { - const router = Router(); - const reportsService = new FinancialReportsService(dataSource); - const integrationService = new ERPIntegrationService(dataSource); - - // ==================== ESTADOS FINANCIEROS ==================== - - /** - * GET /balance-sheet - * Genera balance general - */ - router.get('/balance-sheet', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const asOfDate = req.query.asOfDate - ? new Date(req.query.asOfDate as string) - : new Date(); - - const options = { - projectId: req.query.projectId as string, - }; - - const report = await reportsService.generateBalanceSheet(ctx, asOfDate, options); - res.json(report); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /income-statement - * Genera estado de resultados - */ - router.get('/income-statement', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const periodStart = new Date(req.query.periodStart as string); - const periodEnd = new Date(req.query.periodEnd as string); - - if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - projectId: req.query.projectId as string, - }; - - const report = await reportsService.generateIncomeStatement( - ctx, - periodStart, - periodEnd, - options - ); - res.json(report); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /cash-flow-statement - * Genera estado de flujo de efectivo - */ - router.get('/cash-flow-statement', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const periodStart = new Date(req.query.periodStart as string); - const periodEnd = new Date(req.query.periodEnd as string); - - if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - projectId: req.query.projectId as string, - }; - - const report = await reportsService.generateCashFlowStatement( - ctx, - periodStart, - periodEnd, - options - ); - res.json(report); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /trial-balance - * Genera balanza de comprobaci贸n - */ - router.get('/trial-balance', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const periodStart = new Date(req.query.periodStart as string); - const periodEnd = new Date(req.query.periodEnd as string); - - if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - projectId: req.query.projectId as string, - includeZeroBalances: req.query.includeZeroBalances === 'true', - }; - - const report = await reportsService.generateTrialBalance( - ctx, - periodStart, - periodEnd, - options - ); - res.json(report); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /account-statement/:accountId - * Genera estado de cuenta - */ - router.get('/account-statement/:accountId', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const periodStart = new Date(req.query.periodStart as string); - const periodEnd = new Date(req.query.periodEnd as string); - - if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const report = await reportsService.generateAccountStatement( - ctx, - req.params.accountId, - periodStart, - periodEnd - ); - res.json(report); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /summary - * Obtiene resumen financiero - */ - router.get('/summary', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const summary = await reportsService.getFinancialSummary(ctx); - res.json(summary); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - // ==================== EXPORTACI脫N ==================== - - /** - * GET /export/sap - * Exporta p贸lizas para SAP - */ - router.get('/export/sap', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const periodStart = new Date(req.query.periodStart as string); - const periodEnd = new Date(req.query.periodEnd as string); - - if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - companyCode: req.query.companyCode as string, - documentType: req.query.documentType as string, - journalNumber: req.query.journalNumber - ? parseInt(req.query.journalNumber as string) - : undefined, - }; - - const result = await integrationService.exportToSAP(ctx, periodStart, periodEnd, options); - - res.setHeader('Content-Type', 'text/plain'); - res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); - res.send(result.data); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /export/contpaqi - * Exporta p贸lizas para CONTPAQi - */ - router.get('/export/contpaqi', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const periodStart = new Date(req.query.periodStart as string); - const periodEnd = new Date(req.query.periodEnd as string); - - if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - polizaTipo: req.query.polizaTipo - ? parseInt(req.query.polizaTipo as string) - : undefined, - diario: req.query.diario ? parseInt(req.query.diario as string) : undefined, - }; - - const result = await integrationService.exportToCONTPAQi( - ctx, - periodStart, - periodEnd, - options - ); - - res.setHeader('Content-Type', 'text/plain'); - res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); - res.send(result.data); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /export/cfdi-polizas - * Exporta p贸lizas en formato CFDI - */ - router.get('/export/cfdi-polizas', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const periodStart = new Date(req.query.periodStart as string); - const periodEnd = new Date(req.query.periodEnd as string); - - if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const options = { - tipoSolicitud: req.query.tipoSolicitud as string, - numOrden: req.query.numOrden as string, - numTramite: req.query.numTramite as string, - }; - - const result = await integrationService.exportCFDIPolizas( - ctx, - periodStart, - periodEnd, - options - ); - - res.setHeader('Content-Type', 'application/xml'); - res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); - res.send(result.data); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /export/chart-of-accounts - * Exporta cat谩logo de cuentas - */ - router.get('/export/chart-of-accounts', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const format = (req.query.format as 'csv' | 'xml' | 'json') || 'csv'; - const result = await integrationService.exportChartOfAccounts(ctx, format); - - const contentType = - format === 'xml' - ? 'application/xml' - : format === 'json' - ? 'application/json' - : 'text/csv'; - - res.setHeader('Content-Type', contentType); - res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); - res.send(typeof result.data === 'object' ? JSON.stringify(result.data, null, 2) : result.data); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - /** - * GET /export/trial-balance - * Exporta balanza de comprobaci贸n - */ - router.get('/export/trial-balance', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const periodStart = new Date(req.query.periodStart as string); - const periodEnd = new Date(req.query.periodEnd as string); - - if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) { - return res.status(400).json({ error: 'Fechas inv谩lidas' }); - } - - const format = (req.query.format as 'csv' | 'xml' | 'json') || 'csv'; - const result = await integrationService.exportTrialBalance( - ctx, - periodStart, - periodEnd, - format - ); - - const contentType = - format === 'xml' - ? 'application/xml' - : format === 'json' - ? 'application/json' - : 'text/csv'; - - res.setHeader('Content-Type', contentType); - res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); - res.send(typeof result.data === 'object' ? JSON.stringify(result.data, null, 2) : result.data); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } - }); - - // ==================== IMPORTACI脫N ==================== - - /** - * POST /import/chart-of-accounts - * Importa cat谩logo de cuentas - */ - router.post('/import/chart-of-accounts', async (req: Request, res: Response) => { - try { - const ctx = { - tenantId: req.headers['x-tenant-id'] as string, - userId: (req as any).user?.id, - }; - - const { data, format } = req.body; - if (!data || !format) { - return res.status(400).json({ error: 'Se requieren datos y formato' }); - } - - const result = await integrationService.importChartOfAccounts(ctx, data, format); - res.json(result); - } catch (error) { - res.status(400).json({ error: (error as Error).message }); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/account-payable.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/account-payable.entity.ts deleted file mode 100644 index c1a6a9948..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/account-payable.entity.ts +++ /dev/null @@ -1,233 +0,0 @@ -/** - * AccountPayable Entity - Cuentas por Pagar - * - * Registro de obligaciones con proveedores y subcontratistas. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { APPayment } from './ap-payment.entity'; - -export type APStatus = 'pending' | 'partial' | 'paid' | 'overdue' | 'cancelled' | 'disputed'; -export type APDocumentType = 'invoice' | 'credit_note' | 'debit_note' | 'advance' | 'retention'; - -@Entity('accounts_payable', { schema: 'finance' }) -@Index(['tenantId', 'supplierId']) -@Index(['tenantId', 'status']) -@Index(['tenantId', 'dueDate']) -@Index(['tenantId', 'projectId']) -export class AccountPayable { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // N煤mero de documento - @Column({ name: 'document_number', length: 100 }) - documentNumber!: string; - - @Column({ - name: 'document_type', - type: 'enum', - enum: ['invoice', 'credit_note', 'debit_note', 'advance', 'retention'], - enumName: 'ap_document_type', - default: 'invoice', - }) - documentType!: APDocumentType; - - // Proveedor - @Column({ name: 'supplier_id', type: 'uuid' }) - supplierId!: string; - - @Column({ name: 'supplier_name', length: 255 }) - supplierName!: string; - - @Column({ name: 'supplier_rfc', length: 13, nullable: true }) - supplierRfc?: string; - - // Estado - @Column({ - type: 'enum', - enum: ['pending', 'partial', 'paid', 'overdue', 'cancelled', 'disputed'], - enumName: 'ap_status', - default: 'pending', - }) - status!: APStatus; - - // Fechas - @Column({ name: 'invoice_date', type: 'date' }) - invoiceDate!: Date; - - @Column({ name: 'received_date', type: 'date', nullable: true }) - receivedDate?: Date; - - @Column({ name: 'due_date', type: 'date' }) - dueDate!: Date; - - @Column({ name: 'payment_date', type: 'date', nullable: true }) - paymentDate?: Date; - - // Montos - @Column({ - type: 'decimal', - precision: 18, - scale: 2, - }) - subtotal!: number; - - @Column({ - name: 'tax_amount', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - taxAmount!: number; - - @Column({ - name: 'retention_amount', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - retentionAmount!: number; - - @Column({ - name: 'total_amount', - type: 'decimal', - precision: 18, - scale: 2, - }) - totalAmount!: number; - - @Column({ - name: 'paid_amount', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - paidAmount!: number; - - @Column({ - name: 'balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - balance!: number; - - // Moneda - @Column({ length: 3, default: 'MXN' }) - currency!: string; - - @Column({ - name: 'exchange_rate', - type: 'decimal', - precision: 12, - scale: 6, - default: 1, - }) - exchangeRate!: number; - - // Origen (orden de compra, contrato) - @Column({ name: 'source_module', length: 50, nullable: true }) - sourceModule?: string; - - @Column({ name: 'source_id', type: 'uuid', nullable: true }) - sourceId?: string; - - @Column({ name: 'purchase_order_number', length: 50, nullable: true }) - purchaseOrderNumber?: string; - - // Proyecto asociado - @Column({ name: 'project_id', type: 'uuid', nullable: true }) - projectId?: string; - - @Column({ name: 'project_code', length: 50, nullable: true }) - projectCode?: string; - - // Centro de costo - @Column({ name: 'cost_center_id', type: 'uuid', nullable: true }) - costCenterId?: string; - - // Cuenta contable de contrapartida - @Column({ name: 'expense_account_id', type: 'uuid', nullable: true }) - expenseAccountId?: string; - - // Condiciones de pago - @Column({ name: 'payment_terms', length: 100, nullable: true }) - paymentTerms?: string; - - @Column({ name: 'payment_days', type: 'int', default: 30 }) - paymentDays!: number; - - // D铆as de atraso (calculado) - @Column({ name: 'days_overdue', type: 'int', default: 0 }) - daysOverdue!: number; - - // CFDI (facturaci贸n electr贸nica M茅xico) - @Column({ name: 'cfdi_uuid', length: 36, nullable: true }) - cfdiUuid?: string; - - @Column({ name: 'cfdi_xml_path', length: 500, nullable: true }) - cfdiXmlPath?: string; - - @Column({ name: 'cfdi_pdf_path', length: 500, nullable: true }) - cfdiPdfPath?: string; - - // Aprobaci贸n - @Column({ name: 'approved_for_payment', type: 'boolean', default: false }) - approvedForPayment!: boolean; - - @Column({ name: 'approved_by_id', type: 'uuid', nullable: true }) - approvedById?: string; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt?: Date; - - // P贸liza contable generada - @Column({ name: 'accounting_entry_id', type: 'uuid', nullable: true }) - accountingEntryId?: string; - - // Pagos asociados - @OneToMany(() => APPayment, (payment) => payment.accountPayable) - payments?: APPayment[]; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/account-receivable.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/account-receivable.entity.ts deleted file mode 100644 index a76e5faf2..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/account-receivable.entity.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * AccountReceivable Entity - Cuentas por Cobrar - * - * Registro de derechos de cobro a clientes. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToMany, - Index, -} from 'typeorm'; -import { ARPayment } from './ar-payment.entity'; - -export type ARStatus = 'pending' | 'partial' | 'collected' | 'overdue' | 'cancelled' | 'written_off'; -export type ARDocumentType = 'invoice' | 'credit_note' | 'debit_note' | 'advance' | 'estimation'; - -@Entity('accounts_receivable', { schema: 'finance' }) -@Index(['tenantId', 'customerId']) -@Index(['tenantId', 'status']) -@Index(['tenantId', 'dueDate']) -@Index(['tenantId', 'projectId']) -export class AccountReceivable { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // N煤mero de documento - @Column({ name: 'document_number', length: 100 }) - documentNumber!: string; - - @Column({ - name: 'document_type', - type: 'enum', - enum: ['invoice', 'credit_note', 'debit_note', 'advance', 'estimation'], - enumName: 'ar_document_type', - default: 'invoice', - }) - documentType!: ARDocumentType; - - // Cliente - @Column({ name: 'customer_id', type: 'uuid' }) - customerId!: string; - - @Column({ name: 'customer_name', length: 255 }) - customerName!: string; - - @Column({ name: 'customer_rfc', length: 13, nullable: true }) - customerRfc?: string; - - // Estado - @Column({ - type: 'enum', - enum: ['pending', 'partial', 'collected', 'overdue', 'cancelled', 'written_off'], - enumName: 'ar_status', - default: 'pending', - }) - status!: ARStatus; - - // Fechas - @Column({ name: 'invoice_date', type: 'date' }) - invoiceDate!: Date; - - @Column({ name: 'due_date', type: 'date' }) - dueDate!: Date; - - @Column({ name: 'collection_date', type: 'date', nullable: true }) - collectionDate?: Date; - - // Montos - @Column({ - type: 'decimal', - precision: 18, - scale: 2, - }) - subtotal!: number; - - @Column({ - name: 'tax_amount', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - taxAmount!: number; - - @Column({ - name: 'retention_amount', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - retentionAmount!: number; - - @Column({ - name: 'total_amount', - type: 'decimal', - precision: 18, - scale: 2, - }) - totalAmount!: number; - - @Column({ - name: 'collected_amount', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - collectedAmount!: number; - - @Column({ - name: 'balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - balance!: number; - - // Moneda - @Column({ length: 3, default: 'MXN' }) - currency!: string; - - @Column({ - name: 'exchange_rate', - type: 'decimal', - precision: 12, - scale: 6, - default: 1, - }) - exchangeRate!: number; - - // Origen (estimaci贸n, venta) - @Column({ name: 'source_module', length: 50, nullable: true }) - sourceModule?: string; - - @Column({ name: 'source_id', type: 'uuid', nullable: true }) - sourceId?: string; - - @Column({ name: 'estimation_number', length: 50, nullable: true }) - estimationNumber?: string; - - // Proyecto asociado - @Column({ name: 'project_id', type: 'uuid', nullable: true }) - projectId?: string; - - @Column({ name: 'project_code', length: 50, nullable: true }) - projectCode?: string; - - // Condiciones de cobro - @Column({ name: 'payment_terms', length: 100, nullable: true }) - paymentTerms?: string; - - @Column({ name: 'payment_days', type: 'int', default: 30 }) - paymentDays!: number; - - // D铆as de atraso (calculado) - @Column({ name: 'days_overdue', type: 'int', default: 0 }) - daysOverdue!: number; - - // CFDI (facturaci贸n electr贸nica M茅xico) - @Column({ name: 'cfdi_uuid', length: 36, nullable: true }) - cfdiUuid?: string; - - @Column({ name: 'cfdi_xml_path', length: 500, nullable: true }) - cfdiXmlPath?: string; - - @Column({ name: 'cfdi_pdf_path', length: 500, nullable: true }) - cfdiPdfPath?: string; - - // Cuenta contable - @Column({ name: 'income_account_id', type: 'uuid', nullable: true }) - incomeAccountId?: string; - - // P贸liza contable generada - @Column({ name: 'accounting_entry_id', type: 'uuid', nullable: true }) - accountingEntryId?: string; - - // Seguimiento de cobranza - @Column({ name: 'last_collection_attempt', type: 'date', nullable: true }) - lastCollectionAttempt?: Date; - - @Column({ name: 'collection_attempts', type: 'int', default: 0 }) - collectionAttempts!: number; - - @Column({ name: 'collection_notes', type: 'text', nullable: true }) - collectionNotes?: string; - - // Cobros asociados - @OneToMany(() => ARPayment, (payment) => payment.accountReceivable) - payments?: ARPayment[]; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/accounting-entry-line.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/accounting-entry-line.entity.ts deleted file mode 100644 index d45ca80f5..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/accounting-entry-line.entity.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * AccountingEntryLine Entity - L铆neas de P贸liza Contable - * - * Detalle de movimientos (debe/haber) de una p贸liza. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { AccountingEntry } from './accounting-entry.entity'; -import { ChartOfAccounts } from './chart-of-accounts.entity'; - -@Entity('accounting_entry_lines', { schema: 'finance' }) -@Index(['tenantId', 'entryId']) -@Index(['tenantId', 'accountId']) -export class AccountingEntryLine { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Referencia a la p贸liza - @Column({ name: 'entry_id', type: 'uuid' }) - entryId!: string; - - @ManyToOne(() => AccountingEntry, (entry) => entry.lines, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'entry_id' }) - entry?: AccountingEntry; - - // N煤mero de l铆nea - @Column({ name: 'line_number', type: 'int' }) - lineNumber!: number; - - // Cuenta contable - @Column({ name: 'account_id', type: 'uuid' }) - accountId!: string; - - @ManyToOne(() => ChartOfAccounts) - @JoinColumn({ name: 'account_id' }) - account?: ChartOfAccounts; - - @Column({ name: 'account_code', length: 50 }) - accountCode!: string; - - // Descripci贸n de la l铆nea - @Column({ type: 'text' }) - description!: string; - - // Montos - @Column({ - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - debit!: number; - - @Column({ - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - credit!: number; - - // Centro de costo (opcional) - @Column({ name: 'cost_center_id', type: 'uuid', nullable: true }) - costCenterId?: string; - - @Column({ name: 'cost_center_code', length: 50, nullable: true }) - costCenterCode?: string; - - // Proyecto (opcional) - @Column({ name: 'project_id', type: 'uuid', nullable: true }) - projectId?: string; - - @Column({ name: 'project_code', length: 50, nullable: true }) - projectCode?: string; - - // Tercero (proveedor/cliente) - @Column({ name: 'partner_id', type: 'uuid', nullable: true }) - partnerId?: string; - - @Column({ name: 'partner_name', length: 255, nullable: true }) - partnerName?: string; - - // Documento de referencia - @Column({ name: 'document_type', length: 50, nullable: true }) - documentType?: string; - - @Column({ name: 'document_number', length: 100, nullable: true }) - documentNumber?: string; - - // Moneda original (si es diferente) - @Column({ name: 'original_currency', length: 3, nullable: true }) - originalCurrency?: string; - - @Column({ - name: 'original_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - originalAmount?: number; - - // Metadatos - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/accounting-entry.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/accounting-entry.entity.ts deleted file mode 100644 index 17e43b927..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/accounting-entry.entity.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * AccountingEntry Entity - P贸lizas Contables - * - * Registro de asientos contables con partida doble. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { AccountingEntryLine } from './accounting-entry-line.entity'; - -export type EntryType = - | 'purchase' - | 'sale' - | 'payment' - | 'collection' - | 'payroll' - | 'adjustment' - | 'depreciation' - | 'transfer' - | 'opening' - | 'closing'; - -export type EntryStatus = 'draft' | 'pending_approval' | 'approved' | 'posted' | 'cancelled' | 'reversed'; - -@Entity('accounting_entries', { schema: 'finance' }) -@Index(['tenantId', 'entryNumber'], { unique: true }) -@Index(['tenantId', 'entryDate']) -@Index(['tenantId', 'status']) -@Index(['tenantId', 'sourceModule', 'sourceId']) -export class AccountingEntry { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // N煤mero de p贸liza - @Column({ name: 'entry_number', length: 50 }) - entryNumber!: string; - - // Tipo y fecha - @Column({ - name: 'entry_type', - type: 'enum', - enum: ['purchase', 'sale', 'payment', 'collection', 'payroll', 'adjustment', 'depreciation', 'transfer', 'opening', 'closing'], - enumName: 'entry_type', - }) - entryType!: EntryType; - - @Column({ name: 'entry_date', type: 'date' }) - entryDate!: Date; - - @Column({ - type: 'enum', - enum: ['draft', 'pending_approval', 'approved', 'posted', 'cancelled', 'reversed'], - enumName: 'entry_status', - default: 'draft', - }) - status!: EntryStatus; - - // Descripci贸n - @Column({ type: 'text' }) - description!: string; - - @Column({ length: 255, nullable: true }) - reference?: string; - - // Origen (m贸dulo que gener贸 la p贸liza) - @Column({ name: 'source_module', length: 50, nullable: true }) - sourceModule?: string; - - @Column({ name: 'source_id', type: 'uuid', nullable: true }) - sourceId?: string; - - // Proyecto asociado - @Column({ name: 'project_id', type: 'uuid', nullable: true }) - projectId?: string; - - // Periodo contable - @Column({ name: 'fiscal_year', type: 'int' }) - fiscalYear!: number; - - @Column({ name: 'fiscal_period', type: 'int' }) - fiscalPeriod!: number; - - // Totales (calculados) - @Column({ - name: 'total_debit', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - totalDebit!: number; - - @Column({ - name: 'total_credit', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - totalCredit!: number; - - @Column({ name: 'is_balanced', type: 'boolean', default: false }) - isBalanced!: boolean; - - // Moneda - @Column({ length: 3, default: 'MXN' }) - currency!: string; - - @Column({ - name: 'exchange_rate', - type: 'decimal', - precision: 12, - scale: 6, - default: 1, - }) - exchangeRate!: number; - - // Aprobaci贸n - @Column({ name: 'approved_by_id', type: 'uuid', nullable: true }) - approvedById?: string; - - @ManyToOne(() => User, { nullable: true }) - @JoinColumn({ name: 'approved_by_id' }) - approvedBy?: User; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt?: Date; - - // Contabilizaci贸n - @Column({ name: 'posted_at', type: 'timestamptz', nullable: true }) - postedAt?: Date; - - @Column({ name: 'posted_by_id', type: 'uuid', nullable: true }) - postedById?: string; - - // Reversi贸n - @Column({ name: 'reversed_entry_id', type: 'uuid', nullable: true }) - reversedEntryId?: string; - - @Column({ name: 'reversal_reason', type: 'text', nullable: true }) - reversalReason?: string; - - // L铆neas de la p贸liza - @OneToMany(() => AccountingEntryLine, (line) => line.entry, { cascade: true }) - lines?: AccountingEntryLine[]; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/ap-payment.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/ap-payment.entity.ts deleted file mode 100644 index 0c3398f90..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/ap-payment.entity.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * APPayment Entity - Pagos a Proveedores - * - * Registro de pagos realizados a cuentas por pagar. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { AccountPayable } from './account-payable.entity'; -import { BankAccount } from './bank-account.entity'; - -export type PaymentMethod = 'cash' | 'check' | 'transfer' | 'card' | 'compensation' | 'other'; -export type PaymentStatus = 'pending' | 'processed' | 'reconciled' | 'cancelled' | 'returned'; - -@Entity('ap_payments', { schema: 'finance' }) -@Index(['tenantId', 'accountPayableId']) -@Index(['tenantId', 'paymentDate']) -@Index(['tenantId', 'bankAccountId']) -export class APPayment { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // N煤mero de pago - @Column({ name: 'payment_number', length: 50 }) - paymentNumber!: string; - - // Cuenta por pagar asociada - @Column({ name: 'account_payable_id', type: 'uuid' }) - accountPayableId!: string; - - @ManyToOne(() => AccountPayable, (ap) => ap.payments) - @JoinColumn({ name: 'account_payable_id' }) - accountPayable?: AccountPayable; - - // M茅todo de pago - @Column({ - name: 'payment_method', - type: 'enum', - enum: ['cash', 'check', 'transfer', 'card', 'compensation', 'other'], - enumName: 'payment_method', - }) - paymentMethod!: PaymentMethod; - - @Column({ - type: 'enum', - enum: ['pending', 'processed', 'reconciled', 'cancelled', 'returned'], - enumName: 'payment_status', - default: 'pending', - }) - status!: PaymentStatus; - - // Fecha de pago - @Column({ name: 'payment_date', type: 'date' }) - paymentDate!: Date; - - // Monto - @Column({ - type: 'decimal', - precision: 18, - scale: 2, - }) - amount!: number; - - @Column({ length: 3, default: 'MXN' }) - currency!: string; - - @Column({ - name: 'exchange_rate', - type: 'decimal', - precision: 12, - scale: 6, - default: 1, - }) - exchangeRate!: number; - - // Cuenta bancaria de origen - @Column({ name: 'bank_account_id', type: 'uuid', nullable: true }) - bankAccountId?: string; - - @ManyToOne(() => BankAccount, { nullable: true }) - @JoinColumn({ name: 'bank_account_id' }) - bankAccount?: BankAccount; - - // Detalles del instrumento de pago - @Column({ name: 'check_number', length: 50, nullable: true }) - checkNumber?: string; - - @Column({ name: 'transfer_reference', length: 100, nullable: true }) - transferReference?: string; - - @Column({ name: 'authorization_code', length: 50, nullable: true }) - authorizationCode?: string; - - // Beneficiario - @Column({ name: 'beneficiary_name', length: 255, nullable: true }) - beneficiaryName?: string; - - @Column({ name: 'beneficiary_bank', length: 100, nullable: true }) - beneficiaryBank?: string; - - @Column({ name: 'beneficiary_account', length: 50, nullable: true }) - beneficiaryAccount?: string; - - @Column({ name: 'beneficiary_clabe', length: 18, nullable: true }) - beneficiaryClabe?: string; - - // P贸liza contable generada - @Column({ name: 'accounting_entry_id', type: 'uuid', nullable: true }) - accountingEntryId?: string; - - // Conciliaci贸n bancaria - @Column({ name: 'bank_movement_id', type: 'uuid', nullable: true }) - bankMovementId?: string; - - @Column({ name: 'reconciled_at', type: 'timestamptz', nullable: true }) - reconciledAt?: Date; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/ar-payment.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/ar-payment.entity.ts deleted file mode 100644 index 8b26b4748..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/ar-payment.entity.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * ARPayment Entity - Cobros de Clientes - * - * Registro de cobros recibidos de cuentas por cobrar. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { AccountReceivable } from './account-receivable.entity'; -import { BankAccount } from './bank-account.entity'; - -export type CollectionMethod = 'cash' | 'check' | 'transfer' | 'card' | 'compensation' | 'other'; -export type CollectionStatus = 'pending' | 'deposited' | 'reconciled' | 'cancelled' | 'returned'; - -@Entity('ar_payments', { schema: 'finance' }) -@Index(['tenantId', 'accountReceivableId']) -@Index(['tenantId', 'collectionDate']) -@Index(['tenantId', 'bankAccountId']) -export class ARPayment { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // N煤mero de cobro - @Column({ name: 'collection_number', length: 50 }) - collectionNumber!: string; - - // Cuenta por cobrar asociada - @Column({ name: 'account_receivable_id', type: 'uuid' }) - accountReceivableId!: string; - - @ManyToOne(() => AccountReceivable, (ar) => ar.payments) - @JoinColumn({ name: 'account_receivable_id' }) - accountReceivable?: AccountReceivable; - - // M茅todo de cobro - @Column({ - name: 'collection_method', - type: 'enum', - enum: ['cash', 'check', 'transfer', 'card', 'compensation', 'other'], - enumName: 'collection_method', - }) - collectionMethod!: CollectionMethod; - - @Column({ - type: 'enum', - enum: ['pending', 'deposited', 'reconciled', 'cancelled', 'returned'], - enumName: 'collection_status', - default: 'pending', - }) - status!: CollectionStatus; - - // Fecha de cobro - @Column({ name: 'collection_date', type: 'date' }) - collectionDate!: Date; - - @Column({ name: 'deposit_date', type: 'date', nullable: true }) - depositDate?: Date; - - // Monto - @Column({ - type: 'decimal', - precision: 18, - scale: 2, - }) - amount!: number; - - @Column({ length: 3, default: 'MXN' }) - currency!: string; - - @Column({ - name: 'exchange_rate', - type: 'decimal', - precision: 12, - scale: 6, - default: 1, - }) - exchangeRate!: number; - - // Cuenta bancaria de destino - @Column({ name: 'bank_account_id', type: 'uuid', nullable: true }) - bankAccountId?: string; - - @ManyToOne(() => BankAccount, { nullable: true }) - @JoinColumn({ name: 'bank_account_id' }) - bankAccount?: BankAccount; - - // Detalles del instrumento de cobro - @Column({ name: 'check_number', length: 50, nullable: true }) - checkNumber?: string; - - @Column({ name: 'check_bank', length: 100, nullable: true }) - checkBank?: string; - - @Column({ name: 'transfer_reference', length: 100, nullable: true }) - transferReference?: string; - - // Pagador (si es diferente al cliente) - @Column({ name: 'payer_name', length: 255, nullable: true }) - payerName?: string; - - @Column({ name: 'payer_bank', length: 100, nullable: true }) - payerBank?: string; - - @Column({ name: 'payer_account', length: 50, nullable: true }) - payerAccount?: string; - - // P贸liza contable generada - @Column({ name: 'accounting_entry_id', type: 'uuid', nullable: true }) - accountingEntryId?: string; - - // Conciliaci贸n bancaria - @Column({ name: 'bank_movement_id', type: 'uuid', nullable: true }) - bankMovementId?: string; - - @Column({ name: 'reconciled_at', type: 'timestamptz', nullable: true }) - reconciledAt?: Date; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/bank-account.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/bank-account.entity.ts deleted file mode 100644 index 95ff2a653..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/bank-account.entity.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * BankAccount Entity - Cuentas Bancarias - * - * Registro de cuentas bancarias de la empresa. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - Index, -} from 'typeorm'; - -export type BankAccountType = 'checking' | 'savings' | 'investment' | 'credit_line' | 'other'; -export type BankAccountStatus = 'active' | 'inactive' | 'blocked' | 'closed'; - -@Entity('bank_accounts', { schema: 'finance' }) -@Index(['tenantId', 'accountNumber'], { unique: true }) -@Index(['tenantId', 'status']) -export class BankAccount { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Informaci贸n de la cuenta - @Column({ length: 100 }) - name!: string; - - @Column({ name: 'account_number', length: 50 }) - accountNumber!: string; - - @Column({ length: 18, nullable: true }) - clabe?: string; - - @Column({ - name: 'account_type', - type: 'enum', - enum: ['checking', 'savings', 'investment', 'credit_line', 'other'], - enumName: 'bank_account_type', - default: 'checking', - }) - accountType!: BankAccountType; - - @Column({ - type: 'enum', - enum: ['active', 'inactive', 'blocked', 'closed'], - enumName: 'bank_account_status', - default: 'active', - }) - status!: BankAccountStatus; - - // Banco - @Column({ name: 'bank_name', length: 100 }) - bankName!: string; - - @Column({ name: 'bank_code', length: 10, nullable: true }) - bankCode?: string; - - @Column({ name: 'branch_name', length: 100, nullable: true }) - branchName?: string; - - @Column({ name: 'branch_code', length: 20, nullable: true }) - branchCode?: string; - - // Moneda - @Column({ length: 3, default: 'MXN' }) - currency!: string; - - // Saldos - @Column({ - name: 'initial_balance', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - initialBalance!: number; - - @Column({ - name: 'current_balance', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - currentBalance!: number; - - @Column({ - name: 'available_balance', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - availableBalance!: number; - - @Column({ name: 'balance_updated_at', type: 'timestamptz', nullable: true }) - balanceUpdatedAt?: Date; - - // L铆mites (para l铆neas de cr茅dito) - @Column({ - name: 'credit_limit', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - creditLimit?: number; - - @Column({ - name: 'minimum_balance', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - minimumBalance?: number; - - // Proyecto asociado (si es cuenta espec铆fica de proyecto) - @Column({ name: 'project_id', type: 'uuid', nullable: true }) - projectId?: string; - - @Column({ name: 'project_code', length: 50, nullable: true }) - projectCode?: string; - - // Cuenta contable vinculada - @Column({ name: 'ledger_account_id', type: 'uuid', nullable: true }) - ledgerAccountId?: string; - - // Contacto del banco - @Column({ name: 'bank_contact_name', length: 255, nullable: true }) - bankContactName?: string; - - @Column({ name: 'bank_contact_phone', length: 50, nullable: true }) - bankContactPhone?: string; - - @Column({ name: 'bank_contact_email', length: 255, nullable: true }) - bankContactEmail?: string; - - // Conciliaci贸n - @Column({ name: 'last_reconciliation_date', type: 'date', nullable: true }) - lastReconciliationDate?: Date; - - @Column({ - name: 'last_reconciled_balance', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - lastReconciledBalance?: number; - - // Acceso banca en l铆nea - @Column({ name: 'online_banking_user', length: 100, nullable: true }) - onlineBankingUser?: string; - - @Column({ name: 'supports_api', type: 'boolean', default: false }) - supportsApi!: boolean; - - // Flags - @Column({ name: 'is_default', type: 'boolean', default: false }) - isDefault!: boolean; - - @Column({ name: 'allows_payments', type: 'boolean', default: true }) - allowsPayments!: boolean; - - @Column({ name: 'allows_collections', type: 'boolean', default: true }) - allowsCollections!: boolean; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/bank-movement.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/bank-movement.entity.ts deleted file mode 100644 index ff17b2caf..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/bank-movement.entity.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * BankMovement Entity - Movimientos Bancarios - * - * Registro de movimientos importados de estados de cuenta. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { BankAccount } from './bank-account.entity'; - -export type MovementType = 'debit' | 'credit'; -export type MovementStatus = 'pending' | 'matched' | 'reconciled' | 'unreconciled' | 'ignored'; -export type MovementSource = 'manual' | 'import_file' | 'api' | 'system'; - -@Entity('bank_movements', { schema: 'finance' }) -@Index(['tenantId', 'bankAccountId']) -@Index(['tenantId', 'movementDate']) -@Index(['tenantId', 'status']) -export class BankMovement { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Cuenta bancaria - @Column({ name: 'bank_account_id', type: 'uuid' }) - bankAccountId!: string; - - @ManyToOne(() => BankAccount) - @JoinColumn({ name: 'bank_account_id' }) - bankAccount?: BankAccount; - - // Referencia del movimiento - @Column({ name: 'movement_reference', length: 100, nullable: true }) - movementReference?: string; - - @Column({ name: 'bank_reference', length: 100, nullable: true }) - bankReference?: string; - - // Tipo y fecha - @Column({ - name: 'movement_type', - type: 'enum', - enum: ['debit', 'credit'], - enumName: 'bank_movement_type', - }) - movementType!: MovementType; - - @Column({ name: 'movement_date', type: 'date' }) - movementDate!: Date; - - @Column({ name: 'value_date', type: 'date', nullable: true }) - valueDate?: Date; - - // Descripci贸n del banco - @Column({ type: 'text' }) - description!: string; - - @Column({ name: 'bank_description', type: 'text', nullable: true }) - bankDescription?: string; - - // Monto - @Column({ - type: 'decimal', - precision: 18, - scale: 2, - }) - amount!: number; - - @Column({ length: 3, default: 'MXN' }) - currency!: string; - - // Saldo despu茅s del movimiento - @Column({ - name: 'balance_after', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - balanceAfter?: number; - - // Estado de conciliaci贸n - @Column({ - type: 'enum', - enum: ['pending', 'matched', 'reconciled', 'unreconciled', 'ignored'], - enumName: 'bank_movement_status', - default: 'pending', - }) - status!: MovementStatus; - - // Origen del movimiento - @Column({ - type: 'enum', - enum: ['manual', 'import_file', 'api', 'system'], - enumName: 'bank_movement_source', - default: 'manual', - }) - source!: MovementSource; - - @Column({ name: 'import_batch_id', type: 'uuid', nullable: true }) - importBatchId?: string; - - // Coincidencia autom谩tica - @Column({ name: 'matched_payment_id', type: 'uuid', nullable: true }) - matchedPaymentId?: string; - - @Column({ name: 'matched_collection_id', type: 'uuid', nullable: true }) - matchedCollectionId?: string; - - @Column({ name: 'matched_entry_id', type: 'uuid', nullable: true }) - matchedEntryId?: string; - - @Column({ - name: 'match_confidence', - type: 'decimal', - precision: 5, - scale: 2, - nullable: true, - }) - matchConfidence?: number; - - // Conciliaci贸n - @Column({ name: 'reconciliation_id', type: 'uuid', nullable: true }) - reconciliationId?: string; - - @Column({ name: 'reconciled_at', type: 'timestamptz', nullable: true }) - reconciledAt?: Date; - - @Column({ name: 'reconciled_by_id', type: 'uuid', nullable: true }) - reconciledById?: string; - - // Categorizaci贸n - @Column({ name: 'category', length: 100, nullable: true }) - category?: string; - - @Column({ name: 'subcategory', length: 100, nullable: true }) - subcategory?: string; - - // Tercero identificado - @Column({ name: 'partner_id', type: 'uuid', nullable: true }) - partnerId?: string; - - @Column({ name: 'partner_name', length: 255, nullable: true }) - partnerName?: string; - - // Flags - @Column({ name: 'is_duplicate', type: 'boolean', default: false }) - isDuplicate!: boolean; - - @Column({ name: 'requires_review', type: 'boolean', default: false }) - requiresReview!: boolean; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Datos originales del banco - @Column({ name: 'raw_data', type: 'jsonb', nullable: true }) - rawData?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/bank-reconciliation.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/bank-reconciliation.entity.ts deleted file mode 100644 index 80e7b9078..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/bank-reconciliation.entity.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * BankReconciliation Entity - Conciliaciones Bancarias - * - * Registro de procesos de conciliaci贸n bancaria. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { User } from '../../core/entities/user.entity'; -import { BankAccount } from './bank-account.entity'; - -export type ReconciliationStatus = 'draft' | 'in_progress' | 'completed' | 'approved' | 'cancelled'; - -@Entity('bank_reconciliations', { schema: 'finance' }) -@Index(['tenantId', 'bankAccountId']) -@Index(['tenantId', 'periodEnd']) -@Index(['tenantId', 'status']) -export class BankReconciliation { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Cuenta bancaria - @Column({ name: 'bank_account_id', type: 'uuid' }) - bankAccountId!: string; - - @ManyToOne(() => BankAccount) - @JoinColumn({ name: 'bank_account_id' }) - bankAccount?: BankAccount; - - // Periodo de conciliaci贸n - @Column({ name: 'period_start', type: 'date' }) - periodStart!: Date; - - @Column({ name: 'period_end', type: 'date' }) - periodEnd!: Date; - - // Estado - @Column({ - type: 'enum', - enum: ['draft', 'in_progress', 'completed', 'approved', 'cancelled'], - enumName: 'reconciliation_status', - default: 'draft', - }) - status!: ReconciliationStatus; - - // Saldos seg煤n banco - @Column({ - name: 'bank_opening_balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - bankOpeningBalance!: number; - - @Column({ - name: 'bank_closing_balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - bankClosingBalance!: number; - - // Saldos seg煤n libros - @Column({ - name: 'book_opening_balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - bookOpeningBalance!: number; - - @Column({ - name: 'book_closing_balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - bookClosingBalance!: number; - - // Partidas de conciliaci贸n - @Column({ - name: 'deposits_in_transit', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - depositsInTransit!: number; - - @Column({ - name: 'checks_in_transit', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - checksInTransit!: number; - - @Column({ - name: 'bank_charges_not_recorded', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - bankChargesNotRecorded!: number; - - @Column({ - name: 'interest_not_recorded', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - interestNotRecorded!: number; - - @Column({ - name: 'other_adjustments', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - otherAdjustments!: number; - - // Saldo conciliado - @Column({ - name: 'reconciled_balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - reconciledBalance!: number; - - // Diferencia (debe ser 0 si est谩 conciliado) - @Column({ - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - difference!: number; - - @Column({ name: 'is_balanced', type: 'boolean', default: false }) - isBalanced!: boolean; - - // Contadores - @Column({ name: 'total_movements', type: 'int', default: 0 }) - totalMovements!: number; - - @Column({ name: 'reconciled_movements', type: 'int', default: 0 }) - reconciledMovements!: number; - - @Column({ name: 'pending_movements', type: 'int', default: 0 }) - pendingMovements!: number; - - // Estado de cuenta bancario (archivo importado) - @Column({ name: 'statement_file_path', length: 500, nullable: true }) - statementFilePath?: string; - - @Column({ name: 'statement_import_date', type: 'timestamptz', nullable: true }) - statementImportDate?: Date; - - // Fechas de proceso - @Column({ name: 'started_at', type: 'timestamptz', nullable: true }) - startedAt?: Date; - - @Column({ name: 'completed_at', type: 'timestamptz', nullable: true }) - completedAt?: Date; - - // Aprobaci贸n - @Column({ name: 'approved_by_id', type: 'uuid', nullable: true }) - approvedById?: string; - - @ManyToOne(() => User, { nullable: true }) - @JoinColumn({ name: 'approved_by_id' }) - approvedBy?: User; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt?: Date; - - // P贸liza de ajuste generada - @Column({ name: 'adjustment_entry_id', type: 'uuid', nullable: true }) - adjustmentEntryId?: string; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ name: 'reconciliation_items', type: 'jsonb', nullable: true }) - reconciliationItems?: Record[]; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/cash-flow-projection.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/cash-flow-projection.entity.ts deleted file mode 100644 index e8e8b1906..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/cash-flow-projection.entity.ts +++ /dev/null @@ -1,357 +0,0 @@ -/** - * CashFlowProjection Entity - Proyecciones de Flujo de Efectivo - * - * Registro de proyecciones y flujo real por periodo. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - Index, -} from 'typeorm'; - -export type CashFlowType = 'projected' | 'actual' | 'comparison'; -export type CashFlowPeriodType = 'daily' | 'weekly' | 'monthly' | 'quarterly'; -export type CashFlowCategory = - | 'operating_income' - | 'operating_expense' - | 'investing_income' - | 'investing_expense' - | 'financing_income' - | 'financing_expense'; - -@Entity('cash_flow_projections', { schema: 'finance' }) -@Index(['tenantId', 'projectId']) -@Index(['tenantId', 'periodStart', 'periodEnd']) -@Index(['tenantId', 'flowType']) -export class CashFlowProjection { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // Tipo de flujo - @Column({ - name: 'flow_type', - type: 'enum', - enum: ['projected', 'actual', 'comparison'], - enumName: 'cash_flow_type', - }) - flowType!: CashFlowType; - - @Column({ - name: 'period_type', - type: 'enum', - enum: ['daily', 'weekly', 'monthly', 'quarterly'], - enumName: 'cash_flow_period_type', - default: 'weekly', - }) - periodType!: CashFlowPeriodType; - - // Periodo - @Column({ name: 'period_start', type: 'date' }) - periodStart!: Date; - - @Column({ name: 'period_end', type: 'date' }) - periodEnd!: Date; - - @Column({ name: 'fiscal_year', type: 'int' }) - fiscalYear!: number; - - @Column({ name: 'fiscal_period', type: 'int' }) - fiscalPeriod!: number; - - // Proyecto (opcional, si es por proyecto) - @Column({ name: 'project_id', type: 'uuid', nullable: true }) - projectId?: string; - - @Column({ name: 'project_code', length: 50, nullable: true }) - projectCode?: string; - - // Saldo inicial - @Column({ - name: 'opening_balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - openingBalance!: number; - - // INGRESOS OPERATIVOS - @Column({ - name: 'income_estimations', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - incomeEstimations!: number; - - @Column({ - name: 'income_sales', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - incomeSales!: number; - - @Column({ - name: 'income_advances', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - incomeAdvances!: number; - - @Column({ - name: 'income_other', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - incomeOther!: number; - - @Column({ - name: 'total_income', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - totalIncome!: number; - - // EGRESOS OPERATIVOS - @Column({ - name: 'expense_suppliers', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - expenseSuppliers!: number; - - @Column({ - name: 'expense_subcontractors', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - expenseSubcontractors!: number; - - @Column({ - name: 'expense_payroll', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - expensePayroll!: number; - - @Column({ - name: 'expense_taxes', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - expenseTaxes!: number; - - @Column({ - name: 'expense_operating', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - expenseOperating!: number; - - @Column({ - name: 'expense_other', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - expenseOther!: number; - - @Column({ - name: 'total_expenses', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - totalExpenses!: number; - - // FLUJO NETO OPERATIVO - @Column({ - name: 'net_operating_flow', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - netOperatingFlow!: number; - - // INVERSI脫N - @Column({ - name: 'investing_income', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - investingIncome!: number; - - @Column({ - name: 'investing_expense', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - investingExpense!: number; - - @Column({ - name: 'net_investing_flow', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - netInvestingFlow!: number; - - // FINANCIAMIENTO - @Column({ - name: 'financing_income', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - financingIncome!: number; - - @Column({ - name: 'financing_expense', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - financingExpense!: number; - - @Column({ - name: 'net_financing_flow', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - netFinancingFlow!: number; - - // TOTALES - @Column({ - name: 'net_cash_flow', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - netCashFlow!: number; - - @Column({ - name: 'closing_balance', - type: 'decimal', - precision: 18, - scale: 2, - }) - closingBalance!: number; - - // Varianza (para comparaciones) - @Column({ - name: 'projected_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - projectedAmount?: number; - - @Column({ - name: 'actual_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - actualAmount?: number; - - @Column({ - name: 'variance_amount', - type: 'decimal', - precision: 18, - scale: 2, - nullable: true, - }) - varianceAmount?: number; - - @Column({ - name: 'variance_percentage', - type: 'decimal', - precision: 8, - scale: 2, - nullable: true, - }) - variancePercentage?: number; - - // Desglose detallado (JSON) - @Column({ name: 'income_breakdown', type: 'jsonb', nullable: true }) - incomeBreakdown?: Record[]; - - @Column({ name: 'expense_breakdown', type: 'jsonb', nullable: true }) - expenseBreakdown?: Record[]; - - // Estado - @Column({ name: 'is_locked', type: 'boolean', default: false }) - isLocked!: boolean; - - @Column({ name: 'locked_at', type: 'timestamptz', nullable: true }) - lockedAt?: Date; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ name: 'variance_notes', type: 'text', nullable: true }) - varianceNotes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/chart-of-accounts.entity.ts b/projects/erp-construccion/backend/src/modules/finance/entities/chart-of-accounts.entity.ts deleted file mode 100644 index 5718f13a2..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/chart-of-accounts.entity.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * ChartOfAccounts Entity - Cat谩logo de Cuentas Contables - * - * Plan de cuentas configurable por proyecto/empresa. - * - * @module Finance - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, - Tree, - TreeChildren, - TreeParent, -} from 'typeorm'; - -export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense'; -export type AccountNature = 'debit' | 'credit'; -export type AccountStatus = 'active' | 'inactive' | 'blocked'; - -@Entity('chart_of_accounts', { schema: 'finance' }) -@Tree('closure-table') -@Index(['tenantId', 'code'], { unique: true }) -@Index(['tenantId', 'accountType']) -export class ChartOfAccounts { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - @Index() - tenantId!: string; - - // C贸digo jer谩rquico de cuenta - @Column({ length: 50 }) - code!: string; - - @Column({ length: 255 }) - name!: string; - - @Column({ type: 'text', nullable: true }) - description?: string; - - // Tipo y naturaleza - @Column({ - name: 'account_type', - type: 'enum', - enum: ['asset', 'liability', 'equity', 'income', 'expense'], - enumName: 'account_type', - }) - accountType!: AccountType; - - @Column({ - type: 'enum', - enum: ['debit', 'credit'], - enumName: 'account_nature', - }) - nature!: AccountNature; - - @Column({ - type: 'enum', - enum: ['active', 'inactive', 'blocked'], - enumName: 'account_status', - default: 'active', - }) - status!: AccountStatus; - - // Jerarqu铆a - @Column({ type: 'int', default: 1 }) - level!: number; - - @TreeParent() - parent?: ChartOfAccounts; - - @TreeChildren() - children?: ChartOfAccounts[]; - - @Column({ name: 'parent_id', type: 'uuid', nullable: true }) - parentId?: string; - - // Configuraci贸n de imputaci贸n - @Column({ name: 'cost_center_required', type: 'boolean', default: false }) - costCenterRequired!: boolean; - - @Column({ name: 'project_required', type: 'boolean', default: false }) - projectRequired!: boolean; - - @Column({ name: 'allows_direct_posting', type: 'boolean', default: true }) - allowsDirectPosting!: boolean; - - // C贸digos de integraci贸n con ERPs externos - @Column({ name: 'sap_code', length: 50, nullable: true }) - sapCode?: string; - - @Column({ name: 'contpaqi_code', length: 50, nullable: true }) - contpaqiCode?: string; - - @Column({ name: 'aspel_code', length: 50, nullable: true }) - aspelCode?: string; - - // Saldos (actualizados peri贸dicamente) - @Column({ - name: 'initial_balance', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - initialBalance!: number; - - @Column({ - name: 'current_balance', - type: 'decimal', - precision: 18, - scale: 2, - default: 0, - }) - currentBalance!: number; - - @Column({ name: 'balance_updated_at', type: 'timestamptz', nullable: true }) - balanceUpdatedAt?: Date; - - // Notas y metadatos - @Column({ type: 'text', nullable: true }) - notes?: string; - - @Column({ type: 'jsonb', nullable: true }) - metadata?: Record; - - // Auditor铆a - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdBy?: string; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedBy?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt!: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt!: Date; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt?: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/finance/entities/index.ts b/projects/erp-construccion/backend/src/modules/finance/entities/index.ts deleted file mode 100644 index 40f6cf7ca..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/entities/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Finance Entities Index - * @module Finance - */ - -export { ChartOfAccounts, AccountType, AccountNature, AccountStatus } from './chart-of-accounts.entity'; -export { AccountingEntry, EntryType, EntryStatus } from './accounting-entry.entity'; -export { AccountingEntryLine } from './accounting-entry-line.entity'; -export { AccountPayable, APStatus, APDocumentType } from './account-payable.entity'; -export { APPayment, PaymentMethod, PaymentStatus } from './ap-payment.entity'; -export { AccountReceivable, ARStatus, ARDocumentType } from './account-receivable.entity'; -export { ARPayment, CollectionMethod, CollectionStatus } from './ar-payment.entity'; -export { BankAccount, BankAccountType, BankAccountStatus } from './bank-account.entity'; -export { BankMovement, MovementType, MovementStatus, MovementSource } from './bank-movement.entity'; -export { BankReconciliation, ReconciliationStatus } from './bank-reconciliation.entity'; -export { CashFlowProjection, CashFlowType, CashFlowPeriodType, CashFlowCategory } from './cash-flow-projection.entity'; diff --git a/projects/erp-construccion/backend/src/modules/finance/index.ts b/projects/erp-construccion/backend/src/modules/finance/index.ts deleted file mode 100644 index 45dda807f..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Finance Module Index - * @module Finance - */ - -// Entities -export * from './entities'; - -// Services -export * from './services'; - -// Controllers -export * from './controllers'; diff --git a/projects/erp-construccion/backend/src/modules/finance/services/accounting.service.ts b/projects/erp-construccion/backend/src/modules/finance/services/accounting.service.ts deleted file mode 100644 index 65669a669..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/services/accounting.service.ts +++ /dev/null @@ -1,813 +0,0 @@ -/** - * AccountingService - Servicio de Contabilidad - * - * Gesti贸n del cat谩logo de cuentas y p贸lizas contables. - * - * @module Finance - */ - -import { DataSource, Repository, IsNull, Not, In } from 'typeorm'; -import { - ChartOfAccounts, - AccountType, - AccountNature, - AccountStatus, - AccountingEntry, - EntryType, - EntryStatus, - AccountingEntryLine, -} from '../entities'; - -interface ServiceContext { - tenantId: string; - userId?: string; -} - -interface PaginatedResult { - data: T[]; - meta: { - total: number; - page: number; - limit: number; - totalPages: number; - }; -} - -interface CreateAccountDto { - code: string; - name: string; - accountType: AccountType; - nature: AccountNature; - parentId?: string; - description?: string; - level?: number; - isGroupAccount?: boolean; - acceptsMovements?: boolean; - satCode?: string; - satDescription?: string; - currencyCode?: string; - metadata?: Record; -} - -interface UpdateAccountDto { - name?: string; - description?: string; - status?: AccountStatus; - acceptsMovements?: boolean; - satCode?: string; - satDescription?: string; - metadata?: Record; -} - -interface CreateEntryDto { - entryType: EntryType; - entryDate: Date; - reference?: string; - description: string; - projectId?: string; - projectCode?: string; - costCenterId?: string; - fiscalYear: number; - fiscalPeriod: number; - currencyCode?: string; - exchangeRate?: number; - sourceDocument?: string; - sourceDocumentId?: string; - notes?: string; - lines: CreateEntryLineDto[]; -} - -interface CreateEntryLineDto { - accountId: string; - accountCode: string; - description?: string; - debitAmount?: number; - creditAmount?: number; - currencyAmount?: number; - currencyCode?: string; - exchangeRate?: number; - costCenterId?: string; - projectId?: string; - partnerId?: string; - reference?: string; - metadata?: Record; -} - -interface AccountWithChildren extends ChartOfAccounts { - children?: AccountWithChildren[]; -} - -export class AccountingService { - private accountRepository: Repository; - private entryRepository: Repository; - private lineRepository: Repository; - - constructor(private dataSource: DataSource) { - this.accountRepository = dataSource.getRepository(ChartOfAccounts); - this.entryRepository = dataSource.getRepository(AccountingEntry); - this.lineRepository = dataSource.getRepository(AccountingEntryLine); - } - - // ==================== CAT脕LOGO DE CUENTAS ==================== - - async findAllAccounts( - ctx: ServiceContext, - options: { - page?: number; - limit?: number; - accountType?: AccountType; - status?: AccountStatus; - parentId?: string; - search?: string; - acceptsMovements?: boolean; - } = {} - ): Promise> { - const { page = 1, limit = 50, accountType, status, parentId, search, acceptsMovements } = options; - - const queryBuilder = this.accountRepository - .createQueryBuilder('account') - .where('account.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('account.deletedAt IS NULL'); - - if (accountType) { - queryBuilder.andWhere('account.accountType = :accountType', { accountType }); - } - - if (status) { - queryBuilder.andWhere('account.status = :status', { status }); - } - - if (parentId !== undefined) { - if (parentId === null || parentId === '') { - queryBuilder.andWhere('account.parentId IS NULL'); - } else { - queryBuilder.andWhere('account.parentId = :parentId', { parentId }); - } - } - - if (search) { - queryBuilder.andWhere( - '(account.code ILIKE :search OR account.name ILIKE :search)', - { search: `%${search}%` } - ); - } - - if (acceptsMovements !== undefined) { - queryBuilder.andWhere('account.acceptsMovements = :acceptsMovements', { acceptsMovements }); - } - - queryBuilder.orderBy('account.code', 'ASC'); - - const total = await queryBuilder.getCount(); - const data = await queryBuilder - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async getAccountTree(ctx: ServiceContext): Promise { - const accounts = await this.accountRepository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - order: { code: 'ASC' }, - }); - - return this.buildTree(accounts, null); - } - - private buildTree( - items: ChartOfAccounts[], - parentId: string | null - ): AccountWithChildren[] { - const result: AccountWithChildren[] = []; - - for (const item of items) { - if (item.parentId === parentId) { - const children = this.buildTree(items, item.id); - const node: AccountWithChildren = { ...item }; - if (children.length > 0) { - node.children = children; - } - result.push(node); - } - } - - return result; - } - - async findAccountById( - ctx: ServiceContext, - id: string - ): Promise { - return this.accountRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - relations: ['parent'], - }); - } - - async findAccountByCode( - ctx: ServiceContext, - code: string - ): Promise { - return this.accountRepository.findOne({ - where: { - code, - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - }); - } - - async createAccount( - ctx: ServiceContext, - data: CreateAccountDto - ): Promise { - // Verificar c贸digo 煤nico - const existing = await this.findAccountByCode(ctx, data.code); - if (existing) { - throw new Error(`Ya existe una cuenta con el c贸digo ${data.code}`); - } - - // Si tiene padre, verificar que existe - let parentAccount: ChartOfAccounts | null = null; - if (data.parentId) { - parentAccount = await this.findAccountById(ctx, data.parentId); - if (!parentAccount) { - throw new Error('Cuenta padre no encontrada'); - } - } - - // Determinar nivel - const level = data.level ?? (parentAccount ? parentAccount.level + 1 : 1); - - // Generar path completo - let fullPath = data.code; - if (parentAccount) { - fullPath = `${parentAccount.fullPath}/${data.code}`; - } - - const account = this.accountRepository.create({ - tenantId: ctx.tenantId, - code: data.code, - name: data.name, - description: data.description, - accountType: data.accountType, - nature: data.nature, - level, - parentId: data.parentId, - fullPath, - isGroupAccount: data.isGroupAccount ?? false, - acceptsMovements: data.acceptsMovements ?? true, - status: 'active', - satCode: data.satCode, - satDescription: data.satDescription, - currencyCode: data.currencyCode ?? 'MXN', - balance: 0, - metadata: data.metadata, - createdBy: ctx.userId, - }); - - return this.accountRepository.save(account); - } - - async updateAccount( - ctx: ServiceContext, - id: string, - data: UpdateAccountDto - ): Promise { - const account = await this.findAccountById(ctx, id); - if (!account) { - throw new Error('Cuenta no encontrada'); - } - - Object.assign(account, { - ...data, - updatedBy: ctx.userId, - }); - - return this.accountRepository.save(account); - } - - async deleteAccount(ctx: ServiceContext, id: string): Promise { - const account = await this.findAccountById(ctx, id); - if (!account) { - throw new Error('Cuenta no encontrada'); - } - - // Verificar que no tenga hijos - const children = await this.accountRepository.count({ - where: { - tenantId: ctx.tenantId, - parentId: id, - deletedAt: IsNull(), - }, - }); - - if (children > 0) { - throw new Error('No se puede eliminar una cuenta con subcuentas'); - } - - // Verificar que no tenga movimientos - const movements = await this.lineRepository.count({ - where: { - accountId: id, - }, - }); - - if (movements > 0) { - throw new Error('No se puede eliminar una cuenta con movimientos'); - } - - account.deletedAt = new Date(); - account.updatedBy = ctx.userId; - await this.accountRepository.save(account); - } - - // ==================== P脫LIZAS CONTABLES ==================== - - async findAllEntries( - ctx: ServiceContext, - options: { - page?: number; - limit?: number; - entryType?: EntryType; - status?: EntryStatus; - startDate?: Date; - endDate?: Date; - projectId?: string; - fiscalYear?: number; - fiscalPeriod?: number; - search?: string; - } = {} - ): Promise> { - const { - page = 1, - limit = 20, - entryType, - status, - startDate, - endDate, - projectId, - fiscalYear, - fiscalPeriod, - search, - } = options; - - const queryBuilder = this.entryRepository - .createQueryBuilder('entry') - .leftJoinAndSelect('entry.lines', 'lines') - .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.deletedAt IS NULL'); - - if (entryType) { - queryBuilder.andWhere('entry.entryType = :entryType', { entryType }); - } - - if (status) { - queryBuilder.andWhere('entry.status = :status', { status }); - } - - if (startDate) { - queryBuilder.andWhere('entry.entryDate >= :startDate', { startDate }); - } - - if (endDate) { - queryBuilder.andWhere('entry.entryDate <= :endDate', { endDate }); - } - - if (projectId) { - queryBuilder.andWhere('entry.projectId = :projectId', { projectId }); - } - - if (fiscalYear) { - queryBuilder.andWhere('entry.fiscalYear = :fiscalYear', { fiscalYear }); - } - - if (fiscalPeriod) { - queryBuilder.andWhere('entry.fiscalPeriod = :fiscalPeriod', { fiscalPeriod }); - } - - if (search) { - queryBuilder.andWhere( - '(entry.entryNumber ILIKE :search OR entry.description ILIKE :search OR entry.reference ILIKE :search)', - { search: `%${search}%` } - ); - } - - queryBuilder.orderBy('entry.entryDate', 'DESC').addOrderBy('entry.entryNumber', 'DESC'); - - const total = await queryBuilder.getCount(); - const data = await queryBuilder - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findEntryById( - ctx: ServiceContext, - id: string - ): Promise { - return this.entryRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - relations: ['lines', 'approvedBy', 'postedBy'], - }); - } - - async createEntry( - ctx: ServiceContext, - data: CreateEntryDto - ): Promise { - // Validar que la p贸liza cuadre - let totalDebit = 0; - let totalCredit = 0; - - for (const line of data.lines) { - totalDebit += line.debitAmount ?? 0; - totalCredit += line.creditAmount ?? 0; - } - - if (Math.abs(totalDebit - totalCredit) > 0.01) { - throw new Error( - `La p贸liza no cuadra. D茅bitos: ${totalDebit}, Cr茅ditos: ${totalCredit}` - ); - } - - // Generar n煤mero de p贸liza - const entryNumber = await this.generateEntryNumber( - ctx, - data.entryType, - data.fiscalYear, - data.fiscalPeriod - ); - - // Crear p贸liza - const entry = this.entryRepository.create({ - tenantId: ctx.tenantId, - entryNumber, - entryType: data.entryType, - entryDate: data.entryDate, - reference: data.reference, - description: data.description, - projectId: data.projectId, - projectCode: data.projectCode, - costCenterId: data.costCenterId, - fiscalYear: data.fiscalYear, - fiscalPeriod: data.fiscalPeriod, - currencyCode: data.currencyCode ?? 'MXN', - exchangeRate: data.exchangeRate ?? 1, - totalDebit, - totalCredit, - lineCount: data.lines.length, - sourceDocument: data.sourceDocument, - sourceDocumentId: data.sourceDocumentId, - notes: data.notes, - status: 'draft', - createdBy: ctx.userId, - }); - - const savedEntry = await this.entryRepository.save(entry); - - // Crear l铆neas - const lines = data.lines.map((lineData, index) => - this.lineRepository.create({ - entryId: savedEntry.id, - lineNumber: index + 1, - accountId: lineData.accountId, - accountCode: lineData.accountCode, - description: lineData.description, - debitAmount: lineData.debitAmount ?? 0, - creditAmount: lineData.creditAmount ?? 0, - currencyAmount: lineData.currencyAmount, - currencyCode: lineData.currencyCode, - exchangeRate: lineData.exchangeRate, - costCenterId: lineData.costCenterId, - projectId: lineData.projectId, - partnerId: lineData.partnerId, - reference: lineData.reference, - metadata: lineData.metadata, - }) - ); - - await this.lineRepository.save(lines); - - return this.findEntryById(ctx, savedEntry.id) as Promise; - } - - private async generateEntryNumber( - ctx: ServiceContext, - entryType: EntryType, - fiscalYear: number, - fiscalPeriod: number - ): Promise { - const typePrefix: Record = { - purchase: 'PC', - sale: 'VT', - payment: 'PG', - collection: 'CB', - payroll: 'NM', - adjustment: 'AJ', - depreciation: 'DP', - transfer: 'TR', - opening: 'AP', - closing: 'CI', - }; - - const prefix = typePrefix[entryType] || 'PL'; - - const lastEntry = await this.entryRepository - .createQueryBuilder('entry') - .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.fiscalYear = :fiscalYear', { fiscalYear }) - .andWhere('entry.fiscalPeriod = :fiscalPeriod', { fiscalPeriod }) - .andWhere('entry.entryType = :entryType', { entryType }) - .orderBy('entry.entryNumber', 'DESC') - .getOne(); - - let sequence = 1; - if (lastEntry) { - const match = lastEntry.entryNumber.match(/(\d+)$/); - if (match) { - sequence = parseInt(match[1], 10) + 1; - } - } - - return `${prefix}-${fiscalYear}${String(fiscalPeriod).padStart(2, '0')}-${String(sequence).padStart(5, '0')}`; - } - - async submitForApproval(ctx: ServiceContext, id: string): Promise { - const entry = await this.findEntryById(ctx, id); - if (!entry) { - throw new Error('P贸liza no encontrada'); - } - - if (entry.status !== 'draft') { - throw new Error('Solo se pueden enviar a aprobaci贸n p贸lizas en borrador'); - } - - entry.status = 'pending_approval'; - entry.updatedBy = ctx.userId; - - return this.entryRepository.save(entry); - } - - async approveEntry(ctx: ServiceContext, id: string): Promise { - const entry = await this.findEntryById(ctx, id); - if (!entry) { - throw new Error('P贸liza no encontrada'); - } - - if (entry.status !== 'pending_approval') { - throw new Error('Solo se pueden aprobar p贸lizas pendientes de aprobaci贸n'); - } - - entry.status = 'approved'; - entry.approvedById = ctx.userId; - entry.approvedAt = new Date(); - entry.updatedBy = ctx.userId; - - return this.entryRepository.save(entry); - } - - async postEntry(ctx: ServiceContext, id: string): Promise { - const entry = await this.findEntryById(ctx, id); - if (!entry) { - throw new Error('P贸liza no encontrada'); - } - - if (entry.status !== 'approved') { - throw new Error('Solo se pueden contabilizar p贸lizas aprobadas'); - } - - // Actualizar saldos de cuentas - for (const line of entry.lines || []) { - await this.updateAccountBalance(line.accountId, line.debitAmount, line.creditAmount); - } - - entry.status = 'posted'; - entry.postedById = ctx.userId; - entry.postedAt = new Date(); - entry.updatedBy = ctx.userId; - - return this.entryRepository.save(entry); - } - - private async updateAccountBalance( - accountId: string, - debitAmount: number, - creditAmount: number - ): Promise { - const account = await this.accountRepository.findOne({ - where: { id: accountId }, - }); - - if (!account) return; - - // Calcular nuevo saldo seg煤n naturaleza de la cuenta - let newBalance = account.balance; - if (account.nature === 'debit') { - newBalance += debitAmount - creditAmount; - } else { - newBalance += creditAmount - debitAmount; - } - - await this.accountRepository.update(accountId, { - balance: newBalance, - lastMovementDate: new Date(), - }); - } - - async cancelEntry(ctx: ServiceContext, id: string, reason: string): Promise { - const entry = await this.findEntryById(ctx, id); - if (!entry) { - throw new Error('P贸liza no encontrada'); - } - - if (entry.status === 'cancelled' || entry.status === 'reversed') { - throw new Error('La p贸liza ya est谩 cancelada o reversada'); - } - - // Si est谩 contabilizada, reversar saldos - if (entry.status === 'posted') { - for (const line of entry.lines || []) { - await this.updateAccountBalance(line.accountId, -line.debitAmount, -line.creditAmount); - } - } - - entry.status = 'cancelled'; - entry.notes = `${entry.notes || ''}\n[CANCELADA]: ${reason}`; - entry.updatedBy = ctx.userId; - - return this.entryRepository.save(entry); - } - - async reverseEntry(ctx: ServiceContext, id: string, reason: string): Promise { - const entry = await this.findEntryById(ctx, id); - if (!entry) { - throw new Error('P贸liza no encontrada'); - } - - if (entry.status !== 'posted') { - throw new Error('Solo se pueden reversar p贸lizas contabilizadas'); - } - - // Crear p贸liza de reverso - const reversalData: CreateEntryDto = { - entryType: entry.entryType, - entryDate: new Date(), - reference: `REV-${entry.entryNumber}`, - description: `Reverso de ${entry.entryNumber}: ${reason}`, - projectId: entry.projectId, - projectCode: entry.projectCode, - costCenterId: entry.costCenterId, - fiscalYear: entry.fiscalYear, - fiscalPeriod: entry.fiscalPeriod, - currencyCode: entry.currencyCode, - exchangeRate: entry.exchangeRate, - notes: `P贸liza de reverso autom谩tico`, - lines: (entry.lines || []).map((line) => ({ - accountId: line.accountId, - accountCode: line.accountCode, - description: `Reverso: ${line.description || ''}`, - debitAmount: line.creditAmount, // Invertir - creditAmount: line.debitAmount, // Invertir - currencyAmount: line.currencyAmount, - currencyCode: line.currencyCode, - exchangeRate: line.exchangeRate, - costCenterId: line.costCenterId, - projectId: line.projectId, - partnerId: line.partnerId, - reference: line.reference, - })), - }; - - const reversalEntry = await this.createEntry(ctx, reversalData); - - // Aprobar y contabilizar autom谩ticamente - await this.submitForApproval(ctx, reversalEntry.id); - await this.approveEntry(ctx, reversalEntry.id); - await this.postEntry(ctx, reversalEntry.id); - - // Marcar original como reversada - entry.status = 'reversed'; - entry.reversalEntryId = reversalEntry.id; - entry.notes = `${entry.notes || ''}\n[REVERSADA]: ${reason}`; - entry.updatedBy = ctx.userId; - - return this.entryRepository.save(entry); - } - - // ==================== REPORTES ==================== - - async getTrialBalance( - ctx: ServiceContext, - fiscalYear: number, - fiscalPeriod?: number - ): Promise { - const queryBuilder = this.lineRepository - .createQueryBuilder('line') - .innerJoin('line.entry', 'entry') - .innerJoin(ChartOfAccounts, 'account', 'account.id = line.accountId') - .select([ - 'line.accountId as "accountId"', - 'line.accountCode as "accountCode"', - 'account.name as "accountName"', - 'account.accountType as "accountType"', - 'account.nature as "nature"', - 'SUM(line.debitAmount) as "totalDebit"', - 'SUM(line.creditAmount) as "totalCredit"', - ]) - .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.fiscalYear = :fiscalYear', { fiscalYear }); - - if (fiscalPeriod) { - queryBuilder.andWhere('entry.fiscalPeriod <= :fiscalPeriod', { fiscalPeriod }); - } - - queryBuilder.groupBy('line.accountId, line.accountCode, account.name, account.accountType, account.nature'); - queryBuilder.orderBy('line.accountCode', 'ASC'); - - const results = await queryBuilder.getRawMany(); - - return results.map((row) => { - const debit = parseFloat(row.totalDebit) || 0; - const credit = parseFloat(row.totalCredit) || 0; - const balance = row.nature === 'debit' ? debit - credit : credit - debit; - - return { - accountId: row.accountId, - accountCode: row.accountCode, - accountName: row.accountName, - accountType: row.accountType, - totalDebit: debit, - totalCredit: credit, - balance, - debitBalance: balance > 0 ? balance : 0, - creditBalance: balance < 0 ? Math.abs(balance) : 0, - }; - }); - } - - async getAccountLedger( - ctx: ServiceContext, - accountId: string, - startDate: Date, - endDate: Date - ): Promise { - const lines = await this.lineRepository - .createQueryBuilder('line') - .innerJoinAndSelect('line.entry', 'entry') - .where('line.accountId = :accountId', { accountId }) - .andWhere('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.entryDate >= :startDate', { startDate }) - .andWhere('entry.entryDate <= :endDate', { endDate }) - .orderBy('entry.entryDate', 'ASC') - .addOrderBy('entry.entryNumber', 'ASC') - .getMany(); - - let runningBalance = 0; - return lines.map((line) => { - runningBalance += line.debitAmount - line.creditAmount; - return { - date: line.entry?.entryDate, - entryNumber: line.entry?.entryNumber, - reference: line.entry?.reference, - description: line.description || line.entry?.description, - debitAmount: line.debitAmount, - creditAmount: line.creditAmount, - balance: runningBalance, - }; - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/finance/services/ap.service.ts b/projects/erp-construccion/backend/src/modules/finance/services/ap.service.ts deleted file mode 100644 index b3b9fbb6a..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/services/ap.service.ts +++ /dev/null @@ -1,673 +0,0 @@ -/** - * APService - Servicio de Cuentas por Pagar - * - * Gesti贸n de cuentas por pagar y pagos a proveedores. - * - * @module Finance - */ - -import { DataSource, Repository, IsNull, LessThan, Between } from 'typeorm'; -import { - AccountPayable, - APStatus, - APDocumentType, - APPayment, - PaymentMethod, - PaymentStatus, -} from '../entities'; - -interface ServiceContext { - tenantId: string; - userId?: string; -} - -interface PaginatedResult { - data: T[]; - meta: { - total: number; - page: number; - limit: number; - totalPages: number; - }; -} - -interface CreateAPDto { - documentType: APDocumentType; - documentNumber: string; - documentDate: Date; - dueDate: Date; - partnerId: string; - partnerName: string; - partnerRfc?: string; - projectId?: string; - projectCode?: string; - contractId?: string; - purchaseOrderId?: string; - originalAmount: number; - currencyCode?: string; - exchangeRate?: number; - taxAmount?: number; - retentionIsr?: number; - retentionIva?: number; - guaranteeFund?: number; - cfdiUuid?: string; - cfdiXml?: string; - description?: string; - paymentTermDays?: number; - ledgerAccountId?: string; - notes?: string; - metadata?: Record; -} - -interface CreatePaymentDto { - paymentMethod: PaymentMethod; - paymentDate: Date; - bankAccountId: string; - paymentAmount: number; - currencyCode?: string; - exchangeRate?: number; - reference?: string; - checkNumber?: string; - transferReference?: string; - beneficiaryName?: string; - beneficiaryAccount?: string; - beneficiaryBank?: string; - paymentConcept?: string; - notes?: string; - accountPayableIds: string[]; -} - -interface AgingBucket { - current: number; - days1to30: number; - days31to60: number; - days61to90: number; - over90: number; - total: number; -} - -export class APService { - private apRepository: Repository; - private paymentRepository: Repository; - - constructor(private dataSource: DataSource) { - this.apRepository = dataSource.getRepository(AccountPayable); - this.paymentRepository = dataSource.getRepository(APPayment); - } - - // ==================== CUENTAS POR PAGAR ==================== - - async findAll( - ctx: ServiceContext, - options: { - page?: number; - limit?: number; - status?: APStatus; - partnerId?: string; - projectId?: string; - startDate?: Date; - endDate?: Date; - overdue?: boolean; - search?: string; - } = {} - ): Promise> { - const { - page = 1, - limit = 20, - status, - partnerId, - projectId, - startDate, - endDate, - overdue, - search, - } = options; - - const queryBuilder = this.apRepository - .createQueryBuilder('ap') - .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ap.deletedAt IS NULL'); - - if (status) { - queryBuilder.andWhere('ap.status = :status', { status }); - } - - if (partnerId) { - queryBuilder.andWhere('ap.partnerId = :partnerId', { partnerId }); - } - - if (projectId) { - queryBuilder.andWhere('ap.projectId = :projectId', { projectId }); - } - - if (startDate) { - queryBuilder.andWhere('ap.documentDate >= :startDate', { startDate }); - } - - if (endDate) { - queryBuilder.andWhere('ap.documentDate <= :endDate', { endDate }); - } - - if (overdue) { - queryBuilder.andWhere('ap.dueDate < :today', { today: new Date() }); - queryBuilder.andWhere('ap.status IN (:...statuses)', { - statuses: ['pending', 'partial'], - }); - } - - if (search) { - queryBuilder.andWhere( - '(ap.documentNumber ILIKE :search OR ap.partnerName ILIKE :search)', - { search: `%${search}%` } - ); - } - - queryBuilder.orderBy('ap.dueDate', 'ASC'); - - const total = await queryBuilder.getCount(); - const data = await queryBuilder - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.apRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - relations: ['payments'], - }); - } - - async create(ctx: ServiceContext, data: CreateAPDto): Promise { - // Calcular monto neto (con retenciones) - const netAmount = - data.originalAmount - - (data.retentionIsr ?? 0) - - (data.retentionIva ?? 0) - - (data.guaranteeFund ?? 0); - - const ap = this.apRepository.create({ - tenantId: ctx.tenantId, - documentType: data.documentType, - documentNumber: data.documentNumber, - documentDate: data.documentDate, - dueDate: data.dueDate, - partnerId: data.partnerId, - partnerName: data.partnerName, - partnerRfc: data.partnerRfc, - projectId: data.projectId, - projectCode: data.projectCode, - contractId: data.contractId, - purchaseOrderId: data.purchaseOrderId, - originalAmount: data.originalAmount, - taxAmount: data.taxAmount ?? 0, - retentionIsr: data.retentionIsr ?? 0, - retentionIva: data.retentionIva ?? 0, - guaranteeFund: data.guaranteeFund ?? 0, - netAmount, - paidAmount: 0, - balanceAmount: netAmount, - currencyCode: data.currencyCode ?? 'MXN', - exchangeRate: data.exchangeRate ?? 1, - cfdiUuid: data.cfdiUuid, - cfdiXml: data.cfdiXml, - description: data.description, - paymentTermDays: data.paymentTermDays, - ledgerAccountId: data.ledgerAccountId, - notes: data.notes, - metadata: data.metadata, - status: 'pending', - createdBy: ctx.userId, - }); - - return this.apRepository.save(ap); - } - - async update( - ctx: ServiceContext, - id: string, - data: Partial - ): Promise { - const ap = await this.findById(ctx, id); - if (!ap) { - throw new Error('Cuenta por pagar no encontrada'); - } - - if (ap.status === 'paid' || ap.status === 'cancelled') { - throw new Error('No se puede modificar una cuenta pagada o cancelada'); - } - - Object.assign(ap, { - ...data, - updatedBy: ctx.userId, - }); - - // Recalcular montos si cambi贸 el monto original - if (data.originalAmount !== undefined) { - ap.netAmount = - ap.originalAmount - - (ap.retentionIsr ?? 0) - - (ap.retentionIva ?? 0) - - (ap.guaranteeFund ?? 0); - ap.balanceAmount = ap.netAmount - ap.paidAmount; - } - - return this.apRepository.save(ap); - } - - async cancel(ctx: ServiceContext, id: string, reason: string): Promise { - const ap = await this.findById(ctx, id); - if (!ap) { - throw new Error('Cuenta por pagar no encontrada'); - } - - if (ap.paidAmount > 0) { - throw new Error('No se puede cancelar una cuenta con pagos aplicados'); - } - - ap.status = 'cancelled'; - ap.notes = `${ap.notes || ''}\n[CANCELADA]: ${reason}`; - ap.updatedBy = ctx.userId; - - return this.apRepository.save(ap); - } - - // ==================== PAGOS ==================== - - async createPayment(ctx: ServiceContext, data: CreatePaymentDto): Promise { - // Validar que las cuentas por pagar existen y calcular total - let totalToApply = 0; - const apRecords: AccountPayable[] = []; - - for (const apId of data.accountPayableIds) { - const ap = await this.findById(ctx, apId); - if (!ap) { - throw new Error(`Cuenta por pagar ${apId} no encontrada`); - } - if (ap.status === 'paid' || ap.status === 'cancelled') { - throw new Error(`Cuenta por pagar ${ap.documentNumber} ya est谩 pagada o cancelada`); - } - apRecords.push(ap); - totalToApply += ap.balanceAmount; - } - - // Validar monto - if (data.paymentAmount > totalToApply) { - throw new Error( - `El monto del pago (${data.paymentAmount}) excede el saldo pendiente (${totalToApply})` - ); - } - - // Generar n煤mero de pago - const paymentNumber = await this.generatePaymentNumber(ctx); - - // Crear pago - const payment = this.paymentRepository.create({ - tenantId: ctx.tenantId, - paymentNumber, - paymentMethod: data.paymentMethod, - paymentDate: data.paymentDate, - bankAccountId: data.bankAccountId, - paymentAmount: data.paymentAmount, - currencyCode: data.currencyCode ?? 'MXN', - exchangeRate: data.exchangeRate ?? 1, - reference: data.reference, - checkNumber: data.checkNumber, - transferReference: data.transferReference, - beneficiaryName: data.beneficiaryName, - beneficiaryAccount: data.beneficiaryAccount, - beneficiaryBank: data.beneficiaryBank, - paymentConcept: data.paymentConcept, - notes: data.notes, - status: 'pending', - documentCount: apRecords.length, - createdBy: ctx.userId, - }); - - const savedPayment = await this.paymentRepository.save(payment); - - // Aplicar pago a las cuentas por pagar (FIFO por fecha de vencimiento) - let remainingAmount = data.paymentAmount; - const sortedAP = apRecords.sort( - (a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime() - ); - - const applications: { apId: string; amount: number }[] = []; - - for (const ap of sortedAP) { - if (remainingAmount <= 0) break; - - const amountToApply = Math.min(remainingAmount, ap.balanceAmount); - applications.push({ apId: ap.id, amount: amountToApply }); - - ap.paidAmount += amountToApply; - ap.balanceAmount -= amountToApply; - ap.lastPaymentDate = data.paymentDate; - - if (ap.balanceAmount <= 0.01) { - ap.status = 'paid'; - } else { - ap.status = 'partial'; - } - - ap.updatedBy = ctx.userId; - await this.apRepository.save(ap); - - remainingAmount -= amountToApply; - } - - // Guardar aplicaciones en metadata del pago - savedPayment.metadata = { applications }; - await this.paymentRepository.save(savedPayment); - - return savedPayment; - } - - private async generatePaymentNumber(ctx: ServiceContext): Promise { - const year = new Date().getFullYear(); - const lastPayment = await this.paymentRepository - .createQueryBuilder('payment') - .where('payment.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('payment.paymentNumber LIKE :pattern', { pattern: `PG-${year}%` }) - .orderBy('payment.paymentNumber', 'DESC') - .getOne(); - - let sequence = 1; - if (lastPayment) { - const match = lastPayment.paymentNumber.match(/(\d+)$/); - if (match) { - sequence = parseInt(match[1], 10) + 1; - } - } - - return `PG-${year}-${String(sequence).padStart(6, '0')}`; - } - - async confirmPayment(ctx: ServiceContext, paymentId: string): Promise { - const payment = await this.paymentRepository.findOne({ - where: { - id: paymentId, - tenantId: ctx.tenantId, - }, - }); - - if (!payment) { - throw new Error('Pago no encontrado'); - } - - if (payment.status !== 'pending') { - throw new Error('Solo se pueden confirmar pagos pendientes'); - } - - payment.status = 'confirmed'; - payment.confirmedAt = new Date(); - payment.confirmedById = ctx.userId; - payment.updatedBy = ctx.userId; - - return this.paymentRepository.save(payment); - } - - async cancelPayment( - ctx: ServiceContext, - paymentId: string, - reason: string - ): Promise { - const payment = await this.paymentRepository.findOne({ - where: { - id: paymentId, - tenantId: ctx.tenantId, - }, - }); - - if (!payment) { - throw new Error('Pago no encontrado'); - } - - if (payment.status === 'cancelled' || payment.status === 'reconciled') { - throw new Error('No se puede cancelar este pago'); - } - - // Reversar aplicaciones - const applications = (payment.metadata as any)?.applications || []; - for (const app of applications) { - const ap = await this.apRepository.findOne({ where: { id: app.apId } }); - if (ap) { - ap.paidAmount -= app.amount; - ap.balanceAmount += app.amount; - ap.status = ap.balanceAmount >= ap.netAmount ? 'pending' : 'partial'; - ap.updatedBy = ctx.userId; - await this.apRepository.save(ap); - } - } - - payment.status = 'cancelled'; - payment.notes = `${payment.notes || ''}\n[CANCELADO]: ${reason}`; - payment.updatedBy = ctx.userId; - - return this.paymentRepository.save(payment); - } - - // ==================== REPORTES ==================== - - async getAgingReport( - ctx: ServiceContext, - options: { - partnerId?: string; - projectId?: string; - asOfDate?: Date; - } = {} - ): Promise<{ - summary: AgingBucket; - byPartner: { partnerId: string; partnerName: string; aging: AgingBucket }[]; - }> { - const { partnerId, projectId, asOfDate = new Date() } = options; - - const queryBuilder = this.apRepository - .createQueryBuilder('ap') - .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ap.deletedAt IS NULL'); - - if (partnerId) { - queryBuilder.andWhere('ap.partnerId = :partnerId', { partnerId }); - } - - if (projectId) { - queryBuilder.andWhere('ap.projectId = :projectId', { projectId }); - } - - const apRecords = await queryBuilder.getMany(); - - const summary: AgingBucket = { - current: 0, - days1to30: 0, - days31to60: 0, - days61to90: 0, - over90: 0, - total: 0, - }; - - const partnerMap = new Map< - string, - { partnerId: string; partnerName: string; aging: AgingBucket } - >(); - - for (const ap of apRecords) { - const daysOverdue = Math.floor( - (asOfDate.getTime() - new Date(ap.dueDate).getTime()) / (1000 * 60 * 60 * 24) - ); - const balance = ap.balanceAmount; - - // Clasificar en bucket - let bucket: keyof AgingBucket; - if (daysOverdue <= 0) { - bucket = 'current'; - } else if (daysOverdue <= 30) { - bucket = 'days1to30'; - } else if (daysOverdue <= 60) { - bucket = 'days31to60'; - } else if (daysOverdue <= 90) { - bucket = 'days61to90'; - } else { - bucket = 'over90'; - } - - summary[bucket] += balance; - summary.total += balance; - - // Por proveedor - if (!partnerMap.has(ap.partnerId)) { - partnerMap.set(ap.partnerId, { - partnerId: ap.partnerId, - partnerName: ap.partnerName, - aging: { - current: 0, - days1to30: 0, - days31to60: 0, - days61to90: 0, - over90: 0, - total: 0, - }, - }); - } - - const partnerData = partnerMap.get(ap.partnerId)!; - partnerData.aging[bucket] += balance; - partnerData.aging.total += balance; - } - - return { - summary, - byPartner: Array.from(partnerMap.values()).sort((a, b) => b.aging.total - a.aging.total), - }; - } - - async getPaymentSchedule( - ctx: ServiceContext, - startDate: Date, - endDate: Date, - options: { partnerId?: string; projectId?: string } = {} - ): Promise<{ date: Date; documents: AccountPayable[]; totalAmount: number }[]> { - const { partnerId, projectId } = options; - - const queryBuilder = this.apRepository - .createQueryBuilder('ap') - .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ap.dueDate BETWEEN :startDate AND :endDate', { startDate, endDate }) - .andWhere('ap.deletedAt IS NULL'); - - if (partnerId) { - queryBuilder.andWhere('ap.partnerId = :partnerId', { partnerId }); - } - - if (projectId) { - queryBuilder.andWhere('ap.projectId = :projectId', { projectId }); - } - - queryBuilder.orderBy('ap.dueDate', 'ASC'); - - const apRecords = await queryBuilder.getMany(); - - // Agrupar por fecha de vencimiento - const scheduleMap = new Map(); - - for (const ap of apRecords) { - const dateKey = new Date(ap.dueDate).toISOString().split('T')[0]; - - if (!scheduleMap.has(dateKey)) { - scheduleMap.set(dateKey, { - date: new Date(ap.dueDate), - documents: [], - totalAmount: 0, - }); - } - - const entry = scheduleMap.get(dateKey)!; - entry.documents.push(ap); - entry.totalAmount += ap.balanceAmount; - } - - return Array.from(scheduleMap.values()).sort( - (a, b) => a.date.getTime() - b.date.getTime() - ); - } - - async getDashboardStats(ctx: ServiceContext): Promise<{ - totalPending: number; - totalOverdue: number; - dueThisWeek: number; - dueThisMonth: number; - countPending: number; - countOverdue: number; - }> { - const today = new Date(); - const endOfWeek = new Date(today); - endOfWeek.setDate(today.getDate() + (7 - today.getDay())); - const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); - - const baseQuery = this.apRepository - .createQueryBuilder('ap') - .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ap.deletedAt IS NULL'); - - // Total pendiente - const totalPending = await baseQuery - .clone() - .select('SUM(ap.balanceAmount)', 'total') - .getRawOne(); - - // Vencido - const totalOverdue = await baseQuery - .clone() - .andWhere('ap.dueDate < :today', { today }) - .select('SUM(ap.balanceAmount)', 'total') - .getRawOne(); - - // Por vencer esta semana - const dueThisWeek = await baseQuery - .clone() - .andWhere('ap.dueDate BETWEEN :today AND :endOfWeek', { today, endOfWeek }) - .select('SUM(ap.balanceAmount)', 'total') - .getRawOne(); - - // Por vencer este mes - const dueThisMonth = await baseQuery - .clone() - .andWhere('ap.dueDate BETWEEN :today AND :endOfMonth', { today, endOfMonth }) - .select('SUM(ap.balanceAmount)', 'total') - .getRawOne(); - - // Conteos - const countPending = await baseQuery.clone().getCount(); - - const countOverdue = await baseQuery - .clone() - .andWhere('ap.dueDate < :today', { today }) - .getCount(); - - return { - totalPending: parseFloat(totalPending?.total) || 0, - totalOverdue: parseFloat(totalOverdue?.total) || 0, - dueThisWeek: parseFloat(dueThisWeek?.total) || 0, - dueThisMonth: parseFloat(dueThisMonth?.total) || 0, - countPending, - countOverdue, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/finance/services/ar.service.ts b/projects/erp-construccion/backend/src/modules/finance/services/ar.service.ts deleted file mode 100644 index ee92b9b41..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/services/ar.service.ts +++ /dev/null @@ -1,728 +0,0 @@ -/** - * ARService - Servicio de Cuentas por Cobrar - * - * Gesti贸n de cuentas por cobrar y cobranza. - * - * @module Finance - */ - -import { DataSource, Repository, IsNull, LessThan, Between } from 'typeorm'; -import { - AccountReceivable, - ARStatus, - ARDocumentType, - ARPayment, - CollectionMethod, - CollectionStatus, -} from '../entities'; - -interface ServiceContext { - tenantId: string; - userId?: string; -} - -interface PaginatedResult { - data: T[]; - meta: { - total: number; - page: number; - limit: number; - totalPages: number; - }; -} - -interface CreateARDto { - documentType: ARDocumentType; - documentNumber: string; - documentDate: Date; - dueDate: Date; - partnerId: string; - partnerName: string; - partnerRfc?: string; - projectId?: string; - projectCode?: string; - contractId?: string; - estimationId?: string; - originalAmount: number; - currencyCode?: string; - exchangeRate?: number; - taxAmount?: number; - retentionIsr?: number; - retentionIva?: number; - guaranteeFund?: number; - cfdiUuid?: string; - cfdiXml?: string; - description?: string; - paymentTermDays?: number; - ledgerAccountId?: string; - notes?: string; - metadata?: Record; -} - -interface CreateCollectionDto { - collectionMethod: CollectionMethod; - collectionDate: Date; - bankAccountId: string; - collectionAmount: number; - currencyCode?: string; - exchangeRate?: number; - reference?: string; - depositReference?: string; - transferReference?: string; - collectionConcept?: string; - notes?: string; - accountReceivableIds: string[]; -} - -interface AgingBucket { - current: number; - days1to30: number; - days31to60: number; - days61to90: number; - over90: number; - total: number; -} - -export class ARService { - private arRepository: Repository; - private collectionRepository: Repository; - - constructor(private dataSource: DataSource) { - this.arRepository = dataSource.getRepository(AccountReceivable); - this.collectionRepository = dataSource.getRepository(ARPayment); - } - - // ==================== CUENTAS POR COBRAR ==================== - - async findAll( - ctx: ServiceContext, - options: { - page?: number; - limit?: number; - status?: ARStatus; - partnerId?: string; - projectId?: string; - startDate?: Date; - endDate?: Date; - overdue?: boolean; - search?: string; - } = {} - ): Promise> { - const { - page = 1, - limit = 20, - status, - partnerId, - projectId, - startDate, - endDate, - overdue, - search, - } = options; - - const queryBuilder = this.arRepository - .createQueryBuilder('ar') - .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ar.deletedAt IS NULL'); - - if (status) { - queryBuilder.andWhere('ar.status = :status', { status }); - } - - if (partnerId) { - queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); - } - - if (projectId) { - queryBuilder.andWhere('ar.projectId = :projectId', { projectId }); - } - - if (startDate) { - queryBuilder.andWhere('ar.documentDate >= :startDate', { startDate }); - } - - if (endDate) { - queryBuilder.andWhere('ar.documentDate <= :endDate', { endDate }); - } - - if (overdue) { - queryBuilder.andWhere('ar.dueDate < :today', { today: new Date() }); - queryBuilder.andWhere('ar.status IN (:...statuses)', { - statuses: ['pending', 'partial'], - }); - } - - if (search) { - queryBuilder.andWhere( - '(ar.documentNumber ILIKE :search OR ar.partnerName ILIKE :search)', - { search: `%${search}%` } - ); - } - - queryBuilder.orderBy('ar.dueDate', 'ASC'); - - const total = await queryBuilder.getCount(); - const data = await queryBuilder - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.arRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - relations: ['collections'], - }); - } - - async create(ctx: ServiceContext, data: CreateARDto): Promise { - // Calcular monto neto (con retenciones) - const netAmount = - data.originalAmount - - (data.retentionIsr ?? 0) - - (data.retentionIva ?? 0) - - (data.guaranteeFund ?? 0); - - const ar = this.arRepository.create({ - tenantId: ctx.tenantId, - documentType: data.documentType, - documentNumber: data.documentNumber, - documentDate: data.documentDate, - dueDate: data.dueDate, - partnerId: data.partnerId, - partnerName: data.partnerName, - partnerRfc: data.partnerRfc, - projectId: data.projectId, - projectCode: data.projectCode, - contractId: data.contractId, - estimationId: data.estimationId, - originalAmount: data.originalAmount, - taxAmount: data.taxAmount ?? 0, - retentionIsr: data.retentionIsr ?? 0, - retentionIva: data.retentionIva ?? 0, - guaranteeFund: data.guaranteeFund ?? 0, - netAmount, - collectedAmount: 0, - balanceAmount: netAmount, - currencyCode: data.currencyCode ?? 'MXN', - exchangeRate: data.exchangeRate ?? 1, - cfdiUuid: data.cfdiUuid, - cfdiXml: data.cfdiXml, - description: data.description, - paymentTermDays: data.paymentTermDays, - ledgerAccountId: data.ledgerAccountId, - notes: data.notes, - metadata: data.metadata, - status: 'pending', - collectionAttempts: 0, - createdBy: ctx.userId, - }); - - return this.arRepository.save(ar); - } - - async update( - ctx: ServiceContext, - id: string, - data: Partial - ): Promise { - const ar = await this.findById(ctx, id); - if (!ar) { - throw new Error('Cuenta por cobrar no encontrada'); - } - - if (ar.status === 'collected' || ar.status === 'cancelled' || ar.status === 'written_off') { - throw new Error('No se puede modificar esta cuenta'); - } - - Object.assign(ar, { - ...data, - updatedBy: ctx.userId, - }); - - // Recalcular montos si cambi贸 el monto original - if (data.originalAmount !== undefined) { - ar.netAmount = - ar.originalAmount - - (ar.retentionIsr ?? 0) - - (ar.retentionIva ?? 0) - - (ar.guaranteeFund ?? 0); - ar.balanceAmount = ar.netAmount - ar.collectedAmount; - } - - return this.arRepository.save(ar); - } - - async cancel(ctx: ServiceContext, id: string, reason: string): Promise { - const ar = await this.findById(ctx, id); - if (!ar) { - throw new Error('Cuenta por cobrar no encontrada'); - } - - if (ar.collectedAmount > 0) { - throw new Error('No se puede cancelar una cuenta con cobros aplicados'); - } - - ar.status = 'cancelled'; - ar.notes = `${ar.notes || ''}\n[CANCELADA]: ${reason}`; - ar.updatedBy = ctx.userId; - - return this.arRepository.save(ar); - } - - async writeOff(ctx: ServiceContext, id: string, reason: string): Promise { - const ar = await this.findById(ctx, id); - if (!ar) { - throw new Error('Cuenta por cobrar no encontrada'); - } - - ar.status = 'written_off'; - ar.notes = `${ar.notes || ''}\n[CASTIGO]: ${reason}`; - ar.updatedBy = ctx.userId; - - return this.arRepository.save(ar); - } - - async recordCollectionAttempt( - ctx: ServiceContext, - id: string, - notes: string - ): Promise { - const ar = await this.findById(ctx, id); - if (!ar) { - throw new Error('Cuenta por cobrar no encontrada'); - } - - ar.collectionAttempts += 1; - ar.lastCollectionAttempt = new Date(); - ar.notes = `${ar.notes || ''}\n[GESTI脫N ${ar.collectionAttempts}]: ${notes}`; - ar.updatedBy = ctx.userId; - - return this.arRepository.save(ar); - } - - // ==================== COBROS ==================== - - async createCollection(ctx: ServiceContext, data: CreateCollectionDto): Promise { - // Validar que las cuentas por cobrar existen y calcular total - let totalToApply = 0; - const arRecords: AccountReceivable[] = []; - - for (const arId of data.accountReceivableIds) { - const ar = await this.findById(ctx, arId); - if (!ar) { - throw new Error(`Cuenta por cobrar ${arId} no encontrada`); - } - if (ar.status === 'collected' || ar.status === 'cancelled' || ar.status === 'written_off') { - throw new Error(`Cuenta por cobrar ${ar.documentNumber} ya est谩 cobrada, cancelada o castigada`); - } - arRecords.push(ar); - totalToApply += ar.balanceAmount; - } - - // Validar monto - if (data.collectionAmount > totalToApply) { - throw new Error( - `El monto del cobro (${data.collectionAmount}) excede el saldo pendiente (${totalToApply})` - ); - } - - // Generar n煤mero de cobro - const collectionNumber = await this.generateCollectionNumber(ctx); - - // Crear cobro - const collection = this.collectionRepository.create({ - tenantId: ctx.tenantId, - collectionNumber, - collectionMethod: data.collectionMethod, - collectionDate: data.collectionDate, - bankAccountId: data.bankAccountId, - collectionAmount: data.collectionAmount, - currencyCode: data.currencyCode ?? 'MXN', - exchangeRate: data.exchangeRate ?? 1, - reference: data.reference, - depositReference: data.depositReference, - transferReference: data.transferReference, - collectionConcept: data.collectionConcept, - notes: data.notes, - status: 'pending', - documentCount: arRecords.length, - createdBy: ctx.userId, - }); - - const savedCollection = await this.collectionRepository.save(collection); - - // Aplicar cobro a las cuentas por cobrar (FIFO por fecha de vencimiento) - let remainingAmount = data.collectionAmount; - const sortedAR = arRecords.sort( - (a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime() - ); - - const applications: { arId: string; amount: number }[] = []; - - for (const ar of sortedAR) { - if (remainingAmount <= 0) break; - - const amountToApply = Math.min(remainingAmount, ar.balanceAmount); - applications.push({ arId: ar.id, amount: amountToApply }); - - ar.collectedAmount += amountToApply; - ar.balanceAmount -= amountToApply; - ar.lastCollectionDate = data.collectionDate; - - if (ar.balanceAmount <= 0.01) { - ar.status = 'collected'; - } else { - ar.status = 'partial'; - } - - ar.updatedBy = ctx.userId; - await this.arRepository.save(ar); - - remainingAmount -= amountToApply; - } - - // Guardar aplicaciones en metadata del cobro - savedCollection.metadata = { applications }; - await this.collectionRepository.save(savedCollection); - - return savedCollection; - } - - private async generateCollectionNumber(ctx: ServiceContext): Promise { - const year = new Date().getFullYear(); - const lastCollection = await this.collectionRepository - .createQueryBuilder('collection') - .where('collection.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('collection.collectionNumber LIKE :pattern', { pattern: `CB-${year}%` }) - .orderBy('collection.collectionNumber', 'DESC') - .getOne(); - - let sequence = 1; - if (lastCollection) { - const match = lastCollection.collectionNumber.match(/(\d+)$/); - if (match) { - sequence = parseInt(match[1], 10) + 1; - } - } - - return `CB-${year}-${String(sequence).padStart(6, '0')}`; - } - - async confirmCollection(ctx: ServiceContext, collectionId: string): Promise { - const collection = await this.collectionRepository.findOne({ - where: { - id: collectionId, - tenantId: ctx.tenantId, - }, - }); - - if (!collection) { - throw new Error('Cobro no encontrado'); - } - - if (collection.status !== 'pending') { - throw new Error('Solo se pueden confirmar cobros pendientes'); - } - - collection.status = 'confirmed'; - collection.confirmedAt = new Date(); - collection.confirmedById = ctx.userId; - collection.updatedBy = ctx.userId; - - return this.collectionRepository.save(collection); - } - - async cancelCollection( - ctx: ServiceContext, - collectionId: string, - reason: string - ): Promise { - const collection = await this.collectionRepository.findOne({ - where: { - id: collectionId, - tenantId: ctx.tenantId, - }, - }); - - if (!collection) { - throw new Error('Cobro no encontrado'); - } - - if (collection.status === 'cancelled' || collection.status === 'reconciled') { - throw new Error('No se puede cancelar este cobro'); - } - - // Reversar aplicaciones - const applications = (collection.metadata as any)?.applications || []; - for (const app of applications) { - const ar = await this.arRepository.findOne({ where: { id: app.arId } }); - if (ar) { - ar.collectedAmount -= app.amount; - ar.balanceAmount += app.amount; - ar.status = ar.balanceAmount >= ar.netAmount ? 'pending' : 'partial'; - ar.updatedBy = ctx.userId; - await this.arRepository.save(ar); - } - } - - collection.status = 'cancelled'; - collection.notes = `${collection.notes || ''}\n[CANCELADO]: ${reason}`; - collection.updatedBy = ctx.userId; - - return this.collectionRepository.save(collection); - } - - // ==================== REPORTES ==================== - - async getAgingReport( - ctx: ServiceContext, - options: { - partnerId?: string; - projectId?: string; - asOfDate?: Date; - } = {} - ): Promise<{ - summary: AgingBucket; - byPartner: { partnerId: string; partnerName: string; aging: AgingBucket }[]; - }> { - const { partnerId, projectId, asOfDate = new Date() } = options; - - const queryBuilder = this.arRepository - .createQueryBuilder('ar') - .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ar.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ar.deletedAt IS NULL'); - - if (partnerId) { - queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); - } - - if (projectId) { - queryBuilder.andWhere('ar.projectId = :projectId', { projectId }); - } - - const arRecords = await queryBuilder.getMany(); - - const summary: AgingBucket = { - current: 0, - days1to30: 0, - days31to60: 0, - days61to90: 0, - over90: 0, - total: 0, - }; - - const partnerMap = new Map< - string, - { partnerId: string; partnerName: string; aging: AgingBucket } - >(); - - for (const ar of arRecords) { - const daysOverdue = Math.floor( - (asOfDate.getTime() - new Date(ar.dueDate).getTime()) / (1000 * 60 * 60 * 24) - ); - const balance = ar.balanceAmount; - - // Clasificar en bucket - let bucket: keyof AgingBucket; - if (daysOverdue <= 0) { - bucket = 'current'; - } else if (daysOverdue <= 30) { - bucket = 'days1to30'; - } else if (daysOverdue <= 60) { - bucket = 'days31to60'; - } else if (daysOverdue <= 90) { - bucket = 'days61to90'; - } else { - bucket = 'over90'; - } - - summary[bucket] += balance; - summary.total += balance; - - // Por cliente - if (!partnerMap.has(ar.partnerId)) { - partnerMap.set(ar.partnerId, { - partnerId: ar.partnerId, - partnerName: ar.partnerName, - aging: { - current: 0, - days1to30: 0, - days31to60: 0, - days61to90: 0, - over90: 0, - total: 0, - }, - }); - } - - const partnerData = partnerMap.get(ar.partnerId)!; - partnerData.aging[bucket] += balance; - partnerData.aging.total += balance; - } - - return { - summary, - byPartner: Array.from(partnerMap.values()).sort((a, b) => b.aging.total - a.aging.total), - }; - } - - async getCollectionForecast( - ctx: ServiceContext, - startDate: Date, - endDate: Date, - options: { partnerId?: string; projectId?: string } = {} - ): Promise<{ date: Date; documents: AccountReceivable[]; totalAmount: number }[]> { - const { partnerId, projectId } = options; - - const queryBuilder = this.arRepository - .createQueryBuilder('ar') - .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ar.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ar.dueDate BETWEEN :startDate AND :endDate', { startDate, endDate }) - .andWhere('ar.deletedAt IS NULL'); - - if (partnerId) { - queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); - } - - if (projectId) { - queryBuilder.andWhere('ar.projectId = :projectId', { projectId }); - } - - queryBuilder.orderBy('ar.dueDate', 'ASC'); - - const arRecords = await queryBuilder.getMany(); - - // Agrupar por fecha de vencimiento - const forecastMap = new Map(); - - for (const ar of arRecords) { - const dateKey = new Date(ar.dueDate).toISOString().split('T')[0]; - - if (!forecastMap.has(dateKey)) { - forecastMap.set(dateKey, { - date: new Date(ar.dueDate), - documents: [], - totalAmount: 0, - }); - } - - const entry = forecastMap.get(dateKey)!; - entry.documents.push(ar); - entry.totalAmount += ar.balanceAmount; - } - - return Array.from(forecastMap.values()).sort( - (a, b) => a.date.getTime() - b.date.getTime() - ); - } - - async getDashboardStats(ctx: ServiceContext): Promise<{ - totalPending: number; - totalOverdue: number; - dueThisWeek: number; - dueThisMonth: number; - countPending: number; - countOverdue: number; - collectionEfficiency: number; - }> { - const today = new Date(); - const endOfWeek = new Date(today); - endOfWeek.setDate(today.getDate() + (7 - today.getDay())); - const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); - const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); - - const baseQuery = this.arRepository - .createQueryBuilder('ar') - .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ar.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ar.deletedAt IS NULL'); - - // Total pendiente - const totalPending = await baseQuery - .clone() - .select('SUM(ar.balanceAmount)', 'total') - .getRawOne(); - - // Vencido - const totalOverdue = await baseQuery - .clone() - .andWhere('ar.dueDate < :today', { today }) - .select('SUM(ar.balanceAmount)', 'total') - .getRawOne(); - - // Por cobrar esta semana - const dueThisWeek = await baseQuery - .clone() - .andWhere('ar.dueDate BETWEEN :today AND :endOfWeek', { today, endOfWeek }) - .select('SUM(ar.balanceAmount)', 'total') - .getRawOne(); - - // Por cobrar este mes - const dueThisMonth = await baseQuery - .clone() - .andWhere('ar.dueDate BETWEEN :today AND :endOfMonth', { today, endOfMonth }) - .select('SUM(ar.balanceAmount)', 'total') - .getRawOne(); - - // Conteos - const countPending = await baseQuery.clone().getCount(); - - const countOverdue = await baseQuery - .clone() - .andWhere('ar.dueDate < :today', { today }) - .getCount(); - - // Eficiencia de cobranza (cobrado vs facturado en el mes) - const monthlyInvoiced = await this.arRepository - .createQueryBuilder('ar') - .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ar.documentDate BETWEEN :startOfMonth AND :endOfMonth', { - startOfMonth, - endOfMonth, - }) - .select('SUM(ar.netAmount)', 'total') - .getRawOne(); - - const monthlyCollected = await this.collectionRepository - .createQueryBuilder('col') - .where('col.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('col.collectionDate BETWEEN :startOfMonth AND :endOfMonth', { - startOfMonth, - endOfMonth, - }) - .andWhere('col.status != :cancelled', { cancelled: 'cancelled' }) - .select('SUM(col.collectionAmount)', 'total') - .getRawOne(); - - const invoicedAmount = parseFloat(monthlyInvoiced?.total) || 0; - const collectedAmount = parseFloat(monthlyCollected?.total) || 0; - const collectionEfficiency = invoicedAmount > 0 ? (collectedAmount / invoicedAmount) * 100 : 0; - - return { - totalPending: parseFloat(totalPending?.total) || 0, - totalOverdue: parseFloat(totalOverdue?.total) || 0, - dueThisWeek: parseFloat(dueThisWeek?.total) || 0, - dueThisMonth: parseFloat(dueThisMonth?.total) || 0, - countPending, - countOverdue, - collectionEfficiency: Math.round(collectionEfficiency * 100) / 100, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/finance/services/bank-reconciliation.service.ts b/projects/erp-construccion/backend/src/modules/finance/services/bank-reconciliation.service.ts deleted file mode 100644 index 66e351040..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/services/bank-reconciliation.service.ts +++ /dev/null @@ -1,846 +0,0 @@ -/** - * BankReconciliationService - Servicio de Conciliaci贸n Bancaria - * - * Gesti贸n de cuentas bancarias, movimientos y conciliaci贸n. - * - * @module Finance - */ - -import { DataSource, Repository, IsNull, Between, In } from 'typeorm'; -import { - BankAccount, - BankAccountType, - BankAccountStatus, - BankMovement, - MovementType, - MovementStatus, - MovementSource, - BankReconciliation, - ReconciliationStatus, -} from '../entities'; - -interface ServiceContext { - tenantId: string; - userId?: string; -} - -interface PaginatedResult { - data: T[]; - meta: { - total: number; - page: number; - limit: number; - totalPages: number; - }; -} - -interface CreateBankAccountDto { - name: string; - accountNumber: string; - clabe?: string; - accountType: BankAccountType; - bankName: string; - bankCode?: string; - branchName?: string; - branchCode?: string; - currency?: string; - initialBalance?: number; - projectId?: string; - projectCode?: string; - ledgerAccountId?: string; - bankContactName?: string; - bankContactPhone?: string; - bankContactEmail?: string; - creditLimit?: number; - minimumBalance?: number; - isDefault?: boolean; - allowsPayments?: boolean; - allowsCollections?: boolean; - notes?: string; - metadata?: Record; -} - -interface CreateMovementDto { - bankAccountId: string; - movementReference?: string; - bankReference?: string; - movementType: MovementType; - movementDate: Date; - valueDate?: Date; - description: string; - bankDescription?: string; - amount: number; - currency?: string; - balanceAfter?: number; - source?: MovementSource; - category?: string; - subcategory?: string; - partnerId?: string; - partnerName?: string; - notes?: string; - rawData?: Record; - metadata?: Record; -} - -interface CreateReconciliationDto { - bankAccountId: string; - periodStart: Date; - periodEnd: Date; - bankOpeningBalance: number; - bankClosingBalance: number; - statementFilePath?: string; - notes?: string; -} - -interface ImportBankStatementDto { - bankAccountId: string; - movements: { - date: Date; - reference?: string; - description: string; - amount: number; - type: MovementType; - balance?: number; - rawData?: Record; - }[]; -} - -export class BankReconciliationService { - private bankAccountRepository: Repository; - private movementRepository: Repository; - private reconciliationRepository: Repository; - - constructor(private dataSource: DataSource) { - this.bankAccountRepository = dataSource.getRepository(BankAccount); - this.movementRepository = dataSource.getRepository(BankMovement); - this.reconciliationRepository = dataSource.getRepository(BankReconciliation); - } - - // ==================== CUENTAS BANCARIAS ==================== - - async findAllAccounts( - ctx: ServiceContext, - options: { - page?: number; - limit?: number; - accountType?: BankAccountType; - status?: BankAccountStatus; - projectId?: string; - search?: string; - } = {} - ): Promise> { - const { page = 1, limit = 20, accountType, status, projectId, search } = options; - - const queryBuilder = this.bankAccountRepository - .createQueryBuilder('ba') - .where('ba.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ba.deletedAt IS NULL'); - - if (accountType) { - queryBuilder.andWhere('ba.accountType = :accountType', { accountType }); - } - - if (status) { - queryBuilder.andWhere('ba.status = :status', { status }); - } - - if (projectId) { - queryBuilder.andWhere('ba.projectId = :projectId', { projectId }); - } - - if (search) { - queryBuilder.andWhere( - '(ba.name ILIKE :search OR ba.accountNumber ILIKE :search OR ba.bankName ILIKE :search)', - { search: `%${search}%` } - ); - } - - queryBuilder.orderBy('ba.name', 'ASC'); - - const total = await queryBuilder.getCount(); - const data = await queryBuilder - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findAccountById(ctx: ServiceContext, id: string): Promise { - return this.bankAccountRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - }); - } - - async createAccount(ctx: ServiceContext, data: CreateBankAccountDto): Promise { - // Verificar n煤mero de cuenta 煤nico - const existing = await this.bankAccountRepository.findOne({ - where: { - tenantId: ctx.tenantId, - accountNumber: data.accountNumber, - deletedAt: IsNull(), - }, - }); - - if (existing) { - throw new Error(`Ya existe una cuenta con el n煤mero ${data.accountNumber}`); - } - - // Si es cuenta por defecto, quitar el flag de las dem谩s - if (data.isDefault) { - await this.bankAccountRepository.update( - { tenantId: ctx.tenantId, isDefault: true }, - { isDefault: false } - ); - } - - const account = this.bankAccountRepository.create({ - tenantId: ctx.tenantId, - name: data.name, - accountNumber: data.accountNumber, - clabe: data.clabe, - accountType: data.accountType, - bankName: data.bankName, - bankCode: data.bankCode, - branchName: data.branchName, - branchCode: data.branchCode, - currency: data.currency ?? 'MXN', - initialBalance: data.initialBalance ?? 0, - currentBalance: data.initialBalance ?? 0, - availableBalance: data.initialBalance ?? 0, - projectId: data.projectId, - projectCode: data.projectCode, - ledgerAccountId: data.ledgerAccountId, - bankContactName: data.bankContactName, - bankContactPhone: data.bankContactPhone, - bankContactEmail: data.bankContactEmail, - creditLimit: data.creditLimit, - minimumBalance: data.minimumBalance, - isDefault: data.isDefault ?? false, - allowsPayments: data.allowsPayments ?? true, - allowsCollections: data.allowsCollections ?? true, - status: 'active', - notes: data.notes, - metadata: data.metadata, - createdBy: ctx.userId, - }); - - return this.bankAccountRepository.save(account); - } - - async updateAccount( - ctx: ServiceContext, - id: string, - data: Partial - ): Promise { - const account = await this.findAccountById(ctx, id); - if (!account) { - throw new Error('Cuenta bancaria no encontrada'); - } - - // Si es cuenta por defecto, quitar el flag de las dem谩s - if (data.isDefault && !account.isDefault) { - await this.bankAccountRepository.update( - { tenantId: ctx.tenantId, isDefault: true }, - { isDefault: false } - ); - } - - Object.assign(account, { - ...data, - updatedBy: ctx.userId, - }); - - return this.bankAccountRepository.save(account); - } - - async updateAccountBalance( - ctx: ServiceContext, - id: string, - currentBalance: number, - availableBalance?: number - ): Promise { - const account = await this.findAccountById(ctx, id); - if (!account) { - throw new Error('Cuenta bancaria no encontrada'); - } - - account.currentBalance = currentBalance; - account.availableBalance = availableBalance ?? currentBalance; - account.balanceUpdatedAt = new Date(); - account.updatedBy = ctx.userId; - - return this.bankAccountRepository.save(account); - } - - // ==================== MOVIMIENTOS BANCARIOS ==================== - - async findAllMovements( - ctx: ServiceContext, - options: { - page?: number; - limit?: number; - bankAccountId?: string; - movementType?: MovementType; - status?: MovementStatus; - startDate?: Date; - endDate?: Date; - search?: string; - } = {} - ): Promise> { - const { - page = 1, - limit = 50, - bankAccountId, - movementType, - status, - startDate, - endDate, - search, - } = options; - - const queryBuilder = this.movementRepository - .createQueryBuilder('mv') - .leftJoinAndSelect('mv.bankAccount', 'bankAccount') - .where('mv.tenantId = :tenantId', { tenantId: ctx.tenantId }); - - if (bankAccountId) { - queryBuilder.andWhere('mv.bankAccountId = :bankAccountId', { bankAccountId }); - } - - if (movementType) { - queryBuilder.andWhere('mv.movementType = :movementType', { movementType }); - } - - if (status) { - queryBuilder.andWhere('mv.status = :status', { status }); - } - - if (startDate) { - queryBuilder.andWhere('mv.movementDate >= :startDate', { startDate }); - } - - if (endDate) { - queryBuilder.andWhere('mv.movementDate <= :endDate', { endDate }); - } - - if (search) { - queryBuilder.andWhere( - '(mv.description ILIKE :search OR mv.movementReference ILIKE :search OR mv.bankReference ILIKE :search)', - { search: `%${search}%` } - ); - } - - queryBuilder.orderBy('mv.movementDate', 'DESC').addOrderBy('mv.createdAt', 'DESC'); - - const total = await queryBuilder.getCount(); - const data = await queryBuilder - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findMovementById(ctx: ServiceContext, id: string): Promise { - return this.movementRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - }, - relations: ['bankAccount'], - }); - } - - async createMovement(ctx: ServiceContext, data: CreateMovementDto): Promise { - const bankAccount = await this.findAccountById(ctx, data.bankAccountId); - if (!bankAccount) { - throw new Error('Cuenta bancaria no encontrada'); - } - - const movement = this.movementRepository.create({ - tenantId: ctx.tenantId, - bankAccountId: data.bankAccountId, - movementReference: data.movementReference, - bankReference: data.bankReference, - movementType: data.movementType, - movementDate: data.movementDate, - valueDate: data.valueDate, - description: data.description, - bankDescription: data.bankDescription, - amount: data.amount, - currency: data.currency ?? bankAccount.currency, - balanceAfter: data.balanceAfter, - source: data.source ?? 'manual', - status: 'pending', - category: data.category, - subcategory: data.subcategory, - partnerId: data.partnerId, - partnerName: data.partnerName, - notes: data.notes, - rawData: data.rawData, - metadata: data.metadata, - createdBy: ctx.userId, - }); - - return this.movementRepository.save(movement); - } - - async importBankStatement( - ctx: ServiceContext, - data: ImportBankStatementDto - ): Promise<{ imported: number; duplicates: number; movements: BankMovement[] }> { - const bankAccount = await this.findAccountById(ctx, data.bankAccountId); - if (!bankAccount) { - throw new Error('Cuenta bancaria no encontrada'); - } - - const importBatchId = crypto.randomUUID(); - const importedMovements: BankMovement[] = []; - let duplicates = 0; - - for (const mv of data.movements) { - // Verificar duplicado por fecha, monto y referencia - const existing = await this.movementRepository.findOne({ - where: { - tenantId: ctx.tenantId, - bankAccountId: data.bankAccountId, - movementDate: mv.date, - amount: mv.amount, - movementType: mv.type, - }, - }); - - if (existing) { - duplicates++; - continue; - } - - const movement = await this.createMovement(ctx, { - bankAccountId: data.bankAccountId, - movementReference: mv.reference, - movementType: mv.type, - movementDate: mv.date, - description: mv.description, - amount: mv.amount, - balanceAfter: mv.balance, - source: 'import_file', - rawData: mv.rawData, - metadata: { importBatchId }, - }); - - importedMovements.push(movement); - } - - return { - imported: importedMovements.length, - duplicates, - movements: importedMovements, - }; - } - - async matchMovement( - ctx: ServiceContext, - movementId: string, - matchData: { - paymentId?: string; - collectionId?: string; - entryId?: string; - confidence?: number; - } - ): Promise { - const movement = await this.findMovementById(ctx, movementId); - if (!movement) { - throw new Error('Movimiento no encontrado'); - } - - movement.matchedPaymentId = matchData.paymentId; - movement.matchedCollectionId = matchData.collectionId; - movement.matchedEntryId = matchData.entryId; - movement.matchConfidence = matchData.confidence; - movement.status = 'matched'; - movement.updatedBy = ctx.userId; - - return this.movementRepository.save(movement); - } - - async ignoreMovement(ctx: ServiceContext, movementId: string): Promise { - const movement = await this.findMovementById(ctx, movementId); - if (!movement) { - throw new Error('Movimiento no encontrado'); - } - - movement.status = 'ignored'; - movement.updatedBy = ctx.userId; - - return this.movementRepository.save(movement); - } - - // ==================== CONCILIACI脫N ==================== - - async findAllReconciliations( - ctx: ServiceContext, - options: { - page?: number; - limit?: number; - bankAccountId?: string; - status?: ReconciliationStatus; - } = {} - ): Promise> { - const { page = 1, limit = 20, bankAccountId, status } = options; - - const queryBuilder = this.reconciliationRepository - .createQueryBuilder('rec') - .leftJoinAndSelect('rec.bankAccount', 'bankAccount') - .where('rec.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('rec.deletedAt IS NULL'); - - if (bankAccountId) { - queryBuilder.andWhere('rec.bankAccountId = :bankAccountId', { bankAccountId }); - } - - if (status) { - queryBuilder.andWhere('rec.status = :status', { status }); - } - - queryBuilder.orderBy('rec.periodEnd', 'DESC'); - - const total = await queryBuilder.getCount(); - const data = await queryBuilder - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findReconciliationById( - ctx: ServiceContext, - id: string - ): Promise { - return this.reconciliationRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - relations: ['bankAccount', 'approvedBy'], - }); - } - - async createReconciliation( - ctx: ServiceContext, - data: CreateReconciliationDto - ): Promise { - const bankAccount = await this.findAccountById(ctx, data.bankAccountId); - if (!bankAccount) { - throw new Error('Cuenta bancaria no encontrada'); - } - - // Obtener saldo en libros - const bookBalance = bankAccount.currentBalance; - - // Contar movimientos en el periodo - const movements = await this.movementRepository.count({ - where: { - tenantId: ctx.tenantId, - bankAccountId: data.bankAccountId, - movementDate: Between(data.periodStart, data.periodEnd), - }, - }); - - const reconciliation = this.reconciliationRepository.create({ - tenantId: ctx.tenantId, - bankAccountId: data.bankAccountId, - periodStart: data.periodStart, - periodEnd: data.periodEnd, - bankOpeningBalance: data.bankOpeningBalance, - bankClosingBalance: data.bankClosingBalance, - bookOpeningBalance: bookBalance, // TODO: Calcular saldo de apertura - bookClosingBalance: bookBalance, - depositsInTransit: 0, - checksInTransit: 0, - bankChargesNotRecorded: 0, - interestNotRecorded: 0, - otherAdjustments: 0, - reconciledBalance: 0, - difference: data.bankClosingBalance - bookBalance, - isBalanced: false, - totalMovements: movements, - reconciledMovements: 0, - pendingMovements: movements, - statementFilePath: data.statementFilePath, - statementImportDate: data.statementFilePath ? new Date() : undefined, - notes: data.notes, - status: 'draft', - createdBy: ctx.userId, - }); - - return this.reconciliationRepository.save(reconciliation); - } - - async startReconciliation(ctx: ServiceContext, id: string): Promise { - const reconciliation = await this.findReconciliationById(ctx, id); - if (!reconciliation) { - throw new Error('Conciliaci贸n no encontrada'); - } - - if (reconciliation.status !== 'draft') { - throw new Error('La conciliaci贸n ya fue iniciada'); - } - - reconciliation.status = 'in_progress'; - reconciliation.startedAt = new Date(); - reconciliation.updatedBy = ctx.userId; - - return this.reconciliationRepository.save(reconciliation); - } - - async reconcileMovement( - ctx: ServiceContext, - reconciliationId: string, - movementId: string - ): Promise { - const reconciliation = await this.findReconciliationById(ctx, reconciliationId); - if (!reconciliation) { - throw new Error('Conciliaci贸n no encontrada'); - } - - const movement = await this.findMovementById(ctx, movementId); - if (!movement) { - throw new Error('Movimiento no encontrado'); - } - - // Marcar movimiento como conciliado - movement.status = 'reconciled'; - movement.reconciliationId = reconciliationId; - movement.reconciledAt = new Date(); - movement.reconciledById = ctx.userId; - movement.updatedBy = ctx.userId; - await this.movementRepository.save(movement); - - // Actualizar contadores - reconciliation.reconciledMovements++; - reconciliation.pendingMovements--; - reconciliation.updatedBy = ctx.userId; - - // Recalcular diferencia - await this.recalculateReconciliation(ctx, reconciliationId); - - return this.findReconciliationById(ctx, reconciliationId) as Promise; - } - - private async recalculateReconciliation( - ctx: ServiceContext, - reconciliationId: string - ): Promise { - const reconciliation = await this.findReconciliationById(ctx, reconciliationId); - if (!reconciliation) return; - - // Obtener movimientos pendientes de conciliaci贸n - const pendingDeposits = await this.movementRepository - .createQueryBuilder('mv') - .where('mv.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('mv.bankAccountId = :bankAccountId', { bankAccountId: reconciliation.bankAccountId }) - .andWhere('mv.movementDate <= :periodEnd', { periodEnd: reconciliation.periodEnd }) - .andWhere('mv.status IN (:...statuses)', { statuses: ['pending', 'matched'] }) - .andWhere('mv.movementType = :type', { type: 'credit' }) - .select('SUM(mv.amount)', 'total') - .getRawOne(); - - const pendingChecks = await this.movementRepository - .createQueryBuilder('mv') - .where('mv.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('mv.bankAccountId = :bankAccountId', { bankAccountId: reconciliation.bankAccountId }) - .andWhere('mv.movementDate <= :periodEnd', { periodEnd: reconciliation.periodEnd }) - .andWhere('mv.status IN (:...statuses)', { statuses: ['pending', 'matched'] }) - .andWhere('mv.movementType = :type', { type: 'debit' }) - .select('SUM(mv.amount)', 'total') - .getRawOne(); - - reconciliation.depositsInTransit = parseFloat(pendingDeposits?.total) || 0; - reconciliation.checksInTransit = parseFloat(pendingChecks?.total) || 0; - - // Calcular saldo conciliado - const reconciledBalance = - reconciliation.bankClosingBalance - - reconciliation.depositsInTransit + - reconciliation.checksInTransit - - reconciliation.bankChargesNotRecorded + - reconciliation.interestNotRecorded + - reconciliation.otherAdjustments; - - reconciliation.reconciledBalance = reconciledBalance; - reconciliation.difference = Math.abs(reconciliation.bookClosingBalance - reconciledBalance); - reconciliation.isBalanced = reconciliation.difference < 0.01; - - await this.reconciliationRepository.save(reconciliation); - } - - async completeReconciliation(ctx: ServiceContext, id: string): Promise { - const reconciliation = await this.findReconciliationById(ctx, id); - if (!reconciliation) { - throw new Error('Conciliaci贸n no encontrada'); - } - - if (!reconciliation.isBalanced) { - throw new Error('La conciliaci贸n no est谩 balanceada'); - } - - reconciliation.status = 'completed'; - reconciliation.completedAt = new Date(); - reconciliation.updatedBy = ctx.userId; - - // Actualizar cuenta bancaria - const bankAccount = await this.findAccountById(ctx, reconciliation.bankAccountId); - if (bankAccount) { - bankAccount.lastReconciliationDate = reconciliation.periodEnd; - bankAccount.lastReconciledBalance = reconciliation.bankClosingBalance; - await this.bankAccountRepository.save(bankAccount); - } - - return this.reconciliationRepository.save(reconciliation); - } - - async approveReconciliation(ctx: ServiceContext, id: string): Promise { - const reconciliation = await this.findReconciliationById(ctx, id); - if (!reconciliation) { - throw new Error('Conciliaci贸n no encontrada'); - } - - if (reconciliation.status !== 'completed') { - throw new Error('Solo se pueden aprobar conciliaciones completadas'); - } - - reconciliation.status = 'approved'; - reconciliation.approvedById = ctx.userId; - reconciliation.approvedAt = new Date(); - reconciliation.updatedBy = ctx.userId; - - return this.reconciliationRepository.save(reconciliation); - } - - // ==================== REPORTES ==================== - - async getBankAccountSummary(ctx: ServiceContext): Promise<{ - totalBalance: number; - byAccount: { id: string; name: string; bankName: string; balance: number; currency: string }[]; - byCurrency: { currency: string; total: number }[]; - }> { - const accounts = await this.bankAccountRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'active', - deletedAt: IsNull(), - }, - }); - - const byAccount = accounts.map((acc) => ({ - id: acc.id, - name: acc.name, - bankName: acc.bankName, - balance: Number(acc.currentBalance), - currency: acc.currency, - })); - - const byCurrencyMap = new Map(); - for (const acc of accounts) { - const current = byCurrencyMap.get(acc.currency) || 0; - byCurrencyMap.set(acc.currency, current + Number(acc.currentBalance)); - } - - const byCurrency = Array.from(byCurrencyMap.entries()).map(([currency, total]) => ({ - currency, - total, - })); - - const totalBalance = accounts - .filter((acc) => acc.currency === 'MXN') - .reduce((sum, acc) => sum + Number(acc.currentBalance), 0); - - return { - totalBalance, - byAccount, - byCurrency, - }; - } - - async getReconciliationStatus(ctx: ServiceContext): Promise<{ - pendingCount: number; - inProgressCount: number; - accountsNotReconciled: { id: string; name: string; lastReconciliationDate?: Date }[]; - }> { - const pending = await this.reconciliationRepository.count({ - where: { - tenantId: ctx.tenantId, - status: 'draft', - deletedAt: IsNull(), - }, - }); - - const inProgress = await this.reconciliationRepository.count({ - where: { - tenantId: ctx.tenantId, - status: 'in_progress', - deletedAt: IsNull(), - }, - }); - - // Cuentas sin conciliaci贸n reciente (m谩s de 30 d铆as) - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - - const accounts = await this.bankAccountRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'active', - deletedAt: IsNull(), - }, - }); - - const accountsNotReconciled = accounts - .filter( - (acc) => - !acc.lastReconciliationDate || new Date(acc.lastReconciliationDate) < thirtyDaysAgo - ) - .map((acc) => ({ - id: acc.id, - name: acc.name, - lastReconciliationDate: acc.lastReconciliationDate, - })); - - return { - pendingCount: pending, - inProgressCount: inProgress, - accountsNotReconciled, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/finance/services/cash-flow.service.ts b/projects/erp-construccion/backend/src/modules/finance/services/cash-flow.service.ts deleted file mode 100644 index ea5229128..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/services/cash-flow.service.ts +++ /dev/null @@ -1,701 +0,0 @@ -/** - * CashFlowService - Servicio de Flujo de Efectivo - * - * Gesti贸n de proyecciones y an谩lisis de flujo de efectivo. - * - * @module Finance - */ - -import { DataSource, Repository, IsNull, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; -import { - CashFlowProjection, - CashFlowType, - CashFlowPeriodType, - AccountPayable, - AccountReceivable, - BankAccount, -} from '../entities'; - -interface ServiceContext { - tenantId: string; - userId?: string; -} - -interface PaginatedResult { - data: T[]; - meta: { - total: number; - page: number; - limit: number; - totalPages: number; - }; -} - -interface CreateProjectionDto { - flowType: CashFlowType; - periodType: CashFlowPeriodType; - periodStart: Date; - periodEnd: Date; - fiscalYear: number; - fiscalPeriod: number; - projectId?: string; - projectCode?: string; - openingBalance: number; - incomeEstimations?: number; - incomeSales?: number; - incomeAdvances?: number; - incomeOther?: number; - expenseSuppliers?: number; - expenseSubcontractors?: number; - expensePayroll?: number; - expenseTaxes?: number; - expenseOperating?: number; - expenseOther?: number; - investingIncome?: number; - investingExpense?: number; - financingIncome?: number; - financingExpense?: number; - incomeBreakdown?: Record[]; - expenseBreakdown?: Record[]; - notes?: string; - metadata?: Record; -} - -interface CashFlowSummary { - period: string; - periodStart: Date; - periodEnd: Date; - openingBalance: number; - totalIncome: number; - totalExpenses: number; - netOperatingFlow: number; - netInvestingFlow: number; - netFinancingFlow: number; - netCashFlow: number; - closingBalance: number; -} - -export class CashFlowService { - private projectionRepository: Repository; - private apRepository: Repository; - private arRepository: Repository; - private bankAccountRepository: Repository; - - constructor(private dataSource: DataSource) { - this.projectionRepository = dataSource.getRepository(CashFlowProjection); - this.apRepository = dataSource.getRepository(AccountPayable); - this.arRepository = dataSource.getRepository(AccountReceivable); - this.bankAccountRepository = dataSource.getRepository(BankAccount); - } - - // ==================== PROYECCIONES ==================== - - async findAll( - ctx: ServiceContext, - options: { - page?: number; - limit?: number; - flowType?: CashFlowType; - periodType?: CashFlowPeriodType; - projectId?: string; - fiscalYear?: number; - startDate?: Date; - endDate?: Date; - } = {} - ): Promise> { - const { - page = 1, - limit = 20, - flowType, - periodType, - projectId, - fiscalYear, - startDate, - endDate, - } = options; - - const queryBuilder = this.projectionRepository - .createQueryBuilder('cf') - .where('cf.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('cf.deletedAt IS NULL'); - - if (flowType) { - queryBuilder.andWhere('cf.flowType = :flowType', { flowType }); - } - - if (periodType) { - queryBuilder.andWhere('cf.periodType = :periodType', { periodType }); - } - - if (projectId) { - queryBuilder.andWhere('cf.projectId = :projectId', { projectId }); - } - - if (fiscalYear) { - queryBuilder.andWhere('cf.fiscalYear = :fiscalYear', { fiscalYear }); - } - - if (startDate) { - queryBuilder.andWhere('cf.periodStart >= :startDate', { startDate }); - } - - if (endDate) { - queryBuilder.andWhere('cf.periodEnd <= :endDate', { endDate }); - } - - queryBuilder.orderBy('cf.periodStart', 'ASC'); - - const total = await queryBuilder.getCount(); - const data = await queryBuilder - .skip((page - 1) * limit) - .take(limit) - .getMany(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.projectionRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - }); - } - - async create(ctx: ServiceContext, data: CreateProjectionDto): Promise { - // Calcular totales - const totalIncome = - (data.incomeEstimations ?? 0) + - (data.incomeSales ?? 0) + - (data.incomeAdvances ?? 0) + - (data.incomeOther ?? 0); - - const totalExpenses = - (data.expenseSuppliers ?? 0) + - (data.expenseSubcontractors ?? 0) + - (data.expensePayroll ?? 0) + - (data.expenseTaxes ?? 0) + - (data.expenseOperating ?? 0) + - (data.expenseOther ?? 0); - - const netOperatingFlow = totalIncome - totalExpenses; - const netInvestingFlow = (data.investingIncome ?? 0) - (data.investingExpense ?? 0); - const netFinancingFlow = (data.financingIncome ?? 0) - (data.financingExpense ?? 0); - const netCashFlow = netOperatingFlow + netInvestingFlow + netFinancingFlow; - const closingBalance = data.openingBalance + netCashFlow; - - const projection = this.projectionRepository.create({ - tenantId: ctx.tenantId, - flowType: data.flowType, - periodType: data.periodType, - periodStart: data.periodStart, - periodEnd: data.periodEnd, - fiscalYear: data.fiscalYear, - fiscalPeriod: data.fiscalPeriod, - projectId: data.projectId, - projectCode: data.projectCode, - openingBalance: data.openingBalance, - incomeEstimations: data.incomeEstimations ?? 0, - incomeSales: data.incomeSales ?? 0, - incomeAdvances: data.incomeAdvances ?? 0, - incomeOther: data.incomeOther ?? 0, - totalIncome, - expenseSuppliers: data.expenseSuppliers ?? 0, - expenseSubcontractors: data.expenseSubcontractors ?? 0, - expensePayroll: data.expensePayroll ?? 0, - expenseTaxes: data.expenseTaxes ?? 0, - expenseOperating: data.expenseOperating ?? 0, - expenseOther: data.expenseOther ?? 0, - totalExpenses, - netOperatingFlow, - investingIncome: data.investingIncome ?? 0, - investingExpense: data.investingExpense ?? 0, - netInvestingFlow, - financingIncome: data.financingIncome ?? 0, - financingExpense: data.financingExpense ?? 0, - netFinancingFlow, - netCashFlow, - closingBalance, - incomeBreakdown: data.incomeBreakdown, - expenseBreakdown: data.expenseBreakdown, - notes: data.notes, - metadata: data.metadata, - isLocked: false, - createdBy: ctx.userId, - }); - - return this.projectionRepository.save(projection); - } - - async update( - ctx: ServiceContext, - id: string, - data: Partial - ): Promise { - const projection = await this.findById(ctx, id); - if (!projection) { - throw new Error('Proyecci贸n no encontrada'); - } - - if (projection.isLocked) { - throw new Error('La proyecci贸n est谩 bloqueada y no se puede modificar'); - } - - Object.assign(projection, data); - - // Recalcular totales - projection.totalIncome = - projection.incomeEstimations + - projection.incomeSales + - projection.incomeAdvances + - projection.incomeOther; - - projection.totalExpenses = - projection.expenseSuppliers + - projection.expenseSubcontractors + - projection.expensePayroll + - projection.expenseTaxes + - projection.expenseOperating + - projection.expenseOther; - - projection.netOperatingFlow = projection.totalIncome - projection.totalExpenses; - projection.netInvestingFlow = projection.investingIncome - projection.investingExpense; - projection.netFinancingFlow = projection.financingIncome - projection.financingExpense; - projection.netCashFlow = - projection.netOperatingFlow + projection.netInvestingFlow + projection.netFinancingFlow; - projection.closingBalance = projection.openingBalance + projection.netCashFlow; - - projection.updatedBy = ctx.userId; - - return this.projectionRepository.save(projection); - } - - async lock(ctx: ServiceContext, id: string): Promise { - const projection = await this.findById(ctx, id); - if (!projection) { - throw new Error('Proyecci贸n no encontrada'); - } - - projection.isLocked = true; - projection.lockedAt = new Date(); - projection.updatedBy = ctx.userId; - - return this.projectionRepository.save(projection); - } - - async delete(ctx: ServiceContext, id: string): Promise { - const projection = await this.findById(ctx, id); - if (!projection) { - throw new Error('Proyecci贸n no encontrada'); - } - - if (projection.isLocked) { - throw new Error('No se puede eliminar una proyecci贸n bloqueada'); - } - - projection.deletedAt = new Date(); - projection.updatedBy = ctx.userId; - await this.projectionRepository.save(projection); - } - - // ==================== GENERACI脫N AUTOM脕TICA ==================== - - async generateProjection( - ctx: ServiceContext, - periodStart: Date, - periodEnd: Date, - options: { - periodType?: CashFlowPeriodType; - projectId?: string; - projectCode?: string; - } = {} - ): Promise { - const { periodType = 'weekly', projectId, projectCode } = options; - - // Obtener saldo inicial de cuentas bancarias - const bankAccounts = await this.bankAccountRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'active', - ...(projectId && { projectId }), - }, - }); - - const openingBalance = bankAccounts.reduce( - (sum, acc) => sum + Number(acc.currentBalance), - 0 - ); - - // Obtener cobranza esperada (AR por vencer en el periodo) - const arQuery = this.arRepository - .createQueryBuilder('ar') - .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ar.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ar.dueDate BETWEEN :periodStart AND :periodEnd', { periodStart, periodEnd }) - .andWhere('ar.deletedAt IS NULL'); - - if (projectId) { - arQuery.andWhere('ar.projectId = :projectId', { projectId }); - } - - const arRecords = await arQuery.getMany(); - const incomeEstimations = arRecords - .filter((ar) => ar.documentType === 'estimation') - .reduce((sum, ar) => sum + Number(ar.balanceAmount), 0); - const incomeSales = arRecords - .filter((ar) => ar.documentType !== 'estimation') - .reduce((sum, ar) => sum + Number(ar.balanceAmount), 0); - - // Obtener pagos esperados (AP por vencer en el periodo) - const apQuery = this.apRepository - .createQueryBuilder('ap') - .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ap.dueDate BETWEEN :periodStart AND :periodEnd', { periodStart, periodEnd }) - .andWhere('ap.deletedAt IS NULL'); - - if (projectId) { - apQuery.andWhere('ap.projectId = :projectId', { projectId }); - } - - const apRecords = await apQuery.getMany(); - const expenseSuppliers = apRecords - .filter((ap) => ap.documentType === 'invoice') - .reduce((sum, ap) => sum + Number(ap.balanceAmount), 0); - - // Determinar a帽o y periodo fiscal - const fiscalYear = periodStart.getFullYear(); - const fiscalPeriod = periodStart.getMonth() + 1; - - // Crear la proyecci贸n - return this.create(ctx, { - flowType: 'projected', - periodType, - periodStart, - periodEnd, - fiscalYear, - fiscalPeriod, - projectId, - projectCode, - openingBalance, - incomeEstimations, - incomeSales, - incomeAdvances: 0, - incomeOther: 0, - expenseSuppliers, - expenseSubcontractors: 0, - expensePayroll: 0, - expenseTaxes: 0, - expenseOperating: 0, - expenseOther: 0, - investingIncome: 0, - investingExpense: 0, - financingIncome: 0, - financingExpense: 0, - incomeBreakdown: arRecords.map((ar) => ({ - id: ar.id, - documentNumber: ar.documentNumber, - partnerName: ar.partnerName, - dueDate: ar.dueDate, - amount: ar.balanceAmount, - })), - expenseBreakdown: apRecords.map((ap) => ({ - id: ap.id, - documentNumber: ap.documentNumber, - partnerName: ap.partnerName, - dueDate: ap.dueDate, - amount: ap.balanceAmount, - })), - notes: `Proyecci贸n autom谩tica generada el ${new Date().toISOString()}`, - }); - } - - async generateMultiplePeriods( - ctx: ServiceContext, - startDate: Date, - weeks: number, - options: { projectId?: string; projectCode?: string } = {} - ): Promise { - const projections: CashFlowProjection[] = []; - let currentStart = new Date(startDate); - - for (let i = 0; i < weeks; i++) { - const periodEnd = new Date(currentStart); - periodEnd.setDate(periodEnd.getDate() + 6); - - const projection = await this.generateProjection(ctx, currentStart, periodEnd, { - periodType: 'weekly', - ...options, - }); - - projections.push(projection); - - // Siguiente semana - currentStart = new Date(periodEnd); - currentStart.setDate(currentStart.getDate() + 1); - } - - return projections; - } - - // ==================== COMPARACI脫N PROYECTADO VS REAL ==================== - - async createComparison( - ctx: ServiceContext, - projectedId: string - ): Promise { - const projected = await this.findById(ctx, projectedId); - if (!projected) { - throw new Error('Proyecci贸n no encontrada'); - } - - if (projected.flowType !== 'projected') { - throw new Error('Solo se pueden comparar proyecciones'); - } - - // Generar flujo real para el mismo periodo - const actual = await this.generateProjection(ctx, projected.periodStart, projected.periodEnd, { - periodType: projected.periodType, - projectId: projected.projectId ?? undefined, - projectCode: projected.projectCode ?? undefined, - }); - - // Actualizar a tipo actual - actual.flowType = 'actual'; - await this.projectionRepository.save(actual); - - // Crear registro de comparaci贸n - const varianceAmount = actual.netCashFlow - projected.netCashFlow; - const variancePercentage = - projected.netCashFlow !== 0 - ? (varianceAmount / Math.abs(projected.netCashFlow)) * 100 - : 0; - - const comparison = this.projectionRepository.create({ - tenantId: ctx.tenantId, - flowType: 'comparison', - periodType: projected.periodType, - periodStart: projected.periodStart, - periodEnd: projected.periodEnd, - fiscalYear: projected.fiscalYear, - fiscalPeriod: projected.fiscalPeriod, - projectId: projected.projectId, - projectCode: projected.projectCode, - openingBalance: projected.openingBalance, - incomeEstimations: actual.incomeEstimations, - incomeSales: actual.incomeSales, - incomeAdvances: actual.incomeAdvances, - incomeOther: actual.incomeOther, - totalIncome: actual.totalIncome, - expenseSuppliers: actual.expenseSuppliers, - expenseSubcontractors: actual.expenseSubcontractors, - expensePayroll: actual.expensePayroll, - expenseTaxes: actual.expenseTaxes, - expenseOperating: actual.expenseOperating, - expenseOther: actual.expenseOther, - totalExpenses: actual.totalExpenses, - netOperatingFlow: actual.netOperatingFlow, - investingIncome: actual.investingIncome, - investingExpense: actual.investingExpense, - netInvestingFlow: actual.netInvestingFlow, - financingIncome: actual.financingIncome, - financingExpense: actual.financingExpense, - netFinancingFlow: actual.netFinancingFlow, - netCashFlow: actual.netCashFlow, - closingBalance: actual.closingBalance, - projectedAmount: projected.netCashFlow, - actualAmount: actual.netCashFlow, - varianceAmount, - variancePercentage, - notes: `Comparaci贸n: Proyectado vs Real`, - metadata: { - projectedId: projected.id, - actualId: actual.id, - }, - createdBy: ctx.userId, - }); - - return this.projectionRepository.save(comparison); - } - - // ==================== REPORTES ==================== - - async getCashFlowSummary( - ctx: ServiceContext, - startDate: Date, - endDate: Date, - options: { - periodType?: CashFlowPeriodType; - flowType?: CashFlowType; - projectId?: string; - } = {} - ): Promise { - const { periodType = 'weekly', flowType = 'projected', projectId } = options; - - const queryBuilder = this.projectionRepository - .createQueryBuilder('cf') - .where('cf.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('cf.flowType = :flowType', { flowType }) - .andWhere('cf.periodType = :periodType', { periodType }) - .andWhere('cf.periodStart >= :startDate', { startDate }) - .andWhere('cf.periodEnd <= :endDate', { endDate }) - .andWhere('cf.deletedAt IS NULL'); - - if (projectId) { - queryBuilder.andWhere('cf.projectId = :projectId', { projectId }); - } - - queryBuilder.orderBy('cf.periodStart', 'ASC'); - - const projections = await queryBuilder.getMany(); - - return projections.map((p) => ({ - period: `${p.periodStart.toISOString().split('T')[0]} - ${p.periodEnd.toISOString().split('T')[0]}`, - periodStart: p.periodStart, - periodEnd: p.periodEnd, - openingBalance: Number(p.openingBalance), - totalIncome: Number(p.totalIncome), - totalExpenses: Number(p.totalExpenses), - netOperatingFlow: Number(p.netOperatingFlow), - netInvestingFlow: Number(p.netInvestingFlow), - netFinancingFlow: Number(p.netFinancingFlow), - netCashFlow: Number(p.netCashFlow), - closingBalance: Number(p.closingBalance), - })); - } - - async getVarianceAnalysis( - ctx: ServiceContext, - fiscalYear: number, - options: { projectId?: string } = {} - ): Promise<{ - periods: { - period: string; - projected: number; - actual: number; - variance: number; - variancePercent: number; - }[]; - totals: { - projected: number; - actual: number; - variance: number; - variancePercent: number; - }; - }> { - const { projectId } = options; - - const queryBuilder = this.projectionRepository - .createQueryBuilder('cf') - .where('cf.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('cf.flowType = :flowType', { flowType: 'comparison' }) - .andWhere('cf.fiscalYear = :fiscalYear', { fiscalYear }) - .andWhere('cf.deletedAt IS NULL'); - - if (projectId) { - queryBuilder.andWhere('cf.projectId = :projectId', { projectId }); - } - - queryBuilder.orderBy('cf.periodStart', 'ASC'); - - const comparisons = await queryBuilder.getMany(); - - const periods = comparisons.map((c) => ({ - period: `${c.periodStart.toISOString().split('T')[0]} - ${c.periodEnd.toISOString().split('T')[0]}`, - projected: Number(c.projectedAmount) || 0, - actual: Number(c.actualAmount) || 0, - variance: Number(c.varianceAmount) || 0, - variancePercent: Number(c.variancePercentage) || 0, - })); - - const totals = periods.reduce( - (acc, p) => ({ - projected: acc.projected + p.projected, - actual: acc.actual + p.actual, - variance: acc.variance + p.variance, - variancePercent: 0, - }), - { projected: 0, actual: 0, variance: 0, variancePercent: 0 } - ); - - totals.variancePercent = - totals.projected !== 0 - ? (totals.variance / Math.abs(totals.projected)) * 100 - : 0; - - return { periods, totals }; - } - - async getDashboardData(ctx: ServiceContext): Promise<{ - currentBalance: number; - projectedIncome: number; - projectedExpenses: number; - projectedNetFlow: number; - cashPosition: { date: Date; balance: number }[]; - }> { - // Saldo actual - const bankAccounts = await this.bankAccountRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'active', - }, - }); - - const currentBalance = bankAccounts.reduce( - (sum, acc) => sum + Number(acc.currentBalance), - 0 - ); - - // Proyecci贸n pr贸ximas 4 semanas - const today = new Date(); - const fourWeeksLater = new Date(today); - fourWeeksLater.setDate(fourWeeksLater.getDate() + 28); - - const projections = await this.projectionRepository.find({ - where: { - tenantId: ctx.tenantId, - flowType: 'projected', - periodStart: MoreThanOrEqual(today), - periodEnd: LessThanOrEqual(fourWeeksLater), - deletedAt: IsNull(), - }, - order: { periodStart: 'ASC' }, - }); - - const projectedIncome = projections.reduce((sum, p) => sum + Number(p.totalIncome), 0); - const projectedExpenses = projections.reduce((sum, p) => sum + Number(p.totalExpenses), 0); - const projectedNetFlow = projectedIncome - projectedExpenses; - - // Posici贸n de caja proyectada - const cashPosition: { date: Date; balance: number }[] = [ - { date: today, balance: currentBalance }, - ]; - - let runningBalance = currentBalance; - for (const p of projections) { - runningBalance += Number(p.netCashFlow); - cashPosition.push({ - date: p.periodEnd, - balance: runningBalance, - }); - } - - return { - currentBalance, - projectedIncome, - projectedExpenses, - projectedNetFlow, - cashPosition, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/finance/services/erp-integration.service.ts b/projects/erp-construccion/backend/src/modules/finance/services/erp-integration.service.ts deleted file mode 100644 index 3f9c5bdd3..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/services/erp-integration.service.ts +++ /dev/null @@ -1,699 +0,0 @@ -/** - * ERPIntegrationService - Servicio de Integraci贸n con ERPs - * - * Exportaci贸n de datos a SAP, CONTPAQi y otros sistemas. - * - * @module Finance - */ - -import { DataSource, Repository, IsNull, Between } from 'typeorm'; -import { - ChartOfAccounts, - AccountingEntry, - AccountingEntryLine, - AccountPayable, - AccountReceivable, - BankMovement, -} from '../entities'; - -interface ServiceContext { - tenantId: string; - userId?: string; -} - -interface ExportConfig { - format: 'csv' | 'xml' | 'json' | 'txt'; - encoding?: string; - delimiter?: string; - includeHeaders?: boolean; -} - -interface SAPExportEntry { - BUKRS: string; // Sociedad - BELNR: string; // N煤mero de documento - GJAHR: number; // Ejercicio - BLART: string; // Clase de documento - BLDAT: string; // Fecha de documento - BUDAT: string; // Fecha de contabilizaci贸n - MONAT: number; // Periodo - WAERS: string; // Moneda - KURSF: number; // Tipo de cambio - BKTXT: string; // Texto de cabecera - lines: SAPExportLine[]; -} - -interface SAPExportLine { - BUZEI: number; // Posici贸n - BSCHL: string; // Clave de contabilizaci贸n - HKONT: string; // Cuenta - WRBTR: number; // Importe en moneda del documento - DMBTR: number; // Importe en moneda local - SGTXT: string; // Texto - ZUONR?: string; // Asignaci贸n - KOSTL?: string; // Centro de costo - PROJK?: string; // Elemento PEP -} - -interface CONTPAQiPoliza { - Tipo: number; - Folio: number; - Fecha: string; - Concepto: string; - Diario: number; - Movimientos: CONTPAQiMovimiento[]; -} - -interface CONTPAQiMovimiento { - NumMovto: number; - Cuenta: string; - Concepto: string; - Cargo: number; - Abono: number; - Referencia?: string; - Diario?: number; -} - -interface ExportResult { - success: boolean; - format: string; - recordCount: number; - data: string | object; - filename: string; - errors?: string[]; -} - -export class ERPIntegrationService { - private accountRepository: Repository; - private entryRepository: Repository; - private lineRepository: Repository; - private apRepository: Repository; - private arRepository: Repository; - private movementRepository: Repository; - - constructor(private dataSource: DataSource) { - this.accountRepository = dataSource.getRepository(ChartOfAccounts); - this.entryRepository = dataSource.getRepository(AccountingEntry); - this.lineRepository = dataSource.getRepository(AccountingEntryLine); - this.apRepository = dataSource.getRepository(AccountPayable); - this.arRepository = dataSource.getRepository(AccountReceivable); - this.movementRepository = dataSource.getRepository(BankMovement); - } - - // ==================== EXPORTACI脫N SAP ==================== - - async exportToSAP( - ctx: ServiceContext, - periodStart: Date, - periodEnd: Date, - options: { - companyCode?: string; - documentType?: string; - journalNumber?: number; - } = {} - ): Promise { - const { companyCode = '1000', documentType = 'SA', journalNumber = 1 } = options; - - const entries = await this.entryRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'posted', - entryDate: Between(periodStart, periodEnd), - deletedAt: IsNull(), - }, - relations: ['lines'], - order: { entryDate: 'ASC', entryNumber: 'ASC' }, - }); - - const sapEntries: SAPExportEntry[] = entries.map((entry) => ({ - BUKRS: companyCode, - BELNR: entry.entryNumber.replace(/[^0-9]/g, '').slice(-10).padStart(10, '0'), - GJAHR: entry.fiscalYear, - BLART: this.mapEntryTypeToSAP(entry.entryType), - BLDAT: this.formatDateSAP(entry.entryDate), - BUDAT: this.formatDateSAP(entry.entryDate), - MONAT: entry.fiscalPeriod, - WAERS: entry.currencyCode, - KURSF: entry.exchangeRate, - BKTXT: entry.description.slice(0, 25), - lines: (entry.lines || []).map((line, idx) => ({ - BUZEI: idx + 1, - BSCHL: line.debitAmount > 0 ? '40' : '50', // 40=Debe, 50=Haber - HKONT: line.accountCode.replace(/\./g, '').padStart(10, '0'), - WRBTR: Math.abs(line.debitAmount || line.creditAmount), - DMBTR: Math.abs(line.debitAmount || line.creditAmount) * entry.exchangeRate, - SGTXT: (line.description || entry.description).slice(0, 50), - ZUONR: line.reference?.slice(0, 18), - KOSTL: line.costCenterId?.slice(0, 10), - PROJK: line.projectId?.slice(0, 24), - })), - })); - - // Generar archivo texto para LSMW o BAPI - const lines: string[] = []; - - for (const entry of sapEntries) { - // Cabecera - lines.push( - `H|${entry.BUKRS}|${entry.BELNR}|${entry.GJAHR}|${entry.BLART}|${entry.BLDAT}|${entry.BUDAT}|${entry.MONAT}|${entry.WAERS}|${entry.KURSF}|${entry.BKTXT}` - ); - - // Posiciones - for (const line of entry.lines) { - lines.push( - `L|${line.BUZEI}|${line.BSCHL}|${line.HKONT}|${line.WRBTR}|${line.DMBTR}|${line.SGTXT}|${line.ZUONR || ''}|${line.KOSTL || ''}|${line.PROJK || ''}` - ); - } - } - - const filename = `SAP_POLIZAS_${periodStart.toISOString().split('T')[0]}_${periodEnd.toISOString().split('T')[0]}.txt`; - - return { - success: true, - format: 'SAP-LSMW', - recordCount: entries.length, - data: lines.join('\n'), - filename, - }; - } - - private mapEntryTypeToSAP(entryType: string): string { - const mapping: Record = { - purchase: 'KR', // Factura de acreedor - sale: 'DR', // Factura de deudor - payment: 'KZ', // Pago a acreedor - collection: 'DZ', // Cobro de deudor - payroll: 'PR', // N贸mina - adjustment: 'SA', // Documento contable - depreciation: 'AF', // Amortizaci贸n - transfer: 'SA', // Traspaso - opening: 'AB', // Apertura - closing: 'SB', // Cierre - }; - return mapping[entryType] || 'SA'; - } - - private formatDateSAP(date: Date): string { - const d = new Date(date); - const year = d.getFullYear(); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - return `${year}${month}${day}`; - } - - // ==================== EXPORTACI脫N CONTPAQi ==================== - - async exportToCONTPAQi( - ctx: ServiceContext, - periodStart: Date, - periodEnd: Date, - options: { - polizaTipo?: number; - diario?: number; - } = {} - ): Promise { - const { polizaTipo = 1, diario = 1 } = options; // 1=Diario - - const entries = await this.entryRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'posted', - entryDate: Between(periodStart, periodEnd), - deletedAt: IsNull(), - }, - relations: ['lines'], - order: { entryDate: 'ASC', entryNumber: 'ASC' }, - }); - - const polizas: CONTPAQiPoliza[] = entries.map((entry, idx) => ({ - Tipo: this.mapEntryTypeToCONTPAQi(entry.entryType), - Folio: idx + 1, - Fecha: this.formatDateCONTPAQi(entry.entryDate), - Concepto: entry.description.slice(0, 200), - Diario: diario, - Movimientos: (entry.lines || []).map((line, lineIdx) => ({ - NumMovto: lineIdx + 1, - Cuenta: line.accountCode, - Concepto: (line.description || entry.description).slice(0, 200), - Cargo: line.debitAmount, - Abono: line.creditAmount, - Referencia: line.reference?.slice(0, 20), - Diario: diario, - })), - })); - - // Formato texto para importaci贸n CONTPAQi - const lines: string[] = []; - - for (const poliza of polizas) { - // Cabecera de p贸liza - lines.push(`P,${poliza.Tipo},${poliza.Folio},${poliza.Fecha},"${poliza.Concepto}",${poliza.Diario}`); - - // Movimientos - for (const mov of poliza.Movimientos) { - lines.push( - `M,${mov.NumMovto},"${mov.Cuenta}","${mov.Concepto}",${mov.Cargo.toFixed(2)},${mov.Abono.toFixed(2)},"${mov.Referencia || ''}"` - ); - } - } - - const filename = `CONTPAQI_POLIZAS_${periodStart.toISOString().split('T')[0]}_${periodEnd.toISOString().split('T')[0]}.txt`; - - return { - success: true, - format: 'CONTPAQi-TXT', - recordCount: entries.length, - data: lines.join('\n'), - filename, - }; - } - - private mapEntryTypeToCONTPAQi(entryType: string): number { - const mapping: Record = { - purchase: 2, // Egresos - sale: 1, // Ingresos - payment: 2, // Egresos - collection: 1, // Ingresos - payroll: 3, // N贸mina - adjustment: 4, // Diario - depreciation: 4, // Diario - transfer: 4, // Diario - opening: 4, // Diario - closing: 4, // Diario - }; - return mapping[entryType] || 4; - } - - private formatDateCONTPAQi(date: Date): string { - const d = new Date(date); - const day = String(d.getDate()).padStart(2, '0'); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const year = d.getFullYear(); - return `${day}/${month}/${year}`; - } - - // ==================== EXPORTACI脫N XML CFDI ==================== - - async exportCFDIPolizas( - ctx: ServiceContext, - periodStart: Date, - periodEnd: Date, - options: { - tipoSolicitud?: string; - numOrden?: string; - numTramite?: string; - } = {} - ): Promise { - const { tipoSolicitud = 'AF', numOrden, numTramite } = options; - - const entries = await this.entryRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'posted', - entryDate: Between(periodStart, periodEnd), - deletedAt: IsNull(), - }, - relations: ['lines'], - order: { entryDate: 'ASC' }, - }); - - // Generar XML seg煤n Anexo 24 del SAT - const xml = ` - -${entries.map((entry) => this.generatePolizaXML(entry)).join('\n')} -`; - - const filename = `CFDI_POLIZAS_${periodStart.getFullYear()}${String(periodStart.getMonth() + 1).padStart(2, '0')}.xml`; - - return { - success: true, - format: 'CFDI-XML', - recordCount: entries.length, - data: xml, - filename, - }; - } - - private generatePolizaXML(entry: AccountingEntry): string { - const fecha = new Date(entry.entryDate).toISOString().split('T')[0]; - const tipoPoliza = this.mapEntryTypeToCFDI(entry.entryType); - - const transacciones = (entry.lines || []) - .map((line) => { - return ` `; - }) - .join('\n'); - - return ` -${transacciones} - `; - } - - private mapEntryTypeToCFDI(entryType: string): string { - const mapping: Record = { - purchase: 'Eg', - sale: 'In', - payment: 'Eg', - collection: 'In', - payroll: 'No', - adjustment: 'Di', - depreciation: 'Di', - transfer: 'Di', - opening: 'Di', - closing: 'Di', - }; - return mapping[entryType] || 'Di'; - } - - private escapeXML(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - // ==================== EXPORTACI脫N CAT脕LOGO DE CUENTAS ==================== - - async exportChartOfAccounts( - ctx: ServiceContext, - format: 'csv' | 'xml' | 'json' = 'csv' - ): Promise { - const accounts = await this.accountRepository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: IsNull(), - }, - order: { code: 'ASC' }, - }); - - let data: string | object; - let filename: string; - - switch (format) { - case 'xml': - data = this.generateChartXML(accounts); - filename = `CATALOGO_CUENTAS.xml`; - break; - - case 'json': - data = accounts.map((acc) => ({ - code: acc.code, - name: acc.name, - type: acc.accountType, - nature: acc.nature, - level: acc.level, - parentCode: acc.parentId, - satCode: acc.satCode, - status: acc.status, - })); - filename = `CATALOGO_CUENTAS.json`; - break; - - default: - const rows = [ - 'C贸digo,Nombre,Tipo,Naturaleza,Nivel,C贸digo SAT,Estado', - ...accounts.map( - (acc) => - `"${acc.code}","${acc.name}","${acc.accountType}","${acc.nature}",${acc.level},"${acc.satCode || ''}","${acc.status}"` - ), - ]; - data = rows.join('\n'); - filename = `CATALOGO_CUENTAS.csv`; - } - - return { - success: true, - format, - recordCount: accounts.length, - data, - filename, - }; - } - - private generateChartXML(accounts: ChartOfAccounts[]): string { - const cuentas = accounts - .map( - (acc) => ` ` - ) - .join('\n'); - - return ` - -${cuentas} -`; - } - - // ==================== EXPORTACI脫N BALANZA ==================== - - async exportTrialBalance( - ctx: ServiceContext, - periodStart: Date, - periodEnd: Date, - format: 'csv' | 'xml' | 'json' = 'csv' - ): Promise { - // Obtener balanza - const accounts = await this.accountRepository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: IsNull(), - acceptsMovements: true, - }, - order: { code: 'ASC' }, - }); - - // Obtener saldos - const balances = await this.lineRepository - .createQueryBuilder('line') - .innerJoin('line.entry', 'entry') - .select([ - 'line.accountCode as "accountCode"', - 'SUM(line.debitAmount) as "debit"', - 'SUM(line.creditAmount) as "credit"', - ]) - .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.entryDate BETWEEN :periodStart AND :periodEnd', { - periodStart, - periodEnd, - }) - .groupBy('line.accountCode') - .getRawMany(); - - const balanceMap = new Map( - balances.map((b) => [ - b.accountCode, - { - debit: parseFloat(b.debit) || 0, - credit: parseFloat(b.credit) || 0, - }, - ]) - ); - - const rows = accounts - .map((acc) => { - const bal = balanceMap.get(acc.code) || { debit: 0, credit: 0 }; - return { - code: acc.code, - name: acc.name, - initialDebit: 0, - initialCredit: 0, - periodDebit: bal.debit, - periodCredit: bal.credit, - finalDebit: bal.debit, - finalCredit: bal.credit, - }; - }) - .filter((r) => r.periodDebit > 0 || r.periodCredit > 0); - - let data: string | object; - let filename: string; - - switch (format) { - case 'xml': - data = this.generateBalanzaXML(rows, periodStart); - filename = `BALANZA_${periodStart.getFullYear()}${String(periodStart.getMonth() + 1).padStart(2, '0')}.xml`; - break; - - case 'json': - data = rows; - filename = `BALANZA_${periodStart.getFullYear()}${String(periodStart.getMonth() + 1).padStart(2, '0')}.json`; - break; - - default: - const csvRows = [ - 'Cuenta,Nombre,Saldo Inicial Debe,Saldo Inicial Haber,Debe,Haber,Saldo Final Debe,Saldo Final Haber', - ...rows.map( - (r) => - `"${r.code}","${r.name}",${r.initialDebit.toFixed(2)},${r.initialCredit.toFixed(2)},${r.periodDebit.toFixed(2)},${r.periodCredit.toFixed(2)},${r.finalDebit.toFixed(2)},${r.finalCredit.toFixed(2)}` - ), - ]; - data = csvRows.join('\n'); - filename = `BALANZA_${periodStart.getFullYear()}${String(periodStart.getMonth() + 1).padStart(2, '0')}.csv`; - } - - return { - success: true, - format, - recordCount: rows.length, - data, - filename, - }; - } - - private generateBalanzaXML( - rows: { - code: string; - name: string; - initialDebit: number; - initialCredit: number; - periodDebit: number; - periodCredit: number; - finalDebit: number; - finalCredit: number; - }[], - periodStart: Date - ): string { - const cuentas = rows - .map( - (r) => ` ` - ) - .join('\n'); - - return ` - -${cuentas} -`; - } - - // ==================== IMPORTACI脫N ==================== - - async importChartOfAccounts( - ctx: ServiceContext, - data: string, - format: 'csv' | 'json' - ): Promise<{ imported: number; errors: string[] }> { - const errors: string[] = []; - let imported = 0; - - let accounts: { - code: string; - name: string; - type: AccountType; - nature: 'debit' | 'credit'; - level: number; - parentCode?: string; - satCode?: string; - }[] = []; - - if (format === 'json') { - accounts = JSON.parse(data); - } else { - const lines = data.split('\n').slice(1); // Skip header - accounts = lines - .filter((line) => line.trim()) - .map((line) => { - const parts = line.split(',').map((p) => p.replace(/"/g, '').trim()); - return { - code: parts[0], - name: parts[1], - type: parts[2] as AccountType, - nature: parts[3] as 'debit' | 'credit', - level: parseInt(parts[4]) || 1, - satCode: parts[5], - }; - }); - } - - for (const acc of accounts) { - try { - const existing = await this.accountRepository.findOne({ - where: { - tenantId: ctx.tenantId, - code: acc.code, - deletedAt: IsNull(), - }, - }); - - if (existing) { - // Actualizar - existing.name = acc.name; - existing.accountType = acc.type; - existing.nature = acc.nature; - existing.level = acc.level; - existing.satCode = acc.satCode; - existing.updatedBy = ctx.userId; - await this.accountRepository.save(existing); - } else { - // Crear - const newAccount = this.accountRepository.create({ - tenantId: ctx.tenantId, - code: acc.code, - name: acc.name, - accountType: acc.type, - nature: acc.nature, - level: acc.level, - satCode: acc.satCode, - fullPath: acc.code, - isGroupAccount: false, - acceptsMovements: true, - status: 'active', - currencyCode: 'MXN', - balance: 0, - createdBy: ctx.userId, - }); - await this.accountRepository.save(newAccount); - } - - imported++; - } catch (error) { - errors.push(`Error en cuenta ${acc.code}: ${(error as Error).message}`); - } - } - - return { imported, errors }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/finance/services/financial-reports.service.ts b/projects/erp-construccion/backend/src/modules/finance/services/financial-reports.service.ts deleted file mode 100644 index 7d3a89a44..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/services/financial-reports.service.ts +++ /dev/null @@ -1,893 +0,0 @@ -/** - * FinancialReportsService - Servicio de Reportes Financieros - * - * Generaci贸n de estados financieros y reportes contables. - * - * @module Finance - */ - -import { DataSource, Repository, IsNull } from 'typeorm'; -import { - ChartOfAccounts, - AccountType, - AccountingEntry, - AccountingEntryLine, - AccountPayable, - AccountReceivable, - BankAccount, -} from '../entities'; - -interface ServiceContext { - tenantId: string; - userId?: string; -} - -interface BalanceSheetAccount { - accountId: string; - accountCode: string; - accountName: string; - level: number; - balance: number; - children?: BalanceSheetAccount[]; -} - -interface BalanceSheet { - asOfDate: Date; - assets: { - current: BalanceSheetAccount[]; - nonCurrent: BalanceSheetAccount[]; - totalCurrent: number; - totalNonCurrent: number; - total: number; - }; - liabilities: { - current: BalanceSheetAccount[]; - nonCurrent: BalanceSheetAccount[]; - totalCurrent: number; - totalNonCurrent: number; - total: number; - }; - equity: { - accounts: BalanceSheetAccount[]; - total: number; - }; - totalLiabilitiesAndEquity: number; - isBalanced: boolean; -} - -interface IncomeStatementLine { - accountId: string; - accountCode: string; - accountName: string; - level: number; - amount: number; - children?: IncomeStatementLine[]; -} - -interface IncomeStatement { - periodStart: Date; - periodEnd: Date; - revenue: { - lines: IncomeStatementLine[]; - total: number; - }; - costOfSales: { - lines: IncomeStatementLine[]; - total: number; - }; - grossProfit: number; - operatingExpenses: { - lines: IncomeStatementLine[]; - total: number; - }; - operatingIncome: number; - otherIncome: { - lines: IncomeStatementLine[]; - total: number; - }; - otherExpenses: { - lines: IncomeStatementLine[]; - total: number; - }; - incomeBeforeTax: number; - taxExpense: number; - netIncome: number; -} - -interface CashFlowStatement { - periodStart: Date; - periodEnd: Date; - operatingActivities: { - netIncome: number; - adjustments: { description: string; amount: number }[]; - changesInWorkingCapital: { description: string; amount: number }[]; - netCash: number; - }; - investingActivities: { - items: { description: string; amount: number }[]; - netCash: number; - }; - financingActivities: { - items: { description: string; amount: number }[]; - netCash: number; - }; - netChangeInCash: number; - beginningCash: number; - endingCash: number; -} - -export class FinancialReportsService { - private accountRepository: Repository; - private entryRepository: Repository; - private lineRepository: Repository; - private apRepository: Repository; - private arRepository: Repository; - private bankAccountRepository: Repository; - - constructor(private dataSource: DataSource) { - this.accountRepository = dataSource.getRepository(ChartOfAccounts); - this.entryRepository = dataSource.getRepository(AccountingEntry); - this.lineRepository = dataSource.getRepository(AccountingEntryLine); - this.apRepository = dataSource.getRepository(AccountPayable); - this.arRepository = dataSource.getRepository(AccountReceivable); - this.bankAccountRepository = dataSource.getRepository(BankAccount); - } - - // ==================== BALANCE GENERAL ==================== - - async generateBalanceSheet( - ctx: ServiceContext, - asOfDate: Date, - options: { projectId?: string } = {} - ): Promise { - const { projectId } = options; - - // Obtener todas las cuentas con saldos - const accounts = await this.getAccountBalances(ctx, asOfDate, projectId); - - // Clasificar cuentas - const assets = accounts.filter((acc) => acc.accountType === 'asset'); - const liabilities = accounts.filter((acc) => acc.accountType === 'liability'); - const equity = accounts.filter((acc) => acc.accountType === 'equity'); - - // Construir 谩rbol de activos - const currentAssets = this.buildAccountTree( - assets.filter((acc) => acc.isCurrentAccount) - ); - const nonCurrentAssets = this.buildAccountTree( - assets.filter((acc) => !acc.isCurrentAccount) - ); - - const totalCurrentAssets = currentAssets.reduce((sum, acc) => sum + acc.balance, 0); - const totalNonCurrentAssets = nonCurrentAssets.reduce((sum, acc) => sum + acc.balance, 0); - const totalAssets = totalCurrentAssets + totalNonCurrentAssets; - - // Construir 谩rbol de pasivos - const currentLiabilities = this.buildAccountTree( - liabilities.filter((acc) => acc.isCurrentAccount) - ); - const nonCurrentLiabilities = this.buildAccountTree( - liabilities.filter((acc) => !acc.isCurrentAccount) - ); - - const totalCurrentLiabilities = currentLiabilities.reduce((sum, acc) => sum + acc.balance, 0); - const totalNonCurrentLiabilities = nonCurrentLiabilities.reduce( - (sum, acc) => sum + acc.balance, - 0 - ); - const totalLiabilities = totalCurrentLiabilities + totalNonCurrentLiabilities; - - // Construir 谩rbol de capital - const equityAccounts = this.buildAccountTree(equity); - const totalEquity = equityAccounts.reduce((sum, acc) => sum + acc.balance, 0); - - const totalLiabilitiesAndEquity = totalLiabilities + totalEquity; - const isBalanced = Math.abs(totalAssets - totalLiabilitiesAndEquity) < 0.01; - - return { - asOfDate, - assets: { - current: currentAssets, - nonCurrent: nonCurrentAssets, - totalCurrent: totalCurrentAssets, - totalNonCurrent: totalNonCurrentAssets, - total: totalAssets, - }, - liabilities: { - current: currentLiabilities, - nonCurrent: nonCurrentLiabilities, - totalCurrent: totalCurrentLiabilities, - totalNonCurrent: totalNonCurrentLiabilities, - total: totalLiabilities, - }, - equity: { - accounts: equityAccounts, - total: totalEquity, - }, - totalLiabilitiesAndEquity, - isBalanced, - }; - } - - private async getAccountBalances( - ctx: ServiceContext, - asOfDate: Date, - projectId?: string - ): Promise< - (ChartOfAccounts & { balance: number; isCurrentAccount: boolean })[] - > { - // Obtener todas las cuentas - const accounts = await this.accountRepository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: IsNull(), - acceptsMovements: true, - }, - order: { code: 'ASC' }, - }); - - // Obtener saldos de movimientos - const queryBuilder = this.lineRepository - .createQueryBuilder('line') - .innerJoin('line.entry', 'entry') - .select('line.accountId', 'accountId') - .addSelect('SUM(line.debitAmount)', 'totalDebit') - .addSelect('SUM(line.creditAmount)', 'totalCredit') - .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.entryDate <= :asOfDate', { asOfDate }); - - if (projectId) { - queryBuilder.andWhere('entry.projectId = :projectId', { projectId }); - } - - queryBuilder.groupBy('line.accountId'); - - const balances = await queryBuilder.getRawMany(); - const balanceMap = new Map( - balances.map((b) => [ - b.accountId, - { - debit: parseFloat(b.totalDebit) || 0, - credit: parseFloat(b.totalCredit) || 0, - }, - ]) - ); - - return accounts.map((acc) => { - const bal = balanceMap.get(acc.id) || { debit: 0, credit: 0 }; - let balance: number; - - // Calcular saldo seg煤n naturaleza - if (acc.nature === 'debit') { - balance = bal.debit - bal.credit; - } else { - balance = bal.credit - bal.debit; - } - - // Determinar si es cuenta corriente (por c贸digo o metadata) - const isCurrentAccount = - acc.code.startsWith('1.1') || // Activo circulante - acc.code.startsWith('2.1') || // Pasivo a corto plazo - (acc.metadata as any)?.isCurrentAccount === true; - - return { - ...acc, - balance, - isCurrentAccount, - }; - }); - } - - private buildAccountTree( - accounts: (ChartOfAccounts & { balance: number })[] - ): BalanceSheetAccount[] { - const rootAccounts = accounts.filter((acc) => !acc.parentId); - - const buildChildren = ( - parentId: string - ): BalanceSheetAccount[] => { - return accounts - .filter((acc) => acc.parentId === parentId) - .map((acc) => { - const children = buildChildren(acc.id); - const childBalance = children.reduce((sum, c) => sum + c.balance, 0); - - return { - accountId: acc.id, - accountCode: acc.code, - accountName: acc.name, - level: acc.level, - balance: acc.balance + childBalance, - children: children.length > 0 ? children : undefined, - }; - }); - }; - - return rootAccounts.map((acc) => { - const children = buildChildren(acc.id); - const childBalance = children.reduce((sum, c) => sum + c.balance, 0); - - return { - accountId: acc.id, - accountCode: acc.code, - accountName: acc.name, - level: acc.level, - balance: acc.balance + childBalance, - children: children.length > 0 ? children : undefined, - }; - }); - } - - // ==================== ESTADO DE RESULTADOS ==================== - - async generateIncomeStatement( - ctx: ServiceContext, - periodStart: Date, - periodEnd: Date, - options: { projectId?: string } = {} - ): Promise { - const { projectId } = options; - - // Obtener movimientos del periodo - const queryBuilder = this.lineRepository - .createQueryBuilder('line') - .innerJoin('line.entry', 'entry') - .innerJoin(ChartOfAccounts, 'account', 'account.id = line.accountId') - .select([ - 'line.accountId as "accountId"', - 'line.accountCode as "accountCode"', - 'account.name as "accountName"', - 'account.accountType as "accountType"', - 'account.level as "level"', - 'account.parentId as "parentId"', - 'SUM(line.debitAmount) as "totalDebit"', - 'SUM(line.creditAmount) as "totalCredit"', - ]) - .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.entryDate BETWEEN :periodStart AND :periodEnd', { - periodStart, - periodEnd, - }) - .andWhere('account.accountType IN (:...types)', { types: ['income', 'expense'] }); - - if (projectId) { - queryBuilder.andWhere('entry.projectId = :projectId', { projectId }); - } - - queryBuilder.groupBy( - 'line.accountId, line.accountCode, account.name, account.accountType, account.level, account.parentId' - ); - - const results = await queryBuilder.getRawMany(); - - // Separar ingresos y gastos - const incomeAccounts = results - .filter((r) => r.accountType === 'income') - .map((r) => ({ - accountId: r.accountId, - accountCode: r.accountCode, - accountName: r.accountName, - level: r.level, - parentId: r.parentId, - amount: (parseFloat(r.totalCredit) || 0) - (parseFloat(r.totalDebit) || 0), - })); - - const expenseAccounts = results - .filter((r) => r.accountType === 'expense') - .map((r) => ({ - accountId: r.accountId, - accountCode: r.accountCode, - accountName: r.accountName, - level: r.level, - parentId: r.parentId, - amount: (parseFloat(r.totalDebit) || 0) - (parseFloat(r.totalCredit) || 0), - })); - - // Clasificar ingresos - const revenue = incomeAccounts.filter((acc) => acc.accountCode.startsWith('4.1')); - const otherIncome = incomeAccounts.filter((acc) => !acc.accountCode.startsWith('4.1')); - - // Clasificar gastos - const costOfSales = expenseAccounts.filter((acc) => acc.accountCode.startsWith('5.1')); - const operatingExpenses = expenseAccounts.filter((acc) => acc.accountCode.startsWith('5.2')); - const otherExpenses = expenseAccounts.filter( - (acc) => !acc.accountCode.startsWith('5.1') && !acc.accountCode.startsWith('5.2') - ); - - // Calcular totales - const totalRevenue = revenue.reduce((sum, acc) => sum + acc.amount, 0); - const totalCostOfSales = costOfSales.reduce((sum, acc) => sum + acc.amount, 0); - const grossProfit = totalRevenue - totalCostOfSales; - - const totalOperatingExpenses = operatingExpenses.reduce((sum, acc) => sum + acc.amount, 0); - const operatingIncome = grossProfit - totalOperatingExpenses; - - const totalOtherIncome = otherIncome.reduce((sum, acc) => sum + acc.amount, 0); - const totalOtherExpenses = otherExpenses.reduce((sum, acc) => sum + acc.amount, 0); - - const incomeBeforeTax = operatingIncome + totalOtherIncome - totalOtherExpenses; - const taxExpense = 0; // TODO: Calcular ISR - const netIncome = incomeBeforeTax - taxExpense; - - return { - periodStart, - periodEnd, - revenue: { - lines: this.buildIncomeTree(revenue), - total: totalRevenue, - }, - costOfSales: { - lines: this.buildIncomeTree(costOfSales), - total: totalCostOfSales, - }, - grossProfit, - operatingExpenses: { - lines: this.buildIncomeTree(operatingExpenses), - total: totalOperatingExpenses, - }, - operatingIncome, - otherIncome: { - lines: this.buildIncomeTree(otherIncome), - total: totalOtherIncome, - }, - otherExpenses: { - lines: this.buildIncomeTree(otherExpenses), - total: totalOtherExpenses, - }, - incomeBeforeTax, - taxExpense, - netIncome, - }; - } - - private buildIncomeTree( - accounts: { - accountId: string; - accountCode: string; - accountName: string; - level: number; - parentId?: string; - amount: number; - }[] - ): IncomeStatementLine[] { - // Por simplicidad, retornamos lista plana ordenada por c贸digo - return accounts - .sort((a, b) => a.accountCode.localeCompare(b.accountCode)) - .map((acc) => ({ - accountId: acc.accountId, - accountCode: acc.accountCode, - accountName: acc.accountName, - level: acc.level, - amount: acc.amount, - })); - } - - // ==================== ESTADO DE FLUJO DE EFECTIVO ==================== - - async generateCashFlowStatement( - ctx: ServiceContext, - periodStart: Date, - periodEnd: Date, - options: { projectId?: string } = {} - ): Promise { - const { projectId } = options; - - // Obtener estado de resultados para utilidad neta - const incomeStatement = await this.generateIncomeStatement( - ctx, - periodStart, - periodEnd, - options - ); - - // Obtener saldos de cuentas bancarias - const bankAccounts = await this.bankAccountRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'active', - ...(projectId && { projectId }), - }, - }); - - const endingCash = bankAccounts.reduce( - (sum, acc) => sum + Number(acc.currentBalance), - 0 - ); - - // Simplificado - en producci贸n se calcular铆an los cambios reales - const operatingActivities = { - netIncome: incomeStatement.netIncome, - adjustments: [ - { description: 'Depreciaci贸n y amortizaci贸n', amount: 0 }, - { description: 'Provisiones', amount: 0 }, - ], - changesInWorkingCapital: [ - { description: 'Cambio en cuentas por cobrar', amount: 0 }, - { description: 'Cambio en inventarios', amount: 0 }, - { description: 'Cambio en cuentas por pagar', amount: 0 }, - ], - netCash: incomeStatement.netIncome, - }; - - const investingActivities = { - items: [ - { description: 'Compra de activos fijos', amount: 0 }, - { description: 'Venta de activos fijos', amount: 0 }, - ], - netCash: 0, - }; - - const financingActivities = { - items: [ - { description: 'Pr茅stamos recibidos', amount: 0 }, - { description: 'Pagos de pr茅stamos', amount: 0 }, - { description: 'Dividendos pagados', amount: 0 }, - ], - netCash: 0, - }; - - const netChangeInCash = - operatingActivities.netCash + - investingActivities.netCash + - financingActivities.netCash; - - const beginningCash = endingCash - netChangeInCash; - - return { - periodStart, - periodEnd, - operatingActivities, - investingActivities, - financingActivities, - netChangeInCash, - beginningCash, - endingCash, - }; - } - - // ==================== BALANZA DE COMPROBACI脫N ==================== - - async generateTrialBalance( - ctx: ServiceContext, - periodStart: Date, - periodEnd: Date, - options: { projectId?: string; includeZeroBalances?: boolean } = {} - ): Promise<{ - accounts: { - accountCode: string; - accountName: string; - accountType: AccountType; - openingDebit: number; - openingCredit: number; - periodDebit: number; - periodCredit: number; - closingDebit: number; - closingCredit: number; - }[]; - totals: { - openingDebit: number; - openingCredit: number; - periodDebit: number; - periodCredit: number; - closingDebit: number; - closingCredit: number; - }; - }> { - const { projectId, includeZeroBalances = false } = options; - - // Obtener todas las cuentas - const accounts = await this.accountRepository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: IsNull(), - acceptsMovements: true, - }, - order: { code: 'ASC' }, - }); - - // Obtener saldos iniciales (movimientos antes del periodo) - const openingQuery = this.lineRepository - .createQueryBuilder('line') - .innerJoin('line.entry', 'entry') - .select('line.accountId', 'accountId') - .addSelect('SUM(line.debitAmount)', 'debit') - .addSelect('SUM(line.creditAmount)', 'credit') - .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.entryDate < :periodStart', { periodStart }); - - if (projectId) { - openingQuery.andWhere('entry.projectId = :projectId', { projectId }); - } - - openingQuery.groupBy('line.accountId'); - const openingBalances = await openingQuery.getRawMany(); - const openingMap = new Map( - openingBalances.map((b) => [ - b.accountId, - { - debit: parseFloat(b.debit) || 0, - credit: parseFloat(b.credit) || 0, - }, - ]) - ); - - // Obtener movimientos del periodo - const periodQuery = this.lineRepository - .createQueryBuilder('line') - .innerJoin('line.entry', 'entry') - .select('line.accountId', 'accountId') - .addSelect('SUM(line.debitAmount)', 'debit') - .addSelect('SUM(line.creditAmount)', 'credit') - .where('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.entryDate BETWEEN :periodStart AND :periodEnd', { - periodStart, - periodEnd, - }); - - if (projectId) { - periodQuery.andWhere('entry.projectId = :projectId', { projectId }); - } - - periodQuery.groupBy('line.accountId'); - const periodMovements = await periodQuery.getRawMany(); - const periodMap = new Map( - periodMovements.map((b) => [ - b.accountId, - { - debit: parseFloat(b.debit) || 0, - credit: parseFloat(b.credit) || 0, - }, - ]) - ); - - // Construir balanza - const trialBalance = accounts - .map((acc) => { - const opening = openingMap.get(acc.id) || { debit: 0, credit: 0 }; - const period = periodMap.get(acc.id) || { debit: 0, credit: 0 }; - - // Calcular saldos seg煤n naturaleza - let openingDebit = 0; - let openingCredit = 0; - const openingBalance = opening.debit - opening.credit; - - if (acc.nature === 'debit') { - if (openingBalance >= 0) { - openingDebit = openingBalance; - } else { - openingCredit = Math.abs(openingBalance); - } - } else { - if (openingBalance <= 0) { - openingCredit = Math.abs(openingBalance); - } else { - openingDebit = openingBalance; - } - } - - const closingBalance = - openingBalance + period.debit - period.credit; - let closingDebit = 0; - let closingCredit = 0; - - if (acc.nature === 'debit') { - if (closingBalance >= 0) { - closingDebit = closingBalance; - } else { - closingCredit = Math.abs(closingBalance); - } - } else { - if (closingBalance <= 0) { - closingCredit = Math.abs(closingBalance); - } else { - closingDebit = closingBalance; - } - } - - return { - accountCode: acc.code, - accountName: acc.name, - accountType: acc.accountType, - openingDebit, - openingCredit, - periodDebit: period.debit, - periodCredit: period.credit, - closingDebit, - closingCredit, - }; - }) - .filter( - (row) => - includeZeroBalances || - row.openingDebit !== 0 || - row.openingCredit !== 0 || - row.periodDebit !== 0 || - row.periodCredit !== 0 - ); - - // Calcular totales - const totals = trialBalance.reduce( - (acc, row) => ({ - openingDebit: acc.openingDebit + row.openingDebit, - openingCredit: acc.openingCredit + row.openingCredit, - periodDebit: acc.periodDebit + row.periodDebit, - periodCredit: acc.periodCredit + row.periodCredit, - closingDebit: acc.closingDebit + row.closingDebit, - closingCredit: acc.closingCredit + row.closingCredit, - }), - { - openingDebit: 0, - openingCredit: 0, - periodDebit: 0, - periodCredit: 0, - closingDebit: 0, - closingCredit: 0, - } - ); - - return { - accounts: trialBalance, - totals, - }; - } - - // ==================== REPORTES AUXILIARES ==================== - - async generateAccountStatement( - ctx: ServiceContext, - accountId: string, - periodStart: Date, - periodEnd: Date - ): Promise<{ - account: ChartOfAccounts; - openingBalance: number; - movements: { - date: Date; - entryNumber: string; - reference?: string; - description: string; - debit: number; - credit: number; - balance: number; - }[]; - closingBalance: number; - }> { - const account = await this.accountRepository.findOne({ - where: { id: accountId, tenantId: ctx.tenantId }, - }); - - if (!account) { - throw new Error('Cuenta no encontrada'); - } - - // Saldo de apertura - const openingResult = await this.lineRepository - .createQueryBuilder('line') - .innerJoin('line.entry', 'entry') - .select('SUM(line.debitAmount)', 'debit') - .addSelect('SUM(line.creditAmount)', 'credit') - .where('line.accountId = :accountId', { accountId }) - .andWhere('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.entryDate < :periodStart', { periodStart }) - .getRawOne(); - - const openingDebit = parseFloat(openingResult?.debit) || 0; - const openingCredit = parseFloat(openingResult?.credit) || 0; - let openingBalance = - account.nature === 'debit' - ? openingDebit - openingCredit - : openingCredit - openingDebit; - - // Movimientos del periodo - const lines = await this.lineRepository - .createQueryBuilder('line') - .innerJoinAndSelect('line.entry', 'entry') - .where('line.accountId = :accountId', { accountId }) - .andWhere('entry.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('entry.status = :status', { status: 'posted' }) - .andWhere('entry.entryDate BETWEEN :periodStart AND :periodEnd', { - periodStart, - periodEnd, - }) - .orderBy('entry.entryDate', 'ASC') - .addOrderBy('entry.entryNumber', 'ASC') - .getMany(); - - let runningBalance = openingBalance; - const movements = lines.map((line) => { - if (account.nature === 'debit') { - runningBalance += line.debitAmount - line.creditAmount; - } else { - runningBalance += line.creditAmount - line.debitAmount; - } - - return { - date: line.entry!.entryDate, - entryNumber: line.entry!.entryNumber, - reference: line.entry!.reference, - description: line.description || line.entry!.description, - debit: line.debitAmount, - credit: line.creditAmount, - balance: runningBalance, - }; - }); - - return { - account, - openingBalance, - movements, - closingBalance: runningBalance, - }; - } - - async getFinancialSummary(ctx: ServiceContext): Promise<{ - totalAssets: number; - totalLiabilities: number; - totalEquity: number; - currentRatio: number; - cashPosition: number; - accountsReceivable: number; - accountsPayable: number; - workingCapital: number; - }> { - const today = new Date(); - - // Balance general simplificado - const balanceSheet = await this.generateBalanceSheet(ctx, today); - - // Cuentas por cobrar - const arResult = await this.arRepository - .createQueryBuilder('ar') - .select('SUM(ar.balanceAmount)', 'total') - .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ar.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ar.deletedAt IS NULL') - .getRawOne(); - - // Cuentas por pagar - const apResult = await this.apRepository - .createQueryBuilder('ap') - .select('SUM(ap.balanceAmount)', 'total') - .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] }) - .andWhere('ap.deletedAt IS NULL') - .getRawOne(); - - // Efectivo - const bankAccounts = await this.bankAccountRepository.find({ - where: { - tenantId: ctx.tenantId, - status: 'active', - }, - }); - - const cashPosition = bankAccounts.reduce( - (sum, acc) => sum + Number(acc.currentBalance), - 0 - ); - - const accountsReceivable = parseFloat(arResult?.total) || 0; - const accountsPayable = parseFloat(apResult?.total) || 0; - - const currentAssets = balanceSheet.assets.totalCurrent; - const currentLiabilities = balanceSheet.liabilities.totalCurrent; - const currentRatio = currentLiabilities > 0 ? currentAssets / currentLiabilities : 0; - const workingCapital = currentAssets - currentLiabilities; - - return { - totalAssets: balanceSheet.assets.total, - totalLiabilities: balanceSheet.liabilities.total, - totalEquity: balanceSheet.equity.total, - currentRatio: Math.round(currentRatio * 100) / 100, - cashPosition, - accountsReceivable, - accountsPayable, - workingCapital, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/finance/services/index.ts b/projects/erp-construccion/backend/src/modules/finance/services/index.ts deleted file mode 100644 index a9eb6c800..000000000 --- a/projects/erp-construccion/backend/src/modules/finance/services/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Finance Services Index - * @module Finance - */ - -export { AccountingService } from './accounting.service'; -export { APService } from './ap.service'; -export { ARService } from './ar.service'; -export { CashFlowService } from './cash-flow.service'; -export { BankReconciliationService } from './bank-reconciliation.service'; -export { FinancialReportsService } from './financial-reports.service'; -export { ERPIntegrationService } from './erp-integration.service'; diff --git a/projects/erp-construccion/backend/src/modules/hr/controllers/employee.controller.ts b/projects/erp-construccion/backend/src/modules/hr/controllers/employee.controller.ts deleted file mode 100644 index 96126140b..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/controllers/employee.controller.ts +++ /dev/null @@ -1,342 +0,0 @@ -/** - * EmployeeController - Controller de empleados - * - * Endpoints REST para gesti贸n de empleados y asignaciones a obras. - * - * @module HR - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - EmployeeService, - CreateEmployeeDto, - UpdateEmployeeDto, - AssignFraccionamientoDto, - EmployeeFilters, -} from '../services/employee.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Employee } from '../entities/employee.entity'; -import { EmployeeFraccionamiento } from '../entities/employee-fraccionamiento.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de empleados - */ -export function createEmployeeController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const employeeRepository = dataSource.getRepository(Employee); - const asignacionRepository = dataSource.getRepository(EmployeeFraccionamiento); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const employeeService = new EmployeeService(employeeRepository, asignacionRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /empleados - * Listar empleados con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: EmployeeFilters = { - estado: req.query.estado as any, - puestoId: req.query.puestoId as string, - departamento: req.query.departamento as string, - fraccionamientoId: req.query.fraccionamientoId as string, - search: req.query.search as string, - }; - - const result = await employeeService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /empleados/stats - * Obtener estad铆sticas de empleados - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await employeeService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /empleados/by-fraccionamiento/:fraccionamientoId - * Obtener empleados asignados a un fraccionamiento - */ - router.get('/by-fraccionamiento/:fraccionamientoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const employees = await employeeService.getEmployeesByFraccionamiento( - getContext(req), - req.params.fraccionamientoId - ); - res.status(200).json({ success: true, data: employees }); - } catch (error) { - next(error); - } - }); - - /** - * GET /empleados/:id - * Obtener empleado por ID con detalles - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const employee = await employeeService.findById(getContext(req), req.params.id); - if (!employee) { - res.status(404).json({ error: 'Not Found', message: 'Employee not found' }); - return; - } - - res.status(200).json({ success: true, data: employee }); - } catch (error) { - next(error); - } - }); - - /** - * POST /empleados - * Crear empleado - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateEmployeeDto = req.body; - - if (!dto.codigo || !dto.nombre || !dto.apellidoPaterno || !dto.fechaIngreso) { - res.status(400).json({ - error: 'Bad Request', - message: 'codigo, nombre, apellidoPaterno and fechaIngreso are required', - }); - return; - } - - const employee = await employeeService.create(getContext(req), dto); - res.status(201).json({ success: true, data: employee }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /empleados/:id - * Actualizar empleado - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateEmployeeDto = req.body; - const employee = await employeeService.update(getContext(req), req.params.id, dto); - - if (!employee) { - res.status(404).json({ error: 'Not Found', message: 'Employee not found' }); - return; - } - - res.status(200).json({ success: true, data: employee }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /empleados/:id/status - * Cambiar estado del empleado - */ - router.post('/:id/status', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { estado, fechaBaja } = req.body; - - if (!estado || !['activo', 'inactivo', 'baja'].includes(estado)) { - res.status(400).json({ - error: 'Bad Request', - message: 'estado must be one of: activo, inactivo, baja', - }); - return; - } - - const employee = await employeeService.changeStatus( - getContext(req), - req.params.id, - estado, - fechaBaja ? new Date(fechaBaja) : undefined - ); - - if (!employee) { - res.status(404).json({ error: 'Not Found', message: 'Employee not found' }); - return; - } - - res.status(200).json({ - success: true, - data: employee, - message: `Employee status changed to ${estado}`, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /empleados/:id/assign - * Asignar empleado a fraccionamiento - */ - router.post('/:id/assign', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: AssignFraccionamientoDto = req.body; - - if (!dto.fraccionamientoId || !dto.fechaInicio) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId and fechaInicio are required', - }); - return; - } - - dto.fechaInicio = new Date(dto.fechaInicio); - if (dto.fechaFin) { - dto.fechaFin = new Date(dto.fechaFin); - } - - const asignacion = await employeeService.assignToFraccionamiento( - getContext(req), - req.params.id, - dto - ); - - res.status(201).json({ - success: true, - data: asignacion, - message: 'Employee assigned to project', - }); - } catch (error) { - if (error instanceof Error && error.message === 'Employee not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * DELETE /empleados/:id/assign/:fraccionamientoId - * Remover empleado de fraccionamiento - */ - router.delete('/:id/assign/:fraccionamientoId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const removed = await employeeService.removeFromFraccionamiento( - getContext(req), - req.params.id, - req.params.fraccionamientoId - ); - - if (!removed) { - res.status(404).json({ error: 'Not Found', message: 'Assignment not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Employee removed from project' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createEmployeeController; diff --git a/projects/erp-construccion/backend/src/modules/hr/controllers/index.ts b/projects/erp-construccion/backend/src/modules/hr/controllers/index.ts deleted file mode 100644 index 83999b875..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/controllers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * HR Controllers Index - * @module HR - */ - -export { createPuestoController } from './puesto.controller'; -export { createEmployeeController } from './employee.controller'; diff --git a/projects/erp-construccion/backend/src/modules/hr/controllers/puesto.controller.ts b/projects/erp-construccion/backend/src/modules/hr/controllers/puesto.controller.ts deleted file mode 100644 index 461c6d288..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/controllers/puesto.controller.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * PuestoController - Controller de puestos de trabajo - * - * Endpoints REST para gesti贸n de cat谩logo de puestos. - * - * @module HR - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { PuestoService, CreatePuestoDto, UpdatePuestoDto, PuestoFilters } from '../services/puesto.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Puesto } from '../entities/puesto.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de puestos - */ -export function createPuestoController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const puestoRepository = dataSource.getRepository(Puesto); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const puestoService = new PuestoService(puestoRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /puestos - * Listar puestos con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: PuestoFilters = { - activo: req.query.activo === 'true' ? true : req.query.activo === 'false' ? false : undefined, - nivelRiesgo: req.query.nivelRiesgo as string, - search: req.query.search as string, - }; - - const result = await puestoService.findAll(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /puestos/:id - * Obtener puesto por ID con empleados asignados - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const puesto = await puestoService.findById(getContext(req), req.params.id); - if (!puesto) { - res.status(404).json({ error: 'Not Found', message: 'Position not found' }); - return; - } - - res.status(200).json({ success: true, data: puesto }); - } catch (error) { - next(error); - } - }); - - /** - * POST /puestos - * Crear puesto - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreatePuestoDto = req.body; - - if (!dto.codigo || !dto.nombre) { - res.status(400).json({ error: 'Bad Request', message: 'codigo and nombre are required' }); - return; - } - - const puesto = await puestoService.create(getContext(req), dto); - res.status(201).json({ success: true, data: puesto }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /puestos/:id - * Actualizar puesto - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdatePuestoDto = req.body; - const puesto = await puestoService.update(getContext(req), req.params.id, dto); - - if (!puesto) { - res.status(404).json({ error: 'Not Found', message: 'Position not found' }); - return; - } - - res.status(200).json({ success: true, data: puesto }); - } catch (error) { - next(error); - } - }); - - /** - * POST /puestos/:id/toggle-active - * Activar/desactivar puesto - */ - router.post('/:id/toggle-active', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const puesto = await puestoService.toggleActive(getContext(req), req.params.id); - - if (!puesto) { - res.status(404).json({ error: 'Not Found', message: 'Position not found' }); - return; - } - - res.status(200).json({ - success: true, - data: puesto, - message: puesto.activo ? 'Position activated' : 'Position deactivated', - }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createPuestoController; diff --git a/projects/erp-construccion/backend/src/modules/hr/entities/employee-fraccionamiento.entity.ts b/projects/erp-construccion/backend/src/modules/hr/entities/employee-fraccionamiento.entity.ts deleted file mode 100644 index 012f74a40..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/entities/employee-fraccionamiento.entity.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * EmployeeFraccionamiento Entity - * Asignaci贸n de empleados a obras/fraccionamientos - * - * @module HR - * @table hr.employee_fraccionamientos - * @ddl schemas/02-hr-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Employee } from './employee.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -@Entity({ schema: 'hr', name: 'employee_fraccionamientos' }) -@Index(['employeeId', 'fraccionamientoId', 'fechaInicio'], { unique: true }) -export class EmployeeFraccionamiento { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'employee_id', type: 'uuid' }) - employeeId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'fecha_inicio', type: 'date' }) - fechaInicio: Date; - - @Column({ name: 'fecha_fin', type: 'date', nullable: true }) - fechaFin: Date; - - @Column({ type: 'varchar', length: 50, nullable: true }) - rol: string; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Employee, (e) => e.asignaciones) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; -} diff --git a/projects/erp-construccion/backend/src/modules/hr/entities/employee.entity.ts b/projects/erp-construccion/backend/src/modules/hr/entities/employee.entity.ts deleted file mode 100644 index b4be02f87..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/entities/employee.entity.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Employee Entity - * Empleados de la empresa - * - * @module HR - * @table hr.employees - * @ddl schemas/02-hr-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Puesto } from './puesto.entity'; -import { EmployeeFraccionamiento } from './employee-fraccionamiento.entity'; - -export type EstadoEmpleado = 'activo' | 'inactivo' | 'baja'; -export type Genero = 'M' | 'F'; - -@Entity({ schema: 'hr', name: 'employees' }) -@Index(['tenantId', 'codigo'], { unique: true }) -@Index(['tenantId', 'curp'], { unique: true }) -export class Employee { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 100 }) - nombre: string; - - @Column({ name: 'apellido_paterno', type: 'varchar', length: 100 }) - apellidoPaterno: string; - - @Column({ name: 'apellido_materno', type: 'varchar', length: 100, nullable: true }) - apellidoMaterno: string; - - @Column({ type: 'varchar', length: 18, nullable: true }) - curp: string; - - @Column({ type: 'varchar', length: 13, nullable: true }) - rfc: string; - - @Column({ type: 'varchar', length: 11, nullable: true }) - nss: string; - - @Column({ name: 'fecha_nacimiento', type: 'date', nullable: true }) - fechaNacimiento: Date; - - @Column({ type: 'varchar', length: 1, nullable: true }) - genero: Genero; - - @Column({ type: 'varchar', length: 255, nullable: true }) - email: string; - - @Column({ type: 'varchar', length: 20, nullable: true }) - telefono: string; - - @Column({ type: 'text', nullable: true }) - direccion: string; - - @Column({ name: 'fecha_ingreso', type: 'date' }) - fechaIngreso: Date; - - @Column({ name: 'fecha_baja', type: 'date', nullable: true }) - fechaBaja: Date; - - @Column({ name: 'puesto_id', type: 'uuid', nullable: true }) - puestoId: string; - - @Column({ type: 'varchar', length: 100, nullable: true }) - departamento: string; - - @Column({ name: 'tipo_contrato', type: 'varchar', length: 50, nullable: true }) - tipoContrato: string; - - @Column({ - name: 'salario_diario', - type: 'decimal', - precision: 10, - scale: 2, - nullable: true - }) - salarioDiario: number; - - @Column({ type: 'varchar', length: 20, default: 'activo' }) - estado: EstadoEmpleado; - - @Column({ name: 'foto_url', type: 'varchar', length: 500, nullable: true }) - fotoUrl: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Puesto, (p) => p.empleados) - @JoinColumn({ name: 'puesto_id' }) - puesto: Puesto; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => EmployeeFraccionamiento, (ef) => ef.employee) - asignaciones: EmployeeFraccionamiento[]; - - // Computed property - get nombreCompleto(): string { - return [this.nombre, this.apellidoPaterno, this.apellidoMaterno] - .filter(Boolean) - .join(' '); - } -} diff --git a/projects/erp-construccion/backend/src/modules/hr/entities/index.ts b/projects/erp-construccion/backend/src/modules/hr/entities/index.ts deleted file mode 100644 index 48752f048..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/entities/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * HR Entities Index - * @module HR - */ - -export * from './puesto.entity'; -export * from './employee.entity'; -export * from './employee-fraccionamiento.entity'; diff --git a/projects/erp-construccion/backend/src/modules/hr/entities/puesto.entity.ts b/projects/erp-construccion/backend/src/modules/hr/entities/puesto.entity.ts deleted file mode 100644 index 26d89f328..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/entities/puesto.entity.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Puesto Entity - * Cat谩logo de puestos de trabajo - * - * @module HR - * @table hr.puestos - * @ddl schemas/02-hr-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Employee } from './employee.entity'; - -@Entity({ schema: 'hr', name: 'puestos' }) -@Index(['tenantId', 'codigo'], { unique: true }) -export class Puesto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 100 }) - nombre: string; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ name: 'nivel_riesgo', type: 'varchar', length: 20, nullable: true }) - nivelRiesgo: string; - - @Column({ - name: 'requiere_capacitacion_especial', - type: 'boolean', - default: false - }) - requiereCapacitacionEspecial: boolean; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @OneToMany(() => Employee, (e) => e.puesto) - empleados: Employee[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hr/services/employee.service.ts b/projects/erp-construccion/backend/src/modules/hr/services/employee.service.ts deleted file mode 100644 index d52cf2e01..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/services/employee.service.ts +++ /dev/null @@ -1,330 +0,0 @@ -/** - * EmployeeService - Servicio para gesti贸n de empleados - * - * CRUD de empleados con gesti贸n de estado y asignaciones a obras. - * - * @module HR - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { Employee, EstadoEmpleado, Genero } from '../entities/employee.entity'; -import { EmployeeFraccionamiento } from '../entities/employee-fraccionamiento.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateEmployeeDto { - codigo: string; - nombre: string; - apellidoPaterno: string; - apellidoMaterno?: string; - curp?: string; - rfc?: string; - nss?: string; - fechaNacimiento?: Date; - genero?: Genero; - email?: string; - telefono?: string; - direccion?: string; - fechaIngreso: Date; - puestoId?: string; - departamento?: string; - tipoContrato?: string; - salarioDiario?: number; - fotoUrl?: string; -} - -export interface UpdateEmployeeDto { - nombre?: string; - apellidoPaterno?: string; - apellidoMaterno?: string; - curp?: string; - rfc?: string; - nss?: string; - fechaNacimiento?: Date; - genero?: Genero; - email?: string; - telefono?: string; - direccion?: string; - puestoId?: string; - departamento?: string; - tipoContrato?: string; - salarioDiario?: number; - fotoUrl?: string; -} - -export interface AssignFraccionamientoDto { - fraccionamientoId: string; - fechaInicio: Date; - fechaFin?: Date; - rol?: string; -} - -export interface EmployeeFilters { - estado?: EstadoEmpleado; - puestoId?: string; - departamento?: string; - fraccionamientoId?: string; - search?: string; -} - -export interface EmployeeStats { - total: number; - activos: number; - inactivos: number; - bajas: number; - porDepartamento: { departamento: string; count: number }[]; -} - -export class EmployeeService { - constructor( - private readonly employeeRepository: Repository, - private readonly asignacionRepository: Repository - ) {} - - async findWithFilters( - ctx: ServiceContext, - filters: EmployeeFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.employeeRepository - .createQueryBuilder('employee') - .leftJoinAndSelect('employee.puesto', 'puesto') - .where('employee.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.estado) { - queryBuilder.andWhere('employee.estado = :estado', { estado: filters.estado }); - } - - if (filters.puestoId) { - queryBuilder.andWhere('employee.puesto_id = :puestoId', { puestoId: filters.puestoId }); - } - - if (filters.departamento) { - queryBuilder.andWhere('employee.departamento = :departamento', { departamento: filters.departamento }); - } - - if (filters.fraccionamientoId) { - queryBuilder - .innerJoin('employee.asignaciones', 'asig') - .andWhere('asig.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId: filters.fraccionamientoId }) - .andWhere('asig.activo = true'); - } - - if (filters.search) { - queryBuilder.andWhere( - '(employee.codigo ILIKE :search OR employee.nombre ILIKE :search OR employee.apellido_paterno ILIKE :search OR employee.curp ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - queryBuilder - .orderBy('employee.apellido_paterno', 'ASC') - .addOrderBy('employee.nombre', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.employeeRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - relations: ['puesto', 'asignaciones', 'asignaciones.fraccionamiento'], - }); - } - - async findByCodigo(ctx: ServiceContext, codigo: string): Promise { - return this.employeeRepository.findOne({ - where: { - codigo, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - }); - } - - async findByCurp(ctx: ServiceContext, curp: string): Promise { - return this.employeeRepository.findOne({ - where: { - curp, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - }); - } - - async create(ctx: ServiceContext, dto: CreateEmployeeDto): Promise { - // Validate unique codigo - const existingCodigo = await this.findByCodigo(ctx, dto.codigo); - if (existingCodigo) { - throw new Error(`Employee with codigo ${dto.codigo} already exists`); - } - - // Validate unique CURP if provided - if (dto.curp) { - const existingCurp = await this.findByCurp(ctx, dto.curp); - if (existingCurp) { - throw new Error(`Employee with CURP ${dto.curp} already exists`); - } - } - - const employee = this.employeeRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - estado: 'activo', - ...dto, - }); - - return this.employeeRepository.save(employee); - } - - async update(ctx: ServiceContext, id: string, dto: UpdateEmployeeDto): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - // Validate unique CURP if changed - if (dto.curp && dto.curp !== existing.curp) { - const existingCurp = await this.findByCurp(ctx, dto.curp); - if (existingCurp) { - throw new Error(`Employee with CURP ${dto.curp} already exists`); - } - } - - const updated = this.employeeRepository.merge(existing, dto); - return this.employeeRepository.save(updated); - } - - async changeStatus(ctx: ServiceContext, id: string, estado: EstadoEmpleado, fechaBaja?: Date): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - existing.estado = estado; - if (estado === 'baja' && fechaBaja) { - existing.fechaBaja = fechaBaja; - } - - return this.employeeRepository.save(existing); - } - - async assignToFraccionamiento( - ctx: ServiceContext, - employeeId: string, - dto: AssignFraccionamientoDto - ): Promise { - const employee = await this.findById(ctx, employeeId); - if (!employee) { - throw new Error('Employee not found'); - } - - // Deactivate previous active assignment to same fraccionamiento - await this.asignacionRepository.update( - { - tenantId: ctx.tenantId, - employeeId, - fraccionamientoId: dto.fraccionamientoId, - activo: true, - } as FindOptionsWhere, - { activo: false, fechaFin: new Date() } - ); - - const asignacion = this.asignacionRepository.create({ - tenantId: ctx.tenantId, - employeeId, - fraccionamientoId: dto.fraccionamientoId, - fechaInicio: dto.fechaInicio, - fechaFin: dto.fechaFin, - rol: dto.rol, - activo: true, - }); - - return this.asignacionRepository.save(asignacion); - } - - async removeFromFraccionamiento( - ctx: ServiceContext, - employeeId: string, - fraccionamientoId: string - ): Promise { - const result = await this.asignacionRepository.update( - { - tenantId: ctx.tenantId, - employeeId, - fraccionamientoId, - activo: true, - } as FindOptionsWhere, - { activo: false, fechaFin: new Date() } - ); - - return (result.affected ?? 0) > 0; - } - - async getEmployeesByFraccionamiento( - ctx: ServiceContext, - fraccionamientoId: string - ): Promise { - const asignaciones = await this.asignacionRepository.find({ - where: { - tenantId: ctx.tenantId, - fraccionamientoId, - activo: true, - } as FindOptionsWhere, - relations: ['employee', 'employee.puesto'], - }); - - return asignaciones.map(a => a.employee); - } - - async getStats(ctx: ServiceContext): Promise { - const [total, activos, inactivos, bajas] = await Promise.all([ - this.employeeRepository.count({ - where: { tenantId: ctx.tenantId } as FindOptionsWhere, - }), - this.employeeRepository.count({ - where: { tenantId: ctx.tenantId, estado: 'activo' } as FindOptionsWhere, - }), - this.employeeRepository.count({ - where: { tenantId: ctx.tenantId, estado: 'inactivo' } as FindOptionsWhere, - }), - this.employeeRepository.count({ - where: { tenantId: ctx.tenantId, estado: 'baja' } as FindOptionsWhere, - }), - ]); - - const porDepartamento = await this.employeeRepository - .createQueryBuilder('employee') - .select('employee.departamento', 'departamento') - .addSelect('COUNT(*)', 'count') - .where('employee.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('employee.estado = :estado', { estado: 'activo' }) - .groupBy('employee.departamento') - .getRawMany(); - - return { - total, - activos, - inactivos, - bajas, - porDepartamento: porDepartamento.map(p => ({ - departamento: p.departamento || 'Sin departamento', - count: parseInt(p.count, 10), - })), - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/hr/services/index.ts b/projects/erp-construccion/backend/src/modules/hr/services/index.ts deleted file mode 100644 index ef97794bd..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/services/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * HR Services Index - * @module HR - */ - -export * from './puesto.service'; -export * from './employee.service'; diff --git a/projects/erp-construccion/backend/src/modules/hr/services/puesto.service.ts b/projects/erp-construccion/backend/src/modules/hr/services/puesto.service.ts deleted file mode 100644 index a58a7d1c6..000000000 --- a/projects/erp-construccion/backend/src/modules/hr/services/puesto.service.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * PuestoService - Servicio para cat谩logo de puestos - * - * Gesti贸n de puestos de trabajo con CRUD b谩sico. - * - * @module HR - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { Puesto } from '../entities/puesto.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreatePuestoDto { - codigo: string; - nombre: string; - descripcion?: string; - nivelRiesgo?: string; - requiereCapacitacionEspecial?: boolean; -} - -export interface UpdatePuestoDto { - nombre?: string; - descripcion?: string; - nivelRiesgo?: string; - requiereCapacitacionEspecial?: boolean; - activo?: boolean; -} - -export interface PuestoFilters { - activo?: boolean; - nivelRiesgo?: string; - search?: string; -} - -export class PuestoService { - constructor(private readonly repository: Repository) {} - - async findAll( - ctx: ServiceContext, - filters: PuestoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.repository - .createQueryBuilder('puesto') - .where('puesto.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.activo !== undefined) { - queryBuilder.andWhere('puesto.activo = :activo', { activo: filters.activo }); - } - - if (filters.nivelRiesgo) { - queryBuilder.andWhere('puesto.nivel_riesgo = :nivelRiesgo', { nivelRiesgo: filters.nivelRiesgo }); - } - - if (filters.search) { - queryBuilder.andWhere( - '(puesto.codigo ILIKE :search OR puesto.nombre ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - queryBuilder - .orderBy('puesto.nombre', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - relations: ['empleados'], - }); - } - - async findByCodigo(ctx: ServiceContext, codigo: string): Promise { - return this.repository.findOne({ - where: { - codigo, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - }); - } - - async create(ctx: ServiceContext, dto: CreatePuestoDto): Promise { - const existing = await this.findByCodigo(ctx, dto.codigo); - if (existing) { - throw new Error(`Puesto with codigo ${dto.codigo} already exists`); - } - - const puesto = this.repository.create({ - tenantId: ctx.tenantId, - codigo: dto.codigo, - nombre: dto.nombre, - descripcion: dto.descripcion, - nivelRiesgo: dto.nivelRiesgo, - requiereCapacitacionEspecial: dto.requiereCapacitacionEspecial || false, - activo: true, - }); - - return this.repository.save(puesto); - } - - async update(ctx: ServiceContext, id: string, dto: UpdatePuestoDto): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - const updated = this.repository.merge(existing, dto); - return this.repository.save(updated); - } - - async toggleActive(ctx: ServiceContext, id: string): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - existing.activo = !existing.activo; - return this.repository.save(existing); - } - - async getActiveCount(ctx: ServiceContext): Promise { - return this.repository.count({ - where: { - tenantId: ctx.tenantId, - activo: true, - } as FindOptionsWhere, - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/ambiental.controller.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/ambiental.controller.ts deleted file mode 100644 index 6364c8ee9..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/ambiental.controller.ts +++ /dev/null @@ -1,598 +0,0 @@ -/** - * AmbientalController - Controller de gesti贸n ambiental HSE - * - * Endpoints REST para gesti贸n de residuos y manifiestos. - * - * @module HSE - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - AmbientalService, - CreateResiduoCatalogoDto, - CreateGeneracionDto, - CreateManifiestoDto, - AddDetalleManifiestoDto, - CreateImpactoDto, - CreateQuejaDto, - ResiduoFilters, - GeneracionFilters, - ManifiestoFilters, - ImpactoFilters, - QuejaFilters, -} from '../services/ambiental.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { ResiduoCatalogo } from '../entities/residuo-catalogo.entity'; -import { ResiduoGeneracion, EstadoResiduo } from '../entities/residuo-generacion.entity'; -import { AlmacenTemporal } from '../entities/almacen-temporal.entity'; -import { ProveedorAmbiental } from '../entities/proveedor-ambiental.entity'; -import { ManifiestoResiduos, EstadoManifiesto } from '../entities/manifiesto-residuos.entity'; -import { ManifiestoDetalle } from '../entities/manifiesto-detalle.entity'; -import { ImpactoAmbiental, EstadoImpacto } from '../entities/impacto-ambiental.entity'; -import { QuejaAmbiental } from '../entities/queja-ambiental.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de gesti贸n ambiental - */ -export function createAmbientalController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const residuoCatalogoRepository = dataSource.getRepository(ResiduoCatalogo); - const generacionRepository = dataSource.getRepository(ResiduoGeneracion); - const almacenRepository = dataSource.getRepository(AlmacenTemporal); - const proveedorRepository = dataSource.getRepository(ProveedorAmbiental); - const manifiestoRepository = dataSource.getRepository(ManifiestoResiduos); - const detalleRepository = dataSource.getRepository(ManifiestoDetalle); - const impactoRepository = dataSource.getRepository(ImpactoAmbiental); - const quejaRepository = dataSource.getRepository(QuejaAmbiental); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const ambientalService = new AmbientalService( - residuoCatalogoRepository, - generacionRepository, - almacenRepository, - proveedorRepository, - manifiestoRepository, - detalleRepository, - impactoRepository, - quejaRepository - ); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - // ========== Cat谩logo de Residuos ========== - - /** - * GET /ambiental/residuos/catalogo - * Listar cat谩logo de residuos - */ - router.get('/residuos/catalogo', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: ResiduoFilters = { - categoria: req.query.categoria as any, - activo: req.query.activo === 'true' ? true : req.query.activo === 'false' ? false : undefined, - search: req.query.search as string, - }; - - const result = await ambientalService.findResiduos(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /ambiental/residuos/catalogo - * Crear tipo de residuo en cat谩logo - */ - router.post('/residuos/catalogo', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateResiduoCatalogoDto = req.body; - - if (!dto.codigo || !dto.nombre || !dto.categoria) { - res.status(400).json({ - error: 'Bad Request', - message: 'codigo, nombre and categoria are required', - }); - return; - } - - const residuo = await ambientalService.createResiduo(dto); - res.status(201).json({ success: true, data: residuo }); - } catch (error) { - next(error); - } - }); - - // ========== Generaci贸n de Residuos ========== - - /** - * GET /ambiental/generaciones - * Listar generaciones de residuos - */ - router.get('/generaciones', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: GeneracionFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - residuoId: req.query.residuoId as string, - estado: req.query.estado as EstadoResiduo, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await ambientalService.findGeneraciones(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /ambiental/generaciones - * Registrar generaci贸n de residuo - */ - router.post('/generaciones', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateGeneracionDto = req.body; - - if (!dto.fraccionamientoId || !dto.residuoId || !dto.cantidad || !dto.fechaGeneracion || !dto.unidad) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, residuoId, cantidad, fechaGeneracion and unidad are required', - }); - return; - } - - dto.fechaGeneracion = new Date(dto.fechaGeneracion); - const generacion = await ambientalService.createGeneracion(getContext(req), dto); - res.status(201).json({ success: true, data: generacion }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /ambiental/generaciones/:id/estado - * Actualizar estado de generaci贸n - */ - router.patch('/generaciones/:id/estado', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { estado } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const generacion = await ambientalService.updateGeneracionEstado(getContext(req), req.params.id, estado); - if (!generacion) { - res.status(404).json({ error: 'Not Found', message: 'Waste generation not found' }); - return; - } - res.status(200).json({ success: true, data: generacion }); - } catch (error) { - next(error); - } - }); - - // ========== Almacenes Temporales ========== - - /** - * GET /ambiental/almacenes - * Listar almacenes temporales - */ - router.get('/almacenes', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const fraccionamientoId = req.query.fraccionamientoId as string; - const almacenes = await ambientalService.findAlmacenes(getContext(req), fraccionamientoId); - res.status(200).json({ success: true, data: almacenes }); - } catch (error) { - next(error); - } - }); - - // ========== Proveedores Ambientales ========== - - /** - * GET /ambiental/proveedores - * Listar proveedores ambientales - */ - router.get('/proveedores', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const proveedores = await ambientalService.findProveedores(getContext(req)); - res.status(200).json({ success: true, data: proveedores }); - } catch (error) { - next(error); - } - }); - - // ========== Manifiestos ========== - - /** - * GET /ambiental/manifiestos - * Listar manifiestos de residuos - */ - router.get('/manifiestos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: ManifiestoFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - estado: req.query.estado as EstadoManifiesto, - transportistaId: req.query.transportistaId as string, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await ambientalService.findManifiestos(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /ambiental/manifiestos/:id - * Obtener manifiesto con detalles - */ - router.get('/manifiestos/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const manifiesto = await ambientalService.findManifiestoWithDetalles(getContext(req), req.params.id); - if (!manifiesto) { - res.status(404).json({ error: 'Not Found', message: 'Manifest not found' }); - return; - } - res.status(200).json({ success: true, data: manifiesto }); - } catch (error) { - next(error); - } - }); - - /** - * POST /ambiental/manifiestos - * Crear manifiesto de residuos - */ - router.post('/manifiestos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateManifiestoDto = req.body; - - if (!dto.fraccionamientoId || !dto.transportistaId || !dto.destinoId || !dto.fechaRecoleccion) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, transportistaId, destinoId and fechaRecoleccion are required', - }); - return; - } - - dto.fechaRecoleccion = new Date(dto.fechaRecoleccion); - const manifiesto = await ambientalService.createManifiesto(getContext(req), dto); - res.status(201).json({ success: true, data: manifiesto }); - } catch (error) { - next(error); - } - }); - - /** - * POST /ambiental/manifiestos/:id/detalles - * Agregar detalle a manifiesto - */ - router.post('/manifiestos/:id/detalles', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: AddDetalleManifiestoDto = req.body; - - if (!dto.residuoId || !dto.cantidad || !dto.unidad) { - res.status(400).json({ - error: 'Bad Request', - message: 'residuoId, cantidad and unidad are required', - }); - return; - } - - const detalle = await ambientalService.addDetalleManifiesto(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: detalle }); - } catch (error) { - if (error instanceof Error && error.message === 'Manifiesto no encontrado') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /ambiental/manifiestos/:id/estado - * Actualizar estado de manifiesto - */ - router.patch('/manifiestos/:id/estado', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { estado } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const manifiesto = await ambientalService.updateManifiestoEstado(getContext(req), req.params.id, estado); - if (!manifiesto) { - res.status(404).json({ error: 'Not Found', message: 'Manifest not found' }); - return; - } - res.status(200).json({ success: true, data: manifiesto }); - } catch (error) { - next(error); - } - }); - - // ========== Impactos Ambientales ========== - - /** - * GET /ambiental/impactos - * Listar impactos ambientales - */ - router.get('/impactos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: ImpactoFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - tipoImpacto: req.query.tipoImpacto as any, - nivelRiesgo: req.query.nivelRiesgo as any, - estado: req.query.estado as any, - }; - - const result = await ambientalService.findImpactos(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /ambiental/impactos - * Crear impacto ambiental - */ - router.post('/impactos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateImpactoDto = req.body; - - if (!dto.fraccionamientoId || !dto.aspecto || !dto.tipoImpacto || !dto.severidad || !dto.probabilidad) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, aspecto, tipoImpacto, severidad and probabilidad are required', - }); - return; - } - - const impacto = await ambientalService.createImpacto(getContext(req), dto); - res.status(201).json({ success: true, data: impacto }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /ambiental/impactos/:id/estado - * Actualizar estado de impacto - */ - router.patch('/impactos/:id/estado', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { estado } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const impacto = await ambientalService.updateImpactoEstado(getContext(req), req.params.id, estado as EstadoImpacto); - if (!impacto) { - res.status(404).json({ error: 'Not Found', message: 'Impact not found' }); - return; - } - res.status(200).json({ success: true, data: impacto }); - } catch (error) { - next(error); - } - }); - - // ========== Quejas Ambientales ========== - - /** - * GET /ambiental/quejas - * Listar quejas ambientales - */ - router.get('/quejas', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: QuejaFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - tipo: req.query.tipo as any, - estado: req.query.estado as any, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await ambientalService.findQuejas(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /ambiental/quejas - * Crear queja ambiental - */ - router.post('/quejas', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateQuejaDto = req.body; - - if (!dto.fraccionamientoId || !dto.origen || !dto.tipo || !dto.descripcion) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, origen, tipo and descripcion are required', - }); - return; - } - - const queja = await ambientalService.createQueja(getContext(req), dto); - res.status(201).json({ success: true, data: queja }); - } catch (error) { - next(error); - } - }); - - /** - * POST /ambiental/quejas/:id/atender - * Atender queja ambiental - */ - router.post('/quejas/:id/atender', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { accionesTomadas } = req.body; - if (!accionesTomadas) { - res.status(400).json({ error: 'Bad Request', message: 'accionesTomadas is required' }); - return; - } - - const queja = await ambientalService.atenderQueja(getContext(req), req.params.id, accionesTomadas); - if (!queja) { - res.status(404).json({ error: 'Not Found', message: 'Complaint not found' }); - return; - } - res.status(200).json({ success: true, data: queja, message: 'Complaint being attended' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /ambiental/quejas/:id/cerrar - * Cerrar queja ambiental - */ - router.post('/quejas/:id/cerrar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const queja = await ambientalService.cerrarQueja(getContext(req), req.params.id); - if (!queja) { - res.status(404).json({ error: 'Not Found', message: 'Complaint not found' }); - return; - } - res.status(200).json({ success: true, data: queja, message: 'Complaint closed' }); - } catch (error) { - next(error); - } - }); - - // ========== Estad铆sticas ========== - - /** - * GET /ambiental/stats - * Obtener estad铆sticas ambientales - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - const stats = await ambientalService.getStats(getContext(req), fraccionamientoId); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createAmbientalController; diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/capacitacion.controller.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/capacitacion.controller.ts deleted file mode 100644 index 9d5066078..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/capacitacion.controller.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * CapacitacionController - Controller de capacitaciones HSE - * - * Endpoints REST para gesti贸n de cat谩logo de capacitaciones. - * - * @module HSE - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - CapacitacionService, - CreateCapacitacionDto, - UpdateCapacitacionDto, - CapacitacionFilters, -} from '../services/capacitacion.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Capacitacion } from '../entities/capacitacion.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de capacitaciones - */ -export function createCapacitacionController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const capacitacionRepository = dataSource.getRepository(Capacitacion); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const capacitacionService = new CapacitacionService(capacitacionRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /capacitaciones - * Listar capacitaciones con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: CapacitacionFilters = { - tipo: req.query.tipo as any, - activo: req.query.activo === 'true' ? true : req.query.activo === 'false' ? false : undefined, - search: req.query.search as string, - }; - - const result = await capacitacionService.findAll(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /capacitaciones/by-tipo/:tipo - * Obtener capacitaciones activas por tipo - */ - router.get('/by-tipo/:tipo', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const validTipos = ['induccion', 'especifica', 'certificacion', 'reentrenamiento']; - if (!validTipos.includes(req.params.tipo)) { - res.status(400).json({ error: 'Bad Request', message: 'Invalid tipo' }); - return; - } - - const capacitaciones = await capacitacionService.getByTipo(getContext(req), req.params.tipo as any); - res.status(200).json({ success: true, data: capacitaciones }); - } catch (error) { - next(error); - } - }); - - /** - * GET /capacitaciones/:id - * Obtener capacitaci贸n por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const capacitacion = await capacitacionService.findById(getContext(req), req.params.id); - if (!capacitacion) { - res.status(404).json({ error: 'Not Found', message: 'Training not found' }); - return; - } - - res.status(200).json({ success: true, data: capacitacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /capacitaciones - * Crear capacitaci贸n - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateCapacitacionDto = req.body; - - if (!dto.codigo || !dto.nombre || !dto.tipo) { - res.status(400).json({ error: 'Bad Request', message: 'codigo, nombre and tipo are required' }); - return; - } - - const capacitacion = await capacitacionService.create(getContext(req), dto); - res.status(201).json({ success: true, data: capacitacion }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /capacitaciones/:id - * Actualizar capacitaci贸n - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateCapacitacionDto = req.body; - const capacitacion = await capacitacionService.update(getContext(req), req.params.id, dto); - - if (!capacitacion) { - res.status(404).json({ error: 'Not Found', message: 'Training not found' }); - return; - } - - res.status(200).json({ success: true, data: capacitacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /capacitaciones/:id/toggle-active - * Activar/desactivar capacitaci贸n - */ - router.post('/:id/toggle-active', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const capacitacion = await capacitacionService.toggleActive(getContext(req), req.params.id); - - if (!capacitacion) { - res.status(404).json({ error: 'Not Found', message: 'Training not found' }); - return; - } - - res.status(200).json({ - success: true, - data: capacitacion, - message: capacitacion.activo ? 'Training activated' : 'Training deactivated', - }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createCapacitacionController; diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/epp.controller.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/epp.controller.ts deleted file mode 100644 index d6bdce29e..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/epp.controller.ts +++ /dev/null @@ -1,464 +0,0 @@ -/** - * EppController - Controller de EPP HSE - * - * Endpoints REST para gesti贸n de equipo de protecci贸n personal. - * - * @module HSE - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - EppService, - CreateEppCatalogoDto, - CreateAsignacionDto, - CreateInspeccionEppDto, - CreateBajaDto, - EppFilters, - AsignacionFilters, -} from '../services/epp.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { EppCatalogo } from '../entities/epp-catalogo.entity'; -import { EppAsignacion } from '../entities/epp-asignacion.entity'; -import { EppInspeccion } from '../entities/epp-inspeccion.entity'; -import { EppBaja } from '../entities/epp-baja.entity'; -import { EppMatrizPuesto } from '../entities/epp-matriz-puesto.entity'; -import { EppInventario } from '../entities/epp-inventario.entity'; -import { EppMovimiento } from '../entities/epp-movimiento.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de EPP - */ -export function createEppController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const catalogoRepository = dataSource.getRepository(EppCatalogo); - const asignacionRepository = dataSource.getRepository(EppAsignacion); - const inspeccionRepository = dataSource.getRepository(EppInspeccion); - const bajaRepository = dataSource.getRepository(EppBaja); - const matrizRepository = dataSource.getRepository(EppMatrizPuesto); - const inventarioRepository = dataSource.getRepository(EppInventario); - const movimientoRepository = dataSource.getRepository(EppMovimiento); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const eppService = new EppService( - catalogoRepository, - asignacionRepository, - inspeccionRepository, - bajaRepository, - matrizRepository, - inventarioRepository, - movimientoRepository - ); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - // ========== Cat谩logo EPP ========== - - /** - * GET /epp/catalogo - * Listar cat谩logo de EPP - */ - router.get('/catalogo', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: EppFilters = { - categoria: req.query.categoria as any, - activo: req.query.activo === 'true' ? true : req.query.activo === 'false' ? false : undefined, - search: req.query.search as string, - }; - - const result = await eppService.findCatalogo(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /epp/catalogo/:id - * Obtener EPP del cat谩logo por ID - */ - router.get('/catalogo/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const epp = await eppService.findCatalogoById(getContext(req), req.params.id); - if (!epp) { - res.status(404).json({ error: 'Not Found', message: 'EPP not found' }); - return; - } - res.status(200).json({ success: true, data: epp }); - } catch (error) { - next(error); - } - }); - - /** - * POST /epp/catalogo - * Crear EPP en cat谩logo - */ - router.post('/catalogo', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateEppCatalogoDto = req.body; - - if (!dto.codigo || !dto.nombre || !dto.categoria || !dto.vidaUtilDias) { - res.status(400).json({ - error: 'Bad Request', - message: 'codigo, nombre, categoria and vidaUtilDias are required', - }); - return; - } - - const epp = await eppService.createCatalogo(getContext(req), dto); - res.status(201).json({ success: true, data: epp }); - } catch (error) { - next(error); - } - }); - - // ========== Matriz por Puesto ========== - - /** - * GET /epp/matriz/:puestoId - * Obtener matriz de EPP por puesto - */ - router.get('/matriz/:puestoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const matriz = await eppService.getMatrizByPuesto(getContext(req), req.params.puestoId); - res.status(200).json({ success: true, data: matriz }); - } catch (error) { - next(error); - } - }); - - /** - * POST /epp/matriz/:puestoId - * Configurar EPP requerido para puesto - */ - router.post('/matriz/:puestoId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { eppId, esObligatorio, actividadEspecifica } = req.body; - - if (!eppId || esObligatorio === undefined) { - res.status(400).json({ - error: 'Bad Request', - message: 'eppId and esObligatorio are required', - }); - return; - } - - const matriz = await eppService.setMatrizPuesto( - getContext(req), - req.params.puestoId, - eppId, - esObligatorio, - actividadEspecifica - ); - res.status(200).json({ success: true, data: matriz }); - } catch (error) { - next(error); - } - }); - - // ========== Asignaciones ========== - - /** - * GET /epp/asignaciones - * Listar asignaciones de EPP - */ - router.get('/asignaciones', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: AsignacionFilters = { - employeeId: req.query.employeeId as string, - eppId: req.query.eppId as string, - estado: req.query.estado as any, - fraccionamientoId: req.query.fraccionamientoId as string, - vencidos: req.query.vencidos === 'true', - }; - - const result = await eppService.findAsignaciones(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /epp/asignaciones/:id - * Obtener asignaci贸n por ID - */ - router.get('/asignaciones/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const asignacion = await eppService.findAsignacionById(getContext(req), req.params.id); - if (!asignacion) { - res.status(404).json({ error: 'Not Found', message: 'Assignment not found' }); - return; - } - res.status(200).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /epp/asignaciones - * Crear asignaci贸n de EPP - */ - router.post('/asignaciones', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateAsignacionDto = req.body; - - if (!dto.eppId || !dto.employeeId || !dto.fechaEntrega || !dto.fechaVencimiento) { - res.status(400).json({ - error: 'Bad Request', - message: 'eppId, employeeId, fechaEntrega and fechaVencimiento are required', - }); - return; - } - - dto.fechaEntrega = new Date(dto.fechaEntrega); - dto.fechaVencimiento = new Date(dto.fechaVencimiento); - const asignacion = await eppService.createAsignacion(getContext(req), dto); - res.status(201).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /epp/asignaciones/:id/estado - * Actualizar estado de asignaci贸n - */ - router.patch('/asignaciones/:id/estado', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { estado } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const asignacion = await eppService.updateAsignacionEstado(getContext(req), req.params.id, estado); - if (!asignacion) { - res.status(404).json({ error: 'Not Found', message: 'Assignment not found' }); - return; - } - res.status(200).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - // ========== Inspecciones EPP ========== - - /** - * GET /epp/asignaciones/:id/inspecciones - * Listar inspecciones de una asignaci贸n - */ - router.get('/asignaciones/:id/inspecciones', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const inspecciones = await eppService.getInspeccionesByAsignacion(getContext(req), req.params.id); - res.status(200).json({ success: true, data: inspecciones }); - } catch (error) { - next(error); - } - }); - - /** - * POST /epp/asignaciones/:id/inspecciones - * Crear inspecci贸n de EPP - */ - router.post('/asignaciones/:id/inspecciones', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateInspeccionEppDto = { - ...req.body, - asignacionId: req.params.id, - }; - - if (!dto.inspectorId || !dto.estadoEpp) { - res.status(400).json({ - error: 'Bad Request', - message: 'inspectorId and estadoEpp are required', - }); - return; - } - - const inspeccion = await eppService.createInspeccion(getContext(req), dto); - res.status(201).json({ success: true, data: inspeccion }); - } catch (error) { - if (error instanceof Error && error.message === 'Asignaci贸n no encontrada') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - // ========== Bajas ========== - - /** - * POST /epp/asignaciones/:id/baja - * Dar de baja EPP - */ - router.post('/asignaciones/:id/baja', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateBajaDto = { - ...req.body, - asignacionId: req.params.id, - }; - - if (!dto.motivo) { - res.status(400).json({ - error: 'Bad Request', - message: 'motivo is required', - }); - return; - } - - const baja = await eppService.createBaja(getContext(req), dto); - res.status(201).json({ success: true, data: baja, message: 'EPP given off' }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Asignaci贸n no encontrada') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message === 'EPP ya fue dado de baja') { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - // ========== Inventario ========== - - /** - * GET /epp/inventario - * Obtener inventario de EPP - */ - router.get('/inventario', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const almacenId = req.query.almacenId as string; - const inventario = await eppService.getInventario(getContext(req), almacenId); - res.status(200).json({ success: true, data: inventario }); - } catch (error) { - next(error); - } - }); - - /** - * POST /epp/movimientos - * Registrar movimiento de inventario - */ - router.post('/movimientos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { eppId, tipo, cantidad, almacenOrigenId, almacenDestinoId, referencia } = req.body; - - if (!eppId || !tipo || !cantidad) { - res.status(400).json({ - error: 'Bad Request', - message: 'eppId, tipo and cantidad are required', - }); - return; - } - - const movimiento = await eppService.registrarMovimiento( - getContext(req), - eppId, - tipo, - cantidad, - almacenOrigenId, - almacenDestinoId, - referencia - ); - res.status(201).json({ success: true, data: movimiento }); - } catch (error) { - next(error); - } - }); - - // ========== Estad铆sticas ========== - - /** - * GET /epp/stats - * Obtener estad铆sticas de EPP - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - const stats = await eppService.getStats(getContext(req), fraccionamientoId); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createEppController; diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/incidente.controller.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/incidente.controller.ts deleted file mode 100644 index 3e979068c..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/incidente.controller.ts +++ /dev/null @@ -1,398 +0,0 @@ -/** - * IncidenteController - Controller de incidentes HSE - * - * Endpoints REST para gesti贸n de incidentes de seguridad. - * Workflow: abierto -> en_investigacion -> cerrado - * - * @module HSE - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - IncidenteService, - CreateIncidenteDto, - UpdateIncidenteDto, - AddInvolucradoDto, - AddAccionDto, - UpdateAccionDto, - IncidenteFilters, -} from '../services/incidente.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Incidente } from '../entities/incidente.entity'; -import { IncidenteInvolucrado } from '../entities/incidente-involucrado.entity'; -import { IncidenteAccion } from '../entities/incidente-accion.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de incidentes - */ -export function createIncidenteController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const incidenteRepository = dataSource.getRepository(Incidente); - const involucradoRepository = dataSource.getRepository(IncidenteInvolucrado); - const accionRepository = dataSource.getRepository(IncidenteAccion); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const incidenteService = new IncidenteService( - incidenteRepository, - involucradoRepository, - accionRepository - ); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /incidentes - * Listar incidentes con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: IncidenteFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - tipo: req.query.tipo as any, - gravedad: req.query.gravedad as any, - estado: req.query.estado as any, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await incidenteService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /incidentes/stats - * Obtener estad铆sticas de incidentes - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - const stats = await incidenteService.getStats(getContext(req), fraccionamientoId); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /incidentes/:id - * Obtener incidente con detalles completos - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const incidente = await incidenteService.findWithDetails(getContext(req), req.params.id); - if (!incidente) { - res.status(404).json({ error: 'Not Found', message: 'Incident not found' }); - return; - } - - res.status(200).json({ success: true, data: incidente }); - } catch (error) { - next(error); - } - }); - - /** - * POST /incidentes - * Crear incidente - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateIncidenteDto = req.body; - - if (!dto.fechaHora || !dto.fraccionamientoId || !dto.tipo || !dto.gravedad || !dto.descripcion) { - res.status(400).json({ - error: 'Bad Request', - message: 'fechaHora, fraccionamientoId, tipo, gravedad and descripcion are required', - }); - return; - } - - dto.fechaHora = new Date(dto.fechaHora); - const incidente = await incidenteService.create(getContext(req), dto); - res.status(201).json({ success: true, data: incidente }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /incidentes/:id - * Actualizar incidente - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateIncidenteDto = req.body; - const incidente = await incidenteService.update(getContext(req), req.params.id, dto); - - if (!incidente) { - res.status(404).json({ error: 'Not Found', message: 'Incident not found' }); - return; - } - - res.status(200).json({ success: true, data: incidente }); - } catch (error) { - if (error instanceof Error && error.message.includes('closed')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /incidentes/:id/investigate - * Iniciar investigaci贸n del incidente - */ - router.post('/:id/investigate', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const incidente = await incidenteService.startInvestigation(getContext(req), req.params.id); - if (!incidente) { - res.status(404).json({ error: 'Not Found', message: 'Incident not found' }); - return; - } - - res.status(200).json({ success: true, data: incidente, message: 'Investigation started' }); - } catch (error) { - if (error instanceof Error && error.message.includes('only start')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /incidentes/:id/close - * Cerrar incidente - */ - router.post('/:id/close', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const incidente = await incidenteService.closeIncident(getContext(req), req.params.id); - if (!incidente) { - res.status(404).json({ error: 'Not Found', message: 'Incident not found' }); - return; - } - - res.status(200).json({ success: true, data: incidente, message: 'Incident closed' }); - } catch (error) { - if (error instanceof Error && (error.message.includes('already closed') || error.message.includes('pending actions'))) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /incidentes/:id/involucrados - * Agregar involucrado al incidente - */ - router.post('/:id/involucrados', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: AddInvolucradoDto = req.body; - - if (!dto.employeeId || !dto.rol) { - res.status(400).json({ error: 'Bad Request', message: 'employeeId and rol are required' }); - return; - } - - const involucrado = await incidenteService.addInvolucrado(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: involucrado }); - } catch (error) { - if (error instanceof Error && error.message === 'Incidente not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * DELETE /incidentes/:id/involucrados/:involucradoId - * Remover involucrado del incidente - */ - router.delete('/:id/involucrados/:involucradoId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const removed = await incidenteService.removeInvolucrado( - getContext(req), - req.params.id, - req.params.involucradoId - ); - - if (!removed) { - res.status(404).json({ error: 'Not Found', message: 'Involved person not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Involved person removed' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /incidentes/:id/acciones - * Agregar acci贸n correctiva al incidente - */ - router.post('/:id/acciones', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: AddAccionDto = req.body; - - if (!dto.descripcion || !dto.tipo || !dto.fechaCompromiso) { - res.status(400).json({ - error: 'Bad Request', - message: 'descripcion, tipo and fechaCompromiso are required', - }); - return; - } - - dto.fechaCompromiso = new Date(dto.fechaCompromiso); - const accion = await incidenteService.addAccion(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: accion }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Incidente not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('closed')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * PATCH /incidentes/:id/acciones/:accionId - * Actualizar acci贸n correctiva - */ - router.patch('/:id/acciones/:accionId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateAccionDto = req.body; - if (dto.fechaCompromiso) { - dto.fechaCompromiso = new Date(dto.fechaCompromiso); - } - - const accion = await incidenteService.updateAccion( - getContext(req), - req.params.id, - req.params.accionId, - dto - ); - - if (!accion) { - res.status(404).json({ error: 'Not Found', message: 'Action not found' }); - return; - } - - res.status(200).json({ success: true, data: accion }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createIncidenteController; diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/index.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/index.ts deleted file mode 100644 index ba9818ddc..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * HSE Controllers Index - * @module HSE - */ - -// RF-MAA017-001: Gesti贸n de Incidentes -export { createIncidenteController } from './incidente.controller'; - -// RF-MAA017-002: Control de Capacitaciones -export { createCapacitacionController } from './capacitacion.controller'; - -// RF-MAA017-003: Inspecciones de Seguridad -export { createInspeccionController } from './inspeccion.controller'; - -// RF-MAA017-004: Control de EPP -export { createEppController } from './epp.controller'; - -// RF-MAA017-005: Cumplimiento STPS -export { createStpsController } from './stps.controller'; - -// RF-MAA017-006: Gesti贸n Ambiental -export { createAmbientalController } from './ambiental.controller'; - -// RF-MAA017-007: Permisos de Trabajo -export { createPermisoTrabajoController } from './permiso-trabajo.controller'; - -// RF-MAA017-008: Indicadores HSE -export { createIndicadorController } from './indicador.controller'; diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/indicador.controller.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/indicador.controller.ts deleted file mode 100644 index 05c14ead0..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/indicador.controller.ts +++ /dev/null @@ -1,354 +0,0 @@ -/** - * IndicadorController - Controller de indicadores HSE - * - * Endpoints REST para gesti贸n de indicadores y reportes HSE. - * - * @module HSE - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - IndicadorService, - CreateIndicadorDto, - IndicadorFilters, -} from '../services/indicador.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { IndicadorConfig } from '../entities/indicador-config.entity'; -import { IndicadorMetaObra } from '../entities/indicador-meta-obra.entity'; -import { IndicadorValor } from '../entities/indicador-valor.entity'; -import { HorasTrabajadas } from '../entities/horas-trabajadas.entity'; -import { DiasSinAccidente } from '../entities/dias-sin-accidente.entity'; -import { ReporteProgramado } from '../entities/reporte-programado.entity'; -import { AlertaIndicador } from '../entities/alerta-indicador.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de indicadores - */ -export function createIndicadorController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const indicadorRepository = dataSource.getRepository(IndicadorConfig); - const metaRepository = dataSource.getRepository(IndicadorMetaObra); - const valorRepository = dataSource.getRepository(IndicadorValor); - const horasRepository = dataSource.getRepository(HorasTrabajadas); - const diasRepository = dataSource.getRepository(DiasSinAccidente); - const reporteRepository = dataSource.getRepository(ReporteProgramado); - const alertaRepository = dataSource.getRepository(AlertaIndicador); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const indicadorService = new IndicadorService( - indicadorRepository, - metaRepository, - valorRepository, - horasRepository, - diasRepository, - reporteRepository, - alertaRepository - ); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - // ========== Configuraci贸n de Indicadores ========== - - /** - * GET /indicadores - * Listar indicadores con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: IndicadorFilters = { - tipo: req.query.tipo as any, - activo: req.query.activo === 'true' ? true : req.query.activo === 'false' ? false : undefined, - search: req.query.search as string, - }; - - const result = await indicadorService.findIndicadores(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /indicadores/stats - * Obtener estad铆sticas de indicadores - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await indicadorService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /indicadores/:id - * Obtener indicador por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const indicador = await indicadorService.findIndicadorById(getContext(req), req.params.id); - if (!indicador) { - res.status(404).json({ error: 'Not Found', message: 'Indicator not found' }); - return; - } - res.status(200).json({ success: true, data: indicador }); - } catch (error) { - next(error); - } - }); - - /** - * POST /indicadores - * Crear indicador - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateIndicadorDto = req.body; - - if (!dto.codigo || !dto.nombre || !dto.tipo) { - res.status(400).json({ - error: 'Bad Request', - message: 'codigo, nombre and tipo are required', - }); - return; - } - - const indicador = await indicadorService.createIndicador(getContext(req), dto); - res.status(201).json({ success: true, data: indicador }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /indicadores/:id - * Actualizar indicador - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: Partial = req.body; - const indicador = await indicadorService.updateIndicador(getContext(req), req.params.id, dto); - - if (!indicador) { - res.status(404).json({ error: 'Not Found', message: 'Indicator not found' }); - return; - } - - res.status(200).json({ success: true, data: indicador }); - } catch (error) { - next(error); - } - }); - - /** - * POST /indicadores/:id/toggle - * Activar/desactivar indicador - */ - router.post('/:id/toggle', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const indicador = await indicadorService.toggleIndicadorActivo(getContext(req), req.params.id); - - if (!indicador) { - res.status(404).json({ error: 'Not Found', message: 'Indicator not found' }); - return; - } - - res.status(200).json({ - success: true, - data: indicador, - message: indicador.activo ? 'Indicator activated' : 'Indicator deactivated', - }); - } catch (error) { - next(error); - } - }); - - // ========== Metas por Obra ========== - - /** - * GET /indicadores/metas - * Listar metas por obra - */ - router.get('/metas/list', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const fraccionamientoId = req.query.fraccionamientoId as string; - const indicadorId = req.query.indicadorId as string; - const metas = await indicadorService.findMetas(getContext(req), fraccionamientoId, indicadorId); - res.status(200).json({ success: true, data: metas }); - } catch (error) { - next(error); - } - }); - - // ========== Valores de Indicadores ========== - - /** - * GET /indicadores/:id/valores - * Listar valores de un indicador - */ - router.get('/:id/valores', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 50, 200); - const fraccionamientoId = req.query.fraccionamientoId as string; - - const result = await indicadorService.findValores( - getContext(req), - req.params.id, - fraccionamientoId, - page, - limit - ); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - // ========== Horas Trabajadas ========== - - /** - * GET /indicadores/horas-trabajadas/:fraccionamientoId - * Obtener horas trabajadas por obra - */ - router.get('/horas-trabajadas/:fraccionamientoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const year = req.query.year ? parseInt(req.query.year as string) : undefined; - const horas = await indicadorService.findHorasTrabajadas( - getContext(req), - req.params.fraccionamientoId, - year - ); - res.status(200).json({ success: true, data: horas }); - } catch (error) { - next(error); - } - }); - - // ========== D铆as Sin Accidente ========== - - /** - * GET /indicadores/dias-sin-accidente/:fraccionamientoId - * Obtener d铆as sin accidente por obra - */ - router.get('/dias-sin-accidente/:fraccionamientoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dias = await indicadorService.findDiasSinAccidente( - getContext(req), - req.params.fraccionamientoId - ); - res.status(200).json({ success: true, data: dias }); - } catch (error) { - next(error); - } - }); - - // ========== Reportes Programados ========== - - /** - * GET /indicadores/reportes - * Listar reportes programados - */ - router.get('/reportes/list', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const reportes = await indicadorService.findReportes(getContext(req)); - res.status(200).json({ success: true, data: reportes }); - } catch (error) { - next(error); - } - }); - - // ========== Alertas ========== - - /** - * GET /indicadores/alertas - * Listar alertas de indicadores - */ - router.get('/alertas/list', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - const fraccionamientoId = req.query.fraccionamientoId as string; - - const result = await indicadorService.findAlertas(getContext(req), fraccionamientoId, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createIndicadorController; diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/inspeccion.controller.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/inspeccion.controller.ts deleted file mode 100644 index 243e271a4..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/inspeccion.controller.ts +++ /dev/null @@ -1,400 +0,0 @@ -/** - * InspeccionController - Controller de inspecciones HSE - * - * Endpoints REST para gesti贸n de inspecciones de seguridad y hallazgos. - * - * @module HSE - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - InspeccionService, - CreateInspeccionDto, - CreateHallazgoDto, - InspeccionFilters, - HallazgoFilters, -} from '../services/inspeccion.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Inspeccion } from '../entities/inspeccion.entity'; -import { InspeccionEvaluacion } from '../entities/inspeccion-evaluacion.entity'; -import { Hallazgo } from '../entities/hallazgo.entity'; -import { HallazgoEvidencia } from '../entities/hallazgo-evidencia.entity'; -import { TipoInspeccion } from '../entities/tipo-inspeccion.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de inspecciones - */ -export function createInspeccionController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const inspeccionRepository = dataSource.getRepository(Inspeccion); - const evaluacionRepository = dataSource.getRepository(InspeccionEvaluacion); - const hallazgoRepository = dataSource.getRepository(Hallazgo); - const evidenciaRepository = dataSource.getRepository(HallazgoEvidencia); - const tipoInspeccionRepository = dataSource.getRepository(TipoInspeccion); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const inspeccionService = new InspeccionService( - inspeccionRepository, - evaluacionRepository, - hallazgoRepository, - evidenciaRepository, - tipoInspeccionRepository - ); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /inspecciones/tipos - * Listar tipos de inspecci贸n - */ - router.get('/tipos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tipos = await inspeccionService.findTiposInspeccion(getContext(req)); - res.status(200).json({ success: true, data: tipos }); - } catch (error) { - next(error); - } - }); - - /** - * GET /inspecciones - * Listar inspecciones con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: InspeccionFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - tipoInspeccionId: req.query.tipoInspeccionId as string, - estado: req.query.estado as string, - inspectorId: req.query.inspectorId as string, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await inspeccionService.findInspecciones(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /inspecciones/stats - * Obtener estad铆sticas de inspecciones - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - const stats = await inspeccionService.getStats(getContext(req), fraccionamientoId); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /inspecciones/:id - * Obtener inspecci贸n con detalles - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const inspeccion = await inspeccionService.findInspeccionWithDetails(getContext(req), req.params.id); - if (!inspeccion) { - res.status(404).json({ error: 'Not Found', message: 'Inspection not found' }); - return; - } - - res.status(200).json({ success: true, data: inspeccion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /inspecciones - * Crear inspecci贸n - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateInspeccionDto = req.body; - - if (!dto.tipoInspeccionId || !dto.fraccionamientoId || !dto.inspectorId) { - res.status(400).json({ - error: 'Bad Request', - message: 'tipoInspeccionId, fraccionamientoId and inspectorId are required', - }); - return; - } - - const inspeccion = await inspeccionService.createInspeccion(getContext(req), dto); - res.status(201).json({ success: true, data: inspeccion }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /inspecciones/:id/estado - * Actualizar estado de inspecci贸n - */ - router.patch('/:id/estado', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { estado } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const inspeccion = await inspeccionService.updateInspeccionEstado(getContext(req), req.params.id, estado); - if (!inspeccion) { - res.status(404).json({ error: 'Not Found', message: 'Inspection not found' }); - return; - } - - res.status(200).json({ success: true, data: inspeccion }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /inspecciones/:id/observaciones - * Actualizar observaciones de inspecci贸n - */ - router.patch('/:id/observaciones', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { observaciones } = req.body; - if (!observaciones) { - res.status(400).json({ error: 'Bad Request', message: 'observaciones is required' }); - return; - } - - const inspeccion = await inspeccionService.updateInspeccionObservaciones(getContext(req), req.params.id, observaciones); - if (!inspeccion) { - res.status(404).json({ error: 'Not Found', message: 'Inspection not found' }); - return; - } - - res.status(200).json({ success: true, data: inspeccion }); - } catch (error) { - next(error); - } - }); - - // ========== Hallazgos ========== - - /** - * GET /inspecciones/hallazgos - * Listar hallazgos con filtros - */ - router.get('/hallazgos/list', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: HallazgoFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - gravedad: req.query.gravedad as any, - estado: req.query.estado as any, - responsableId: req.query.responsableId as string, - vencidos: req.query.vencidos === 'true', - }; - - const result = await inspeccionService.findHallazgos(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /inspecciones/:id/hallazgos - * Agregar hallazgo a inspecci贸n - */ - router.post('/:id/hallazgos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateHallazgoDto = req.body; - - if (!dto.gravedad || !dto.tipo || !dto.descripcion || !dto.fechaLimite) { - res.status(400).json({ - error: 'Bad Request', - message: 'gravedad, tipo, descripcion and fechaLimite are required', - }); - return; - } - - dto.fechaLimite = new Date(dto.fechaLimite); - const hallazgo = await inspeccionService.createHallazgo(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: hallazgo }); - } catch (error) { - if (error instanceof Error && error.message === 'Inspecci贸n no encontrada') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /inspecciones/hallazgos/:hallazgoId/correccion - * Registrar correcci贸n de hallazgo - */ - router.post('/hallazgos/:hallazgoId/correccion', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { descripcionCorreccion } = req.body; - if (!descripcionCorreccion) { - res.status(400).json({ error: 'Bad Request', message: 'descripcionCorreccion is required' }); - return; - } - - const hallazgo = await inspeccionService.registrarCorreccion( - getContext(req), - req.params.hallazgoId, - descripcionCorreccion - ); - - if (!hallazgo) { - res.status(404).json({ error: 'Not Found', message: 'Finding not found' }); - return; - } - - res.status(200).json({ success: true, data: hallazgo, message: 'Correction registered' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /inspecciones/hallazgos/:hallazgoId/verificar - * Verificar correcci贸n de hallazgo - */ - router.post('/hallazgos/:hallazgoId/verificar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { aprobado } = req.body; - if (aprobado === undefined) { - res.status(400).json({ error: 'Bad Request', message: 'aprobado is required' }); - return; - } - - const ctx = getContext(req); - const hallazgo = await inspeccionService.verificarHallazgo( - ctx, - req.params.hallazgoId, - ctx.userId || '', - aprobado - ); - - if (!hallazgo) { - res.status(404).json({ error: 'Not Found', message: 'Finding not found' }); - return; - } - - res.status(200).json({ - success: true, - data: hallazgo, - message: aprobado ? 'Finding closed' : 'Finding reopened', - }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createInspeccionController; diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/permiso-trabajo.controller.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/permiso-trabajo.controller.ts deleted file mode 100644 index 384e3c6fc..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/permiso-trabajo.controller.ts +++ /dev/null @@ -1,323 +0,0 @@ -/** - * PermisoTrabajoController - Controller de permisos de trabajo HSE - * - * Endpoints REST para gesti贸n de permisos de trabajo de alto riesgo. - * - * @module HSE - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - PermisoTrabajoService, - CreatePermisoDto, - PermisoFilters, -} from '../services/permiso-trabajo.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { TipoPermisoTrabajo } from '../entities/tipo-permiso-trabajo.entity'; -import { PermisoTrabajo, EstadoPermiso } from '../entities/permiso-trabajo.entity'; -import { PermisoPersonal } from '../entities/permiso-personal.entity'; -import { PermisoAutorizacion } from '../entities/permiso-autorizacion.entity'; -import { PermisoChecklist } from '../entities/permiso-checklist.entity'; -import { PermisoMonitoreo } from '../entities/permiso-monitoreo.entity'; -import { PermisoEvento } from '../entities/permiso-evento.entity'; -import { PermisoDocumento } from '../entities/permiso-documento.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de permisos de trabajo - */ -export function createPermisoTrabajoController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const tipoPermisoRepository = dataSource.getRepository(TipoPermisoTrabajo); - const permisoRepository = dataSource.getRepository(PermisoTrabajo); - const personalRepository = dataSource.getRepository(PermisoPersonal); - const autorizacionRepository = dataSource.getRepository(PermisoAutorizacion); - const checklistRepository = dataSource.getRepository(PermisoChecklist); - const monitoreoRepository = dataSource.getRepository(PermisoMonitoreo); - const eventoRepository = dataSource.getRepository(PermisoEvento); - const documentoRepository = dataSource.getRepository(PermisoDocumento); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const permisoService = new PermisoTrabajoService( - tipoPermisoRepository, - permisoRepository, - personalRepository, - autorizacionRepository, - checklistRepository, - monitoreoRepository, - eventoRepository, - documentoRepository - ); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /permisos-trabajo/tipos - * Listar tipos de permisos de trabajo - */ - router.get('/tipos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tipos = await permisoService.findTiposPermiso(getContext(req)); - res.status(200).json({ success: true, data: tipos }); - } catch (error) { - next(error); - } - }); - - /** - * GET /permisos-trabajo - * Listar permisos de trabajo con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: PermisoFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - tipoPermisoId: req.query.tipoPermisoId as string, - estado: req.query.estado as EstadoPermiso, - solicitanteId: req.query.solicitanteId as string, - vigentes: req.query.vigentes === 'true', - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await permisoService.findPermisos(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /permisos-trabajo/stats - * Obtener estad铆sticas de permisos - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - const stats = await permisoService.getStats(getContext(req), fraccionamientoId); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /permisos-trabajo/:id - * Obtener permiso con detalles - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const permiso = await permisoService.findPermisoWithDetails(getContext(req), req.params.id); - if (!permiso) { - res.status(404).json({ error: 'Not Found', message: 'Work permit not found' }); - return; - } - - res.status(200).json({ success: true, data: permiso }); - } catch (error) { - next(error); - } - }); - - /** - * POST /permisos-trabajo - * Crear permiso de trabajo - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreatePermisoDto = req.body; - - if (!dto.fraccionamientoId || !dto.tipoPermisoId || !dto.descripcionTrabajo || !dto.ubicacion || !dto.fechaInicioProgramada || !dto.fechaFinProgramada) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, tipoPermisoId, descripcionTrabajo, ubicacion, fechaInicioProgramada and fechaFinProgramada are required', - }); - return; - } - - dto.fechaInicioProgramada = new Date(dto.fechaInicioProgramada); - dto.fechaFinProgramada = new Date(dto.fechaFinProgramada); - const permiso = await permisoService.createPermiso(getContext(req), dto); - res.status(201).json({ success: true, data: permiso }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /permisos-trabajo/:id/estado - * Actualizar estado del permiso - */ - router.patch('/:id/estado', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { estado, motivo } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const permiso = await permisoService.updatePermisoEstado(getContext(req), req.params.id, estado, motivo); - if (!permiso) { - res.status(404).json({ error: 'Not Found', message: 'Work permit not found' }); - return; - } - - res.status(200).json({ success: true, data: permiso }); - } catch (error) { - next(error); - } - }); - - // ========== Personal Autorizado ========== - - /** - * GET /permisos-trabajo/:id/personal - * Obtener personal autorizado del permiso - */ - router.get('/:id/personal', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const personal = await permisoService.getPersonalByPermiso(getContext(req), req.params.id); - res.status(200).json({ success: true, data: personal }); - } catch (error) { - next(error); - } - }); - - // ========== Autorizaciones ========== - - /** - * GET /permisos-trabajo/:id/autorizaciones - * Obtener autorizaciones del permiso - */ - router.get('/:id/autorizaciones', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const autorizaciones = await permisoService.getAutorizacionesByPermiso(getContext(req), req.params.id); - res.status(200).json({ success: true, data: autorizaciones }); - } catch (error) { - next(error); - } - }); - - // ========== Checklist ========== - - /** - * GET /permisos-trabajo/:id/checklist - * Obtener checklist del permiso - */ - router.get('/:id/checklist', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const checklist = await permisoService.getChecklistByPermiso(getContext(req), req.params.id); - res.status(200).json({ success: true, data: checklist }); - } catch (error) { - next(error); - } - }); - - // ========== Monitoreo ========== - - /** - * GET /permisos-trabajo/:id/monitoreos - * Obtener monitoreos del permiso - */ - router.get('/:id/monitoreos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const monitoreos = await permisoService.getMonitoreosByPermiso(getContext(req), req.params.id); - res.status(200).json({ success: true, data: monitoreos }); - } catch (error) { - next(error); - } - }); - - // ========== Eventos ========== - - /** - * GET /permisos-trabajo/:id/eventos - * Obtener eventos del permiso - */ - router.get('/:id/eventos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const eventos = await permisoService.getEventosByPermiso(getContext(req), req.params.id); - res.status(200).json({ success: true, data: eventos }); - } catch (error) { - next(error); - } - }); - - // ========== Documentos ========== - - /** - * GET /permisos-trabajo/:id/documentos - * Obtener documentos del permiso - */ - router.get('/:id/documentos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const documentos = await permisoService.getDocumentosByPermiso(getContext(req), req.params.id); - res.status(200).json({ success: true, data: documentos }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createPermisoTrabajoController; diff --git a/projects/erp-construccion/backend/src/modules/hse/controllers/stps.controller.ts b/projects/erp-construccion/backend/src/modules/hse/controllers/stps.controller.ts deleted file mode 100644 index 499030529..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/controllers/stps.controller.ts +++ /dev/null @@ -1,794 +0,0 @@ -/** - * StpsController - Controller de cumplimiento STPS - * - * Endpoints REST para gesti贸n de normas, comisiones y auditor铆as STPS. - * - * @module HSE - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - StpsService, - CreateCumplimientoDto, - CreateComisionDto, - AddIntegranteDto, - CreateRecorridoDto, - CreateProgramaDto, - CreateActividadDto, - CreateAuditoriaDto, - CreateDocumentoDto, - CumplimientoFilters, - ComisionFilters, - AuditoriaFilters, -} from '../services/stps.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { NormaStps } from '../entities/norma-stps.entity'; -import { NormaRequisito } from '../entities/norma-requisito.entity'; -import { CumplimientoObra, EstadoCumplimiento } from '../entities/cumplimiento-obra.entity'; -import { ComisionSeguridad, EstadoComision } from '../entities/comision-seguridad.entity'; -import { ComisionIntegrante } from '../entities/comision-integrante.entity'; -import { ComisionRecorrido } from '../entities/comision-recorrido.entity'; -import { ProgramaSeguridad } from '../entities/programa-seguridad.entity'; -import { ProgramaActividad, EstadoActividad } from '../entities/programa-actividad.entity'; -import { DocumentoStps } from '../entities/documento-stps.entity'; -import { Auditoria, ResultadoAuditoria } from '../entities/auditoria.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de STPS - */ -export function createStpsController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const normaRepository = dataSource.getRepository(NormaStps); - const requisitoRepository = dataSource.getRepository(NormaRequisito); - const cumplimientoRepository = dataSource.getRepository(CumplimientoObra); - const comisionRepository = dataSource.getRepository(ComisionSeguridad); - const integranteRepository = dataSource.getRepository(ComisionIntegrante); - const recorridoRepository = dataSource.getRepository(ComisionRecorrido); - const programaRepository = dataSource.getRepository(ProgramaSeguridad); - const actividadRepository = dataSource.getRepository(ProgramaActividad); - const documentoRepository = dataSource.getRepository(DocumentoStps); - const auditoriaRepository = dataSource.getRepository(Auditoria); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const stpsService = new StpsService( - normaRepository, - requisitoRepository, - cumplimientoRepository, - comisionRepository, - integranteRepository, - recorridoRepository, - programaRepository, - actividadRepository, - documentoRepository, - auditoriaRepository - ); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - // ========== Normas y Requisitos ========== - - /** - * GET /stps/normas - * Listar normas STPS - */ - router.get('/normas', authMiddleware.authenticate, async (_req: Request, res: Response, next: NextFunction): Promise => { - try { - const normas = await stpsService.findNormas(); - res.status(200).json({ success: true, data: normas }); - } catch (error) { - next(error); - } - }); - - /** - * GET /stps/normas/:id - * Obtener norma con requisitos - */ - router.get('/normas/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const norma = await stpsService.findNormaById(req.params.id); - if (!norma) { - res.status(404).json({ error: 'Not Found', message: 'Norm not found' }); - return; - } - res.status(200).json({ success: true, data: norma }); - } catch (error) { - next(error); - } - }); - - /** - * GET /stps/normas/:id/requisitos - * Listar requisitos de una norma - */ - router.get('/normas/:id/requisitos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const requisitos = await stpsService.findRequisitosByNorma(req.params.id); - res.status(200).json({ success: true, data: requisitos }); - } catch (error) { - next(error); - } - }); - - // ========== Cumplimiento por Obra ========== - - /** - * GET /stps/cumplimiento - * Listar estado de cumplimiento - */ - router.get('/cumplimiento', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: CumplimientoFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - normaId: req.query.normaId as string, - estado: req.query.estado as EstadoCumplimiento, - }; - - const result = await stpsService.findCumplimientos(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/cumplimiento - * Registrar evaluaci贸n de cumplimiento - */ - router.post('/cumplimiento', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateCumplimientoDto = req.body; - - if (!dto.fraccionamientoId || !dto.normaId || !dto.fechaEvaluacion) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, normaId and fechaEvaluacion are required', - }); - return; - } - - dto.fechaEvaluacion = new Date(dto.fechaEvaluacion); - if (dto.fechaCompromiso) dto.fechaCompromiso = new Date(dto.fechaCompromiso); - const cumplimiento = await stpsService.createCumplimiento(getContext(req), dto); - res.status(201).json({ success: true, data: cumplimiento }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /stps/cumplimiento/:id - * Actualizar estado de cumplimiento - */ - router.patch('/cumplimiento/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { estado, evidenciaUrl, observaciones } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const cumplimiento = await stpsService.updateCumplimientoEstado( - getContext(req), - req.params.id, - estado, - evidenciaUrl, - observaciones - ); - if (!cumplimiento) { - res.status(404).json({ error: 'Not Found', message: 'Compliance record not found' }); - return; - } - res.status(200).json({ success: true, data: cumplimiento }); - } catch (error) { - next(error); - } - }); - - // ========== Comisiones de Seguridad ========== - - /** - * GET /stps/comisiones - * Listar comisiones de seguridad - */ - router.get('/comisiones', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const filters: ComisionFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - estado: req.query.estado as EstadoComision, - }; - - const comisiones = await stpsService.findComisiones(getContext(req), filters); - res.status(200).json({ success: true, data: comisiones }); - } catch (error) { - next(error); - } - }); - - /** - * GET /stps/comisiones/:id - * Obtener comisi贸n con integrantes - */ - router.get('/comisiones/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const comision = await stpsService.findComisionById(getContext(req), req.params.id); - if (!comision) { - res.status(404).json({ error: 'Not Found', message: 'Commission not found' }); - return; - } - res.status(200).json({ success: true, data: comision }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/comisiones - * Crear comisi贸n de seguridad - */ - router.post('/comisiones', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateComisionDto = req.body; - - if (!dto.fraccionamientoId || !dto.fechaConstitucion || !dto.vigenciaInicio || !dto.vigenciaFin) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, fechaConstitucion, vigenciaInicio and vigenciaFin are required', - }); - return; - } - - dto.fechaConstitucion = new Date(dto.fechaConstitucion); - dto.vigenciaInicio = new Date(dto.vigenciaInicio); - dto.vigenciaFin = new Date(dto.vigenciaFin); - const comision = await stpsService.createComision(getContext(req), dto); - res.status(201).json({ success: true, data: comision }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /stps/comisiones/:id/estado - * Actualizar estado de comisi贸n - */ - router.patch('/comisiones/:id/estado', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { estado } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const comision = await stpsService.updateComisionEstado(getContext(req), req.params.id, estado); - if (!comision) { - res.status(404).json({ error: 'Not Found', message: 'Commission not found' }); - return; - } - res.status(200).json({ success: true, data: comision }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/comisiones/:id/integrantes - * Agregar integrante a comisi贸n - */ - router.post('/comisiones/:id/integrantes', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: AddIntegranteDto = req.body; - - if (!dto.employeeId || !dto.rol || !dto.representacion || !dto.fechaNombramiento) { - res.status(400).json({ - error: 'Bad Request', - message: 'employeeId, rol, representacion and fechaNombramiento are required', - }); - return; - } - - dto.fechaNombramiento = new Date(dto.fechaNombramiento); - const integrante = await stpsService.addIntegrante(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: integrante }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /stps/comisiones/integrantes/:integranteId - * Desactivar integrante de comisi贸n - */ - router.delete('/comisiones/integrantes/:integranteId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const removed = await stpsService.removeIntegrante(getContext(req), req.params.integranteId); - if (!removed) { - res.status(404).json({ error: 'Not Found', message: 'Member not found' }); - return; - } - res.status(200).json({ success: true, message: 'Member deactivated' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/comisiones/:id/recorridos - * Programar recorrido de verificaci贸n - */ - router.post('/comisiones/:id/recorridos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateRecorridoDto = { - ...req.body, - comisionId: req.params.id, - }; - - if (!dto.fechaProgramada) { - res.status(400).json({ - error: 'Bad Request', - message: 'fechaProgramada is required', - }); - return; - } - - dto.fechaProgramada = new Date(dto.fechaProgramada); - const recorrido = await stpsService.createRecorrido(getContext(req), dto); - res.status(201).json({ success: true, data: recorrido }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/recorridos/:id/completar - * Completar recorrido de verificaci贸n - */ - router.post('/recorridos/:id/completar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { hallazgos, recomendaciones, numeroActa, documentoActaUrl } = req.body; - - const recorrido = await stpsService.completarRecorrido( - getContext(req), - req.params.id, - hallazgos, - recomendaciones, - numeroActa, - documentoActaUrl - ); - if (!recorrido) { - res.status(404).json({ error: 'Not Found', message: 'Tour not found' }); - return; - } - res.status(200).json({ success: true, data: recorrido, message: 'Tour completed' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/recorridos/:id/cancelar - * Cancelar recorrido - */ - router.post('/recorridos/:id/cancelar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const recorrido = await stpsService.cancelarRecorrido(getContext(req), req.params.id); - if (!recorrido) { - res.status(404).json({ error: 'Not Found', message: 'Tour not found' }); - return; - } - res.status(200).json({ success: true, data: recorrido, message: 'Tour cancelled' }); - } catch (error) { - next(error); - } - }); - - // ========== Programas de Seguridad ========== - - /** - * GET /stps/programas - * Listar programas de seguridad - */ - router.get('/programas', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const fraccionamientoId = req.query.fraccionamientoId as string; - const anio = req.query.anio ? parseInt(req.query.anio as string) : undefined; - const programas = await stpsService.findProgramas(getContext(req), fraccionamientoId, anio); - res.status(200).json({ success: true, data: programas }); - } catch (error) { - next(error); - } - }); - - /** - * GET /stps/programas/:id - * Obtener programa con actividades - */ - router.get('/programas/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const programa = await stpsService.findProgramaById(getContext(req), req.params.id); - if (!programa) { - res.status(404).json({ error: 'Not Found', message: 'Program not found' }); - return; - } - res.status(200).json({ success: true, data: programa }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/programas - * Crear programa de seguridad - */ - router.post('/programas', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateProgramaDto = req.body; - - if (!dto.fraccionamientoId || !dto.anio) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId and anio are required', - }); - return; - } - - const programa = await stpsService.createPrograma(getContext(req), dto); - res.status(201).json({ success: true, data: programa }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/programas/:id/aprobar - * Aprobar programa de seguridad - */ - router.post('/programas/:id/aprobar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const programa = await stpsService.aprobarPrograma(getContext(req), req.params.id); - if (!programa) { - res.status(404).json({ error: 'Not Found', message: 'Program not found' }); - return; - } - res.status(200).json({ success: true, data: programa, message: 'Program approved' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/programas/:id/finalizar - * Finalizar programa de seguridad - */ - router.post('/programas/:id/finalizar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const programa = await stpsService.finalizarPrograma(getContext(req), req.params.id); - if (!programa) { - res.status(404).json({ error: 'Not Found', message: 'Program not found' }); - return; - } - res.status(200).json({ success: true, data: programa, message: 'Program finalized' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/programas/:id/actividades - * Agregar actividad al programa - */ - router.post('/programas/:id/actividades', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateActividadDto = { - ...req.body, - programaId: req.params.id, - }; - - if (!dto.actividad || !dto.tipo || !dto.fechaProgramada) { - res.status(400).json({ - error: 'Bad Request', - message: 'actividad, tipo and fechaProgramada are required', - }); - return; - } - - dto.fechaProgramada = new Date(dto.fechaProgramada); - const actividad = await stpsService.createActividad(getContext(req), dto); - res.status(201).json({ success: true, data: actividad }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /stps/actividades/:id/estado - * Actualizar estado de actividad - */ - router.patch('/actividades/:id/estado', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { estado } = req.body; - if (!estado) { - res.status(400).json({ error: 'Bad Request', message: 'estado is required' }); - return; - } - - const actividad = await stpsService.updateActividadEstado(getContext(req), req.params.id, estado as EstadoActividad); - if (!actividad) { - res.status(404).json({ error: 'Not Found', message: 'Activity not found' }); - return; - } - res.status(200).json({ success: true, data: actividad }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/actividades/:id/completar - * Completar actividad - */ - router.post('/actividades/:id/completar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { evidenciaUrl } = req.body; - - const actividad = await stpsService.completarActividad(getContext(req), req.params.id, evidenciaUrl); - if (!actividad) { - res.status(404).json({ error: 'Not Found', message: 'Activity not found' }); - return; - } - res.status(200).json({ success: true, data: actividad, message: 'Activity completed' }); - } catch (error) { - next(error); - } - }); - - // ========== Documentos STPS ========== - - /** - * GET /stps/documentos - * Listar documentos STPS - */ - router.get('/documentos', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const fraccionamientoId = req.query.fraccionamientoId as string; - const tipo = req.query.tipo as any; - const documentos = await stpsService.findDocumentos(getContext(req), fraccionamientoId, tipo); - res.status(200).json({ success: true, data: documentos }); - } catch (error) { - next(error); - } - }); - - /** - * GET /stps/documentos/:id - * Obtener documento por ID - */ - router.get('/documentos/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const documento = await stpsService.findDocumentoById(getContext(req), req.params.id); - if (!documento) { - res.status(404).json({ error: 'Not Found', message: 'Document not found' }); - return; - } - res.status(200).json({ success: true, data: documento }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/documentos - * Crear documento STPS - */ - router.post('/documentos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateDocumentoDto = req.body; - - if (!dto.tipo || !dto.folio || !dto.fechaEmision) { - res.status(400).json({ - error: 'Bad Request', - message: 'tipo, folio and fechaEmision are required', - }); - return; - } - - dto.fechaEmision = new Date(dto.fechaEmision); - if (dto.fechaVencimiento) dto.fechaVencimiento = new Date(dto.fechaVencimiento); - const documento = await stpsService.createDocumento(getContext(req), dto); - res.status(201).json({ success: true, data: documento }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/documentos/:id/firmar - * Marcar documento como firmado - */ - router.post('/documentos/:id/firmar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const documento = await stpsService.marcarDocumentoFirmado(getContext(req), req.params.id); - if (!documento) { - res.status(404).json({ error: 'Not Found', message: 'Document not found' }); - return; - } - res.status(200).json({ success: true, data: documento, message: 'Document signed' }); - } catch (error) { - next(error); - } - }); - - // ========== Auditor铆as ========== - - /** - * GET /stps/auditorias - * Listar auditor铆as - */ - router.get('/auditorias', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: AuditoriaFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - tipo: req.query.tipo as any, - resultado: req.query.resultado as ResultadoAuditoria, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await stpsService.findAuditorias(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /stps/auditorias/:id - * Obtener auditor铆a por ID - */ - router.get('/auditorias/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const auditoria = await stpsService.findAuditoriaById(getContext(req), req.params.id); - if (!auditoria) { - res.status(404).json({ error: 'Not Found', message: 'Audit not found' }); - return; - } - res.status(200).json({ success: true, data: auditoria }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/auditorias - * Programar auditor铆a - */ - router.post('/auditorias', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const dto: CreateAuditoriaDto = req.body; - - if (!dto.fraccionamientoId || !dto.tipo || !dto.fechaProgramada) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, tipo and fechaProgramada are required', - }); - return; - } - - dto.fechaProgramada = new Date(dto.fechaProgramada); - const auditoria = await stpsService.createAuditoria(getContext(req), dto); - res.status(201).json({ success: true, data: auditoria }); - } catch (error) { - next(error); - } - }); - - /** - * POST /stps/auditorias/:id/completar - * Completar auditor铆a - */ - router.post('/auditorias/:id/completar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { resultado, noConformidades, observaciones, informeUrl } = req.body; - - if (!resultado || noConformidades === undefined) { - res.status(400).json({ - error: 'Bad Request', - message: 'resultado and noConformidades are required', - }); - return; - } - - const auditoria = await stpsService.completarAuditoria( - getContext(req), - req.params.id, - resultado, - noConformidades, - observaciones, - informeUrl - ); - if (!auditoria) { - res.status(404).json({ error: 'Not Found', message: 'Audit not found' }); - return; - } - res.status(200).json({ success: true, data: auditoria, message: 'Audit completed' }); - } catch (error) { - next(error); - } - }); - - // ========== Estad铆sticas ========== - - /** - * GET /stps/stats - * Obtener estad铆sticas STPS - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - const stats = await stpsService.getStats(getContext(req), fraccionamientoId); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createStpsController; diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/alerta-indicador.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/alerta-indicador.entity.ts deleted file mode 100644 index b6664dd94..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/alerta-indicador.entity.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * AlertaIndicador Entity - * Alertas generadas por indicadores HSE - * - * @module HSE - * @table hse.alertas_indicadores - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-008 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { IndicadorConfig } from './indicador-config.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -export type TipoAlertaIndicador = 'meta_superada' | 'tendencia_negativa' | 'sin_datos'; - -@Entity({ schema: 'hse', name: 'alertas_indicadores' }) -export class AlertaIndicador { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'indicador_id', type: 'uuid' }) - indicadorId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid', nullable: true }) - fraccionamientoId: string; - - @Column({ - name: 'tipo_alerta', - type: 'enum', - enum: ['meta_superada', 'tendencia_negativa', 'sin_datos'], - }) - tipoAlerta: TipoAlertaIndicador; - - @Column({ type: 'text' }) - mensaje: string; - - @Column({ name: 'valor_actual', type: 'decimal', precision: 10, scale: 4, nullable: true }) - valorActual: number; - - @Column({ name: 'valor_meta', type: 'decimal', precision: 10, scale: 4, nullable: true }) - valorMeta: number; - - @Column({ type: 'boolean', default: false }) - leida: boolean; - - @Column({ name: 'fecha_alerta', type: 'timestamptz', default: () => 'NOW()' }) - fechaAlerta: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => IndicadorConfig) - @JoinColumn({ name: 'indicador_id' }) - indicador: IndicadorConfig; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/almacen-temporal.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/almacen-temporal.entity.ts deleted file mode 100644 index 1694b8572..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/almacen-temporal.entity.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * AlmacenTemporal Entity - * Almacenes temporales de residuos - * - * @module HSE - * @table hse.almacen_temporal - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-006 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -export type EstadoAlmacen = 'operativo' | 'lleno' | 'mantenimiento'; - -@Entity({ schema: 'hse', name: 'almacen_temporal' }) -export class AlmacenTemporal { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ type: 'varchar', length: 100 }) - nombre: string; - - @Column({ type: 'varchar', length: 200, nullable: true }) - ubicacion: string; - - @Column({ name: 'capacidad_m3', type: 'decimal', precision: 8, scale: 2, nullable: true }) - capacidadM3: number; - - @Column({ name: 'tiene_contencion', type: 'boolean', default: true }) - tieneContencion: boolean; - - @Column({ name: 'tiene_techo', type: 'boolean', default: true }) - tieneTecho: boolean; - - @Column({ name: 'senalizacion_ok', type: 'boolean', default: true }) - senalizacionOk: boolean; - - @Column({ - type: 'enum', - enum: ['operativo', 'lleno', 'mantenimiento'], - default: 'operativo', - }) - estado: EstadoAlmacen; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/auditoria.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/auditoria.entity.ts deleted file mode 100644 index ca2082a27..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/auditoria.entity.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Auditoria Entity - * Auditor铆as de seguridad - * - * @module HSE - * @table hse.auditorias - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -export type TipoAuditoria = 'interna' | 'simulada' | 'stps' | 'cliente' | 'certificadora'; -export type ResultadoAuditoria = 'aprobada' | 'aprobada_observaciones' | 'no_aprobada'; - -@Entity({ schema: 'hse', name: 'auditorias' }) -export class Auditoria { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ - type: 'enum', - enum: ['interna', 'simulada', 'stps', 'cliente', 'certificadora'], - }) - tipo: TipoAuditoria; - - @Column({ name: 'fecha_programada', type: 'date' }) - fechaProgramada: Date; - - @Column({ name: 'fecha_realizada', type: 'date', nullable: true }) - fechaRealizada: Date; - - @Column({ type: 'varchar', length: 200, nullable: true }) - auditor: string; - - @Column({ - type: 'enum', - enum: ['aprobada', 'aprobada_observaciones', 'no_aprobada'], - nullable: true, - }) - resultado: ResultadoAuditoria; - - @Column({ name: 'no_conformidades', type: 'integer', default: 0 }) - noConformidades: number; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @Column({ name: 'informe_url', type: 'varchar', length: 500, nullable: true }) - informeUrl: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-asistente.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-asistente.entity.ts deleted file mode 100644 index 846d60730..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-asistente.entity.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * CapacitacionAsistente Entity - * Asistencia a sesiones de capacitaci贸n - * - * @module HSE - * @table hse.capacitacion_asistentes - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-002 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { CapacitacionSesion } from './capacitacion-sesion.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -@Entity({ schema: 'hse', name: 'capacitacion_asistentes' }) -export class CapacitacionAsistente { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'sesion_id', type: 'uuid' }) - sesionId: string; - - @Column({ name: 'employee_id', type: 'uuid' }) - employeeId: string; - - @Column({ type: 'boolean', default: false }) - asistio: boolean; - - @Column({ name: 'hora_entrada', type: 'time', nullable: true }) - horaEntrada: string; - - @Column({ name: 'hora_salida', type: 'time', nullable: true }) - horaSalida: string; - - @Column({ type: 'integer', nullable: true }) - calificacion: number; - - @Column({ type: 'boolean', nullable: true }) - aprobado: boolean; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => CapacitacionSesion, (s) => s.asistentes, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'sesion_id' }) - sesion: CapacitacionSesion; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-matriz.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-matriz.entity.ts deleted file mode 100644 index 45cb358c0..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-matriz.entity.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * CapacitacionMatriz Entity - * Matriz de capacitaci贸n por puesto - * - * @module HSE - * @table hse.capacitacion_matriz - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-002 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Capacitacion } from './capacitacion.entity'; - -@Entity({ schema: 'hse', name: 'capacitacion_matriz' }) -export class CapacitacionMatriz { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'puesto_id', type: 'uuid' }) - puestoId: string; - - @Column({ name: 'capacitacion_id', type: 'uuid' }) - capacitacionId: string; - - @Column({ name: 'es_obligatoria', type: 'boolean', default: true }) - esObligatoria: boolean; - - @Column({ name: 'plazo_dias', type: 'integer', default: 30 }) - plazoDias: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Capacitacion) - @JoinColumn({ name: 'capacitacion_id' }) - capacitacion: Capacitacion; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-sesion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-sesion.entity.ts deleted file mode 100644 index 3e51b387d..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion-sesion.entity.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * CapacitacionSesion Entity - * Sesiones de capacitaci贸n programadas - * - * @module HSE - * @table hse.capacitacion_sesiones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-002 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Capacitacion } from './capacitacion.entity'; -import { Instructor } from './instructor.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { CapacitacionAsistente } from './capacitacion-asistente.entity'; - -export type EstadoSesion = 'programada' | 'en_curso' | 'completada' | 'cancelada'; - -@Entity({ schema: 'hse', name: 'capacitacion_sesiones' }) -export class CapacitacionSesion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'capacitacion_id', type: 'uuid' }) - capacitacionId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid', nullable: true }) - fraccionamientoId: string; - - @Column({ name: 'instructor_id', type: 'uuid', nullable: true }) - instructorId: string; - - @Column({ name: 'fecha_programada', type: 'date' }) - fechaProgramada: Date; - - @Column({ name: 'hora_inicio', type: 'time' }) - horaInicio: string; - - @Column({ name: 'hora_fin', type: 'time' }) - horaFin: string; - - @Column({ type: 'varchar', length: 200, nullable: true }) - lugar: string; - - @Column({ name: 'cupo_maximo', type: 'integer', nullable: true }) - cupoMaximo: number; - - @Column({ - type: 'enum', - enum: ['programada', 'en_curso', 'completada', 'cancelada'], - default: 'programada', - }) - estado: EstadoSesion; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Capacitacion) - @JoinColumn({ name: 'capacitacion_id' }) - capacitacion: Capacitacion; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => Instructor) - @JoinColumn({ name: 'instructor_id' }) - instructor: Instructor; - - @OneToMany(() => CapacitacionAsistente, (a) => a.sesion) - asistentes: CapacitacionAsistente[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion.entity.ts deleted file mode 100644 index ecbd699f9..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/capacitacion.entity.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Capacitacion Entity - * Cat谩logo de capacitaciones HSE - * - * @module HSE - * @table hse.capacitaciones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-002 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; - -export type TipoCapacitacion = 'induccion' | 'especifica' | 'certificacion' | 'reentrenamiento'; - -@Entity({ schema: 'hse', name: 'capacitaciones' }) -@Index(['tenantId', 'codigo'], { unique: true }) -export class Capacitacion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ - type: 'enum', - enum: ['induccion', 'especifica', 'certificacion', 'reentrenamiento'] - }) - tipo: TipoCapacitacion; - - @Column({ name: 'duracion_horas', type: 'integer', default: 1 }) - duracionHoras: number; - - @Column({ name: 'vigencia_meses', type: 'integer', nullable: true }) - vigenciaMeses: number; - - @Column({ name: 'requiere_evaluacion', type: 'boolean', default: false }) - requiereEvaluacion: boolean; - - @Column({ name: 'calificacion_minima', type: 'integer', nullable: true }) - calificacionMinima: number; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/checklist-item.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/checklist-item.entity.ts deleted file mode 100644 index abe726129..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/checklist-item.entity.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * ChecklistItem Entity - * Items de checklist de inspecci贸n - * - * @module HSE - * @table hse.checklist_items - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-003 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { TipoInspeccion } from './tipo-inspeccion.entity'; - -@Entity({ schema: 'hse', name: 'checklist_items' }) -export class ChecklistItem { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tipo_inspeccion_id', type: 'uuid' }) - tipoInspeccionId: string; - - @Column({ name: 'numero_orden', type: 'integer' }) - numeroOrden: number; - - @Column({ type: 'varchar', length: 100, nullable: true }) - categoria: string; - - @Column({ type: 'text' }) - descripcion: string; - - @Column({ name: 'criterio_cumplimiento', type: 'text', nullable: true }) - criterioCumplimiento: string; - - @Column({ name: 'es_critico', type: 'boolean', default: false }) - esCritico: boolean; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => TipoInspeccion, (t) => t.checklistItems, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'tipo_inspeccion_id' }) - tipoInspeccion: TipoInspeccion; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/comision-integrante.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/comision-integrante.entity.ts deleted file mode 100644 index 487b443c5..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/comision-integrante.entity.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * ComisionIntegrante Entity - * Integrantes de comisiones de seguridad - * - * @module HSE - * @table hse.comision_integrantes - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { ComisionSeguridad } from './comision-seguridad.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type RolComision = 'presidente' | 'secretario' | 'vocal_patronal' | 'vocal_trabajador'; -export type Representacion = 'patronal' | 'trabajadores'; - -@Entity({ schema: 'hse', name: 'comision_integrantes' }) -export class ComisionIntegrante { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'comision_id', type: 'uuid' }) - comisionId: string; - - @Column({ name: 'employee_id', type: 'uuid' }) - employeeId: string; - - @Column({ - type: 'enum', - enum: ['presidente', 'secretario', 'vocal_patronal', 'vocal_trabajador'], - }) - rol: RolComision; - - @Column({ - type: 'enum', - enum: ['patronal', 'trabajadores'], - }) - representacion: Representacion; - - @Column({ name: 'fecha_nombramiento', type: 'date' }) - fechaNombramiento: Date; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => ComisionSeguridad, (c) => c.integrantes, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'comision_id' }) - comision: ComisionSeguridad; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/comision-recorrido.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/comision-recorrido.entity.ts deleted file mode 100644 index 884729d47..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/comision-recorrido.entity.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * ComisionRecorrido Entity - * Recorridos de verificaci贸n de comisi贸n de seguridad - * - * @module HSE - * @table hse.comision_recorridos - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { ComisionSeguridad } from './comision-seguridad.entity'; - -export type EstadoRecorrido = 'programado' | 'realizado' | 'cancelado' | 'pendiente'; - -@Entity({ schema: 'hse', name: 'comision_recorridos' }) -export class ComisionRecorrido { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'comision_id', type: 'uuid' }) - comisionId: string; - - @Column({ name: 'fecha_programada', type: 'date' }) - fechaProgramada: Date; - - @Column({ name: 'fecha_realizada', type: 'date', nullable: true }) - fechaRealizada: Date; - - @Column({ name: 'numero_acta', type: 'varchar', length: 50, nullable: true }) - numeroActa: string; - - @Column({ name: 'areas_recorridas', type: 'text', nullable: true }) - areasRecorridas: string; - - @Column({ type: 'text', nullable: true }) - hallazgos: string; - - @Column({ type: 'text', nullable: true }) - recomendaciones: string; - - @Column({ - type: 'enum', - enum: ['programado', 'realizado', 'cancelado', 'pendiente'], - default: 'programado', - }) - estado: EstadoRecorrido; - - @Column({ name: 'documento_acta_url', type: 'varchar', length: 500, nullable: true }) - documentoActaUrl: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => ComisionSeguridad, (c) => c.recorridos, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'comision_id' }) - comision: ComisionSeguridad; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/comision-seguridad.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/comision-seguridad.entity.ts deleted file mode 100644 index 22ee33e1e..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/comision-seguridad.entity.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * ComisionSeguridad Entity - * Comisiones de seguridad e higiene - * - * @module HSE - * @table hse.comision_seguridad - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { ComisionIntegrante } from './comision-integrante.entity'; -import { ComisionRecorrido } from './comision-recorrido.entity'; - -export type EstadoComision = 'activa' | 'vencida' | 'renovada'; - -@Entity({ schema: 'hse', name: 'comision_seguridad' }) -@Index(['vigenciaFin']) -export class ComisionSeguridad { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'fecha_constitucion', type: 'date' }) - fechaConstitucion: Date; - - @Column({ name: 'numero_acta', type: 'varchar', length: 50, nullable: true }) - numeroActa: string; - - @Column({ name: 'vigencia_inicio', type: 'date' }) - vigenciaInicio: Date; - - @Column({ name: 'vigencia_fin', type: 'date' }) - vigenciaFin: Date; - - @Column({ - type: 'enum', - enum: ['activa', 'vencida', 'renovada'], - default: 'activa', - }) - estado: EstadoComision; - - @Column({ name: 'documento_acta_url', type: 'varchar', length: 500, nullable: true }) - documentoActaUrl: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @OneToMany(() => ComisionIntegrante, (i) => i.comision) - integrantes: ComisionIntegrante[]; - - @OneToMany(() => ComisionRecorrido, (r) => r.comision) - recorridos: ComisionRecorrido[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/constancia-dc3.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/constancia-dc3.entity.ts deleted file mode 100644 index 82ed0b428..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/constancia-dc3.entity.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * ConstanciaDc3 Entity - * Constancias DC-3 de capacitaci贸n STPS - * - * @module HSE - * @table hse.constancias_dc3 - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-002 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { CapacitacionAsistente } from './capacitacion-asistente.entity'; -import { Employee } from '../../hr/entities/employee.entity'; -import { Capacitacion } from './capacitacion.entity'; - -@Entity({ schema: 'hse', name: 'constancias_dc3' }) -@Index(['tenantId', 'folio'], { unique: true }) -export class ConstanciaDc3 { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 30 }) - folio: string; - - @Column({ name: 'asistente_id', type: 'uuid' }) - asistenteId: string; - - @Column({ name: 'employee_id', type: 'uuid' }) - employeeId: string; - - @Column({ name: 'capacitacion_id', type: 'uuid' }) - capacitacionId: string; - - @Column({ name: 'fecha_emision', type: 'date' }) - fechaEmision: Date; - - @Column({ name: 'fecha_vencimiento', type: 'date', nullable: true }) - fechaVencimiento: Date; - - @Column({ name: 'documento_url', type: 'varchar', length: 500, nullable: true }) - documentoUrl: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => CapacitacionAsistente) - @JoinColumn({ name: 'asistente_id' }) - asistente: CapacitacionAsistente; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; - - @ManyToOne(() => Capacitacion) - @JoinColumn({ name: 'capacitacion_id' }) - capacitacion: Capacitacion; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/cumplimiento-obra.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/cumplimiento-obra.entity.ts deleted file mode 100644 index 8e1c11b60..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/cumplimiento-obra.entity.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * CumplimientoObra Entity - * Estado de cumplimiento de normas por obra - * - * @module HSE - * @table hse.cumplimiento_obra - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { NormaStps } from './norma-stps.entity'; -import { NormaRequisito } from './norma-requisito.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type EstadoCumplimiento = 'cumple' | 'parcial' | 'no_cumple' | 'no_aplica'; - -@Entity({ schema: 'hse', name: 'cumplimiento_obra' }) -export class CumplimientoObra { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'norma_id', type: 'uuid' }) - normaId: string; - - @Column({ name: 'requisito_id', type: 'uuid', nullable: true }) - requisitoId: string; - - @Column({ - type: 'enum', - enum: ['cumple', 'parcial', 'no_cumple', 'no_aplica'], - default: 'no_cumple', - }) - estado: EstadoCumplimiento; - - @Column({ name: 'evidencia_url', type: 'varchar', length: 500, nullable: true }) - evidenciaUrl: string; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @Column({ name: 'fecha_evaluacion', type: 'date' }) - fechaEvaluacion: Date; - - @Column({ name: 'evaluador_id', type: 'uuid', nullable: true }) - evaluadorId: string; - - @Column({ name: 'fecha_compromiso', type: 'date', nullable: true }) - fechaCompromiso: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => NormaStps) - @JoinColumn({ name: 'norma_id' }) - norma: NormaStps; - - @ManyToOne(() => NormaRequisito) - @JoinColumn({ name: 'requisito_id' }) - requisito: NormaRequisito; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'evaluador_id' }) - evaluador: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/dias-sin-accidente.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/dias-sin-accidente.entity.ts deleted file mode 100644 index 551294504..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/dias-sin-accidente.entity.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * DiasSinAccidente Entity - * Contador de d铆as sin accidente por obra - * - * @module HSE - * @table hse.dias_sin_accidente - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-008 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Unique, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { Incidente } from './incidente.entity'; - -@Entity({ schema: 'hse', name: 'dias_sin_accidente' }) -@Unique(['tenantId', 'fraccionamientoId']) -export class DiasSinAccidente { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'fecha_inicio_conteo', type: 'date' }) - fechaInicioConteo: Date; - - @Column({ name: 'dias_acumulados', type: 'integer', default: 0 }) - diasAcumulados: number; - - @Column({ name: 'record_historico', type: 'integer', default: 0 }) - recordHistorico: number; - - @Column({ name: 'ultimo_incidente_id', type: 'uuid', nullable: true }) - ultimoIncidenteId: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => Incidente) - @JoinColumn({ name: 'ultimo_incidente_id' }) - ultimoIncidente: Incidente; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/documento-stps.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/documento-stps.entity.ts deleted file mode 100644 index 59d46dfb6..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/documento-stps.entity.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * DocumentoStps Entity - * Documentos STPS emitidos (DC-1, DC-2, DC-3, etc.) - * - * @module HSE - * @table hse.documentos_stps - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { Employee } from '../../hr/entities/employee.entity'; -import { User } from '../../core/entities/user.entity'; - -export type TipoDocumentoStps = 'dc1' | 'dc2' | 'dc3' | 'dc4' | 'st7' | 'st9'; - -@Entity({ schema: 'hse', name: 'documentos_stps' }) -@Index(['tenantId', 'tipo', 'folio'], { unique: true }) -@Index(['fechaVencimiento']) -export class DocumentoStps { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ - type: 'enum', - enum: ['dc1', 'dc2', 'dc3', 'dc4', 'st7', 'st9'], - }) - tipo: TipoDocumentoStps; - - @Column({ type: 'varchar', length: 30 }) - folio: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid', nullable: true }) - fraccionamientoId: string; - - @Column({ name: 'employee_id', type: 'uuid', nullable: true }) - employeeId: string; - - @Column({ name: 'fecha_emision', type: 'date' }) - fechaEmision: Date; - - @Column({ name: 'fecha_vencimiento', type: 'date', nullable: true }) - fechaVencimiento: Date; - - @Column({ name: 'datos_documento', type: 'jsonb', nullable: true }) - datosDocumento: Record; - - @Column({ name: 'documento_url', type: 'varchar', length: 500, nullable: true }) - documentoUrl: string; - - @Column({ type: 'boolean', default: false }) - firmado: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/epp-asignacion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/epp-asignacion.entity.ts deleted file mode 100644 index 9c660c04a..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/epp-asignacion.entity.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * EppAsignacion Entity - * Asignaciones de EPP a trabajadores - * - * @module HSE - * @table hse.epp_asignaciones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-004 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Employee } from '../../hr/entities/employee.entity'; -import { EppCatalogo } from './epp-catalogo.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { User } from '../../core/entities/user.entity'; - -export type EstadoEpp = 'activo' | 'vencido' | 'danado' | 'perdido' | 'devuelto'; - -@Entity({ schema: 'hse', name: 'epp_asignaciones' }) -@Index(['employeeId']) -@Index(['fechaVencimiento']) -@Index(['estado']) -export class EppAsignacion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'employee_id', type: 'uuid' }) - employeeId: string; - - @Column({ name: 'epp_id', type: 'uuid' }) - eppId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid', nullable: true }) - fraccionamientoId: string; - - @Column({ name: 'fecha_entrega', type: 'date' }) - fechaEntrega: Date; - - @Column({ name: 'fecha_vencimiento', type: 'date' }) - fechaVencimiento: Date; - - @Column({ name: 'numero_serie', type: 'varchar', length: 100, nullable: true }) - numeroSerie: string; - - @Column({ name: 'numero_lote', type: 'varchar', length: 100, nullable: true }) - numeroLote: string; - - @Column({ name: 'firma_trabajador', type: 'text', nullable: true }) - firmaTrabajador: string; - - @Column({ name: 'foto_entrega_url', type: 'varchar', length: 500, nullable: true }) - fotoEntregaUrl: string; - - @Column({ name: 'capacitacion_uso', type: 'boolean', default: false }) - capacitacionUso: boolean; - - @Column({ - type: 'enum', - enum: ['activo', 'vencido', 'danado', 'perdido', 'devuelto'], - default: 'activo', - }) - estado: EstadoEpp; - - @Column({ name: 'costo_unitario', type: 'decimal', precision: 10, scale: 2, nullable: true }) - costoUnitario: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; - - @ManyToOne(() => EppCatalogo) - @JoinColumn({ name: 'epp_id' }) - epp: EppCatalogo; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/epp-baja.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/epp-baja.entity.ts deleted file mode 100644 index f67432945..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/epp-baja.entity.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * EppBaja Entity - * Bajas de EPP - * - * @module HSE - * @table hse.epp_bajas - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-004 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { EppAsignacion } from './epp-asignacion.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type MotivoBajaEpp = 'vencimiento' | 'danado' | 'perdido' | 'terminacion_laboral'; - -@Entity({ schema: 'hse', name: 'epp_bajas' }) -export class EppBaja { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'asignacion_id', type: 'uuid' }) - asignacionId: string; - - @Column({ name: 'fecha_baja', type: 'date' }) - fechaBaja: Date; - - @Column({ - type: 'enum', - enum: ['vencimiento', 'danado', 'perdido', 'terminacion_laboral'], - }) - motivo: MotivoBajaEpp; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ name: 'descuento_aplicado', type: 'boolean', default: false }) - descuentoAplicado: boolean; - - @Column({ name: 'monto_descuento', type: 'decimal', precision: 10, scale: 2, nullable: true }) - montoDescuento: number; - - @Column({ name: 'autorizado_por', type: 'uuid', nullable: true }) - autorizadoPorId: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => EppAsignacion) - @JoinColumn({ name: 'asignacion_id' }) - asignacion: EppAsignacion; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'autorizado_por' }) - autorizadoPor: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/epp-catalogo.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/epp-catalogo.entity.ts deleted file mode 100644 index a5e2b683c..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/epp-catalogo.entity.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * EppCatalogo Entity - * Cat谩logo de Equipos de Protecci贸n Personal - * - * @module HSE - * @table hse.epp_catalogo - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-004 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; - -export type CategoriaEpp = 'cabeza' | 'ojos' | 'auditiva' | 'respiratoria' | 'manos' | 'pies' | 'caidas' | 'ropa'; - -@Entity({ schema: 'hse', name: 'epp_catalogo' }) -@Index(['tenantId', 'codigo'], { unique: true }) -export class EppCatalogo { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ - type: 'enum', - enum: ['cabeza', 'ojos', 'auditiva', 'respiratoria', 'manos', 'pies', 'caidas', 'ropa'], - }) - categoria: CategoriaEpp; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ type: 'text', nullable: true }) - especificaciones: string; - - @Column({ name: 'vida_util_dias', type: 'integer' }) - vidaUtilDias: number; - - @Column({ name: 'norma_referencia', type: 'varchar', length: 50, nullable: true }) - normaReferencia: string; - - @Column({ name: 'requiere_certificacion', type: 'boolean', default: false }) - requiereCertificacion: boolean; - - @Column({ name: 'requiere_inspeccion_periodica', type: 'boolean', default: false }) - requiereInspeccionPeriodica: boolean; - - @Column({ name: 'frecuencia_inspeccion_dias', type: 'integer', nullable: true }) - frecuenciaInspeccionDias: number; - - @Column({ name: 'alerta_dias_antes', type: 'integer', default: 15 }) - alertaDiasAntes: number; - - @Column({ name: 'imagen_url', type: 'varchar', length: 500, nullable: true }) - imagenUrl: string; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/epp-inspeccion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/epp-inspeccion.entity.ts deleted file mode 100644 index beb13d8a7..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/epp-inspeccion.entity.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * EppInspeccion Entity - * Inspecciones peri贸dicas de EPP asignado - * - * @module HSE - * @table hse.epp_inspecciones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-004 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { EppAsignacion } from './epp-asignacion.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type EstadoInspeccionEpp = 'bueno' | 'regular' | 'malo' | 'danado'; - -@Entity({ schema: 'hse', name: 'epp_inspecciones' }) -export class EppInspeccion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'asignacion_id', type: 'uuid' }) - asignacionId: string; - - @Column({ name: 'inspector_id', type: 'uuid' }) - inspectorId: string; - - @Column({ name: 'fecha_inspeccion', type: 'date' }) - fechaInspeccion: Date; - - @Column({ - name: 'estado_epp', - type: 'enum', - enum: ['bueno', 'regular', 'malo', 'danado'], - }) - estadoEpp: EstadoInspeccionEpp; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @Column({ name: 'requiere_reemplazo', type: 'boolean', default: false }) - requiereReemplazo: boolean; - - @Column({ name: 'foto_url', type: 'varchar', length: 500, nullable: true }) - fotoUrl: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => EppAsignacion, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'asignacion_id' }) - asignacion: EppAsignacion; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'inspector_id' }) - inspector: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/epp-inventario.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/epp-inventario.entity.ts deleted file mode 100644 index a507b0f06..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/epp-inventario.entity.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * EppInventario Entity - * Inventario de EPP en almac茅n - * - * @module HSE - * @table hse.epp_inventario - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-004 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { EppCatalogo } from './epp-catalogo.entity'; - -@Entity({ schema: 'hse', name: 'epp_inventario' }) -export class EppInventario { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'epp_id', type: 'uuid' }) - eppId: string; - - @Column({ name: 'almacen_id', type: 'uuid', nullable: true }) - almacenId: string; - - @Column({ name: 'cantidad_disponible', type: 'integer', default: 0 }) - cantidadDisponible: number; - - @Column({ name: 'cantidad_minima', type: 'integer', default: 0 }) - cantidadMinima: number; - - @Column({ name: 'cantidad_maxima', type: 'integer', nullable: true }) - cantidadMaxima: number; - - @Column({ name: 'costo_promedio', type: 'decimal', precision: 10, scale: 2, nullable: true }) - costoPromedio: number; - - @Column({ name: 'ultima_entrada', type: 'date', nullable: true }) - ultimaEntrada: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => EppCatalogo) - @JoinColumn({ name: 'epp_id' }) - epp: EppCatalogo; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/epp-matriz-puesto.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/epp-matriz-puesto.entity.ts deleted file mode 100644 index f4f3c7f35..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/epp-matriz-puesto.entity.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * EppMatrizPuesto Entity - * Matriz de EPP requerido por puesto - * - * @module HSE - * @table hse.epp_matriz_puesto - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-004 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { EppCatalogo } from './epp-catalogo.entity'; - -@Entity({ schema: 'hse', name: 'epp_matriz_puesto' }) -export class EppMatrizPuesto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'puesto_id', type: 'uuid' }) - puestoId: string; - - @Column({ name: 'epp_id', type: 'uuid' }) - eppId: string; - - @Column({ name: 'es_obligatorio', type: 'boolean', default: true }) - esObligatorio: boolean; - - @Column({ name: 'actividad_especifica', type: 'varchar', length: 200, nullable: true }) - actividadEspecifica: string; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => EppCatalogo) - @JoinColumn({ name: 'epp_id' }) - epp: EppCatalogo; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/epp-movimiento.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/epp-movimiento.entity.ts deleted file mode 100644 index 3bf0c1cfa..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/epp-movimiento.entity.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * EppMovimiento Entity - * Movimientos de inventario de EPP - * - * @module HSE - * @table hse.epp_movimientos - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-004 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { EppCatalogo } from './epp-catalogo.entity'; -import { User } from '../../core/entities/user.entity'; - -export type TipoMovimientoEpp = 'entrada' | 'salida' | 'transferencia' | 'ajuste'; - -@Entity({ schema: 'hse', name: 'epp_movimientos' }) -export class EppMovimiento { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'epp_id', type: 'uuid' }) - eppId: string; - - @Column({ name: 'almacen_origen_id', type: 'uuid', nullable: true }) - almacenOrigenId: string; - - @Column({ name: 'almacen_destino_id', type: 'uuid', nullable: true }) - almacenDestinoId: string; - - @Column({ - type: 'enum', - enum: ['entrada', 'salida', 'transferencia', 'ajuste'], - }) - tipo: TipoMovimientoEpp; - - @Column({ type: 'integer' }) - cantidad: number; - - @Column({ type: 'varchar', length: 100, nullable: true }) - referencia: string; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => EppCatalogo) - @JoinColumn({ name: 'epp_id' }) - epp: EppCatalogo; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/hallazgo-evidencia.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/hallazgo-evidencia.entity.ts deleted file mode 100644 index 42ab8e982..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/hallazgo-evidencia.entity.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * HallazgoEvidencia Entity - * Evidencias de hallazgos de inspecci贸n - * - * @module HSE - * @table hse.hallazgo_evidencias - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-003 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Hallazgo } from './hallazgo.entity'; -import { User } from '../../core/entities/user.entity'; - -export type TipoEvidencia = 'hallazgo' | 'correccion'; - -@Entity({ schema: 'hse', name: 'hallazgo_evidencias' }) -export class HallazgoEvidencia { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'hallazgo_id', type: 'uuid' }) - hallazgoId: string; - - @Column({ - type: 'enum', - enum: ['hallazgo', 'correccion'], - }) - tipo: TipoEvidencia; - - @Column({ name: 'archivo_url', type: 'varchar', length: 500 }) - archivoUrl: string; - - @Column({ type: 'varchar', length: 200, nullable: true }) - descripcion: string; - - @Column({ - name: 'ubicacion_geo', - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true, - }) - ubicacionGeo: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Hallazgo, (h) => h.evidencias, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'hallazgo_id' }) - hallazgo: Hallazgo; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/hallazgo.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/hallazgo.entity.ts deleted file mode 100644 index b1a7b13a4..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/hallazgo.entity.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Hallazgo Entity - * Hallazgos de inspecciones de seguridad - * - * @module HSE - * @table hse.hallazgos - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-003 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Inspeccion } from './inspeccion.entity'; -import { InspeccionEvaluacion } from './inspeccion-evaluacion.entity'; -import { Employee } from '../../hr/entities/employee.entity'; -import { HallazgoEvidencia } from './hallazgo-evidencia.entity'; -import { FactorCausa } from './incidente-investigacion.entity'; - -export type GravedadHallazgo = 'critico' | 'mayor' | 'menor'; -export type EstadoHallazgo = 'abierto' | 'en_correccion' | 'verificando' | 'cerrado' | 'reabierto'; - -@Entity({ schema: 'hse', name: 'hallazgos' }) -@Index(['tenantId', 'folio'], { unique: true }) -@Index(['estado']) -@Index(['fechaLimite']) -export class Hallazgo { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'inspeccion_id', type: 'uuid' }) - inspeccionId: string; - - @Column({ name: 'evaluacion_id', type: 'uuid', nullable: true }) - evaluacionId: string; - - @Column({ type: 'varchar', length: 20 }) - folio: string; - - @Column({ - type: 'enum', - enum: ['critico', 'mayor', 'menor'], - }) - gravedad: GravedadHallazgo; - - @Column({ - type: 'enum', - enum: ['acto_inseguro', 'condicion_insegura'], - }) - tipo: FactorCausa; - - @Column({ type: 'text' }) - descripcion: string; - - @Column({ name: 'ubicacion_descripcion', type: 'varchar', length: 500, nullable: true }) - ubicacionDescripcion: string; - - @Column({ - name: 'ubicacion_geo', - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true, - }) - ubicacionGeo: string; - - @Column({ name: 'responsable_correccion_id', type: 'uuid', nullable: true }) - responsableCorreccionId: string; - - @Column({ name: 'fecha_limite', type: 'date' }) - fechaLimite: Date; - - @Column({ - type: 'enum', - enum: ['abierto', 'en_correccion', 'verificando', 'cerrado', 'reabierto'], - default: 'abierto', - }) - estado: EstadoHallazgo; - - @Column({ name: 'fecha_correccion', type: 'timestamptz', nullable: true }) - fechaCorreccion: Date; - - @Column({ name: 'descripcion_correccion', type: 'text', nullable: true }) - descripcionCorreccion: string; - - @Column({ name: 'verificador_id', type: 'uuid', nullable: true }) - verificadorId: string; - - @Column({ name: 'fecha_verificacion', type: 'timestamptz', nullable: true }) - fechaVerificacion: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Inspeccion, (i) => i.hallazgos) - @JoinColumn({ name: 'inspeccion_id' }) - inspeccion: Inspeccion; - - @ManyToOne(() => InspeccionEvaluacion) - @JoinColumn({ name: 'evaluacion_id' }) - evaluacion: InspeccionEvaluacion; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'responsable_correccion_id' }) - responsableCorreccion: Employee; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'verificador_id' }) - verificador: Employee; - - @OneToMany(() => HallazgoEvidencia, (e) => e.hallazgo) - evidencias: HallazgoEvidencia[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/horas-trabajadas.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/horas-trabajadas.entity.ts deleted file mode 100644 index 3f1b81703..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/horas-trabajadas.entity.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * HorasTrabajadas Entity - * Registro de horas trabajadas para c谩lculo de indicadores - * - * @module HSE - * @table hse.horas_trabajadas - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-008 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, - Unique, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -export type FuenteHoras = 'asistencia' | 'manual'; - -@Entity({ schema: 'hse', name: 'horas_trabajadas' }) -@Index(['fecha']) -@Unique(['tenantId', 'fraccionamientoId', 'fecha']) -export class HorasTrabajadas { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ type: 'date' }) - fecha: Date; - - @Column({ name: 'horas_totales', type: 'decimal', precision: 12, scale: 2 }) - horasTotales: number; - - @Column({ name: 'trabajadores_promedio', type: 'integer' }) - trabajadoresPromedio: number; - - @Column({ - type: 'enum', - enum: ['asistencia', 'manual'], - default: 'manual', - }) - fuente: FuenteHoras; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/impacto-ambiental.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/impacto-ambiental.entity.ts deleted file mode 100644 index b013a1e7d..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/impacto-ambiental.entity.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * ImpactoAmbiental Entity - * Identificaci贸n y gesti贸n de impactos ambientales - * - * @module HSE - * @table hse.impacto_ambiental - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-006 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type TipoImpacto = 'ruido' | 'polvo' | 'vibraciones' | 'agua' | 'emision' | 'vegetacion' | 'otro'; -export type Severidad = 'bajo' | 'medio' | 'alto'; -export type Probabilidad = 'baja' | 'media' | 'alta'; -export type NivelRiesgo = 'tolerable' | 'moderado' | 'significativo'; -export type EstadoImpacto = 'identificado' | 'mitigando' | 'controlado'; - -@Entity({ schema: 'hse', name: 'impacto_ambiental' }) -export class ImpactoAmbiental { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ type: 'varchar', length: 200 }) - aspecto: string; - - @Column({ - name: 'tipo_impacto', - type: 'enum', - enum: ['ruido', 'polvo', 'vibraciones', 'agua', 'emision', 'vegetacion', 'otro'], - }) - tipoImpacto: TipoImpacto; - - @Column({ - type: 'enum', - enum: ['bajo', 'medio', 'alto'], - }) - severidad: Severidad; - - @Column({ - type: 'enum', - enum: ['baja', 'media', 'alta'], - }) - probabilidad: Probabilidad; - - @Column({ - name: 'nivel_riesgo', - type: 'enum', - enum: ['tolerable', 'moderado', 'significativo'], - }) - nivelRiesgo: NivelRiesgo; - - @Column({ name: 'medidas_mitigacion', type: 'text', nullable: true }) - medidasMitigacion: string; - - @Column({ name: 'responsable_id', type: 'uuid', nullable: true }) - responsableId: string; - - @Column({ - type: 'enum', - enum: ['identificado', 'mitigando', 'controlado'], - default: 'identificado', - }) - estado: EstadoImpacto; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'responsable_id' }) - responsable: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/incidente-accion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/incidente-accion.entity.ts deleted file mode 100644 index 4fae042e0..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/incidente-accion.entity.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * IncidenteAccion Entity - * Acciones correctivas de incidentes - * - * @module HSE - * @table hse.incidente_acciones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-001 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Incidente } from './incidente.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type EstadoAccion = 'pendiente' | 'en_progreso' | 'completada' | 'verificada'; - -@Entity({ schema: 'hse', name: 'incidente_acciones' }) -export class IncidenteAccion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'incidente_id', type: 'uuid' }) - incidenteId: string; - - @Column({ type: 'text' }) - descripcion: string; - - @Column({ type: 'varchar', length: 50 }) - tipo: string; - - @Column({ name: 'responsable_id', type: 'uuid', nullable: true }) - responsableId: string; - - @Column({ name: 'fecha_compromiso', type: 'date' }) - fechaCompromiso: Date; - - @Column({ name: 'fecha_cierre', type: 'date', nullable: true }) - fechaCierre: Date; - - @Column({ type: 'varchar', length: 20, default: 'pendiente' }) - estado: EstadoAccion; - - @Column({ name: 'evidencia_url', type: 'varchar', length: 500, nullable: true }) - evidenciaUrl: string; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Incidente, (i) => i.acciones, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'incidente_id' }) - incidente: Incidente; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'responsable_id' }) - responsable: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/incidente-evidencia.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/incidente-evidencia.entity.ts deleted file mode 100644 index cdbfd428a..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/incidente-evidencia.entity.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * IncidenteEvidencia Entity - * Evidencias fotogr谩ficas de incidentes - * - * @module HSE - * @table hse.incidente_evidencias - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-001 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Incidente } from './incidente.entity'; -import { User } from '../../core/entities/user.entity'; - -@Entity({ schema: 'hse', name: 'incidente_evidencias' }) -export class IncidenteEvidencia { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'incidente_id', type: 'uuid' }) - incidenteId: string; - - @Column({ type: 'varchar', length: 50, default: 'foto' }) - tipo: string; - - @Column({ name: 'archivo_url', type: 'varchar', length: 500 }) - archivoUrl: string; - - @Column({ type: 'varchar', length: 200, nullable: true }) - descripcion: string; - - @Column({ - name: 'ubicacion_geo', - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true, - }) - ubicacionGeo: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Incidente, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'incidente_id' }) - incidente: Incidente; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/incidente-investigacion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/incidente-investigacion.entity.ts deleted file mode 100644 index 6aca63955..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/incidente-investigacion.entity.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * IncidenteInvestigacion Entity - * Investigaci贸n de incidentes de seguridad - * - * @module HSE - * @table hse.incidente_investigacion - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-001 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Incidente } from './incidente.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type FactorCausa = 'acto_inseguro' | 'condicion_insegura'; - -@Entity({ schema: 'hse', name: 'incidente_investigacion' }) -export class IncidenteInvestigacion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'incidente_id', type: 'uuid' }) - incidenteId: string; - - @Column({ name: 'fecha_inicio', type: 'date' }) - fechaInicio: Date; - - @Column({ name: 'fecha_cierre', type: 'date', nullable: true }) - fechaCierre: Date; - - @Column({ name: 'investigador_id', type: 'uuid', nullable: true }) - investigadorId: string; - - @Column({ type: 'varchar', length: 100, nullable: true }) - metodologia: string; - - @Column({ - name: 'factor_causa', - type: 'enum', - enum: ['acto_inseguro', 'condicion_insegura'], - nullable: true, - }) - factorCausa: FactorCausa; - - @Column({ name: 'analisis_causas', type: 'text', nullable: true }) - analisisCausas: string; - - @Column({ type: 'text', nullable: true }) - conclusiones: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Incidente, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'incidente_id' }) - incidente: Incidente; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'investigador_id' }) - investigador: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/incidente-involucrado.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/incidente-involucrado.entity.ts deleted file mode 100644 index b92eb6772..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/incidente-involucrado.entity.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * IncidenteInvolucrado Entity - * Personas involucradas en incidentes - * - * @module HSE - * @table hse.incidente_involucrados - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-001 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Incidente } from './incidente.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type RolInvolucrado = 'lesionado' | 'testigo' | 'responsable'; - -@Entity({ schema: 'hse', name: 'incidente_involucrados' }) -export class IncidenteInvolucrado { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'incidente_id', type: 'uuid' }) - incidenteId: string; - - @Column({ name: 'employee_id', type: 'uuid' }) - employeeId: string; - - @Column({ type: 'enum', enum: ['lesionado', 'testigo', 'responsable'] }) - rol: RolInvolucrado; - - @Column({ name: 'descripcion_lesion', type: 'text', nullable: true }) - descripcionLesion: string; - - @Column({ name: 'parte_cuerpo', type: 'varchar', length: 100, nullable: true }) - parteCuerpo: string; - - @Column({ name: 'dias_incapacidad', type: 'integer', default: 0 }) - diasIncapacidad: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Incidente, (i) => i.involucrados, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'incidente_id' }) - incidente: Incidente; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/incidente.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/incidente.entity.ts deleted file mode 100644 index 249c5a6b6..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/incidente.entity.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Incidente Entity - * Gesti贸n de incidentes de seguridad - * - * @module HSE - * @table hse.incidentes - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-001 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { IncidenteInvolucrado } from './incidente-involucrado.entity'; -import { IncidenteAccion } from './incidente-accion.entity'; - -export type TipoIncidente = 'accidente' | 'incidente' | 'casi_accidente'; -export type GravedadIncidente = 'leve' | 'moderado' | 'grave' | 'fatal'; -export type EstadoIncidente = 'abierto' | 'en_investigacion' | 'cerrado'; - -@Entity({ schema: 'hse', name: 'incidentes' }) -@Index(['tenantId', 'folio'], { unique: true }) -export class Incidente { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - folio: string; - - @Column({ name: 'fecha_hora', type: 'timestamptz' }) - fechaHora: Date; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'ubicacion_descripcion', type: 'text', nullable: true }) - ubicacionDescripcion: string; - - @Column({ - name: 'ubicacion_geo', - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true - }) - ubicacionGeo: string; - - @Column({ type: 'enum', enum: ['accidente', 'incidente', 'casi_accidente'] }) - tipo: TipoIncidente; - - @Column({ type: 'enum', enum: ['leve', 'moderado', 'grave', 'fatal'] }) - gravedad: GravedadIncidente; - - @Column({ type: 'text' }) - descripcion: string; - - @Column({ name: 'causa_inmediata', type: 'text', nullable: true }) - causaInmediata: string; - - @Column({ name: 'causa_basica', type: 'text', nullable: true }) - causaBasica: string; - - @Column({ - type: 'enum', - enum: ['abierto', 'en_investigacion', 'cerrado'], - default: 'abierto' - }) - estado: EstadoIncidente; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => IncidenteInvolucrado, (ii) => ii.incidente) - involucrados: IncidenteInvolucrado[]; - - @OneToMany(() => IncidenteAccion, (ia) => ia.incidente) - acciones: IncidenteAccion[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/index.ts b/projects/erp-construccion/backend/src/modules/hse/entities/index.ts deleted file mode 100644 index ae620063b..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * HSE Entities Index - * @module HSE - * - * Entities for Health, Safety & Environment module - * Based on RF-MAA017-001 to RF-MAA017-008 - * Updated: 2025-12-18 - */ - -// RF-MAA017-001: Gesti贸n de Incidentes -export * from './incidente.entity'; -export * from './incidente-involucrado.entity'; -export * from './incidente-accion.entity'; -export * from './incidente-investigacion.entity'; -export * from './incidente-evidencia.entity'; - -// RF-MAA017-002: Control de Capacitaciones -export * from './capacitacion.entity'; -export * from './capacitacion-matriz.entity'; -export * from './instructor.entity'; -export * from './capacitacion-sesion.entity'; -export * from './capacitacion-asistente.entity'; -export * from './constancia-dc3.entity'; - -// RF-MAA017-003: Inspecciones de Seguridad -export * from './tipo-inspeccion.entity'; -export * from './checklist-item.entity'; -export * from './programa-inspeccion.entity'; -export * from './inspeccion.entity'; -export * from './inspeccion-evaluacion.entity'; -export * from './hallazgo.entity'; -export * from './hallazgo-evidencia.entity'; - -// RF-MAA017-004: Control de EPP -export * from './epp-catalogo.entity'; -export * from './epp-matriz-puesto.entity'; -export * from './epp-asignacion.entity'; -export * from './epp-inspeccion.entity'; -export * from './epp-baja.entity'; -export * from './epp-inventario.entity'; -export * from './epp-movimiento.entity'; - -// RF-MAA017-005: Cumplimiento STPS -export * from './norma-stps.entity'; -export * from './norma-requisito.entity'; -export * from './cumplimiento-obra.entity'; -export * from './comision-seguridad.entity'; -export * from './comision-integrante.entity'; -export * from './comision-recorrido.entity'; -export * from './programa-seguridad.entity'; -export * from './programa-actividad.entity'; -export * from './documento-stps.entity'; -export * from './auditoria.entity'; - -// RF-MAA017-006: Gesti贸n Ambiental -export * from './residuo-catalogo.entity'; -export * from './residuo-generacion.entity'; -export * from './almacen-temporal.entity'; -export * from './proveedor-ambiental.entity'; -export * from './manifiesto-residuos.entity'; -export * from './manifiesto-detalle.entity'; -export * from './impacto-ambiental.entity'; -export * from './queja-ambiental.entity'; - -// RF-MAA017-007: Permisos de Trabajo -export * from './tipo-permiso-trabajo.entity'; -export * from './permiso-trabajo.entity'; -export * from './permiso-personal.entity'; -export * from './permiso-autorizacion.entity'; -export * from './permiso-checklist.entity'; -export * from './permiso-monitoreo.entity'; -export * from './permiso-evento.entity'; -export * from './permiso-documento.entity'; - -// RF-MAA017-008: Indicadores HSE -export * from './indicador-config.entity'; -export * from './indicador-meta-obra.entity'; -export * from './indicador-valor.entity'; -export * from './horas-trabajadas.entity'; -export * from './dias-sin-accidente.entity'; -export * from './reporte-programado.entity'; -export * from './alerta-indicador.entity'; diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/indicador-config.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/indicador-config.entity.ts deleted file mode 100644 index f7829d27c..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/indicador-config.entity.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * IndicadorConfig Entity - * Configuraci贸n de indicadores HSE - * - * @module HSE - * @table hse.indicadores_config - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-008 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; - -export type TipoIndicador = 'reactivo' | 'proactivo' | 'ambiental'; -export type FrecuenciaCalculo = 'diario' | 'semanal' | 'mensual'; - -@Entity({ schema: 'hse', name: 'indicadores_config' }) -@Index(['tenantId', 'codigo'], { unique: true }) -export class IndicadorConfig { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ type: 'text', nullable: true }) - formula: string; - - @Column({ type: 'varchar', length: 50, nullable: true }) - unidad: string; - - @Column({ - type: 'enum', - enum: ['reactivo', 'proactivo', 'ambiental'], - }) - tipo: TipoIndicador; - - @Column({ name: 'meta_global', type: 'decimal', precision: 10, scale: 4, nullable: true }) - metaGlobal: number; - - @Column({ name: 'umbral_verde', type: 'decimal', precision: 10, scale: 4, nullable: true }) - umbralVerde: number; - - @Column({ name: 'umbral_amarillo', type: 'decimal', precision: 10, scale: 4, nullable: true }) - umbralAmarillo: number; - - @Column({ name: 'umbral_rojo', type: 'decimal', precision: 10, scale: 4, nullable: true }) - umbralRojo: number; - - @Column({ - name: 'frecuencia_calculo', - type: 'enum', - enum: ['diario', 'semanal', 'mensual'], - default: 'mensual', - }) - frecuenciaCalculo: FrecuenciaCalculo; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/indicador-meta-obra.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/indicador-meta-obra.entity.ts deleted file mode 100644 index c308242a5..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/indicador-meta-obra.entity.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * IndicadorMetaObra Entity - * Metas de indicadores por obra/fraccionamiento - * - * @module HSE - * @table hse.indicadores_meta_obra - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-008 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { IndicadorConfig } from './indicador-config.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -@Entity({ schema: 'hse', name: 'indicadores_meta_obra' }) -export class IndicadorMetaObra { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'indicador_id', type: 'uuid' }) - indicadorId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ type: 'integer' }) - anio: number; - - @Column({ type: 'decimal', precision: 10, scale: 4 }) - meta: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => IndicadorConfig) - @JoinColumn({ name: 'indicador_id' }) - indicador: IndicadorConfig; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/indicador-valor.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/indicador-valor.entity.ts deleted file mode 100644 index e6fc4bbc7..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/indicador-valor.entity.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * IndicadorValor Entity - * Valores calculados de indicadores HSE - * - * @module HSE - * @table hse.indicadores_valores - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-008 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { IndicadorConfig } from './indicador-config.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -export type PeriodoTipo = 'diario' | 'semanal' | 'mensual' | 'anual'; -export type EstadoSemaforo = 'verde' | 'amarillo' | 'rojo'; - -@Entity({ schema: 'hse', name: 'indicadores_valores' }) -@Index(['periodoFecha']) -export class IndicadorValor { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'indicador_id', type: 'uuid' }) - indicadorId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid', nullable: true }) - fraccionamientoId: string; - - @Column({ - name: 'periodo_tipo', - type: 'enum', - enum: ['diario', 'semanal', 'mensual', 'anual'], - }) - periodoTipo: PeriodoTipo; - - @Column({ name: 'periodo_fecha', type: 'date' }) - periodoFecha: Date; - - @Column({ type: 'decimal', precision: 15, scale: 4, nullable: true }) - valor: number; - - @Column({ type: 'decimal', precision: 15, scale: 4, nullable: true }) - numerador: number; - - @Column({ type: 'decimal', precision: 15, scale: 4, nullable: true }) - denominador: number; - - @Column({ - type: 'enum', - enum: ['verde', 'amarillo', 'rojo'], - nullable: true, - }) - estado: EstadoSemaforo; - - @Column({ name: 'calculado_at', type: 'timestamptz', default: () => 'NOW()' }) - calculadoAt: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => IndicadorConfig) - @JoinColumn({ name: 'indicador_id' }) - indicador: IndicadorConfig; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/inspeccion-evaluacion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/inspeccion-evaluacion.entity.ts deleted file mode 100644 index 9c8bfc91c..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/inspeccion-evaluacion.entity.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * InspeccionEvaluacion Entity - * Evaluaciones de items en inspecciones - * - * @module HSE - * @table hse.inspeccion_evaluaciones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-003 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Inspeccion } from './inspeccion.entity'; -import { ChecklistItem } from './checklist-item.entity'; - -export type ResultadoEvaluacion = 'cumple' | 'no_cumple' | 'no_aplica'; - -@Entity({ schema: 'hse', name: 'inspeccion_evaluaciones' }) -export class InspeccionEvaluacion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'inspeccion_id', type: 'uuid' }) - inspeccionId: string; - - @Column({ name: 'checklist_item_id', type: 'uuid' }) - checklistItemId: string; - - @Column({ - type: 'enum', - enum: ['cumple', 'no_cumple', 'no_aplica'], - }) - resultado: ResultadoEvaluacion; - - @Column({ type: 'text', nullable: true }) - observacion: string; - - @Column({ name: 'genera_hallazgo', type: 'boolean', default: false }) - generaHallazgo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Inspeccion, (i) => i.evaluaciones, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'inspeccion_id' }) - inspeccion: Inspeccion; - - @ManyToOne(() => ChecklistItem) - @JoinColumn({ name: 'checklist_item_id' }) - checklistItem: ChecklistItem; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/inspeccion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/inspeccion.entity.ts deleted file mode 100644 index 2111696ea..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/inspeccion.entity.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Inspeccion Entity - * Inspecciones de seguridad ejecutadas - * - * @module HSE - * @table hse.inspecciones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-003 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { TipoInspeccion } from './tipo-inspeccion.entity'; -import { ProgramaInspeccion } from './programa-inspeccion.entity'; -import { Employee } from '../../hr/entities/employee.entity'; -import { InspeccionEvaluacion } from './inspeccion-evaluacion.entity'; -import { Hallazgo } from './hallazgo.entity'; - -@Entity({ schema: 'hse', name: 'inspecciones' }) -@Index(['tenantId']) -@Index(['fraccionamientoId']) -@Index(['fechaInicio']) -export class Inspeccion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'programa_id', type: 'uuid', nullable: true }) - programaId: string; - - @Column({ name: 'tipo_inspeccion_id', type: 'uuid' }) - tipoInspeccionId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'inspector_id', type: 'uuid' }) - inspectorId: string; - - @Column({ name: 'fecha_inicio', type: 'timestamptz' }) - fechaInicio: Date; - - @Column({ name: 'fecha_fin', type: 'timestamptz', nullable: true }) - fechaFin: Date; - - @Column({ - name: 'ubicacion_geo', - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true, - }) - ubicacionGeo: string; - - @Column({ name: 'items_evaluados', type: 'integer', default: 0 }) - itemsEvaluados: number; - - @Column({ name: 'items_cumple', type: 'integer', default: 0 }) - itemsCumple: number; - - @Column({ name: 'items_no_cumple', type: 'integer', default: 0 }) - itemsNoCumple: number; - - @Column({ name: 'items_no_aplica', type: 'integer', default: 0 }) - itemsNoAplica: number; - - @Column({ name: 'porcentaje_cumplimiento', type: 'decimal', precision: 5, scale: 2, nullable: true }) - porcentajeCumplimiento: number; - - @Column({ name: 'observaciones_generales', type: 'text', nullable: true }) - observacionesGenerales: string; - - @Column({ name: 'firma_inspector', type: 'text', nullable: true }) - firmaInspector: string; - - @Column({ type: 'varchar', length: 20, default: 'borrador' }) - estado: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => ProgramaInspeccion) - @JoinColumn({ name: 'programa_id' }) - programa: ProgramaInspeccion; - - @ManyToOne(() => TipoInspeccion) - @JoinColumn({ name: 'tipo_inspeccion_id' }) - tipoInspeccion: TipoInspeccion; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'inspector_id' }) - inspector: Employee; - - @OneToMany(() => InspeccionEvaluacion, (e) => e.inspeccion) - evaluaciones: InspeccionEvaluacion[]; - - @OneToMany(() => Hallazgo, (h) => h.inspeccion) - hallazgos: Hallazgo[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/instructor.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/instructor.entity.ts deleted file mode 100644 index d69102e70..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/instructor.entity.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Instructor Entity - * Instructores de capacitaci贸n HSE - * - * @module HSE - * @table hse.instructores - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-002 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -@Entity({ schema: 'hse', name: 'instructores' }) -export class Instructor { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ name: 'registro_stps', type: 'varchar', length: 50, nullable: true }) - registroStps: string; - - @Column({ type: 'text', nullable: true }) - especialidades: string; - - @Column({ name: 'es_interno', type: 'boolean', default: false }) - esInterno: boolean; - - @Column({ name: 'employee_id', type: 'uuid', nullable: true }) - employeeId: string; - - @Column({ name: 'contacto_telefono', type: 'varchar', length: 20, nullable: true }) - contactoTelefono: string; - - @Column({ name: 'contacto_email', type: 'varchar', length: 100, nullable: true }) - contactoEmail: string; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/manifiesto-detalle.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/manifiesto-detalle.entity.ts deleted file mode 100644 index 6c8f4d844..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/manifiesto-detalle.entity.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * ManifiestoDetalle Entity - * Detalle de residuos por manifiesto - * - * @module HSE - * @table hse.manifiesto_detalle - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-006 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { ManifiestoResiduos } from './manifiesto-residuos.entity'; -import { ResiduoCatalogo } from './residuo-catalogo.entity'; -import { UnidadResiduo } from './residuo-generacion.entity'; - -@Entity({ schema: 'hse', name: 'manifiesto_detalle' }) -export class ManifiestoDetalle { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'manifiesto_id', type: 'uuid' }) - manifiestoId: string; - - @Column({ name: 'residuo_id', type: 'uuid' }) - residuoId: string; - - @Column({ name: 'generacion_ids', type: 'uuid', array: true, nullable: true }) - generacionIds: string[]; - - @Column({ type: 'decimal', precision: 10, scale: 2 }) - cantidad: number; - - @Column({ - type: 'enum', - enum: ['kg', 'litros', 'm3', 'piezas'], - }) - unidad: UnidadResiduo; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => ManifiestoResiduos, (m) => m.detalles, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'manifiesto_id' }) - manifiesto: ManifiestoResiduos; - - @ManyToOne(() => ResiduoCatalogo) - @JoinColumn({ name: 'residuo_id' }) - residuo: ResiduoCatalogo; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/manifiesto-residuos.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/manifiesto-residuos.entity.ts deleted file mode 100644 index 164f4bb1b..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/manifiesto-residuos.entity.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * ManifiestoResiduos Entity - * Manifiestos de entrega de residuos peligrosos - * - * @module HSE - * @table hse.manifiestos_residuos - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-006 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { ProveedorAmbiental } from './proveedor-ambiental.entity'; -import { ManifiestoDetalle } from './manifiesto-detalle.entity'; - -export type EstadoManifiesto = 'emitido' | 'en_transito' | 'entregado' | 'cerrado'; - -@Entity({ schema: 'hse', name: 'manifiestos_residuos' }) -@Index(['tenantId', 'folio'], { unique: true }) -@Index(['estado']) -export class ManifiestoResiduos { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 30 }) - folio: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'transportista_id', type: 'uuid' }) - transportistaId: string; - - @Column({ name: 'destino_id', type: 'uuid' }) - destinoId: string; - - @Column({ name: 'fecha_recoleccion', type: 'date' }) - fechaRecoleccion: Date; - - @Column({ name: 'fecha_entrega', type: 'date', nullable: true }) - fechaEntrega: Date; - - @Column({ - type: 'enum', - enum: ['emitido', 'en_transito', 'entregado', 'cerrado'], - default: 'emitido', - }) - estado: EstadoManifiesto; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @Column({ name: 'documento_url', type: 'varchar', length: 500, nullable: true }) - documentoUrl: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => ProveedorAmbiental) - @JoinColumn({ name: 'transportista_id' }) - transportista: ProveedorAmbiental; - - @ManyToOne(() => ProveedorAmbiental) - @JoinColumn({ name: 'destino_id' }) - destino: ProveedorAmbiental; - - @OneToMany(() => ManifiestoDetalle, (d) => d.manifiesto) - detalles: ManifiestoDetalle[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/norma-requisito.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/norma-requisito.entity.ts deleted file mode 100644 index bfc99ae30..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/norma-requisito.entity.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * NormaRequisito Entity - * Requisitos espec铆ficos por norma STPS - * - * @module HSE - * @table hse.norma_requisitos - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { NormaStps } from './norma-stps.entity'; - -@Entity({ schema: 'hse', name: 'norma_requisitos' }) -export class NormaRequisito { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'norma_id', type: 'uuid' }) - normaId: string; - - @Column({ type: 'varchar', length: 20 }) - numero: string; - - @Column({ type: 'text' }) - descripcion: string; - - @Column({ name: 'tipo_evidencia', type: 'varchar', length: 200, nullable: true }) - tipoEvidencia: string; - - @Column({ name: 'es_critico', type: 'boolean', default: false }) - esCritico: boolean; - - @Column({ name: 'aplica_a', type: 'varchar', length: 100, nullable: true }) - aplicaA: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => NormaStps, (n) => n.requisitos, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'norma_id' }) - norma: NormaStps; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/norma-stps.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/norma-stps.entity.ts deleted file mode 100644 index ff70e4a9e..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/norma-stps.entity.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * NormaStps Entity - * Cat谩logo de normas STPS - * - * @module HSE - * @table hse.normas_stps - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToMany, - Index, -} from 'typeorm'; -import { NormaRequisito } from './norma-requisito.entity'; - -@Entity({ schema: 'hse', name: 'normas_stps' }) -@Index(['codigo'], { unique: true }) -export class NormaStps { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 30, unique: true }) - codigo: string; - - @Column({ type: 'varchar', length: 300 }) - nombre: string; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ name: 'fecha_publicacion', type: 'date', nullable: true }) - fechaPublicacion: Date; - - @Column({ name: 'ultima_actualizacion', type: 'date', nullable: true }) - ultimaActualizacion: Date; - - @Column({ name: 'aplica_construccion', type: 'boolean', default: true }) - aplicaConstruccion: boolean; - - @Column({ name: 'documento_url', type: 'varchar', length: 500, nullable: true }) - documentoUrl: string; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @OneToMany(() => NormaRequisito, (r) => r.norma) - requisitos: NormaRequisito[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-autorizacion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/permiso-autorizacion.entity.ts deleted file mode 100644 index 8ac84fe5f..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-autorizacion.entity.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * PermisoAutorizacion Entity - * Autorizaciones de permisos de trabajo - * - * @module HSE - * @table hse.permiso_autorizaciones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-007 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { PermisoTrabajo } from './permiso-trabajo.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type DecisionAutorizacion = 'aprobado' | 'rechazado'; - -@Entity({ schema: 'hse', name: 'permiso_autorizaciones' }) -export class PermisoAutorizacion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'permiso_id', type: 'uuid' }) - permisoId: string; - - @Column({ type: 'integer' }) - nivel: number; - - @Column({ name: 'autorizador_id', type: 'uuid' }) - autorizadorId: string; - - @Column({ name: 'rol_autorizador', type: 'varchar', length: 100, nullable: true }) - rolAutorizador: string; - - @Column({ - type: 'enum', - enum: ['aprobado', 'rechazado'], - }) - decision: DecisionAutorizacion; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @Column({ name: 'firma_digital', type: 'text', nullable: true }) - firmaDigital: string; - - @Column({ name: 'fecha_decision', type: 'timestamptz', default: () => 'NOW()' }) - fechaDecision: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => PermisoTrabajo, (p) => p.autorizaciones, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'permiso_id' }) - permiso: PermisoTrabajo; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'autorizador_id' }) - autorizador: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-checklist.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/permiso-checklist.entity.ts deleted file mode 100644 index e97f50563..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-checklist.entity.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * PermisoChecklist Entity - * Checklist de verificaci贸n de permisos de trabajo - * - * @module HSE - * @table hse.permiso_checklist - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-007 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { PermisoTrabajo } from './permiso-trabajo.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type MomentoChecklist = 'pre_trabajo' | 'durante' | 'post_trabajo'; - -@Entity({ schema: 'hse', name: 'permiso_checklist' }) -export class PermisoChecklist { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'permiso_id', type: 'uuid' }) - permisoId: string; - - @Column({ - type: 'enum', - enum: ['pre_trabajo', 'durante', 'post_trabajo'], - }) - momento: MomentoChecklist; - - @Column({ name: 'item_verificacion', type: 'varchar', length: 300 }) - itemVerificacion: string; - - @Column({ type: 'boolean', nullable: true }) - cumple: boolean; - - @Column({ type: 'text', nullable: true }) - observacion: string; - - @Column({ name: 'verificador_id', type: 'uuid', nullable: true }) - verificadorId: string; - - @Column({ name: 'fecha_verificacion', type: 'timestamptz', nullable: true }) - fechaVerificacion: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => PermisoTrabajo, (p) => p.checklist, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'permiso_id' }) - permiso: PermisoTrabajo; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'verificador_id' }) - verificador: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-documento.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/permiso-documento.entity.ts deleted file mode 100644 index ef0654a4d..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-documento.entity.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * PermisoDocumento Entity - * Documentos adjuntos a permisos de trabajo - * - * @module HSE - * @table hse.permiso_documentos - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-007 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { PermisoTrabajo } from './permiso-trabajo.entity'; -import { User } from '../../core/entities/user.entity'; - -@Entity({ schema: 'hse', name: 'permiso_documentos' }) -export class PermisoDocumento { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'permiso_id', type: 'uuid' }) - permisoId: string; - - @Column({ name: 'tipo_documento', type: 'varchar', length: 100 }) - tipoDocumento: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ name: 'archivo_url', type: 'varchar', length: 500 }) - archivoUrl: string; - - @Column({ name: 'fecha_subida', type: 'timestamptz', default: () => 'NOW()' }) - fechaSubida: Date; - - @Column({ name: 'subido_por', type: 'uuid', nullable: true }) - subidoPorId: string; - - // Relations - @ManyToOne(() => PermisoTrabajo, (p) => p.documentos, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'permiso_id' }) - permiso: PermisoTrabajo; - - @ManyToOne(() => User) - @JoinColumn({ name: 'subido_por' }) - subidoPor: User; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-evento.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/permiso-evento.entity.ts deleted file mode 100644 index 4b976ffdb..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-evento.entity.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * PermisoEvento Entity - * Eventos durante ejecuci贸n de permisos de trabajo - * - * @module HSE - * @table hse.permiso_eventos - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-007 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { PermisoTrabajo } from './permiso-trabajo.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type TipoEventoPermiso = 'inicio' | 'suspension' | 'reanudacion' | 'extension' | 'anomalia' | 'cierre'; - -@Entity({ schema: 'hse', name: 'permiso_eventos' }) -export class PermisoEvento { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'permiso_id', type: 'uuid' }) - permisoId: string; - - @Column({ name: 'fecha_hora', type: 'timestamptz', default: () => 'NOW()' }) - fechaHora: Date; - - @Column({ - name: 'tipo_evento', - type: 'enum', - enum: ['inicio', 'suspension', 'reanudacion', 'extension', 'anomalia', 'cierre'], - }) - tipoEvento: TipoEventoPermiso; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ name: 'accion_tomada', type: 'text', nullable: true }) - accionTomada: string; - - @Column({ name: 'responsable_id', type: 'uuid' }) - responsableId: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => PermisoTrabajo, (p) => p.eventos, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'permiso_id' }) - permiso: PermisoTrabajo; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'responsable_id' }) - responsable: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-monitoreo.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/permiso-monitoreo.entity.ts deleted file mode 100644 index c5c3ccdde..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-monitoreo.entity.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * PermisoMonitoreo Entity - * Monitoreos durante ejecuci贸n de permisos de trabajo - * - * @module HSE - * @table hse.permiso_monitoreos - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-007 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { PermisoTrabajo } from './permiso-trabajo.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -@Entity({ schema: 'hse', name: 'permiso_monitoreos' }) -export class PermisoMonitoreo { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'permiso_id', type: 'uuid' }) - permisoId: string; - - @Column({ name: 'fecha_hora', type: 'timestamptz' }) - fechaHora: Date; - - @Column({ type: 'varchar', length: 100 }) - tipo: string; - - @Column({ name: 'valor_medicion', type: 'varchar', length: 50, nullable: true }) - valorMedicion: string; - - @Column({ type: 'varchar', length: 20, nullable: true }) - unidad: string; - - @Column({ name: 'dentro_rango', type: 'boolean', default: true }) - dentroRango: boolean; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @Column({ name: 'responsable_id', type: 'uuid' }) - responsableId: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => PermisoTrabajo, (p) => p.monitoreos, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'permiso_id' }) - permiso: PermisoTrabajo; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'responsable_id' }) - responsable: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-personal.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/permiso-personal.entity.ts deleted file mode 100644 index a3dcf92c8..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-personal.entity.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * PermisoPersonal Entity - * Personal asignado a permisos de trabajo - * - * @module HSE - * @table hse.permiso_personal - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-007 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { PermisoTrabajo } from './permiso-trabajo.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type RolPermiso = 'ejecutor' | 'supervisor' | 'vigia' | 'operador' | 'senalero'; - -@Entity({ schema: 'hse', name: 'permiso_personal' }) -export class PermisoPersonal { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'permiso_id', type: 'uuid' }) - permisoId: string; - - @Column({ name: 'employee_id', type: 'uuid' }) - employeeId: string; - - @Column({ - type: 'enum', - enum: ['ejecutor', 'supervisor', 'vigia', 'operador', 'senalero'], - }) - rol: RolPermiso; - - @Column({ name: 'verificacion_capacitacion', type: 'boolean', default: false }) - verificacionCapacitacion: boolean; - - @Column({ name: 'verificacion_epp', type: 'boolean', default: false }) - verificacionEpp: boolean; - - @Column({ name: 'verificacion_aptitud', type: 'boolean', default: false }) - verificacionAptitud: boolean; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => PermisoTrabajo, (p) => p.personal, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'permiso_id' }) - permiso: PermisoTrabajo; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'employee_id' }) - employee: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-trabajo.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/permiso-trabajo.entity.ts deleted file mode 100644 index 143352406..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/permiso-trabajo.entity.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * PermisoTrabajo Entity - * Permisos de trabajo de alto riesgo - * - * @module HSE - * @table hse.permisos_trabajo - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-007 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { TipoPermisoTrabajo } from './tipo-permiso-trabajo.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { Employee } from '../../hr/entities/employee.entity'; -import { PermisoPersonal } from './permiso-personal.entity'; -import { PermisoAutorizacion } from './permiso-autorizacion.entity'; -import { PermisoChecklist } from './permiso-checklist.entity'; -import { PermisoMonitoreo } from './permiso-monitoreo.entity'; -import { PermisoEvento } from './permiso-evento.entity'; -import { PermisoDocumento } from './permiso-documento.entity'; - -export type EstadoPermiso = 'borrador' | 'solicitado' | 'aprobado_parcial' | 'autorizado' | 'en_ejecucion' | 'suspendido' | 'cerrado' | 'rechazado' | 'vencido'; - -@Entity({ schema: 'hse', name: 'permisos_trabajo' }) -@Index(['tenantId', 'folio'], { unique: true }) -@Index(['estado']) -@Index(['fechaInicioProgramada']) -export class PermisoTrabajo { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 30 }) - folio: string; - - @Column({ name: 'tipo_permiso_id', type: 'uuid' }) - tipoPermisoId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'solicitante_id', type: 'uuid' }) - solicitanteId: string; - - @Column({ name: 'descripcion_trabajo', type: 'text' }) - descripcionTrabajo: string; - - @Column({ type: 'varchar', length: 300 }) - ubicacion: string; - - @Column({ - name: 'ubicacion_geo', - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true, - }) - ubicacionGeo: string; - - @Column({ name: 'fecha_inicio_programada', type: 'timestamptz' }) - fechaInicioProgramada: Date; - - @Column({ name: 'fecha_fin_programada', type: 'timestamptz' }) - fechaFinProgramada: Date; - - @Column({ name: 'fecha_inicio_real', type: 'timestamptz', nullable: true }) - fechaInicioReal: Date; - - @Column({ name: 'fecha_fin_real', type: 'timestamptz', nullable: true }) - fechaFinReal: Date; - - @Column({ - type: 'enum', - enum: ['borrador', 'solicitado', 'aprobado_parcial', 'autorizado', 'en_ejecucion', 'suspendido', 'cerrado', 'rechazado', 'vencido'], - default: 'borrador', - }) - estado: EstadoPermiso; - - @Column({ name: 'motivo_rechazo', type: 'text', nullable: true }) - motivoRechazo: string; - - @Column({ name: 'motivo_suspension', type: 'text', nullable: true }) - motivoSuspension: string; - - @Column({ type: 'text', nullable: true }) - observaciones: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => TipoPermisoTrabajo) - @JoinColumn({ name: 'tipo_permiso_id' }) - tipoPermiso: TipoPermisoTrabajo; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'solicitante_id' }) - solicitante: Employee; - - @OneToMany(() => PermisoPersonal, (p) => p.permiso) - personal: PermisoPersonal[]; - - @OneToMany(() => PermisoAutorizacion, (a) => a.permiso) - autorizaciones: PermisoAutorizacion[]; - - @OneToMany(() => PermisoChecklist, (c) => c.permiso) - checklist: PermisoChecklist[]; - - @OneToMany(() => PermisoMonitoreo, (m) => m.permiso) - monitoreos: PermisoMonitoreo[]; - - @OneToMany(() => PermisoEvento, (e) => e.permiso) - eventos: PermisoEvento[]; - - @OneToMany(() => PermisoDocumento, (d) => d.permiso) - documentos: PermisoDocumento[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/programa-actividad.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/programa-actividad.entity.ts deleted file mode 100644 index ee8c20e26..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/programa-actividad.entity.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * ProgramaActividad Entity - * Actividades del programa de seguridad - * - * @module HSE - * @table hse.programa_actividades - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { ProgramaSeguridad } from './programa-seguridad.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type TipoActividadPrograma = 'capacitacion' | 'inspeccion' | 'simulacro' | 'campana' | 'otro'; -export type EstadoActividad = 'pendiente' | 'en_progreso' | 'completada' | 'cancelada'; - -@Entity({ schema: 'hse', name: 'programa_actividades' }) -export class ProgramaActividad { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'programa_id', type: 'uuid' }) - programaId: string; - - @Column({ type: 'varchar', length: 300 }) - actividad: string; - - @Column({ - type: 'enum', - enum: ['capacitacion', 'inspeccion', 'simulacro', 'campana', 'otro'], - }) - tipo: TipoActividadPrograma; - - @Column({ name: 'fecha_programada', type: 'date' }) - fechaProgramada: Date; - - @Column({ name: 'fecha_realizada', type: 'date', nullable: true }) - fechaRealizada: Date; - - @Column({ name: 'responsable_id', type: 'uuid', nullable: true }) - responsableId: string; - - @Column({ type: 'text', nullable: true }) - recursos: string; - - @Column({ - type: 'enum', - enum: ['pendiente', 'en_progreso', 'completada', 'cancelada'], - default: 'pendiente', - }) - estado: EstadoActividad; - - @Column({ name: 'evidencia_url', type: 'varchar', length: 500, nullable: true }) - evidenciaUrl: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => ProgramaSeguridad, (p) => p.actividades, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'programa_id' }) - programa: ProgramaSeguridad; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'responsable_id' }) - responsable: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/programa-inspeccion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/programa-inspeccion.entity.ts deleted file mode 100644 index 3fcefe86b..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/programa-inspeccion.entity.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * ProgramaInspeccion Entity - * Programa de inspecciones de seguridad - * - * @module HSE - * @table hse.programa_inspecciones - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-003 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { TipoInspeccion } from './tipo-inspeccion.entity'; -import { Employee } from '../../hr/entities/employee.entity'; - -export type EstadoInspeccion = 'programada' | 'en_progreso' | 'completada' | 'cancelada' | 'vencida'; - -@Entity({ schema: 'hse', name: 'programa_inspecciones' }) -export class ProgramaInspeccion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'tipo_inspeccion_id', type: 'uuid' }) - tipoInspeccionId: string; - - @Column({ name: 'inspector_id', type: 'uuid', nullable: true }) - inspectorId: string; - - @Column({ name: 'fecha_programada', type: 'date' }) - fechaProgramada: Date; - - @Column({ name: 'hora_programada', type: 'time', nullable: true }) - horaProgramada: string; - - @Column({ name: 'zona_area', type: 'varchar', length: 200, nullable: true }) - zonaArea: string; - - @Column({ - type: 'enum', - enum: ['programada', 'en_progreso', 'completada', 'cancelada', 'vencida'], - default: 'programada', - }) - estado: EstadoInspeccion; - - @Column({ name: 'motivo_cancelacion', type: 'text', nullable: true }) - motivoCancelacion: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => TipoInspeccion) - @JoinColumn({ name: 'tipo_inspeccion_id' }) - tipoInspeccion: TipoInspeccion; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'inspector_id' }) - inspector: Employee; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/programa-seguridad.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/programa-seguridad.entity.ts deleted file mode 100644 index bc53dcedd..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/programa-seguridad.entity.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * ProgramaSeguridad Entity - * Programa anual de seguridad - * - * @module HSE - * @table hse.programa_seguridad - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-005 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { Employee } from '../../hr/entities/employee.entity'; -import { ProgramaActividad } from './programa-actividad.entity'; - -export type EstadoPrograma = 'borrador' | 'activo' | 'finalizado'; - -@Entity({ schema: 'hse', name: 'programa_seguridad' }) -export class ProgramaSeguridad { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ type: 'integer' }) - anio: number; - - @Column({ name: 'objetivo_general', type: 'text', nullable: true }) - objetivoGeneral: string; - - @Column({ type: 'jsonb', nullable: true }) - metas: Record; - - @Column({ type: 'decimal', precision: 12, scale: 2, nullable: true }) - presupuesto: number; - - @Column({ - type: 'enum', - enum: ['borrador', 'activo', 'finalizado'], - default: 'borrador', - }) - estado: EstadoPrograma; - - @Column({ name: 'aprobado_por', type: 'uuid', nullable: true }) - aprobadoPorId: string; - - @Column({ name: 'fecha_aprobacion', type: 'date', nullable: true }) - fechaAprobacion: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => Employee) - @JoinColumn({ name: 'aprobado_por' }) - aprobadoPor: Employee; - - @OneToMany(() => ProgramaActividad, (a) => a.programa) - actividades: ProgramaActividad[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/proveedor-ambiental.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/proveedor-ambiental.entity.ts deleted file mode 100644 index 463e702a7..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/proveedor-ambiental.entity.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * ProveedorAmbiental Entity - * Proveedores autorizados para manejo de residuos - * - * @module HSE - * @table hse.proveedores_ambientales - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-006 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; - -export type TipoProveedorAmbiental = 'transportista' | 'reciclador' | 'confinamiento'; - -@Entity({ schema: 'hse', name: 'proveedores_ambientales' }) -export class ProveedorAmbiental { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'razon_social', type: 'varchar', length: 300 }) - razonSocial: string; - - @Column({ type: 'varchar', length: 13, nullable: true }) - rfc: string; - - @Column({ - type: 'enum', - enum: ['transportista', 'reciclador', 'confinamiento'], - }) - tipo: TipoProveedorAmbiental; - - @Column({ name: 'numero_autorizacion', type: 'varchar', length: 50, nullable: true }) - numeroAutorizacion: string; - - @Column({ name: 'entidad_autorizadora', type: 'varchar', length: 100, nullable: true }) - entidadAutorizadora: string; - - @Column({ name: 'fecha_autorizacion', type: 'date', nullable: true }) - fechaAutorizacion: Date; - - @Column({ name: 'fecha_vencimiento', type: 'date', nullable: true }) - fechaVencimiento: Date; - - @Column({ type: 'text', nullable: true }) - servicios: string; - - @Column({ name: 'contacto_nombre', type: 'varchar', length: 200, nullable: true }) - contactoNombre: string; - - @Column({ name: 'contacto_telefono', type: 'varchar', length: 20, nullable: true }) - contactoTelefono: string; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/queja-ambiental.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/queja-ambiental.entity.ts deleted file mode 100644 index 2f9a4967f..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/queja-ambiental.entity.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * QuejaAmbiental Entity - * Quejas ambientales de vecinos y autoridades - * - * @module HSE - * @table hse.quejas_ambientales - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-006 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -export type OrigenQueja = 'vecino' | 'autoridad' | 'interno' | 'anonimo'; -export type TipoQueja = 'ruido' | 'polvo' | 'olores' | 'agua' | 'otro'; -export type EstadoQueja = 'recibida' | 'atendiendo' | 'cerrada'; - -@Entity({ schema: 'hse', name: 'quejas_ambientales' }) -export class QuejaAmbiental { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'fecha_queja', type: 'timestamptz' }) - fechaQueja: Date; - - @Column({ - type: 'enum', - enum: ['vecino', 'autoridad', 'interno', 'anonimo'], - }) - origen: OrigenQueja; - - @Column({ - type: 'enum', - enum: ['ruido', 'polvo', 'olores', 'agua', 'otro'], - }) - tipo: TipoQueja; - - @Column({ type: 'text' }) - descripcion: string; - - @Column({ name: 'nombre_quejoso', type: 'varchar', length: 200, nullable: true }) - nombreQuejoso: string; - - @Column({ name: 'contacto_quejoso', type: 'varchar', length: 100, nullable: true }) - contactoQuejoso: string; - - @Column({ name: 'acciones_tomadas', type: 'text', nullable: true }) - accionesTomadas: string; - - @Column({ - type: 'enum', - enum: ['recibida', 'atendiendo', 'cerrada'], - default: 'recibida', - }) - estado: EstadoQueja; - - @Column({ name: 'fecha_cierre', type: 'date', nullable: true }) - fechaCierre: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/reporte-programado.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/reporte-programado.entity.ts deleted file mode 100644 index 49bc26b26..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/reporte-programado.entity.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * ReporteProgramado Entity - * Reportes HSE programados para env铆o autom谩tico - * - * @module HSE - * @table hse.reportes_programados - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-008 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; - -export type TipoReporteHse = 'semanal' | 'mensual' | 'trimestral' | 'anual'; -export type FormatoReporte = 'pdf' | 'excel' | 'ambos'; - -@Entity({ schema: 'hse', name: 'reportes_programados' }) -export class ReporteProgramado { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ - name: 'tipo_reporte', - type: 'enum', - enum: ['semanal', 'mensual', 'trimestral', 'anual'], - }) - tipoReporte: TipoReporteHse; - - @Column({ type: 'uuid', array: true, nullable: true }) - indicadores: string[]; - - @Column({ type: 'uuid', array: true, nullable: true }) - fraccionamientos: string[]; - - @Column({ type: 'varchar', array: true, nullable: true }) - destinatarios: string[]; - - @Column({ name: 'dia_envio', type: 'integer', nullable: true }) - diaEnvio: number; - - @Column({ name: 'hora_envio', type: 'time', nullable: true }) - horaEnvio: string; - - @Column({ - type: 'enum', - enum: ['pdf', 'excel', 'ambos'], - default: 'pdf', - }) - formato: FormatoReporte; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @Column({ name: 'ultimo_envio', type: 'timestamptz', nullable: true }) - ultimoEnvio: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/residuo-catalogo.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/residuo-catalogo.entity.ts deleted file mode 100644 index d5779d677..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/residuo-catalogo.entity.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * ResiduoCatalogo Entity - * Cat谩logo de tipos de residuos - * - * @module HSE - * @table hse.residuos_catalogo - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-006 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - Index, -} from 'typeorm'; - -export type CategoriaResiduo = 'peligroso' | 'manejo_especial' | 'urbano'; - -@Entity({ schema: 'hse', name: 'residuos_catalogo' }) -@Index(['codigo'], { unique: true }) -export class ResiduoCatalogo { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 20, unique: true }) - codigo: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ - type: 'enum', - enum: ['peligroso', 'manejo_especial', 'urbano'], - }) - categoria: CategoriaResiduo; - - @Column({ name: 'caracteristicas_cretib', type: 'varchar', length: 6, nullable: true }) - caracteristicasCretib: string; - - @Column({ name: 'norma_referencia', type: 'varchar', length: 50, nullable: true }) - normaReferencia: string; - - @Column({ name: 'manejo_requerido', type: 'text', nullable: true }) - manejoRequerido: string; - - @Column({ name: 'tiempo_max_almacen_dias', type: 'integer', nullable: true }) - tiempoMaxAlmacenDias: number; - - @Column({ name: 'requiere_manifiesto', type: 'boolean', default: false }) - requiereManifiesto: boolean; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/residuo-generacion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/residuo-generacion.entity.ts deleted file mode 100644 index 4fe7ed592..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/residuo-generacion.entity.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * ResiduoGeneracion Entity - * Registro de generaci贸n de residuos - * - * @module HSE - * @table hse.residuos_generacion - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-006 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { ResiduoCatalogo } from './residuo-catalogo.entity'; -import { User } from '../../core/entities/user.entity'; - -export type UnidadResiduo = 'kg' | 'litros' | 'm3' | 'piezas'; -export type EstadoResiduo = 'almacenado' | 'en_transito' | 'dispuesto'; - -@Entity({ schema: 'hse', name: 'residuos_generacion' }) -@Index(['fechaGeneracion']) -export class ResiduoGeneracion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'residuo_id', type: 'uuid' }) - residuoId: string; - - @Column({ name: 'fecha_generacion', type: 'date' }) - fechaGeneracion: Date; - - @Column({ type: 'decimal', precision: 10, scale: 2 }) - cantidad: number; - - @Column({ - type: 'enum', - enum: ['kg', 'litros', 'm3', 'piezas'], - }) - unidad: UnidadResiduo; - - @Column({ name: 'area_generacion', type: 'varchar', length: 200, nullable: true }) - areaGeneracion: string; - - @Column({ type: 'varchar', length: 200, nullable: true }) - fuente: string; - - @Column({ name: 'contenedor_id', type: 'varchar', length: 50, nullable: true }) - contenedorId: string; - - @Column({ name: 'foto_url', type: 'varchar', length: 500, nullable: true }) - fotoUrl: string; - - @Column({ - name: 'ubicacion_geo', - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true, - }) - ubicacionGeo: string; - - @Column({ - type: 'enum', - enum: ['almacenado', 'en_transito', 'dispuesto'], - default: 'almacenado', - }) - estado: EstadoResiduo; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => ResiduoCatalogo) - @JoinColumn({ name: 'residuo_id' }) - residuo: ResiduoCatalogo; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/tipo-inspeccion.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/tipo-inspeccion.entity.ts deleted file mode 100644 index 26ac20c72..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/tipo-inspeccion.entity.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * TipoInspeccion Entity - * Tipos de inspecci贸n de seguridad - * - * @module HSE - * @table hse.tipos_inspeccion - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-003 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { ChecklistItem } from './checklist-item.entity'; - -export type Frecuencia = 'diaria' | 'semanal' | 'quincenal' | 'mensual' | 'eventual'; - -@Entity({ schema: 'hse', name: 'tipos_inspeccion' }) -@Index(['tenantId', 'codigo'], { unique: true }) -export class TipoInspeccion { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ - type: 'enum', - enum: ['diaria', 'semanal', 'quincenal', 'mensual', 'eventual'], - }) - frecuencia: Frecuencia; - - @Column({ name: 'norma_referencia', type: 'varchar', length: 50, nullable: true }) - normaReferencia: string; - - @Column({ name: 'duracion_estimada_min', type: 'integer', nullable: true }) - duracionEstimadaMin: number; - - @Column({ name: 'requiere_firma', type: 'boolean', default: true }) - requiereFirma: boolean; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @OneToMany(() => ChecklistItem, (item) => item.tipoInspeccion) - checklistItems: ChecklistItem[]; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/entities/tipo-permiso-trabajo.entity.ts b/projects/erp-construccion/backend/src/modules/hse/entities/tipo-permiso-trabajo.entity.ts deleted file mode 100644 index ca5b622c7..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/entities/tipo-permiso-trabajo.entity.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * TipoPermisoTrabajo Entity - * Tipos de permisos de trabajo de alto riesgo - * - * @module HSE - * @table hse.tipos_permiso_trabajo - * @ddl schemas/03-hse-schema-ddl.sql - * @rf RF-MAA017-007 - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; - -@Entity({ schema: 'hse', name: 'tipos_permiso_trabajo' }) -@Index(['tenantId', 'codigo'], { unique: true }) -export class TipoPermisoTrabajo { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 20 }) - codigo: string; - - @Column({ type: 'varchar', length: 200 }) - nombre: string; - - @Column({ type: 'text', nullable: true }) - descripcion: string; - - @Column({ name: 'norma_referencia', type: 'varchar', length: 50, nullable: true }) - normaReferencia: string; - - @Column({ name: 'vigencia_max_horas', type: 'integer' }) - vigenciaMaxHoras: number; - - @Column({ name: 'requiere_autorizacion_nivel', type: 'integer', default: 2 }) - requiereAutorizacionNivel: number; - - @Column({ name: 'documentos_requeridos', type: 'jsonb', nullable: true }) - documentosRequeridos: Record; - - @Column({ name: 'requisitos_personal', type: 'jsonb', nullable: true }) - requisitosPersonal: Record; - - @Column({ name: 'equipos_requeridos', type: 'jsonb', nullable: true }) - equiposRequeridos: Record; - - @Column({ type: 'boolean', default: true }) - activo: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} diff --git a/projects/erp-construccion/backend/src/modules/hse/services/ambiental.service.ts b/projects/erp-construccion/backend/src/modules/hse/services/ambiental.service.ts deleted file mode 100644 index afa549865..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/ambiental.service.ts +++ /dev/null @@ -1,632 +0,0 @@ -/** - * AmbientalService - Servicio para gesti贸n ambiental - * - * RF-MAA017-006: Gesti贸n de residuos, manifiestos e impactos - * - * @module HSE - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { ResiduoCatalogo, CategoriaResiduo } from '../entities/residuo-catalogo.entity'; -import { ResiduoGeneracion, UnidadResiduo, EstadoResiduo } from '../entities/residuo-generacion.entity'; -import { AlmacenTemporal } from '../entities/almacen-temporal.entity'; -import { ProveedorAmbiental } from '../entities/proveedor-ambiental.entity'; -import { ManifiestoResiduos, EstadoManifiesto } from '../entities/manifiesto-residuos.entity'; -import { ManifiestoDetalle } from '../entities/manifiesto-detalle.entity'; -import { ImpactoAmbiental, TipoImpacto, NivelRiesgo, EstadoImpacto } from '../entities/impacto-ambiental.entity'; -import { QuejaAmbiental, OrigenQueja, TipoQueja, EstadoQueja } from '../entities/queja-ambiental.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface ResiduoFilters { - categoria?: CategoriaResiduo; - activo?: boolean; - search?: string; -} - -export interface GeneracionFilters { - fraccionamientoId?: string; - residuoId?: string; - estado?: EstadoResiduo; - dateFrom?: Date; - dateTo?: Date; -} - -export interface ManifiestoFilters { - fraccionamientoId?: string; - estado?: EstadoManifiesto; - transportistaId?: string; - dateFrom?: Date; - dateTo?: Date; -} - -export interface ImpactoFilters { - fraccionamientoId?: string; - tipoImpacto?: TipoImpacto; - nivelRiesgo?: NivelRiesgo; - estado?: EstadoImpacto; -} - -export interface QuejaFilters { - fraccionamientoId?: string; - tipo?: TipoQueja; - estado?: EstadoQueja; - dateFrom?: Date; - dateTo?: Date; -} - -export interface CreateResiduoCatalogoDto { - codigo: string; - nombre: string; - categoria: CategoriaResiduo; - caracteristicasCretib?: string; - normaReferencia?: string; - manejoRequerido?: string; - tiempoMaxAlmacenDias?: number; - requiereManifiesto?: boolean; -} - -export interface CreateGeneracionDto { - fraccionamientoId: string; - residuoId: string; - fechaGeneracion: Date; - cantidad: number; - unidad: UnidadResiduo; - areaGeneracion?: string; - fuente?: string; - contenedorId?: string; -} - -export interface CreateManifiestoDto { - fraccionamientoId: string; - transportistaId: string; - destinoId: string; - fechaRecoleccion: Date; - observaciones?: string; -} - -export interface AddDetalleManifiestoDto { - residuoId: string; - generacionIds?: string[]; - cantidad: number; - unidad: UnidadResiduo; - descripcion?: string; -} - -export interface CreateImpactoDto { - fraccionamientoId: string; - aspecto: string; - tipoImpacto: TipoImpacto; - severidad: 'bajo' | 'medio' | 'alto'; - probabilidad: 'baja' | 'media' | 'alta'; - medidasMitigacion?: string; - responsableId?: string; -} - -export interface CreateQuejaDto { - fraccionamientoId: string; - origen: OrigenQueja; - tipo: TipoQueja; - descripcion: string; - nombreQuejoso?: string; - contactoQuejoso?: string; -} - -export interface AmbientalStats { - residuosGenerados: number; - residuosPendientes: number; - manifiestosPendientes: number; - impactosIdentificados: number; - impactosSignificativos: number; - quejasAbiertas: number; - residuosPorCategoria: { categoria: string; cantidad: number }[]; -} - -export class AmbientalService { - constructor( - private readonly residuoCatalogoRepository: Repository, - private readonly generacionRepository: Repository, - private readonly almacenRepository: Repository, - private readonly proveedorRepository: Repository, - private readonly manifiestoRepository: Repository, - private readonly detalleRepository: Repository, - private readonly impactoRepository: Repository, - private readonly quejaRepository: Repository - ) {} - - private generateFolio(prefix: string): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - return `${prefix}-${year}${month}-${random}`; - } - - // ========== Cat谩logo de Residuos ========== - async findResiduos( - _ctx: ServiceContext, - filters: ResiduoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.residuoCatalogoRepository - .createQueryBuilder('r'); - - if (filters.categoria) { - queryBuilder.andWhere('r.categoria = :categoria', { categoria: filters.categoria }); - } - - if (filters.activo !== undefined) { - queryBuilder.andWhere('r.activo = :activo', { activo: filters.activo }); - } - - if (filters.search) { - queryBuilder.andWhere( - '(r.codigo ILIKE :search OR r.nombre ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - queryBuilder - .orderBy('r.nombre', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async createResiduo(dto: CreateResiduoCatalogoDto): Promise { - const residuo = this.residuoCatalogoRepository.create({ - codigo: dto.codigo, - nombre: dto.nombre, - categoria: dto.categoria, - caracteristicasCretib: dto.caracteristicasCretib, - normaReferencia: dto.normaReferencia, - manejoRequerido: dto.manejoRequerido, - tiempoMaxAlmacenDias: dto.tiempoMaxAlmacenDias, - requiereManifiesto: dto.requiereManifiesto ?? false, - activo: true, - }); - return this.residuoCatalogoRepository.save(residuo); - } - - // ========== Generaci贸n de Residuos ========== - async findGeneraciones( - ctx: ServiceContext, - filters: GeneracionFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.generacionRepository - .createQueryBuilder('g') - .leftJoinAndSelect('g.residuo', 'residuo') - .leftJoinAndSelect('g.fraccionamiento', 'fraccionamiento') - .where('g.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('g.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.residuoId) { - queryBuilder.andWhere('g.residuo_id = :residuoId', { residuoId: filters.residuoId }); - } - - if (filters.estado) { - queryBuilder.andWhere('g.estado = :estado', { estado: filters.estado }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('g.fecha_generacion >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('g.fecha_generacion <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('g.fecha_generacion', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async createGeneracion(ctx: ServiceContext, dto: CreateGeneracionDto): Promise { - const generacion = this.generacionRepository.create({ - tenantId: ctx.tenantId, - fraccionamientoId: dto.fraccionamientoId, - residuoId: dto.residuoId, - fechaGeneracion: dto.fechaGeneracion, - cantidad: dto.cantidad, - unidad: dto.unidad, - areaGeneracion: dto.areaGeneracion, - fuente: dto.fuente, - contenedorId: dto.contenedorId, - estado: 'almacenado', - createdById: ctx.userId, - }); - - return this.generacionRepository.save(generacion); - } - - async updateGeneracionEstado(ctx: ServiceContext, id: string, estado: EstadoResiduo): Promise { - const generacion = await this.generacionRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!generacion) return null; - - generacion.estado = estado; - return this.generacionRepository.save(generacion); - } - - // ========== Almacenes Temporales ========== - async findAlmacenes(ctx: ServiceContext, fraccionamientoId?: string): Promise { - const where: FindOptionsWhere = { tenantId: ctx.tenantId }; - if (fraccionamientoId) { - where.fraccionamientoId = fraccionamientoId; - } - - return this.almacenRepository.find({ - where, - relations: ['fraccionamiento'], - }); - } - - // ========== Proveedores Ambientales ========== - async findProveedores(ctx: ServiceContext): Promise { - return this.proveedorRepository.find({ - where: { tenantId: ctx.tenantId, activo: true } as FindOptionsWhere, - }); - } - - // ========== Manifiestos ========== - async findManifiestos( - ctx: ServiceContext, - filters: ManifiestoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.manifiestoRepository - .createQueryBuilder('m') - .leftJoinAndSelect('m.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('m.transportista', 'transportista') - .leftJoinAndSelect('m.destino', 'destino') - .where('m.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('m.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.estado) { - queryBuilder.andWhere('m.estado = :estado', { estado: filters.estado }); - } - - if (filters.transportistaId) { - queryBuilder.andWhere('m.transportista_id = :transportistaId', { transportistaId: filters.transportistaId }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('m.fecha_recoleccion >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('m.fecha_recoleccion <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('m.fecha_recoleccion', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async findManifiestoWithDetalles(ctx: ServiceContext, id: string): Promise { - return this.manifiestoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['fraccionamiento', 'transportista', 'destino', 'detalles', 'detalles.residuo'], - }); - } - - async createManifiesto(ctx: ServiceContext, dto: CreateManifiestoDto): Promise { - const manifiesto = this.manifiestoRepository.create({ - tenantId: ctx.tenantId, - folio: this.generateFolio('MAN'), - fraccionamientoId: dto.fraccionamientoId, - transportistaId: dto.transportistaId, - destinoId: dto.destinoId, - fechaRecoleccion: dto.fechaRecoleccion, - observaciones: dto.observaciones, - estado: 'emitido', - }); - - return this.manifiestoRepository.save(manifiesto); - } - - async addDetalleManifiesto(ctx: ServiceContext, manifiestoId: string, dto: AddDetalleManifiestoDto): Promise { - const manifiesto = await this.manifiestoRepository.findOne({ - where: { id: manifiestoId, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!manifiesto) throw new Error('Manifiesto no encontrado'); - - const detalle = this.detalleRepository.create({ - manifiestoId, - residuoId: dto.residuoId, - generacionIds: dto.generacionIds, - cantidad: dto.cantidad, - unidad: dto.unidad, - descripcion: dto.descripcion, - }); - - return this.detalleRepository.save(detalle); - } - - async updateManifiestoEstado(ctx: ServiceContext, id: string, estado: EstadoManifiesto): Promise { - const manifiesto = await this.manifiestoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!manifiesto) return null; - - manifiesto.estado = estado; - if (estado === 'entregado') { - manifiesto.fechaEntrega = new Date(); - } - - return this.manifiestoRepository.save(manifiesto); - } - - // ========== Impactos Ambientales ========== - async findImpactos( - ctx: ServiceContext, - filters: ImpactoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.impactoRepository - .createQueryBuilder('i') - .leftJoinAndSelect('i.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('i.responsable', 'responsable') - .where('i.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('i.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.tipoImpacto) { - queryBuilder.andWhere('i.tipo_impacto = :tipoImpacto', { tipoImpacto: filters.tipoImpacto }); - } - - if (filters.nivelRiesgo) { - queryBuilder.andWhere('i.nivel_riesgo = :nivelRiesgo', { nivelRiesgo: filters.nivelRiesgo }); - } - - if (filters.estado) { - queryBuilder.andWhere('i.estado = :estado', { estado: filters.estado }); - } - - queryBuilder - .orderBy('i.created_at', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async createImpacto(ctx: ServiceContext, dto: CreateImpactoDto): Promise { - // Calculate risk level - const riskMatrix: Record> = { - bajo: { baja: 'tolerable', media: 'tolerable', alta: 'moderado' }, - medio: { baja: 'tolerable', media: 'moderado', alta: 'significativo' }, - alto: { baja: 'moderado', media: 'significativo', alta: 'significativo' }, - }; - - const nivelRiesgo = riskMatrix[dto.severidad][dto.probabilidad]; - - const impacto = this.impactoRepository.create({ - tenantId: ctx.tenantId, - fraccionamientoId: dto.fraccionamientoId, - aspecto: dto.aspecto, - tipoImpacto: dto.tipoImpacto, - severidad: dto.severidad, - probabilidad: dto.probabilidad, - nivelRiesgo, - medidasMitigacion: dto.medidasMitigacion, - responsableId: dto.responsableId, - estado: 'identificado', - }); - - return this.impactoRepository.save(impacto); - } - - async updateImpactoEstado(ctx: ServiceContext, id: string, estado: EstadoImpacto): Promise { - const impacto = await this.impactoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!impacto) return null; - - impacto.estado = estado; - return this.impactoRepository.save(impacto); - } - - // ========== Quejas Ambientales ========== - async findQuejas( - ctx: ServiceContext, - filters: QuejaFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.quejaRepository - .createQueryBuilder('q') - .leftJoinAndSelect('q.fraccionamiento', 'fraccionamiento') - .where('q.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('q.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.tipo) { - queryBuilder.andWhere('q.tipo = :tipo', { tipo: filters.tipo }); - } - - if (filters.estado) { - queryBuilder.andWhere('q.estado = :estado', { estado: filters.estado }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('q.fecha_queja >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('q.fecha_queja <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('q.fecha_queja', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async createQueja(ctx: ServiceContext, dto: CreateQuejaDto): Promise { - const queja = this.quejaRepository.create({ - tenantId: ctx.tenantId, - fraccionamientoId: dto.fraccionamientoId, - fechaQueja: new Date(), - origen: dto.origen, - tipo: dto.tipo, - descripcion: dto.descripcion, - nombreQuejoso: dto.nombreQuejoso, - contactoQuejoso: dto.contactoQuejoso, - estado: 'recibida', - }); - - return this.quejaRepository.save(queja); - } - - async atenderQueja(ctx: ServiceContext, id: string, accionesTomadas: string): Promise { - const queja = await this.quejaRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!queja) return null; - - queja.accionesTomadas = accionesTomadas; - queja.estado = 'atendiendo'; - - return this.quejaRepository.save(queja); - } - - async cerrarQueja(ctx: ServiceContext, id: string): Promise { - const queja = await this.quejaRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!queja) return null; - - queja.fechaCierre = new Date(); - queja.estado = 'cerrada'; - - return this.quejaRepository.save(queja); - } - - // ========== Estad铆sticas ========== - async getStats(ctx: ServiceContext, fraccionamientoId?: string): Promise { - const genQuery = this.generacionRepository - .createQueryBuilder('g') - .where('g.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - if (fraccionamientoId) { - genQuery.andWhere('g.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - const residuosGenerados = await genQuery.getCount(); - - const residuosPendientes = await this.generacionRepository - .createQueryBuilder('g') - .where('g.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('g.estado = :estado', { estado: 'almacenado' }) - .getCount(); - - const manifiestosPendientes = await this.manifiestoRepository - .createQueryBuilder('m') - .where('m.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('m.estado IN (:...estados)', { estados: ['emitido', 'en_transito'] }) - .getCount(); - - const impactosIdentificados = await this.impactoRepository.count({ - where: { tenantId: ctx.tenantId } as FindOptionsWhere, - }); - - const impactosSignificativos = await this.impactoRepository.count({ - where: { tenantId: ctx.tenantId, nivelRiesgo: 'significativo' } as FindOptionsWhere, - }); - - const quejasAbiertas = await this.quejaRepository - .createQueryBuilder('q') - .where('q.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('q.estado != :cerrada', { cerrada: 'cerrada' }) - .getCount(); - - // Residuos por categor铆a - const residuosPorCategoria = await this.generacionRepository - .createQueryBuilder('g') - .innerJoin('g.residuo', 'r') - .select('r.categoria', 'categoria') - .addSelect('SUM(g.cantidad)', 'cantidad') - .where('g.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .groupBy('r.categoria') - .getRawMany(); - - return { - residuosGenerados, - residuosPendientes, - manifiestosPendientes, - impactosIdentificados, - impactosSignificativos, - quejasAbiertas, - residuosPorCategoria: residuosPorCategoria.map((r) => ({ - categoria: r.categoria, - cantidad: parseFloat(r.cantidad || '0'), - })), - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/hse/services/capacitacion.service.ts b/projects/erp-construccion/backend/src/modules/hse/services/capacitacion.service.ts deleted file mode 100644 index 62740cc9d..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/capacitacion.service.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * CapacitacionService - Servicio para cat谩logo de capacitaciones HSE - * - * Gesti贸n de capacitaciones con CRUD y filtros. - * - * @module HSE - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { Capacitacion, TipoCapacitacion } from '../entities/capacitacion.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateCapacitacionDto { - codigo: string; - nombre: string; - descripcion?: string; - tipo: TipoCapacitacion; - duracionHoras?: number; - vigenciaMeses?: number; - requiereEvaluacion?: boolean; - calificacionMinima?: number; -} - -export interface UpdateCapacitacionDto { - nombre?: string; - descripcion?: string; - tipo?: TipoCapacitacion; - duracionHoras?: number; - vigenciaMeses?: number; - requiereEvaluacion?: boolean; - calificacionMinima?: number; - activo?: boolean; -} - -export interface CapacitacionFilters { - tipo?: TipoCapacitacion; - activo?: boolean; - search?: string; -} - -export class CapacitacionService { - constructor(private readonly repository: Repository) {} - - async findAll( - ctx: ServiceContext, - filters: CapacitacionFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.repository - .createQueryBuilder('capacitacion') - .where('capacitacion.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.tipo) { - queryBuilder.andWhere('capacitacion.tipo = :tipo', { tipo: filters.tipo }); - } - - if (filters.activo !== undefined) { - queryBuilder.andWhere('capacitacion.activo = :activo', { activo: filters.activo }); - } - - if (filters.search) { - queryBuilder.andWhere( - '(capacitacion.codigo ILIKE :search OR capacitacion.nombre ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - queryBuilder - .orderBy('capacitacion.nombre', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - }); - } - - async findByCodigo(ctx: ServiceContext, codigo: string): Promise { - return this.repository.findOne({ - where: { - codigo, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - }); - } - - async create(ctx: ServiceContext, dto: CreateCapacitacionDto): Promise { - const existing = await this.findByCodigo(ctx, dto.codigo); - if (existing) { - throw new Error(`Capacitacion with codigo ${dto.codigo} already exists`); - } - - const capacitacion = this.repository.create({ - tenantId: ctx.tenantId, - codigo: dto.codigo, - nombre: dto.nombre, - descripcion: dto.descripcion, - tipo: dto.tipo, - duracionHoras: dto.duracionHoras || 1, - vigenciaMeses: dto.vigenciaMeses, - requiereEvaluacion: dto.requiereEvaluacion || false, - calificacionMinima: dto.calificacionMinima, - activo: true, - }); - - return this.repository.save(capacitacion); - } - - async update(ctx: ServiceContext, id: string, dto: UpdateCapacitacionDto): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - const updated = this.repository.merge(existing, dto); - return this.repository.save(updated); - } - - async toggleActive(ctx: ServiceContext, id: string): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - existing.activo = !existing.activo; - return this.repository.save(existing); - } - - async getByTipo(ctx: ServiceContext, tipo: TipoCapacitacion): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - tipo, - activo: true, - } as FindOptionsWhere, - order: { nombre: 'ASC' }, - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/hse/services/epp.service.ts b/projects/erp-construccion/backend/src/modules/hse/services/epp.service.ts deleted file mode 100644 index d5567efa4..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/epp.service.ts +++ /dev/null @@ -1,442 +0,0 @@ -/** - * EppService - Servicio para control de EPP - * - * RF-MAA017-004: Gesti贸n de equipo de protecci贸n personal - * - * @module HSE - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { EppCatalogo, CategoriaEpp } from '../entities/epp-catalogo.entity'; -import { EppAsignacion, EstadoEpp } from '../entities/epp-asignacion.entity'; -import { EppInspeccion, EstadoInspeccionEpp } from '../entities/epp-inspeccion.entity'; -import { EppBaja, MotivoBajaEpp } from '../entities/epp-baja.entity'; -import { EppMatrizPuesto } from '../entities/epp-matriz-puesto.entity'; -import { EppInventario } from '../entities/epp-inventario.entity'; -import { EppMovimiento, TipoMovimientoEpp } from '../entities/epp-movimiento.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateEppCatalogoDto { - codigo: string; - nombre: string; - descripcion?: string; - categoria: CategoriaEpp; - especificaciones?: string; - vidaUtilDias: number; - normaReferencia?: string; - requiereCertificacion?: boolean; - requiereInspeccionPeriodica?: boolean; - frecuenciaInspeccionDias?: number; - alertaDiasAntes?: number; - imagenUrl?: string; -} - -export interface CreateAsignacionDto { - eppId: string; - employeeId: string; - fraccionamientoId?: string; - fechaEntrega: Date; - fechaVencimiento: Date; - numeroSerie?: string; - numeroLote?: string; - capacitacionUso?: boolean; - costoUnitario?: number; -} - -export interface CreateInspeccionEppDto { - asignacionId: string; - inspectorId: string; - estadoEpp: EstadoInspeccionEpp; - observaciones?: string; - requiereReemplazo?: boolean; - fotoUrl?: string; -} - -export interface CreateBajaDto { - asignacionId: string; - motivo: MotivoBajaEpp; - descripcion?: string; - descuentoAplicado?: boolean; - montoDescuento?: number; - autorizadoPorId?: string; -} - -export interface EppFilters { - categoria?: CategoriaEpp; - activo?: boolean; - search?: string; -} - -export interface AsignacionFilters { - employeeId?: string; - eppId?: string; - estado?: EstadoEpp; - fraccionamientoId?: string; - vencidos?: boolean; -} - -export interface EppStats { - totalCatalogo: number; - totalAsignaciones: number; - asignacionesActivas: number; - proximasAVencer: number; - bajasPorMotivo: { motivo: string; count: number }[]; -} - -export class EppService { - constructor( - private readonly catalogoRepository: Repository, - private readonly asignacionRepository: Repository, - private readonly inspeccionRepository: Repository, - private readonly bajaRepository: Repository, - private readonly matrizRepository: Repository, - private readonly inventarioRepository: Repository, - private readonly movimientoRepository: Repository - ) {} - - // ========== Cat谩logo EPP ========== - async findCatalogo( - ctx: ServiceContext, - filters: EppFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.catalogoRepository - .createQueryBuilder('epp') - .where('epp.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.categoria) { - queryBuilder.andWhere('epp.categoria = :categoria', { categoria: filters.categoria }); - } - - if (filters.activo !== undefined) { - queryBuilder.andWhere('epp.activo = :activo', { activo: filters.activo }); - } - - if (filters.search) { - queryBuilder.andWhere( - '(epp.codigo ILIKE :search OR epp.nombre ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - queryBuilder - .orderBy('epp.nombre', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async findCatalogoById(ctx: ServiceContext, id: string): Promise { - return this.catalogoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - } - - async createCatalogo(ctx: ServiceContext, dto: CreateEppCatalogoDto): Promise { - const epp = this.catalogoRepository.create({ - tenantId: ctx.tenantId, - codigo: dto.codigo, - nombre: dto.nombre, - descripcion: dto.descripcion, - categoria: dto.categoria, - especificaciones: dto.especificaciones, - vidaUtilDias: dto.vidaUtilDias, - normaReferencia: dto.normaReferencia, - requiereCertificacion: dto.requiereCertificacion ?? false, - requiereInspeccionPeriodica: dto.requiereInspeccionPeriodica ?? false, - frecuenciaInspeccionDias: dto.frecuenciaInspeccionDias, - alertaDiasAntes: dto.alertaDiasAntes ?? 15, - imagenUrl: dto.imagenUrl, - activo: true, - }); - return this.catalogoRepository.save(epp); - } - - // ========== Matriz por Puesto ========== - async getMatrizByPuesto(ctx: ServiceContext, puestoId: string): Promise { - return this.matrizRepository.find({ - where: { tenantId: ctx.tenantId, puestoId } as FindOptionsWhere, - relations: ['epp'], - }); - } - - async setMatrizPuesto( - ctx: ServiceContext, - puestoId: string, - eppId: string, - esObligatorio: boolean, - actividadEspecifica?: string - ): Promise { - let matriz = await this.matrizRepository.findOne({ - where: { tenantId: ctx.tenantId, puestoId, eppId } as FindOptionsWhere, - }); - - if (matriz) { - matriz.esObligatorio = esObligatorio; - if (actividadEspecifica !== undefined) { - matriz.actividadEspecifica = actividadEspecifica; - } - } else { - matriz = this.matrizRepository.create({ - tenantId: ctx.tenantId, - puestoId, - eppId, - esObligatorio, - actividadEspecifica, - }); - } - - return this.matrizRepository.save(matriz); - } - - // ========== Asignaciones ========== - async findAsignaciones( - ctx: ServiceContext, - filters: AsignacionFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.asignacionRepository - .createQueryBuilder('asignacion') - .leftJoinAndSelect('asignacion.epp', 'epp') - .leftJoinAndSelect('asignacion.employee', 'employee') - .where('asignacion.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.employeeId) { - queryBuilder.andWhere('asignacion.employee_id = :employeeId', { - employeeId: filters.employeeId, - }); - } - - if (filters.eppId) { - queryBuilder.andWhere('asignacion.epp_id = :eppId', { - eppId: filters.eppId, - }); - } - - if (filters.estado) { - queryBuilder.andWhere('asignacion.estado = :estado', { estado: filters.estado }); - } - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('asignacion.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.vencidos) { - queryBuilder.andWhere('asignacion.fecha_vencimiento < :today', { today: new Date() }); - queryBuilder.andWhere('asignacion.estado = :activo', { activo: 'activo' }); - } - - queryBuilder - .orderBy('asignacion.fecha_entrega', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async createAsignacion(ctx: ServiceContext, dto: CreateAsignacionDto): Promise { - const asignacion = this.asignacionRepository.create({ - tenantId: ctx.tenantId, - eppId: dto.eppId, - employeeId: dto.employeeId, - fraccionamientoId: dto.fraccionamientoId, - fechaEntrega: dto.fechaEntrega, - fechaVencimiento: dto.fechaVencimiento, - numeroSerie: dto.numeroSerie, - numeroLote: dto.numeroLote, - capacitacionUso: dto.capacitacionUso ?? false, - costoUnitario: dto.costoUnitario, - estado: 'activo', - createdById: ctx.userId, - }); - - return this.asignacionRepository.save(asignacion); - } - - async findAsignacionById(ctx: ServiceContext, id: string): Promise { - return this.asignacionRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['epp', 'employee', 'fraccionamiento'], - }); - } - - async updateAsignacionEstado(ctx: ServiceContext, id: string, estado: EstadoEpp): Promise { - const asignacion = await this.asignacionRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!asignacion) return null; - - asignacion.estado = estado; - return this.asignacionRepository.save(asignacion); - } - - // ========== Inspecciones ========== - async createInspeccion(ctx: ServiceContext, dto: CreateInspeccionEppDto): Promise { - const asignacion = await this.findAsignacionById(ctx, dto.asignacionId); - if (!asignacion) throw new Error('Asignaci贸n no encontrada'); - - const inspeccion = this.inspeccionRepository.create({ - asignacionId: dto.asignacionId, - inspectorId: dto.inspectorId, - fechaInspeccion: new Date(), - estadoEpp: dto.estadoEpp, - observaciones: dto.observaciones, - requiereReemplazo: dto.requiereReemplazo ?? false, - fotoUrl: dto.fotoUrl, - }); - - const saved = await this.inspeccionRepository.save(inspeccion); - - // If requires replacement, update assignment status - if (dto.requiereReemplazo || dto.estadoEpp === 'danado' || dto.estadoEpp === 'malo') { - asignacion.estado = 'danado'; - await this.asignacionRepository.save(asignacion); - } - - return saved; - } - - async getInspeccionesByAsignacion(_ctx: ServiceContext, asignacionId: string): Promise { - return this.inspeccionRepository.find({ - where: { asignacionId } as FindOptionsWhere, - relations: ['inspector'], - order: { fechaInspeccion: 'DESC' }, - }); - } - - // ========== Bajas ========== - async createBaja(ctx: ServiceContext, dto: CreateBajaDto): Promise { - const asignacion = await this.findAsignacionById(ctx, dto.asignacionId); - if (!asignacion) throw new Error('Asignaci贸n no encontrada'); - - if (asignacion.estado === 'devuelto') { - throw new Error('EPP ya fue dado de baja'); - } - - const baja = this.bajaRepository.create({ - asignacionId: dto.asignacionId, - fechaBaja: new Date(), - motivo: dto.motivo, - descripcion: dto.descripcion, - descuentoAplicado: dto.descuentoAplicado ?? false, - montoDescuento: dto.montoDescuento, - autorizadoPorId: dto.autorizadoPorId, - }); - - const saved = await this.bajaRepository.save(baja); - - // Update assignment status based on motivo - const estadoMap: Record = { - vencimiento: 'vencido', - danado: 'danado', - perdido: 'perdido', - terminacion_laboral: 'devuelto', - }; - asignacion.estado = estadoMap[dto.motivo]; - await this.asignacionRepository.save(asignacion); - - return saved; - } - - // ========== Inventario ========== - async getInventario(ctx: ServiceContext, almacenId?: string): Promise { - const where: FindOptionsWhere = { tenantId: ctx.tenantId }; - if (almacenId) { - where.almacenId = almacenId; - } - - return this.inventarioRepository.find({ - where, - relations: ['epp'], - }); - } - - async registrarMovimiento( - ctx: ServiceContext, - eppId: string, - tipo: TipoMovimientoEpp, - cantidad: number, - almacenOrigenId?: string, - almacenDestinoId?: string, - referencia?: string - ): Promise { - const movimiento = this.movimientoRepository.create({ - tenantId: ctx.tenantId, - eppId, - tipo, - cantidad, - almacenOrigenId, - almacenDestinoId, - referencia, - createdById: ctx.userId, - }); - - return this.movimientoRepository.save(movimiento); - } - - // ========== Estad铆sticas ========== - async getStats(ctx: ServiceContext, fraccionamientoId?: string): Promise { - const totalCatalogo = await this.catalogoRepository.count({ - where: { tenantId: ctx.tenantId, activo: true } as FindOptionsWhere, - }); - - const asignacionWhere: FindOptionsWhere = { tenantId: ctx.tenantId }; - if (fraccionamientoId) { - asignacionWhere.fraccionamientoId = fraccionamientoId; - } - - const totalAsignaciones = await this.asignacionRepository.count({ where: asignacionWhere }); - - const asignacionesActivas = await this.asignacionRepository.count({ - where: { ...asignacionWhere, estado: 'activo' } as FindOptionsWhere, - }); - - // Pr贸ximas a vencer (30 d铆as) - const fechaLimite = new Date(); - fechaLimite.setDate(fechaLimite.getDate() + 30); - const proximasAVencer = await this.asignacionRepository - .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.estado = :estado', { estado: 'activo' }) - .andWhere('a.fecha_vencimiento <= :fechaLimite', { fechaLimite }) - .andWhere('a.fecha_vencimiento >= :today', { today: new Date() }) - .getCount(); - - // Bajas por motivo - const bajasPorMotivo = await this.bajaRepository - .createQueryBuilder('b') - .innerJoin('b.asignacion', 'a') - .select('b.motivo', 'motivo') - .addSelect('COUNT(*)', 'count') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .groupBy('b.motivo') - .getRawMany(); - - return { - totalCatalogo, - totalAsignaciones, - asignacionesActivas, - proximasAVencer, - bajasPorMotivo: bajasPorMotivo.map((b) => ({ motivo: b.motivo, count: parseInt(b.count, 10) })), - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/hse/services/incidente.service.ts b/projects/erp-construccion/backend/src/modules/hse/services/incidente.service.ts deleted file mode 100644 index 030a263b3..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/incidente.service.ts +++ /dev/null @@ -1,396 +0,0 @@ -/** - * IncidenteService - Servicio para gesti贸n de incidentes HSE - * - * Gesti贸n de incidentes de seguridad con workflow y relaciones. - * Workflow: abierto -> en_investigacion -> cerrado - * - * @module HSE - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { - Incidente, - TipoIncidente, - GravedadIncidente, - EstadoIncidente, -} from '../entities/incidente.entity'; -import { IncidenteInvolucrado, RolInvolucrado } from '../entities/incidente-involucrado.entity'; -import { IncidenteAccion, EstadoAccion } from '../entities/incidente-accion.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateIncidenteDto { - fechaHora: Date; - fraccionamientoId: string; - ubicacionDescripcion?: string; - tipo: TipoIncidente; - gravedad: GravedadIncidente; - descripcion: string; - causaInmediata?: string; - causaBasica?: string; -} - -export interface UpdateIncidenteDto { - ubicacionDescripcion?: string; - tipo?: TipoIncidente; - gravedad?: GravedadIncidente; - descripcion?: string; - causaInmediata?: string; - causaBasica?: string; -} - -export interface AddInvolucradoDto { - employeeId: string; - rol: RolInvolucrado; - descripcionLesion?: string; - parteCuerpo?: string; - diasIncapacidad?: number; -} - -export interface AddAccionDto { - descripcion: string; - tipo: string; - responsableId?: string; - fechaCompromiso: Date; -} - -export interface UpdateAccionDto { - descripcion?: string; - responsableId?: string; - fechaCompromiso?: Date; - estado?: EstadoAccion; - evidenciaUrl?: string; - observaciones?: string; -} - -export interface IncidenteFilters { - fraccionamientoId?: string; - tipo?: TipoIncidente; - gravedad?: GravedadIncidente; - estado?: EstadoIncidente; - dateFrom?: Date; - dateTo?: Date; -} - -export interface IncidenteStats { - total: number; - porTipo: { tipo: string; count: number }[]; - porGravedad: { gravedad: string; count: number }[]; - porEstado: { estado: string; count: number }[]; - diasSinAccidente: number; -} - -export class IncidenteService { - constructor( - private readonly incidenteRepository: Repository, - private readonly involucradoRepository: Repository, - private readonly accionRepository: Repository - ) {} - - private generateFolio(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - return `INC-${year}${month}-${random}`; - } - - async findWithFilters( - ctx: ServiceContext, - filters: IncidenteFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.incidenteRepository - .createQueryBuilder('incidente') - .leftJoinAndSelect('incidente.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('incidente.createdBy', 'createdBy') - .where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('incidente.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.tipo) { - queryBuilder.andWhere('incidente.tipo = :tipo', { tipo: filters.tipo }); - } - - if (filters.gravedad) { - queryBuilder.andWhere('incidente.gravedad = :gravedad', { gravedad: filters.gravedad }); - } - - if (filters.estado) { - queryBuilder.andWhere('incidente.estado = :estado', { estado: filters.estado }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('incidente.fecha_hora >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('incidente.fecha_hora <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('incidente.fecha_hora', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.incidenteRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - }); - } - - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.incidenteRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - relations: [ - 'fraccionamiento', - 'createdBy', - 'involucrados', - 'involucrados.employee', - 'acciones', - 'acciones.responsable', - ], - }); - } - - async create(ctx: ServiceContext, dto: CreateIncidenteDto): Promise { - const incidente = this.incidenteRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - folio: this.generateFolio(), - fechaHora: dto.fechaHora, - fraccionamientoId: dto.fraccionamientoId, - ubicacionDescripcion: dto.ubicacionDescripcion, - tipo: dto.tipo, - gravedad: dto.gravedad, - descripcion: dto.descripcion, - causaInmediata: dto.causaInmediata, - causaBasica: dto.causaBasica, - estado: 'abierto', - }); - - return this.incidenteRepository.save(incidente); - } - - async update(ctx: ServiceContext, id: string, dto: UpdateIncidenteDto): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - if (existing.estado === 'cerrado') { - throw new Error('Cannot update a closed incident'); - } - - const updated = this.incidenteRepository.merge(existing, dto); - return this.incidenteRepository.save(updated); - } - - async startInvestigation(ctx: ServiceContext, id: string): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - if (existing.estado !== 'abierto') { - throw new Error('Can only start investigation on open incidents'); - } - - existing.estado = 'en_investigacion'; - return this.incidenteRepository.save(existing); - } - - async closeIncident(ctx: ServiceContext, id: string): Promise { - const existing = await this.findWithDetails(ctx, id); - if (!existing) { - return null; - } - - if (existing.estado === 'cerrado') { - throw new Error('Incident is already closed'); - } - - // Check if all actions are completed or verified - const pendingActions = existing.acciones?.filter( - (a) => a.estado !== 'completada' && a.estado !== 'verificada' - ); - - if (pendingActions && pendingActions.length > 0) { - throw new Error('Cannot close incident with pending actions'); - } - - existing.estado = 'cerrado'; - return this.incidenteRepository.save(existing); - } - - async addInvolucrado( - ctx: ServiceContext, - incidenteId: string, - dto: AddInvolucradoDto - ): Promise { - const incidente = await this.findById(ctx, incidenteId); - if (!incidente) { - throw new Error('Incidente not found'); - } - - const involucrado = this.involucradoRepository.create({ - incidenteId, - employeeId: dto.employeeId, - rol: dto.rol, - descripcionLesion: dto.descripcionLesion, - parteCuerpo: dto.parteCuerpo, - diasIncapacidad: dto.diasIncapacidad || 0, - }); - - return this.involucradoRepository.save(involucrado); - } - - async removeInvolucrado(ctx: ServiceContext, incidenteId: string, involucradoId: string): Promise { - const incidente = await this.findById(ctx, incidenteId); - if (!incidente) { - return false; - } - - const result = await this.involucradoRepository.delete({ - id: involucradoId, - incidenteId, - }); - - return (result.affected ?? 0) > 0; - } - - async addAccion(ctx: ServiceContext, incidenteId: string, dto: AddAccionDto): Promise { - const incidente = await this.findById(ctx, incidenteId); - if (!incidente) { - throw new Error('Incidente not found'); - } - - if (incidente.estado === 'cerrado') { - throw new Error('Cannot add actions to a closed incident'); - } - - const accion = this.accionRepository.create({ - incidenteId, - descripcion: dto.descripcion, - tipo: dto.tipo, - responsableId: dto.responsableId, - fechaCompromiso: dto.fechaCompromiso, - estado: 'pendiente', - }); - - return this.accionRepository.save(accion); - } - - async updateAccion( - ctx: ServiceContext, - incidenteId: string, - accionId: string, - dto: UpdateAccionDto - ): Promise { - const incidente = await this.findById(ctx, incidenteId); - if (!incidente) { - return null; - } - - const accion = await this.accionRepository.findOne({ - where: { id: accionId, incidenteId }, - }); - - if (!accion) { - return null; - } - - // If completing the action, set the close date - if (dto.estado === 'completada' && accion.estado !== 'completada') { - dto.fechaCompromiso = dto.fechaCompromiso; // keep existing - accion.fechaCierre = new Date(); - } - - const updated = this.accionRepository.merge(accion, dto); - return this.accionRepository.save(updated); - } - - async getStats(ctx: ServiceContext, fraccionamientoId?: string): Promise { - const queryBuilder = this.incidenteRepository - .createQueryBuilder('incidente') - .where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (fraccionamientoId) { - queryBuilder.andWhere('incidente.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - const total = await queryBuilder.getCount(); - - const porTipo = await this.incidenteRepository - .createQueryBuilder('incidente') - .select('incidente.tipo', 'tipo') - .addSelect('COUNT(*)', 'count') - .where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .groupBy('incidente.tipo') - .getRawMany(); - - const porGravedad = await this.incidenteRepository - .createQueryBuilder('incidente') - .select('incidente.gravedad', 'gravedad') - .addSelect('COUNT(*)', 'count') - .where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .groupBy('incidente.gravedad') - .getRawMany(); - - const porEstado = await this.incidenteRepository - .createQueryBuilder('incidente') - .select('incidente.estado', 'estado') - .addSelect('COUNT(*)', 'count') - .where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .groupBy('incidente.estado') - .getRawMany(); - - // Calculate days since last accident - const lastAccident = await this.incidenteRepository.findOne({ - where: { - tenantId: ctx.tenantId, - tipo: 'accidente', - } as FindOptionsWhere, - order: { fechaHora: 'DESC' }, - }); - - let diasSinAccidente = 0; - if (lastAccident) { - const diff = Date.now() - new Date(lastAccident.fechaHora).getTime(); - diasSinAccidente = Math.floor(diff / (1000 * 60 * 60 * 24)); - } - - return { - total, - porTipo: porTipo.map((p) => ({ tipo: p.tipo, count: parseInt(p.count, 10) })), - porGravedad: porGravedad.map((p) => ({ gravedad: p.gravedad, count: parseInt(p.count, 10) })), - porEstado: porEstado.map((p) => ({ estado: p.estado, count: parseInt(p.count, 10) })), - diasSinAccidente, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/hse/services/index.ts b/projects/erp-construccion/backend/src/modules/hse/services/index.ts deleted file mode 100644 index ef62c530b..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * HSE Services Index - * @module HSE - * - * Services for Health, Safety & Environment module - * Based on RF-MAA017-001 to RF-MAA017-008 - * Updated: 2025-12-18 - */ - -// RF-MAA017-001: Gesti贸n de Incidentes -export * from './incidente.service'; - -// RF-MAA017-002: Control de Capacitaciones -export * from './capacitacion.service'; - -// RF-MAA017-003: Inspecciones de Seguridad -export * from './inspeccion.service'; - -// RF-MAA017-004: Control de EPP -export * from './epp.service'; - -// RF-MAA017-005: Cumplimiento STPS -export * from './stps.service'; - -// RF-MAA017-006: Gesti贸n Ambiental -export * from './ambiental.service'; - -// RF-MAA017-007: Permisos de Trabajo -export * from './permiso-trabajo.service'; - -// RF-MAA017-008: Indicadores HSE -export * from './indicador.service'; diff --git a/projects/erp-construccion/backend/src/modules/hse/services/indicador.service.ts b/projects/erp-construccion/backend/src/modules/hse/services/indicador.service.ts deleted file mode 100644 index bc2c97d68..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/indicador.service.ts +++ /dev/null @@ -1,282 +0,0 @@ -/** - * IndicadorService - Servicio para indicadores HSE - * - * RF-MAA017-008: Gesti贸n de indicadores y reportes HSE - * - * @module HSE - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { IndicadorConfig, TipoIndicador, FrecuenciaCalculo } from '../entities/indicador-config.entity'; -import { IndicadorMetaObra } from '../entities/indicador-meta-obra.entity'; -import { IndicadorValor } from '../entities/indicador-valor.entity'; -import { HorasTrabajadas } from '../entities/horas-trabajadas.entity'; -import { DiasSinAccidente } from '../entities/dias-sin-accidente.entity'; -import { ReporteProgramado } from '../entities/reporte-programado.entity'; -import { AlertaIndicador } from '../entities/alerta-indicador.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface IndicadorFilters { - tipo?: TipoIndicador; - activo?: boolean; - search?: string; -} - -export interface CreateIndicadorDto { - codigo: string; - nombre: string; - descripcion?: string; - tipo: TipoIndicador; - formula?: string; - unidad?: string; - metaGlobal?: number; - umbralVerde?: number; - umbralAmarillo?: number; - umbralRojo?: number; - frecuenciaCalculo?: FrecuenciaCalculo; -} - -export interface IndicadorStats { - totalIndicadores: number; - indicadoresActivos: number; - valoresRegistrados: number; - alertasActivas: number; -} - -export class IndicadorService { - constructor( - private readonly indicadorRepository: Repository, - private readonly metaRepository: Repository, - private readonly valorRepository: Repository, - private readonly horasRepository: Repository, - private readonly diasRepository: Repository, - private readonly reporteRepository: Repository, - private readonly alertaRepository: Repository - ) {} - - // ========== Configuraci贸n de Indicadores ========== - async findIndicadores( - ctx: ServiceContext, - filters: IndicadorFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.indicadorRepository - .createQueryBuilder('i') - .where('i.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.tipo) { - queryBuilder.andWhere('i.tipo = :tipo', { tipo: filters.tipo }); - } - - if (filters.activo !== undefined) { - queryBuilder.andWhere('i.activo = :activo', { activo: filters.activo }); - } - - if (filters.search) { - queryBuilder.andWhere( - '(i.codigo ILIKE :search OR i.nombre ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - queryBuilder - .orderBy('i.nombre', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async findIndicadorById(ctx: ServiceContext, id: string): Promise { - return this.indicadorRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - } - - async createIndicador(ctx: ServiceContext, dto: CreateIndicadorDto): Promise { - const indicador = this.indicadorRepository.create({ - tenantId: ctx.tenantId, - codigo: dto.codigo, - nombre: dto.nombre, - descripcion: dto.descripcion, - tipo: dto.tipo, - formula: dto.formula, - unidad: dto.unidad, - metaGlobal: dto.metaGlobal, - umbralVerde: dto.umbralVerde, - umbralAmarillo: dto.umbralAmarillo, - umbralRojo: dto.umbralRojo, - frecuenciaCalculo: dto.frecuenciaCalculo || 'mensual', - activo: true, - }); - return this.indicadorRepository.save(indicador); - } - - async updateIndicador(ctx: ServiceContext, id: string, dto: Partial): Promise { - const indicador = await this.findIndicadorById(ctx, id); - if (!indicador) return null; - - Object.assign(indicador, dto); - return this.indicadorRepository.save(indicador); - } - - async toggleIndicadorActivo(ctx: ServiceContext, id: string): Promise { - const indicador = await this.findIndicadorById(ctx, id); - if (!indicador) return null; - - indicador.activo = !indicador.activo; - return this.indicadorRepository.save(indicador); - } - - // ========== Metas por Obra ========== - async findMetas(ctx: ServiceContext, fraccionamientoId?: string, indicadorId?: string): Promise { - const queryBuilder = this.metaRepository - .createQueryBuilder('m') - .leftJoinAndSelect('m.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('m.indicador', 'indicador') - .innerJoin('m.indicador', 'ind') - .where('ind.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (fraccionamientoId) { - queryBuilder.andWhere('m.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - if (indicadorId) { - queryBuilder.andWhere('m.indicador_id = :indicadorId', { indicadorId }); - } - - return queryBuilder.getMany(); - } - - // ========== Valores de Indicadores ========== - async findValores( - ctx: ServiceContext, - indicadorId: string, - fraccionamientoId?: string, - page: number = 1, - limit: number = 50 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.valorRepository - .createQueryBuilder('v') - .leftJoinAndSelect('v.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('v.indicador', 'indicador') - .innerJoin('v.indicador', 'ind') - .where('ind.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('v.indicador_id = :indicadorId', { indicadorId }); - - if (fraccionamientoId) { - queryBuilder.andWhere('v.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - queryBuilder - .orderBy('v.created_at', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - // ========== Horas Trabajadas ========== - async findHorasTrabajadas(ctx: ServiceContext, fraccionamientoId: string, year?: number): Promise { - const queryBuilder = this.horasRepository - .createQueryBuilder('h') - .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('h.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - - if (year) { - queryBuilder.andWhere('EXTRACT(YEAR FROM h.fecha) = :year', { year }); - } - - return queryBuilder.orderBy('h.fecha', 'DESC').getMany(); - } - - // ========== D铆as Sin Accidente ========== - async findDiasSinAccidente(ctx: ServiceContext, fraccionamientoId: string): Promise { - return this.diasRepository.findOne({ - where: { tenantId: ctx.tenantId, fraccionamientoId } as FindOptionsWhere, - }); - } - - // ========== Reportes Programados ========== - async findReportes(ctx: ServiceContext): Promise { - return this.reporteRepository.find({ - where: { tenantId: ctx.tenantId } as FindOptionsWhere, - order: { createdAt: 'DESC' }, - }); - } - - // ========== Alertas ========== - async findAlertas( - ctx: ServiceContext, - fraccionamientoId?: string, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.alertaRepository - .createQueryBuilder('a') - .leftJoinAndSelect('a.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('a.indicador', 'indicador') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (fraccionamientoId) { - queryBuilder.andWhere('a.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - queryBuilder - .orderBy('a.created_at', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - // ========== Estad铆sticas ========== - async getStats(ctx: ServiceContext): Promise { - const totalIndicadores = await this.indicadorRepository.count({ - where: { tenantId: ctx.tenantId } as FindOptionsWhere, - }); - - const indicadoresActivos = await this.indicadorRepository.count({ - where: { tenantId: ctx.tenantId, activo: true } as FindOptionsWhere, - }); - - const valoresRegistrados = await this.valorRepository - .createQueryBuilder('v') - .innerJoin('v.indicador', 'i') - .where('i.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .getCount(); - - const alertasActivas = await this.alertaRepository.count({ - where: { tenantId: ctx.tenantId } as FindOptionsWhere, - }); - - return { - totalIndicadores, - indicadoresActivos, - valoresRegistrados, - alertasActivas, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/hse/services/inspeccion.service.ts b/projects/erp-construccion/backend/src/modules/hse/services/inspeccion.service.ts deleted file mode 100644 index 9f3d190dc..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/inspeccion.service.ts +++ /dev/null @@ -1,354 +0,0 @@ -/** - * InspeccionService - Servicio para inspecciones de seguridad - * - * RF-MAA017-003: Gesti贸n de inspecciones, hallazgos y seguimiento - * - * @module HSE - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { Inspeccion } from '../entities/inspeccion.entity'; -import { InspeccionEvaluacion } from '../entities/inspeccion-evaluacion.entity'; -import { Hallazgo, GravedadHallazgo, EstadoHallazgo } from '../entities/hallazgo.entity'; -import { HallazgoEvidencia } from '../entities/hallazgo-evidencia.entity'; -import { TipoInspeccion } from '../entities/tipo-inspeccion.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface InspeccionFilters { - fraccionamientoId?: string; - tipoInspeccionId?: string; - estado?: string; - inspectorId?: string; - dateFrom?: Date; - dateTo?: Date; -} - -export interface HallazgoFilters { - fraccionamientoId?: string; - gravedad?: GravedadHallazgo; - estado?: EstadoHallazgo; - responsableId?: string; - vencidos?: boolean; -} - -export interface CreateInspeccionDto { - tipoInspeccionId: string; - fraccionamientoId: string; - inspectorId: string; - programaId?: string; -} - -export interface CreateHallazgoDto { - evaluacionId?: string; - gravedad: GravedadHallazgo; - tipo: 'acto_inseguro' | 'condicion_insegura'; - descripcion: string; - ubicacionDescripcion?: string; - responsableCorreccionId?: string; - fechaLimite: Date; -} - -export interface InspeccionStats { - totalInspecciones: number; - hallazgosAbiertos: number; - hallazgosVencidos: number; -} - -export class InspeccionService { - constructor( - private readonly inspeccionRepository: Repository, - private readonly evaluacionRepository: Repository, - private readonly hallazgoRepository: Repository, - private readonly evidenciaRepository: Repository, - private readonly tipoInspeccionRepository: Repository - ) {} - - // Getters para repositorios (para uso futuro) - getEvaluacionRepository() { - return this.evaluacionRepository; - } - - getEvidenciaRepository() { - return this.evidenciaRepository; - } - - private generateFolio(prefix: string): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - return `${prefix}-${year}${month}-${random}`; - } - - // ========== Tipos de Inspecci贸n ========== - async findTiposInspeccion(ctx: ServiceContext): Promise { - return this.tipoInspeccionRepository.find({ - where: { tenantId: ctx.tenantId, activo: true } as FindOptionsWhere, - order: { nombre: 'ASC' }, - }); - } - - // ========== Inspecciones ========== - async findInspecciones( - ctx: ServiceContext, - filters: InspeccionFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.inspeccionRepository - .createQueryBuilder('inspeccion') - .leftJoinAndSelect('inspeccion.tipoInspeccion', 'tipoInspeccion') - .leftJoinAndSelect('inspeccion.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('inspeccion.inspector', 'inspector') - .where('inspeccion.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('inspeccion.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.tipoInspeccionId) { - queryBuilder.andWhere('inspeccion.tipo_inspeccion_id = :tipoInspeccionId', { - tipoInspeccionId: filters.tipoInspeccionId, - }); - } - - if (filters.estado) { - queryBuilder.andWhere('inspeccion.estado = :estado', { estado: filters.estado }); - } - - if (filters.inspectorId) { - queryBuilder.andWhere('inspeccion.inspector_id = :inspectorId', { - inspectorId: filters.inspectorId, - }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('inspeccion.fecha_inicio >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('inspeccion.fecha_inicio <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('inspeccion.fecha_inicio', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findInspeccionById(ctx: ServiceContext, id: string): Promise { - return this.inspeccionRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['tipoInspeccion', 'fraccionamiento', 'inspector'], - }); - } - - async findInspeccionWithDetails(ctx: ServiceContext, id: string): Promise { - return this.inspeccionRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: [ - 'tipoInspeccion', - 'fraccionamiento', - 'inspector', - 'evaluaciones', - 'evaluaciones.checklistItem', - 'hallazgos', - 'hallazgos.responsableCorreccion', - ], - }); - } - - async createInspeccion(ctx: ServiceContext, dto: CreateInspeccionDto): Promise { - const inspeccion = this.inspeccionRepository.create({ - tenantId: ctx.tenantId, - tipoInspeccionId: dto.tipoInspeccionId, - fraccionamientoId: dto.fraccionamientoId, - inspectorId: dto.inspectorId, - programaId: dto.programaId, - fechaInicio: new Date(), - estado: 'borrador', - }); - - return this.inspeccionRepository.save(inspeccion); - } - - async updateInspeccionEstado(ctx: ServiceContext, id: string, estado: string): Promise { - const inspeccion = await this.findInspeccionById(ctx, id); - if (!inspeccion) return null; - - inspeccion.estado = estado; - if (estado === 'completado' || estado === 'finalizado') { - inspeccion.fechaFin = new Date(); - } - - return this.inspeccionRepository.save(inspeccion); - } - - async updateInspeccionObservaciones(ctx: ServiceContext, id: string, observaciones: string): Promise { - const inspeccion = await this.findInspeccionById(ctx, id); - if (!inspeccion) return null; - - inspeccion.observacionesGenerales = observaciones; - return this.inspeccionRepository.save(inspeccion); - } - - // ========== Hallazgos ========== - async findHallazgos( - ctx: ServiceContext, - filters: HallazgoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.hallazgoRepository - .createQueryBuilder('hallazgo') - .leftJoinAndSelect('hallazgo.inspeccion', 'inspeccion') - .leftJoinAndSelect('inspeccion.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('hallazgo.responsableCorreccion', 'responsable') - .where('hallazgo.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('inspeccion.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.gravedad) { - queryBuilder.andWhere('hallazgo.gravedad = :gravedad', { gravedad: filters.gravedad }); - } - - if (filters.estado) { - queryBuilder.andWhere('hallazgo.estado = :estado', { estado: filters.estado }); - } - - if (filters.responsableId) { - queryBuilder.andWhere('hallazgo.responsable_correccion_id = :responsableId', { - responsableId: filters.responsableId, - }); - } - - if (filters.vencidos) { - queryBuilder.andWhere('hallazgo.fecha_limite < :today', { today: new Date() }); - queryBuilder.andWhere('hallazgo.estado NOT IN (:...cerrados)', { cerrados: ['cerrado'] }); - } - - queryBuilder - .orderBy('hallazgo.fecha_limite', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async createHallazgo(ctx: ServiceContext, inspeccionId: string, dto: CreateHallazgoDto): Promise { - const inspeccion = await this.findInspeccionById(ctx, inspeccionId); - if (!inspeccion) throw new Error('Inspecci贸n no encontrada'); - - const hallazgo = this.hallazgoRepository.create({ - tenantId: ctx.tenantId, - inspeccionId, - folio: this.generateFolio('HAL'), - evaluacionId: dto.evaluacionId, - gravedad: dto.gravedad, - tipo: dto.tipo, - descripcion: dto.descripcion, - ubicacionDescripcion: dto.ubicacionDescripcion, - responsableCorreccionId: dto.responsableCorreccionId, - fechaLimite: dto.fechaLimite, - estado: 'abierto', - }); - - return this.hallazgoRepository.save(hallazgo); - } - - async registrarCorreccion( - ctx: ServiceContext, - hallazgoId: string, - descripcionCorreccion: string - ): Promise { - const hallazgo = await this.hallazgoRepository.findOne({ - where: { id: hallazgoId, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!hallazgo) return null; - - hallazgo.estado = 'verificando'; - hallazgo.descripcionCorreccion = descripcionCorreccion; - hallazgo.fechaCorreccion = new Date(); - - return this.hallazgoRepository.save(hallazgo); - } - - async verificarHallazgo( - ctx: ServiceContext, - hallazgoId: string, - verificadorId: string, - aprobado: boolean - ): Promise { - const hallazgo = await this.hallazgoRepository.findOne({ - where: { id: hallazgoId, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!hallazgo) return null; - - hallazgo.verificadorId = verificadorId; - hallazgo.fechaVerificacion = new Date(); - hallazgo.estado = aprobado ? 'cerrado' : 'reabierto'; - - return this.hallazgoRepository.save(hallazgo); - } - - // ========== Estad铆sticas ========== - async getStats(ctx: ServiceContext, fraccionamientoId?: string): Promise { - const inspQuery = this.inspeccionRepository.createQueryBuilder('i') - .where('i.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - if (fraccionamientoId) { - inspQuery.andWhere('i.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - const totalInspecciones = await inspQuery.getCount(); - - const hallazgosAbiertos = await this.hallazgoRepository - .createQueryBuilder('h') - .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('h.estado NOT IN (:...cerrados)', { cerrados: ['cerrado'] }) - .getCount(); - - const hallazgosVencidos = await this.hallazgoRepository - .createQueryBuilder('h') - .where('h.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('h.fecha_limite < :today', { today: new Date() }) - .andWhere('h.estado NOT IN (:...cerrados)', { cerrados: ['cerrado'] }) - .getCount(); - - return { - totalInspecciones, - hallazgosAbiertos, - hallazgosVencidos, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/hse/services/permiso-trabajo.service.ts b/projects/erp-construccion/backend/src/modules/hse/services/permiso-trabajo.service.ts deleted file mode 100644 index 05a538ebb..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/permiso-trabajo.service.ts +++ /dev/null @@ -1,298 +0,0 @@ -/** - * PermisoTrabajoService - Servicio para permisos de trabajo - * - * RF-MAA017-007: Gesti贸n de permisos de trabajo de alto riesgo - * - * @module HSE - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { TipoPermisoTrabajo } from '../entities/tipo-permiso-trabajo.entity'; -import { PermisoTrabajo, EstadoPermiso } from '../entities/permiso-trabajo.entity'; -import { PermisoPersonal } from '../entities/permiso-personal.entity'; -import { PermisoAutorizacion } from '../entities/permiso-autorizacion.entity'; -import { PermisoChecklist } from '../entities/permiso-checklist.entity'; -import { PermisoMonitoreo } from '../entities/permiso-monitoreo.entity'; -import { PermisoEvento } from '../entities/permiso-evento.entity'; -import { PermisoDocumento } from '../entities/permiso-documento.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface PermisoFilters { - fraccionamientoId?: string; - tipoPermisoId?: string; - estado?: EstadoPermiso; - solicitanteId?: string; - vigentes?: boolean; - dateFrom?: Date; - dateTo?: Date; -} - -export interface CreatePermisoDto { - fraccionamientoId: string; - tipoPermisoId: string; - descripcionTrabajo: string; - ubicacion: string; - fechaInicioProgramada: Date; - fechaFinProgramada: Date; - observaciones?: string; -} - -export interface PermisoStats { - totalPermisos: number; - permisosVigentes: number; - permisosPorEstado: { estado: string; count: number }[]; -} - -export class PermisoTrabajoService { - constructor( - private readonly tipoPermisoRepository: Repository, - private readonly permisoRepository: Repository, - private readonly personalRepository: Repository, - private readonly autorizacionRepository: Repository, - private readonly checklistRepository: Repository, - private readonly monitoreoRepository: Repository, - private readonly eventoRepository: Repository, - private readonly documentoRepository: Repository - ) {} - - private generateFolio(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - return `PT-${year}${month}-${random}`; - } - - // ========== Tipos de Permiso ========== - async findTiposPermiso(ctx: ServiceContext): Promise { - return this.tipoPermisoRepository.find({ - where: { tenantId: ctx.tenantId, activo: true } as FindOptionsWhere, - order: { nombre: 'ASC' }, - }); - } - - // ========== Permisos de Trabajo ========== - async findPermisos( - ctx: ServiceContext, - filters: PermisoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.permisoRepository - .createQueryBuilder('p') - .leftJoinAndSelect('p.tipoPermiso', 'tipo') - .leftJoinAndSelect('p.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('p.solicitante', 'solicitante') - .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('p.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.tipoPermisoId) { - queryBuilder.andWhere('p.tipo_permiso_id = :tipoPermisoId', { - tipoPermisoId: filters.tipoPermisoId, - }); - } - - if (filters.estado) { - queryBuilder.andWhere('p.estado = :estado', { estado: filters.estado }); - } - - if (filters.solicitanteId) { - queryBuilder.andWhere('p.solicitante_id = :solicitanteId', { - solicitanteId: filters.solicitanteId, - }); - } - - if (filters.vigentes) { - const now = new Date(); - queryBuilder.andWhere('p.fecha_inicio_programada <= :now', { now }); - queryBuilder.andWhere('p.fecha_fin_programada >= :now', { now }); - queryBuilder.andWhere('p.estado IN (:...activos)', { activos: ['autorizado', 'en_ejecucion'] }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('p.fecha_inicio_programada >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('p.fecha_fin_programada <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('p.fecha_inicio_programada', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async findPermisoById(ctx: ServiceContext, id: string): Promise { - return this.permisoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['tipoPermiso', 'fraccionamiento', 'solicitante'], - }); - } - - async findPermisoWithDetails(ctx: ServiceContext, id: string): Promise { - return this.permisoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: [ - 'tipoPermiso', - 'fraccionamiento', - 'solicitante', - 'personal', - 'personal.employee', - 'autorizaciones', - 'autorizaciones.autorizador', - 'checklist', - 'monitoreos', - 'eventos', - 'documentos', - ], - }); - } - - async createPermiso(ctx: ServiceContext, dto: CreatePermisoDto): Promise { - const permiso = this.permisoRepository.create({ - tenantId: ctx.tenantId, - folio: this.generateFolio(), - tipoPermisoId: dto.tipoPermisoId, - fraccionamientoId: dto.fraccionamientoId, - solicitanteId: ctx.userId, - descripcionTrabajo: dto.descripcionTrabajo, - ubicacion: dto.ubicacion, - fechaInicioProgramada: dto.fechaInicioProgramada, - fechaFinProgramada: dto.fechaFinProgramada, - observaciones: dto.observaciones, - estado: 'borrador', - }); - - return this.permisoRepository.save(permiso); - } - - async updatePermisoEstado(ctx: ServiceContext, id: string, estado: EstadoPermiso, motivo?: string): Promise { - const permiso = await this.findPermisoById(ctx, id); - if (!permiso) return null; - - permiso.estado = estado; - - if (estado === 'rechazado' && motivo) { - permiso.motivoRechazo = motivo; - } - if (estado === 'suspendido' && motivo) { - permiso.motivoSuspension = motivo; - } - if (estado === 'en_ejecucion') { - permiso.fechaInicioReal = new Date(); - } - if (estado === 'cerrado') { - permiso.fechaFinReal = new Date(); - } - - return this.permisoRepository.save(permiso); - } - - // ========== Personal Autorizado ========== - async getPersonalByPermiso(ctx: ServiceContext, permisoId: string): Promise { - const permiso = await this.findPermisoById(ctx, permisoId); - if (!permiso) return []; - - return this.personalRepository.find({ - where: { permisoId } as FindOptionsWhere, - relations: ['employee'], - }); - } - - // ========== Autorizaciones ========== - async getAutorizacionesByPermiso(ctx: ServiceContext, permisoId: string): Promise { - const permiso = await this.findPermisoById(ctx, permisoId); - if (!permiso) return []; - - return this.autorizacionRepository.find({ - where: { permisoId } as FindOptionsWhere, - relations: ['autorizador'], - order: { createdAt: 'ASC' }, - }); - } - - // ========== Checklist ========== - async getChecklistByPermiso(_ctx: ServiceContext, permisoId: string): Promise { - return this.checklistRepository.find({ - where: { permisoId } as FindOptionsWhere, - order: { createdAt: 'ASC' }, - }); - } - - // ========== Monitoreo ========== - async getMonitoreosByPermiso(_ctx: ServiceContext, permisoId: string): Promise { - return this.monitoreoRepository.find({ - where: { permisoId } as FindOptionsWhere, - order: { createdAt: 'DESC' }, - }); - } - - // ========== Eventos ========== - async getEventosByPermiso(_ctx: ServiceContext, permisoId: string): Promise { - return this.eventoRepository.find({ - where: { permisoId } as FindOptionsWhere, - order: { createdAt: 'DESC' }, - }); - } - - // ========== Documentos ========== - async getDocumentosByPermiso(_ctx: ServiceContext, permisoId: string): Promise { - return this.documentoRepository.find({ - where: { permisoId } as FindOptionsWhere, - order: { fechaSubida: 'DESC' }, - }); - } - - // ========== Estad铆sticas ========== - async getStats(ctx: ServiceContext, fraccionamientoId?: string): Promise { - const baseQuery = this.permisoRepository - .createQueryBuilder('p') - .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (fraccionamientoId) { - baseQuery.andWhere('p.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - const totalPermisos = await baseQuery.getCount(); - - // Permisos vigentes - const now = new Date(); - const permisosVigentes = await this.permisoRepository - .createQueryBuilder('p') - .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('p.fecha_inicio_programada <= :now', { now }) - .andWhere('p.fecha_fin_programada >= :now', { now }) - .andWhere('p.estado IN (:...activos)', { activos: ['autorizado', 'en_ejecucion'] }) - .getCount(); - - // Por estado - const permisosPorEstado = await this.permisoRepository - .createQueryBuilder('p') - .select('p.estado', 'estado') - .addSelect('COUNT(*)', 'count') - .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .groupBy('p.estado') - .getRawMany(); - - return { - totalPermisos, - permisosVigentes, - permisosPorEstado: permisosPorEstado.map((p) => ({ estado: p.estado, count: parseInt(p.count, 10) })), - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/hse/services/stps.service.ts b/projects/erp-construccion/backend/src/modules/hse/services/stps.service.ts deleted file mode 100644 index 06cec5ce6..000000000 --- a/projects/erp-construccion/backend/src/modules/hse/services/stps.service.ts +++ /dev/null @@ -1,675 +0,0 @@ -/** - * StpsService - Servicio para cumplimiento STPS - * - * RF-MAA017-005: Gesti贸n de normas, comisiones y auditor铆as - * - * @module HSE - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { NormaStps } from '../entities/norma-stps.entity'; -import { NormaRequisito } from '../entities/norma-requisito.entity'; -import { CumplimientoObra, EstadoCumplimiento } from '../entities/cumplimiento-obra.entity'; -import { ComisionSeguridad, EstadoComision } from '../entities/comision-seguridad.entity'; -import { ComisionIntegrante, RolComision, Representacion } from '../entities/comision-integrante.entity'; -import { ComisionRecorrido } from '../entities/comision-recorrido.entity'; -import { ProgramaSeguridad } from '../entities/programa-seguridad.entity'; -import { ProgramaActividad, TipoActividadPrograma, EstadoActividad } from '../entities/programa-actividad.entity'; -import { DocumentoStps, TipoDocumentoStps } from '../entities/documento-stps.entity'; -import { Auditoria, TipoAuditoria, ResultadoAuditoria } from '../entities/auditoria.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CumplimientoFilters { - fraccionamientoId?: string; - normaId?: string; - estado?: EstadoCumplimiento; -} - -export interface ComisionFilters { - fraccionamientoId?: string; - estado?: EstadoComision; -} - -export interface AuditoriaFilters { - fraccionamientoId?: string; - tipo?: TipoAuditoria; - resultado?: ResultadoAuditoria; - dateFrom?: Date; - dateTo?: Date; -} - -export interface CreateCumplimientoDto { - fraccionamientoId: string; - normaId: string; - requisitoId?: string; - evaluadorId?: string; - fechaEvaluacion: Date; - estado?: EstadoCumplimiento; - evidenciaUrl?: string; - observaciones?: string; - fechaCompromiso?: Date; -} - -export interface CreateComisionDto { - fraccionamientoId: string; - fechaConstitucion: Date; - numeroActa?: string; - vigenciaInicio: Date; - vigenciaFin: Date; - documentoActaUrl?: string; -} - -export interface AddIntegranteDto { - employeeId: string; - rol: RolComision; - representacion: Representacion; - fechaNombramiento: Date; -} - -export interface CreateRecorridoDto { - comisionId: string; - fechaProgramada: Date; - areasRecorridas?: string; -} - -export interface CreateProgramaDto { - fraccionamientoId: string; - anio: number; - objetivoGeneral?: string; - metas?: Record; - presupuesto?: number; -} - -export interface CreateActividadDto { - programaId: string; - actividad: string; - tipo: TipoActividadPrograma; - fechaProgramada: Date; - responsableId?: string; - recursos?: string; -} - -export interface CreateAuditoriaDto { - fraccionamientoId: string; - tipo: TipoAuditoria; - fechaProgramada: Date; - auditor?: string; -} - -export interface CreateDocumentoDto { - tipo: TipoDocumentoStps; - folio: string; - fraccionamientoId?: string; - employeeId?: string; - fechaEmision: Date; - fechaVencimiento?: Date; - datosDocumento?: Record; - documentoUrl?: string; -} - -export interface StpsStats { - totalNormas: number; - cumplimientoPorEstado: { estado: string; count: number }[]; - porcentajeCumplimiento: number; - comisionesActivas: number; - recorridosPendientes: number; - actividadesPendientes: number; - auditoriasProgramadas: number; -} - -export class StpsService { - constructor( - private readonly normaRepository: Repository, - private readonly requisitoRepository: Repository, - private readonly cumplimientoRepository: Repository, - private readonly comisionRepository: Repository, - private readonly integranteRepository: Repository, - private readonly recorridoRepository: Repository, - private readonly programaRepository: Repository, - private readonly actividadRepository: Repository, - private readonly documentoRepository: Repository, - private readonly auditoriaRepository: Repository - ) {} - - // ========== Normas y Requisitos ========== - async findNormas(): Promise { - // NormaStps is a shared catalog (no tenant_id) - return this.normaRepository.find({ - where: { activo: true } as FindOptionsWhere, - order: { codigo: 'ASC' }, - }); - } - - async findNormaById(id: string): Promise { - return this.normaRepository.findOne({ - where: { id } as FindOptionsWhere, - relations: ['requisitos'], - }); - } - - async findRequisitosByNorma(normaId: string): Promise { - return this.requisitoRepository.find({ - where: { normaId } as FindOptionsWhere, - order: { numero: 'ASC' }, - }); - } - - // ========== Cumplimiento por Obra ========== - async findCumplimientos( - ctx: ServiceContext, - filters: CumplimientoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.cumplimientoRepository - .createQueryBuilder('c') - .leftJoinAndSelect('c.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('c.norma', 'norma') - .leftJoinAndSelect('c.requisito', 'requisito') - .leftJoinAndSelect('c.evaluador', 'evaluador') - .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('c.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.normaId) { - queryBuilder.andWhere('c.norma_id = :normaId', { normaId: filters.normaId }); - } - - if (filters.estado) { - queryBuilder.andWhere('c.estado = :estado', { estado: filters.estado }); - } - - queryBuilder - .orderBy('norma.codigo', 'ASC') - .addOrderBy('requisito.numero', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async createCumplimiento(ctx: ServiceContext, dto: CreateCumplimientoDto): Promise { - const cumplimiento = this.cumplimientoRepository.create({ - tenantId: ctx.tenantId, - fraccionamientoId: dto.fraccionamientoId, - normaId: dto.normaId, - requisitoId: dto.requisitoId, - evaluadorId: dto.evaluadorId, - fechaEvaluacion: dto.fechaEvaluacion, - estado: dto.estado || 'no_cumple', - evidenciaUrl: dto.evidenciaUrl, - observaciones: dto.observaciones, - fechaCompromiso: dto.fechaCompromiso, - }); - - return this.cumplimientoRepository.save(cumplimiento); - } - - async updateCumplimientoEstado( - ctx: ServiceContext, - id: string, - estado: EstadoCumplimiento, - evidenciaUrl?: string, - observaciones?: string - ): Promise { - const cumplimiento = await this.cumplimientoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!cumplimiento) return null; - - cumplimiento.estado = estado; - cumplimiento.fechaEvaluacion = new Date(); - if (evidenciaUrl) cumplimiento.evidenciaUrl = evidenciaUrl; - if (observaciones) cumplimiento.observaciones = observaciones; - - return this.cumplimientoRepository.save(cumplimiento); - } - - // ========== Comisiones de Seguridad ========== - async findComisiones( - ctx: ServiceContext, - filters: ComisionFilters = {} - ): Promise { - const queryBuilder = this.comisionRepository - .createQueryBuilder('c') - .leftJoinAndSelect('c.fraccionamiento', 'fraccionamiento') - .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('c.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.estado) { - queryBuilder.andWhere('c.estado = :estado', { estado: filters.estado }); - } - - return queryBuilder.orderBy('c.fecha_constitucion', 'DESC').getMany(); - } - - async findComisionById(ctx: ServiceContext, id: string): Promise { - return this.comisionRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['fraccionamiento', 'integrantes', 'integrantes.employee', 'recorridos'], - }); - } - - async createComision(ctx: ServiceContext, dto: CreateComisionDto): Promise { - const comision = this.comisionRepository.create({ - tenantId: ctx.tenantId, - fraccionamientoId: dto.fraccionamientoId, - fechaConstitucion: dto.fechaConstitucion, - numeroActa: dto.numeroActa, - vigenciaInicio: dto.vigenciaInicio, - vigenciaFin: dto.vigenciaFin, - documentoActaUrl: dto.documentoActaUrl, - estado: 'activa', - }); - - return this.comisionRepository.save(comision); - } - - async updateComisionEstado(ctx: ServiceContext, id: string, estado: EstadoComision): Promise { - const comision = await this.comisionRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!comision) return null; - - comision.estado = estado; - return this.comisionRepository.save(comision); - } - - async addIntegrante(_ctx: ServiceContext, comisionId: string, dto: AddIntegranteDto): Promise { - const integrante = this.integranteRepository.create({ - comisionId, - employeeId: dto.employeeId, - rol: dto.rol, - representacion: dto.representacion, - fechaNombramiento: dto.fechaNombramiento, - activo: true, - }); - - return this.integranteRepository.save(integrante); - } - - async removeIntegrante(_ctx: ServiceContext, integranteId: string): Promise { - const integrante = await this.integranteRepository.findOne({ - where: { id: integranteId } as FindOptionsWhere, - }); - if (!integrante) return false; - - integrante.activo = false; - await this.integranteRepository.save(integrante); - return true; - } - - async createRecorrido(_ctx: ServiceContext, dto: CreateRecorridoDto): Promise { - const recorrido = this.recorridoRepository.create({ - comisionId: dto.comisionId, - fechaProgramada: dto.fechaProgramada, - areasRecorridas: dto.areasRecorridas, - estado: 'programado', - }); - - return this.recorridoRepository.save(recorrido); - } - - async completarRecorrido( - _ctx: ServiceContext, - id: string, - hallazgos?: string, - recomendaciones?: string, - numeroActa?: string, - documentoActaUrl?: string - ): Promise { - const recorrido = await this.recorridoRepository.findOne({ - where: { id } as FindOptionsWhere, - }); - if (!recorrido) return null; - - recorrido.fechaRealizada = new Date(); - if (hallazgos) recorrido.hallazgos = hallazgos; - if (recomendaciones) recorrido.recomendaciones = recomendaciones; - if (numeroActa) recorrido.numeroActa = numeroActa; - if (documentoActaUrl) recorrido.documentoActaUrl = documentoActaUrl; - recorrido.estado = 'realizado'; - - return this.recorridoRepository.save(recorrido); - } - - async cancelarRecorrido(_ctx: ServiceContext, id: string): Promise { - const recorrido = await this.recorridoRepository.findOne({ - where: { id } as FindOptionsWhere, - }); - if (!recorrido) return null; - - recorrido.estado = 'cancelado'; - return this.recorridoRepository.save(recorrido); - } - - // ========== Programas de Seguridad ========== - async findProgramas(ctx: ServiceContext, fraccionamientoId?: string, anio?: number): Promise { - const queryBuilder = this.programaRepository - .createQueryBuilder('p') - .leftJoinAndSelect('p.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('p.aprobadoPor', 'aprobadoPor') - .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (fraccionamientoId) { - queryBuilder.andWhere('p.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - if (anio) { - queryBuilder.andWhere('p.anio = :anio', { anio }); - } - - return queryBuilder.orderBy('p.anio', 'DESC').getMany(); - } - - async findProgramaById(ctx: ServiceContext, id: string): Promise { - return this.programaRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['fraccionamiento', 'aprobadoPor', 'actividades', 'actividades.responsable'], - }); - } - - async createPrograma(ctx: ServiceContext, dto: CreateProgramaDto): Promise { - const programa = this.programaRepository.create({ - tenantId: ctx.tenantId, - fraccionamientoId: dto.fraccionamientoId, - anio: dto.anio, - objetivoGeneral: dto.objetivoGeneral, - metas: dto.metas, - presupuesto: dto.presupuesto, - estado: 'borrador', - }); - - return this.programaRepository.save(programa); - } - - async aprobarPrograma(ctx: ServiceContext, id: string): Promise { - const programa = await this.programaRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!programa) return null; - - programa.estado = 'activo'; - if (ctx.userId) programa.aprobadoPorId = ctx.userId; - programa.fechaAprobacion = new Date(); - return this.programaRepository.save(programa); - } - - async finalizarPrograma(ctx: ServiceContext, id: string): Promise { - const programa = await this.programaRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!programa) return null; - - programa.estado = 'finalizado'; - return this.programaRepository.save(programa); - } - - async createActividad(_ctx: ServiceContext, dto: CreateActividadDto): Promise { - const actividad = this.actividadRepository.create({ - programaId: dto.programaId, - actividad: dto.actividad, - tipo: dto.tipo, - fechaProgramada: dto.fechaProgramada, - responsableId: dto.responsableId, - recursos: dto.recursos, - estado: 'pendiente', - }); - - return this.actividadRepository.save(actividad); - } - - async updateActividadEstado(_ctx: ServiceContext, id: string, estado: EstadoActividad): Promise { - const actividad = await this.actividadRepository.findOne({ - where: { id } as FindOptionsWhere, - }); - if (!actividad) return null; - - actividad.estado = estado; - if (estado === 'completada') { - actividad.fechaRealizada = new Date(); - } - return this.actividadRepository.save(actividad); - } - - async completarActividad(_ctx: ServiceContext, id: string, evidenciaUrl?: string): Promise { - const actividad = await this.actividadRepository.findOne({ - where: { id } as FindOptionsWhere, - }); - if (!actividad) return null; - - actividad.fechaRealizada = new Date(); - if (evidenciaUrl) actividad.evidenciaUrl = evidenciaUrl; - actividad.estado = 'completada'; - - return this.actividadRepository.save(actividad); - } - - // ========== Documentos STPS ========== - async createDocumento(ctx: ServiceContext, dto: CreateDocumentoDto): Promise { - const documento = this.documentoRepository.create({ - tenantId: ctx.tenantId, - tipo: dto.tipo, - folio: dto.folio, - fraccionamientoId: dto.fraccionamientoId, - employeeId: dto.employeeId, - fechaEmision: dto.fechaEmision, - fechaVencimiento: dto.fechaVencimiento, - datosDocumento: dto.datosDocumento, - documentoUrl: dto.documentoUrl, - createdById: ctx.userId, - }); - - return this.documentoRepository.save(documento); - } - - async findDocumentos(ctx: ServiceContext, fraccionamientoId?: string, tipo?: TipoDocumentoStps): Promise { - const queryBuilder = this.documentoRepository - .createQueryBuilder('d') - .leftJoinAndSelect('d.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('d.employee', 'employee') - .where('d.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (fraccionamientoId) { - queryBuilder.andWhere('d.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - if (tipo) { - queryBuilder.andWhere('d.tipo = :tipo', { tipo }); - } - - return queryBuilder.orderBy('d.created_at', 'DESC').getMany(); - } - - async findDocumentoById(ctx: ServiceContext, id: string): Promise { - return this.documentoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['fraccionamiento', 'employee', 'createdBy'], - }); - } - - async marcarDocumentoFirmado(ctx: ServiceContext, id: string): Promise { - const documento = await this.documentoRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!documento) return null; - - documento.firmado = true; - return this.documentoRepository.save(documento); - } - - // ========== Auditor铆as ========== - async findAuditorias( - ctx: ServiceContext, - filters: AuditoriaFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.auditoriaRepository - .createQueryBuilder('a') - .leftJoinAndSelect('a.fraccionamiento', 'fraccionamiento') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('a.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.tipo) { - queryBuilder.andWhere('a.tipo = :tipo', { tipo: filters.tipo }); - } - - if (filters.resultado) { - queryBuilder.andWhere('a.resultado = :resultado', { resultado: filters.resultado }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('a.fecha_programada >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('a.fecha_programada <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('a.fecha_programada', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { total, page, limit, totalPages: Math.ceil(total / limit) }, - }; - } - - async findAuditoriaById(ctx: ServiceContext, id: string): Promise { - return this.auditoriaRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['fraccionamiento'], - }); - } - - async createAuditoria(ctx: ServiceContext, dto: CreateAuditoriaDto): Promise { - const auditoria = this.auditoriaRepository.create({ - tenantId: ctx.tenantId, - fraccionamientoId: dto.fraccionamientoId, - tipo: dto.tipo, - fechaProgramada: dto.fechaProgramada, - auditor: dto.auditor, - }); - - return this.auditoriaRepository.save(auditoria); - } - - async completarAuditoria( - ctx: ServiceContext, - id: string, - resultado: ResultadoAuditoria, - noConformidades: number, - observaciones?: string, - informeUrl?: string - ): Promise { - const auditoria = await this.auditoriaRepository.findOne({ - where: { id, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (!auditoria) return null; - - auditoria.fechaRealizada = new Date(); - auditoria.resultado = resultado; - auditoria.noConformidades = noConformidades; - if (observaciones) auditoria.observaciones = observaciones; - if (informeUrl) auditoria.informeUrl = informeUrl; - - return this.auditoriaRepository.save(auditoria); - } - - // ========== Estad铆sticas ========== - async getStats(ctx: ServiceContext, fraccionamientoId?: string): Promise { - // Total normas (shared catalog) - const totalNormas = await this.normaRepository.count({ - where: { activo: true } as FindOptionsWhere, - }); - - // Cumplimiento por estado - const cumplQuery = this.cumplimientoRepository - .createQueryBuilder('c') - .select('c.estado', 'estado') - .addSelect('COUNT(*)', 'count') - .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - if (fraccionamientoId) { - cumplQuery.andWhere('c.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - const cumplimientoPorEstado = await cumplQuery.groupBy('c.estado').getRawMany(); - - // Porcentaje cumplimiento - const totalCumplimientos = cumplimientoPorEstado.reduce((sum, c) => sum + parseInt(c.count, 10), 0); - const cumple = cumplimientoPorEstado.find((c) => c.estado === 'cumple'); - const porcentajeCumplimiento = totalCumplimientos > 0 - ? Math.round((parseInt(cumple?.count || '0', 10) / totalCumplimientos) * 100) - : 0; - - // Comisiones activas - const comisionQuery = this.comisionRepository - .createQueryBuilder('c') - .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('c.estado = :estado', { estado: 'activa' }); - if (fraccionamientoId) { - comisionQuery.andWhere('c.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - const comisionesActivas = await comisionQuery.getCount(); - - // Recorridos pendientes - const recorridosPendientes = await this.recorridoRepository - .createQueryBuilder('r') - .innerJoin('r.comision', 'c') - .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('r.estado IN (:...estados)', { estados: ['programado', 'pendiente'] }) - .getCount(); - - // Actividades pendientes - const actividadesPendientes = await this.actividadRepository - .createQueryBuilder('a') - .innerJoin('a.programa', 'p') - .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.estado IN (:...estados)', { estados: ['pendiente', 'en_progreso'] }) - .getCount(); - - // Auditor铆as programadas (sin resultado) - const auditoriasProgramadas = await this.auditoriaRepository - .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.resultado IS NULL') - .getCount(); - - return { - totalNormas, - cumplimientoPorEstado: cumplimientoPorEstado.map((c) => ({ estado: c.estado, count: parseInt(c.count, 10) })), - porcentajeCumplimiento, - comisionesActivas, - recorridosPendientes, - actividadesPendientes, - auditoriasProgramadas, - }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/controllers/asignacion.controller.ts b/projects/erp-construccion/backend/src/modules/infonavit/controllers/asignacion.controller.ts deleted file mode 100644 index 57a511753..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/controllers/asignacion.controller.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * AsignacionController - REST API for housing assignments - * - * Endpoints para gesti贸n de asignaciones de vivienda INFONAVIT. - * - * @module Infonavit - * @routes /api/asignaciones - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { AsignacionService, AsignacionFilters } from '../services/asignacion.service'; -import { AsignacionVivienda } from '../entities/asignacion-vivienda.entity'; -import { OfertaVivienda } from '../entities/oferta-vivienda.entity'; -import { Derechohabiente } from '../entities/derechohabiente.entity'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createAsignacionController(dataSource: DataSource): Router { - const router = Router(); - - // Repositories - const asignacionRepo = dataSource.getRepository(AsignacionVivienda); - const ofertaRepo = dataSource.getRepository(OfertaVivienda); - const derechohabienteRepo = dataSource.getRepository(Derechohabiente); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Services - const service = new AsignacionService(asignacionRepo, ofertaRepo, derechohabienteRepo); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper for service context - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /api/asignaciones - * List assignments with filters - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: AsignacionFilters = {}; - if (req.query.ofertaId) filters.ofertaId = req.query.ofertaId as string; - if (req.query.derechohabienteId) filters.derechohabienteId = req.query.derechohabienteId as string; - if (req.query.status) filters.status = req.query.status as AsignacionFilters['status']; - if (req.query.dateFrom) filters.dateFrom = new Date(req.query.dateFrom as string); - if (req.query.dateTo) filters.dateTo = new Date(req.query.dateTo as string); - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/asignaciones/:id - * Get assignment with details - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const asignacion = await service.findWithDetails(getContext(req), req.params.id); - if (!asignacion) { - res.status(404).json({ error: 'Not Found', message: 'Assignment not found' }); - return; - } - - res.status(200).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/asignaciones - * Create new assignment - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const asignacion = await service.create(getContext(req), { - ofertaId: req.body.ofertaId, - derechohabienteId: req.body.derechohabienteId, - assignmentDate: new Date(req.body.assignmentDate), - creditAmount: req.body.creditAmount, - subsidyAmount: req.body.subsidyAmount, - downPayment: req.body.downPayment, - notes: req.body.notes, - }); - - res.status(201).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/asignaciones/:id/approve - * Approve assignment - */ - router.post('/:id/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const asignacion = await service.approve(getContext(req), req.params.id); - if (!asignacion) { - res.status(404).json({ error: 'Not Found', message: 'Assignment not found' }); - return; - } - - res.status(200).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/asignaciones/:id/formalize - * Formalize assignment - */ - router.post('/:id/formalize', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const asignacion = await service.formalize(getContext(req), req.params.id, { - formalizationDate: new Date(req.body.formalizationDate), - notaryName: req.body.notaryName, - notaryNumber: req.body.notaryNumber, - deedNumber: req.body.deedNumber, - deedDate: new Date(req.body.deedDate), - }); - - if (!asignacion) { - res.status(404).json({ error: 'Not Found', message: 'Assignment not found' }); - return; - } - - res.status(200).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/asignaciones/:id/deliver - * Deliver housing - */ - router.post('/:id/deliver', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const asignacion = await service.deliver( - getContext(req), - req.params.id, - new Date(req.body.deliveryDate) - ); - - if (!asignacion) { - res.status(404).json({ error: 'Not Found', message: 'Assignment not found' }); - return; - } - - res.status(200).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/asignaciones/:id/cancel - * Cancel assignment - */ - router.post('/:id/cancel', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const asignacion = await service.cancel(getContext(req), req.params.id, req.body.reason); - if (!asignacion) { - res.status(404).json({ error: 'Not Found', message: 'Assignment not found' }); - return; - } - - res.status(200).json({ success: true, data: asignacion }); - } catch (error) { - next(error); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/controllers/derechohabiente.controller.ts b/projects/erp-construccion/backend/src/modules/infonavit/controllers/derechohabiente.controller.ts deleted file mode 100644 index 3ea777ae5..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/controllers/derechohabiente.controller.ts +++ /dev/null @@ -1,291 +0,0 @@ -/** - * DerechohabienteController - REST API for INFONAVIT workers - * - * Endpoints para gesti贸n de derechohabientes INFONAVIT. - * - * @module Infonavit - * @routes /api/derechohabientes - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { DerechohabienteService, DerechohabienteFilters } from '../services/derechohabiente.service'; -import { Derechohabiente } from '../entities/derechohabiente.entity'; -import { HistoricoPuntos } from '../entities/historico-puntos.entity'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createDerechohabienteController(dataSource: DataSource): Router { - const router = Router(); - - // Repositories - const derechohabienteRepo = dataSource.getRepository(Derechohabiente); - const historicoPuntosRepo = dataSource.getRepository(HistoricoPuntos); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Services - const service = new DerechohabienteService(derechohabienteRepo, historicoPuntosRepo); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper for service context - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /api/derechohabientes - * List workers with filters - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: DerechohabienteFilters = {}; - if (req.query.nss) filters.nss = req.query.nss as string; - if (req.query.curp) filters.curp = req.query.curp as string; - if (req.query.status) filters.status = req.query.status as DerechohabienteFilters['status']; - if (req.query.search) filters.search = req.query.search as string; - if (req.query.minPoints) filters.minPoints = parseInt(req.query.minPoints as string); - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/derechohabientes/:id - * Get worker with details - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const derechohabiente = await service.findWithDetails(getContext(req), req.params.id); - if (!derechohabiente) { - res.status(404).json({ error: 'Not Found', message: 'Worker not found' }); - return; - } - - res.status(200).json({ success: true, data: derechohabiente }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/derechohabientes/nss/:nss - * Get worker by NSS - */ - router.get('/nss/:nss', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const derechohabiente = await service.findByNss(getContext(req), req.params.nss); - if (!derechohabiente) { - res.status(404).json({ error: 'Not Found', message: 'Worker not found' }); - return; - } - - res.status(200).json({ success: true, data: derechohabiente }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/derechohabientes - * Create new worker - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const derechohabiente = await service.create(getContext(req), req.body); - res.status(201).json({ success: true, data: derechohabiente }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /api/derechohabientes/:id - * Update worker - */ - router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const derechohabiente = await service.update(getContext(req), req.params.id, req.body); - if (!derechohabiente) { - res.status(404).json({ error: 'Not Found', message: 'Worker not found' }); - return; - } - - res.status(200).json({ success: true, data: derechohabiente }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/derechohabientes/:id/points - * Update worker points - */ - router.post('/:id/points', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { points, source, notes } = req.body; - const derechohabiente = await service.updatePoints(getContext(req), req.params.id, points, source, notes); - if (!derechohabiente) { - res.status(404).json({ error: 'Not Found', message: 'Worker not found' }); - return; - } - - res.status(200).json({ success: true, data: derechohabiente }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/derechohabientes/:id/precalify - * Precalify worker - */ - router.post('/:id/precalify', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const derechohabiente = await service.precalify(getContext(req), req.params.id, { - precalificationDate: new Date(req.body.precalificationDate), - precalificationAmount: req.body.precalificationAmount, - creditType: req.body.creditType, - creditPoints: req.body.creditPoints, - }); - - if (!derechohabiente) { - res.status(404).json({ error: 'Not Found', message: 'Worker not found' }); - return; - } - - res.status(200).json({ success: true, data: derechohabiente }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/derechohabientes/:id/qualify - * Qualify worker - */ - router.post('/:id/qualify', authMiddleware.authenticate, authMiddleware.authorize('admin', 'infonavit'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const derechohabiente = await service.qualify(getContext(req), req.params.id); - if (!derechohabiente) { - res.status(404).json({ error: 'Not Found', message: 'Worker not found' }); - return; - } - - res.status(200).json({ success: true, data: derechohabiente }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/derechohabientes/:id/points-history - * Get points history - */ - router.get('/:id/points-history', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const history = await service.getPointsHistory(getContext(req), req.params.id); - res.status(200).json({ success: true, data: history }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /api/derechohabientes/:id - * Soft delete worker - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await service.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Worker not found' }); - return; - } - - res.status(204).send(); - } catch (error) { - next(error); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/controllers/index.ts b/projects/erp-construccion/backend/src/modules/infonavit/controllers/index.ts deleted file mode 100644 index acf23a208..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/controllers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Infonavit Controllers Index - * @module Infonavit - */ - -export * from './derechohabiente.controller'; -export * from './asignacion.controller'; diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/acta-vivienda.entity.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/acta-vivienda.entity.ts deleted file mode 100644 index a61cbf667..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/acta-vivienda.entity.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * ActaVivienda Entity - * Viviendas incluidas en actas oficiales - * - * @module Infonavit - * @table infonavit.acta_viviendas - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Acta } from './acta.entity'; -import { OfertaVivienda } from './oferta-vivienda.entity'; - -export type ActaViviendaResult = 'pending' | 'approved' | 'rejected' | 'observations'; - -@Entity({ schema: 'infonavit', name: 'acta_viviendas' }) -@Index(['tenantId', 'actaId', 'ofertaId'], { unique: true }) -export class ActaVivienda { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'acta_id', type: 'uuid' }) - actaId: string; - - @Column({ name: 'oferta_id', type: 'uuid' }) - ofertaId: string; - - @Column({ name: 'sequence_number', type: 'integer' }) - sequenceNumber: number; - - @Column({ type: 'varchar', length: 20, default: 'pending' }) - result: ActaViviendaResult; - - @Column({ type: 'text', nullable: true }) - observations: string; - - @Column({ name: 'inspected_at', type: 'timestamptz', nullable: true }) - inspectedAt: Date; - - @Column({ name: 'inspected_by', type: 'uuid', nullable: true }) - inspectedById: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Acta, (a) => a.viviendas, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'acta_id' }) - acta: Acta; - - @ManyToOne(() => OfertaVivienda) - @JoinColumn({ name: 'oferta_id' }) - oferta: OfertaVivienda; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'inspected_by' }) - inspectedBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/acta.entity.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/acta.entity.ts deleted file mode 100644 index 7ca60ff66..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/acta.entity.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Acta Entity - * Actas oficiales INFONAVIT (entregas, inspecciones, etc.) - * - * @module Infonavit - * @table infonavit.actas - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { ActaVivienda } from './acta-vivienda.entity'; - -export type ActaTipo = 'delivery' | 'inspection' | 'verification' | 'closure' | 'other'; -export type ActaStatus = 'draft' | 'scheduled' | 'in_progress' | 'completed' | 'cancelled'; - -@Entity({ schema: 'infonavit', name: 'actas' }) -@Index(['tenantId', 'actaNumber'], { unique: true }) -export class Acta { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'registro_id', type: 'uuid' }) - registroId: string; - - @Column({ name: 'acta_number', type: 'varchar', length: 50 }) - actaNumber: string; - - @Column({ name: 'acta_type', type: 'varchar', length: 20 }) - actaType: ActaTipo; - - @Column({ name: 'scheduled_date', type: 'date' }) - scheduledDate: Date; - - @Column({ name: 'execution_date', type: 'date', nullable: true }) - executionDate: Date; - - @Column({ type: 'varchar', length: 255, nullable: true }) - location: string; - - @Column({ name: 'infonavit_representative', type: 'varchar', length: 200, nullable: true }) - infonavitRepresentative: string; - - @Column({ name: 'developer_representative', type: 'varchar', length: 200, nullable: true }) - developerRepresentative: string; - - @Column({ name: 'total_units', type: 'integer', default: 0 }) - totalUnits: number; - - @Column({ type: 'varchar', length: 20, default: 'draft' }) - status: ActaStatus; - - @Column({ type: 'text', nullable: true }) - observations: string; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => ActaVivienda, (av) => av.acta) - viviendas: ActaVivienda[]; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/asignacion-vivienda.entity.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/asignacion-vivienda.entity.ts deleted file mode 100644 index 7fa6a651b..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/asignacion-vivienda.entity.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * AsignacionVivienda Entity - * Asignaciones de vivienda a derechohabientes - * - * @module Infonavit - * @table infonavit.asignaciones_vivienda - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { OfertaVivienda } from './oferta-vivienda.entity'; -import { Derechohabiente } from './derechohabiente.entity'; - -export type AsignacionStatus = 'pending' | 'approved' | 'formalized' | 'delivered' | 'cancelled'; - -@Entity({ schema: 'infonavit', name: 'asignaciones_vivienda' }) -@Index(['tenantId', 'assignmentNumber'], { unique: true }) -export class AsignacionVivienda { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'oferta_id', type: 'uuid' }) - ofertaId: string; - - @Column({ name: 'derechohabiente_id', type: 'uuid' }) - derechohabienteId: string; - - @Column({ name: 'assignment_number', type: 'varchar', length: 50 }) - assignmentNumber: string; - - @Column({ name: 'assignment_date', type: 'date' }) - assignmentDate: Date; - - @Column({ name: 'approval_date', type: 'date', nullable: true }) - approvalDate: Date; - - @Column({ name: 'formalization_date', type: 'date', nullable: true }) - formalizationDate: Date; - - @Column({ name: 'delivery_date', type: 'date', nullable: true }) - deliveryDate: Date; - - @Column({ name: 'credit_amount', type: 'decimal', precision: 14, scale: 2 }) - creditAmount: number; - - @Column({ name: 'subsidy_amount', type: 'decimal', precision: 14, scale: 2, default: 0 }) - subsidyAmount: number; - - @Column({ name: 'down_payment', type: 'decimal', precision: 14, scale: 2, default: 0 }) - downPayment: number; - - @Column({ type: 'varchar', length: 20, default: 'pending' }) - status: AsignacionStatus; - - @Column({ name: 'notary_name', type: 'varchar', length: 200, nullable: true }) - notaryName: string; - - @Column({ name: 'notary_number', type: 'varchar', length: 20, nullable: true }) - notaryNumber: string; - - @Column({ name: 'deed_number', type: 'varchar', length: 50, nullable: true }) - deedNumber: string; - - @Column({ name: 'deed_date', type: 'date', nullable: true }) - deedDate: Date; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Computed property - get totalAmount(): number { - return Number(this.creditAmount) + Number(this.subsidyAmount) + Number(this.downPayment); - } - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => OfertaVivienda, (o) => o.asignaciones, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'oferta_id' }) - oferta: OfertaVivienda; - - @ManyToOne(() => Derechohabiente, (d) => d.asignaciones) - @JoinColumn({ name: 'derechohabiente_id' }) - derechohabiente: Derechohabiente; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/derechohabiente.entity.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/derechohabiente.entity.ts deleted file mode 100644 index 23d4d2a36..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/derechohabiente.entity.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Derechohabiente Entity - * Derechohabientes INFONAVIT (trabajadores con cr茅dito) - * - * @module Infonavit - * @table infonavit.derechohabientes - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { AsignacionVivienda } from './asignacion-vivienda.entity'; -import { HistoricoPuntos } from './historico-puntos.entity'; - -export type DerechohabienteStatus = 'active' | 'pre_qualified' | 'qualified' | 'assigned' | 'inactive'; - -@Entity({ schema: 'infonavit', name: 'derechohabientes' }) -@Index(['tenantId', 'nss'], { unique: true }) -@Index(['tenantId', 'curp'], { unique: true }) -export class Derechohabiente { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 11 }) - nss: string; - - @Column({ type: 'varchar', length: 18 }) - curp: string; - - @Column({ type: 'varchar', length: 13, nullable: true }) - rfc: string; - - @Column({ name: 'first_name', type: 'varchar', length: 100 }) - firstName: string; - - @Column({ name: 'last_name', type: 'varchar', length: 100 }) - lastName: string; - - @Column({ name: 'second_last_name', type: 'varchar', length: 100, nullable: true }) - secondLastName: string; - - @Column({ type: 'varchar', length: 20, nullable: true }) - phone: string; - - @Column({ type: 'varchar', length: 255, nullable: true }) - email: string; - - @Column({ type: 'text', nullable: true }) - address: string; - - @Column({ name: 'credit_points', type: 'integer', default: 0 }) - creditPoints: number; - - @Column({ name: 'precalification_date', type: 'date', nullable: true }) - precalificationDate: Date; - - @Column({ name: 'precalification_amount', type: 'decimal', precision: 14, scale: 2, nullable: true }) - precalificationAmount: number; - - @Column({ name: 'credit_type', type: 'varchar', length: 50, nullable: true }) - creditType: string; - - @Column({ type: 'varchar', length: 20, default: 'active' }) - status: DerechohabienteStatus; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Computed property - get fullName(): string { - return `${this.firstName} ${this.lastName}${this.secondLastName ? ' ' + this.secondLastName : ''}`; - } - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => AsignacionVivienda, (a) => a.derechohabiente) - asignaciones: AsignacionVivienda[]; - - @OneToMany(() => HistoricoPuntos, (h) => h.derechohabiente) - historicoPuntos: HistoricoPuntos[]; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/historico-puntos.entity.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/historico-puntos.entity.ts deleted file mode 100644 index 577b66a86..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/historico-puntos.entity.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * HistoricoPuntos Entity - * Hist贸rico de puntos de cr茅dito INFONAVIT - * - * @module Infonavit - * @table infonavit.historico_puntos - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Derechohabiente } from './derechohabiente.entity'; - -export type MovimientoTipo = 'initial' | 'update' | 'adjustment' | 'precalification'; - -@Entity({ schema: 'infonavit', name: 'historico_puntos' }) -@Index(['tenantId', 'derechohabienteId', 'recordDate']) -export class HistoricoPuntos { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'derechohabiente_id', type: 'uuid' }) - derechohabienteId: string; - - @Column({ name: 'record_date', type: 'date' }) - recordDate: Date; - - @Column({ name: 'previous_points', type: 'integer' }) - previousPoints: number; - - @Column({ name: 'new_points', type: 'integer' }) - newPoints: number; - - @Column({ name: 'points_change', type: 'integer' }) - pointsChange: number; - - @Column({ name: 'movement_type', type: 'varchar', length: 20 }) - movementType: MovimientoTipo; - - @Column({ type: 'varchar', length: 255, nullable: true }) - source: string; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Derechohabiente, (d) => d.historicoPuntos, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'derechohabiente_id' }) - derechohabiente: Derechohabiente; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/index.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/index.ts deleted file mode 100644 index 3c57cc93a..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Infonavit Entities Index - * @module Infonavit - * - * Gesti贸n de viviendas INFONAVIT (MAI-011) - */ - -export * from './registro-infonavit.entity'; -export * from './oferta-vivienda.entity'; -export * from './derechohabiente.entity'; -export * from './asignacion-vivienda.entity'; -export * from './acta.entity'; -export * from './acta-vivienda.entity'; -export * from './reporte-infonavit.entity'; -export * from './historico-puntos.entity'; diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/oferta-vivienda.entity.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/oferta-vivienda.entity.ts deleted file mode 100644 index d3364cd1b..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/oferta-vivienda.entity.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * OfertaVivienda Entity - * Ofertas de vivienda registradas ante INFONAVIT - * - * @module Infonavit - * @table infonavit.ofertas_vivienda - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { RegistroInfonavit } from './registro-infonavit.entity'; -import { AsignacionVivienda } from './asignacion-vivienda.entity'; - -export type OfertaStatus = 'available' | 'reserved' | 'assigned' | 'delivered' | 'cancelled'; - -@Entity({ schema: 'infonavit', name: 'ofertas_vivienda' }) -@Index(['tenantId', 'infonavitCode'], { unique: true }) -export class OfertaVivienda { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'registro_id', type: 'uuid' }) - registroId: string; - - @Column({ name: 'lote_id', type: 'uuid' }) - loteId: string; - - @Column({ name: 'infonavit_code', type: 'varchar', length: 50 }) - infonavitCode: string; - - @Column({ name: 'offer_date', type: 'date' }) - offerDate: Date; - - @Column({ name: 'sale_price', type: 'decimal', precision: 14, scale: 2 }) - salePrice: number; - - @Column({ name: 'infonavit_value', type: 'decimal', precision: 14, scale: 2 }) - infonavitValue: number; - - @Column({ name: 'housing_type', type: 'varchar', length: 50 }) - housingType: string; - - @Column({ name: 'construction_area', type: 'decimal', precision: 10, scale: 2 }) - constructionArea: number; - - @Column({ name: 'land_area', type: 'decimal', precision: 10, scale: 2 }) - landArea: number; - - @Column({ type: 'varchar', length: 20, default: 'available' }) - status: OfertaStatus; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => RegistroInfonavit, (r) => r.ofertas, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'registro_id' }) - registro: RegistroInfonavit; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => AsignacionVivienda, (a) => a.oferta) - asignaciones: AsignacionVivienda[]; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/registro-infonavit.entity.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/registro-infonavit.entity.ts deleted file mode 100644 index 8bef050f8..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/registro-infonavit.entity.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * RegistroInfonavit Entity - * Registro de proyectos ante INFONAVIT - * - * @module Infonavit - * @table infonavit.registros_infonavit - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { OfertaVivienda } from './oferta-vivienda.entity'; - -export type RegistroStatus = 'draft' | 'submitted' | 'approved' | 'active' | 'closed'; - -@Entity({ schema: 'infonavit', name: 'registros_infonavit' }) -@Index(['tenantId', 'registrationNumber'], { unique: true }) -export class RegistroInfonavit { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'registration_number', type: 'varchar', length: 50 }) - registrationNumber: string; - - @Column({ name: 'registration_date', type: 'date' }) - registrationDate: Date; - - @Column({ name: 'approval_date', type: 'date', nullable: true }) - approvalDate: Date; - - @Column({ name: 'developer_code', type: 'varchar', length: 30, nullable: true }) - developerCode: string; - - @Column({ name: 'total_units', type: 'integer' }) - totalUnits: number; - - @Column({ name: 'registered_units', type: 'integer', default: 0 }) - registeredUnits: number; - - @Column({ name: 'assigned_units', type: 'integer', default: 0 }) - assignedUnits: number; - - @Column({ type: 'varchar', length: 20, default: 'draft' }) - status: RegistroStatus; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => OfertaVivienda, (oferta) => oferta.registro) - ofertas: OfertaVivienda[]; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/entities/reporte-infonavit.entity.ts b/projects/erp-construccion/backend/src/modules/infonavit/entities/reporte-infonavit.entity.ts deleted file mode 100644 index 57db266ba..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/entities/reporte-infonavit.entity.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * ReporteInfonavit Entity - * Reportes generados para INFONAVIT - * - * @module Infonavit - * @table infonavit.reportes_infonavit - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; - -export type ReporteTipo = 'monthly' | 'quarterly' | 'annual' | 'progress' | 'delivery' | 'custom'; -export type ReporteStatus = 'draft' | 'generated' | 'submitted' | 'accepted' | 'rejected'; - -@Entity({ schema: 'infonavit', name: 'reportes_infonavit' }) -@Index(['tenantId', 'reportNumber'], { unique: true }) -export class ReporteInfonavit { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'registro_id', type: 'uuid' }) - registroId: string; - - @Column({ name: 'report_number', type: 'varchar', length: 50 }) - reportNumber: string; - - @Column({ name: 'report_type', type: 'varchar', length: 20 }) - reportType: ReporteTipo; - - @Column({ name: 'period_start', type: 'date' }) - periodStart: Date; - - @Column({ name: 'period_end', type: 'date' }) - periodEnd: Date; - - @Column({ name: 'generation_date', type: 'date' }) - generationDate: Date; - - @Column({ name: 'submission_date', type: 'date', nullable: true }) - submissionDate: Date; - - @Column({ name: 'total_units_reported', type: 'integer', default: 0 }) - totalUnitsReported: number; - - @Column({ name: 'units_in_progress', type: 'integer', default: 0 }) - unitsInProgress: number; - - @Column({ name: 'units_completed', type: 'integer', default: 0 }) - unitsCompleted: number; - - @Column({ name: 'units_delivered', type: 'integer', default: 0 }) - unitsDelivered: number; - - @Column({ type: 'varchar', length: 20, default: 'draft' }) - status: ReporteStatus; - - @Column({ name: 'file_path', type: 'varchar', length: 500, nullable: true }) - filePath: string; - - @Column({ type: 'jsonb', nullable: true }) - data: Record; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @Column({ name: 'rejection_reason', type: 'text', nullable: true }) - rejectionReason: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'submitted_by', type: 'uuid', nullable: true }) - submittedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'submitted_by' }) - submittedBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/services/asignacion.service.ts b/projects/erp-construccion/backend/src/modules/infonavit/services/asignacion.service.ts deleted file mode 100644 index c61f5eadb..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/services/asignacion.service.ts +++ /dev/null @@ -1,290 +0,0 @@ -/** - * AsignacionService - Servicio de asignaciones de vivienda INFONAVIT - * - * Gesti贸n de asignaciones de vivienda a derechohabientes. - * - * @module Infonavit - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { AsignacionVivienda, AsignacionStatus } from '../entities/asignacion-vivienda.entity'; -import { OfertaVivienda } from '../entities/oferta-vivienda.entity'; -import { Derechohabiente } from '../entities/derechohabiente.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateAsignacionDto { - ofertaId: string; - derechohabienteId: string; - assignmentDate: Date; - creditAmount: number; - subsidyAmount?: number; - downPayment?: number; - notes?: string; -} - -export interface FormalizeDto { - formalizationDate: Date; - notaryName: string; - notaryNumber: string; - deedNumber: string; - deedDate: Date; -} - -export interface AsignacionFilters { - registroId?: string; - ofertaId?: string; - derechohabienteId?: string; - status?: AsignacionStatus; - dateFrom?: Date; - dateTo?: Date; -} - -export class AsignacionService { - constructor( - private readonly asignacionRepository: Repository, - private readonly ofertaRepository: Repository, - private readonly derechohabienteRepository: Repository - ) {} - - private generateAssignmentNumber(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 8).toUpperCase(); - return `ASG-${year}${month}-${random}`; - } - - async findWithFilters( - ctx: ServiceContext, - filters: AsignacionFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.asignacionRepository - .createQueryBuilder('asig') - .leftJoinAndSelect('asig.oferta', 'oferta') - .leftJoinAndSelect('asig.derechohabiente', 'derechohabiente') - .leftJoinAndSelect('asig.createdBy', 'createdBy') - .where('asig.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.ofertaId) { - queryBuilder.andWhere('asig.oferta_id = :ofertaId', { ofertaId: filters.ofertaId }); - } - - if (filters.derechohabienteId) { - queryBuilder.andWhere('asig.derechohabiente_id = :derechohabienteId', { - derechohabienteId: filters.derechohabienteId, - }); - } - - if (filters.status) { - queryBuilder.andWhere('asig.status = :status', { status: filters.status }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('asig.assignment_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('asig.assignment_date <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('asig.assignment_date', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.asignacionRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as unknown as FindOptionsWhere, - }); - } - - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.asignacionRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as unknown as FindOptionsWhere, - relations: ['oferta', 'oferta.registro', 'derechohabiente', 'createdBy'], - }); - } - - async create(ctx: ServiceContext, dto: CreateAsignacionDto): Promise { - // Validate oferta exists and is available - const oferta = await this.ofertaRepository.findOne({ - where: { - id: dto.ofertaId, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - }); - - if (!oferta) { - throw new Error('Housing offer not found'); - } - - if (oferta.status !== 'available' && oferta.status !== 'reserved') { - throw new Error('Housing offer is not available for assignment'); - } - - // Validate derechohabiente exists and is qualified - const derechohabiente = await this.derechohabienteRepository.findOne({ - where: { - id: dto.derechohabienteId, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - - if (!derechohabiente) { - throw new Error('Worker not found'); - } - - if (derechohabiente.status !== 'qualified' && derechohabiente.status !== 'pre_qualified') { - throw new Error('Worker must be pre-qualified or qualified for assignment'); - } - - // Create assignment - const asignacion = this.asignacionRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - ofertaId: dto.ofertaId, - derechohabienteId: dto.derechohabienteId, - assignmentNumber: this.generateAssignmentNumber(), - assignmentDate: dto.assignmentDate, - creditAmount: dto.creditAmount, - subsidyAmount: dto.subsidyAmount || 0, - downPayment: dto.downPayment || 0, - notes: dto.notes, - status: 'pending', - }); - - const saved = await this.asignacionRepository.save(asignacion); - - // Update oferta status - oferta.status = 'reserved'; - await this.ofertaRepository.save(oferta); - - return saved; - } - - async approve(ctx: ServiceContext, id: string): Promise { - const asignacion = await this.findWithDetails(ctx, id); - if (!asignacion) { - return null; - } - - if (asignacion.status !== 'pending') { - throw new Error('Can only approve pending assignments'); - } - - asignacion.status = 'approved'; - asignacion.approvalDate = new Date(); - asignacion.updatedById = ctx.userId || ''; - - // Update oferta status - asignacion.oferta.status = 'assigned'; - await this.ofertaRepository.save(asignacion.oferta); - - // Update derechohabiente status - await this.derechohabienteRepository.update( - { id: asignacion.derechohabienteId } as FindOptionsWhere, - { status: 'assigned', updatedById: ctx.userId || '' } - ); - - return this.asignacionRepository.save(asignacion); - } - - async formalize(ctx: ServiceContext, id: string, dto: FormalizeDto): Promise { - const asignacion = await this.findWithDetails(ctx, id); - if (!asignacion) { - return null; - } - - if (asignacion.status !== 'approved') { - throw new Error('Can only formalize approved assignments'); - } - - asignacion.status = 'formalized'; - asignacion.formalizationDate = dto.formalizationDate; - asignacion.notaryName = dto.notaryName; - asignacion.notaryNumber = dto.notaryNumber; - asignacion.deedNumber = dto.deedNumber; - asignacion.deedDate = dto.deedDate; - asignacion.updatedById = ctx.userId || ''; - - return this.asignacionRepository.save(asignacion); - } - - async deliver(ctx: ServiceContext, id: string, deliveryDate: Date): Promise { - const asignacion = await this.findWithDetails(ctx, id); - if (!asignacion) { - return null; - } - - if (asignacion.status !== 'formalized') { - throw new Error('Can only deliver formalized assignments'); - } - - asignacion.status = 'delivered'; - asignacion.deliveryDate = deliveryDate; - asignacion.updatedById = ctx.userId || ''; - - // Update oferta status - asignacion.oferta.status = 'delivered'; - await this.ofertaRepository.save(asignacion.oferta); - - return this.asignacionRepository.save(asignacion); - } - - async cancel(ctx: ServiceContext, id: string, reason: string): Promise { - const asignacion = await this.findWithDetails(ctx, id); - if (!asignacion) { - return null; - } - - if (asignacion.status === 'delivered') { - throw new Error('Cannot cancel delivered assignments'); - } - - const previousStatus = asignacion.status; - asignacion.status = 'cancelled'; - asignacion.notes = `${asignacion.notes || ''}\n[CANCELLED] ${reason}`; - asignacion.updatedById = ctx.userId || ''; - - // Revert oferta status if it was assigned/reserved - if (previousStatus === 'approved' || previousStatus === 'formalized') { - asignacion.oferta.status = 'available'; - await this.ofertaRepository.save(asignacion.oferta); - - // Revert derechohabiente status - await this.derechohabienteRepository.update( - { id: asignacion.derechohabienteId } as FindOptionsWhere, - { status: 'qualified', updatedById: ctx.userId || '' } - ); - } else if (previousStatus === 'pending') { - asignacion.oferta.status = 'available'; - await this.ofertaRepository.save(asignacion.oferta); - } - - return this.asignacionRepository.save(asignacion); - } -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/services/derechohabiente.service.ts b/projects/erp-construccion/backend/src/modules/infonavit/services/derechohabiente.service.ts deleted file mode 100644 index 6cdc87488..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/services/derechohabiente.service.ts +++ /dev/null @@ -1,328 +0,0 @@ -/** - * DerechohabienteService - Servicio de gesti贸n de derechohabientes INFONAVIT - * - * Gesti贸n de trabajadores con cr茅dito INFONAVIT. - * - * @module Infonavit - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { Derechohabiente, DerechohabienteStatus } from '../entities/derechohabiente.entity'; -import { HistoricoPuntos } from '../entities/historico-puntos.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateDerechohabienteDto { - nss: string; - curp: string; - rfc?: string; - firstName: string; - lastName: string; - secondLastName?: string; - phone?: string; - email?: string; - address?: string; - creditPoints?: number; - notes?: string; -} - -export interface UpdateDerechohabienteDto { - phone?: string; - email?: string; - address?: string; - notes?: string; -} - -export interface PrecalificationDto { - precalificationDate: Date; - precalificationAmount: number; - creditType: string; - creditPoints: number; -} - -export interface DerechohabienteFilters { - nss?: string; - curp?: string; - status?: DerechohabienteStatus; - search?: string; - minPoints?: number; -} - -export class DerechohabienteService { - constructor( - private readonly derechohabienteRepository: Repository, - private readonly historicoPuntosRepository: Repository - ) {} - - async findWithFilters( - ctx: ServiceContext, - filters: DerechohabienteFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.derechohabienteRepository - .createQueryBuilder('dh') - .leftJoinAndSelect('dh.createdBy', 'createdBy') - .where('dh.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('dh.deleted_at IS NULL'); - - if (filters.nss) { - queryBuilder.andWhere('dh.nss = :nss', { nss: filters.nss }); - } - - if (filters.curp) { - queryBuilder.andWhere('dh.curp = :curp', { curp: filters.curp }); - } - - if (filters.status) { - queryBuilder.andWhere('dh.status = :status', { status: filters.status }); - } - - if (filters.search) { - queryBuilder.andWhere( - '(dh.first_name ILIKE :search OR dh.last_name ILIKE :search OR dh.nss ILIKE :search)', - { search: `%${filters.search}%` } - ); - } - - if (filters.minPoints !== undefined) { - queryBuilder.andWhere('dh.credit_points >= :minPoints', { minPoints: filters.minPoints }); - } - - queryBuilder - .orderBy('dh.last_name', 'ASC') - .addOrderBy('dh.first_name', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.derechohabienteRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - } - - async findByNss(ctx: ServiceContext, nss: string): Promise { - return this.derechohabienteRepository.findOne({ - where: { - nss, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - } - - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.derechohabienteRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - relations: ['createdBy', 'asignaciones', 'asignaciones.oferta', 'historicoPuntos'], - }); - } - - async create(ctx: ServiceContext, dto: CreateDerechohabienteDto): Promise { - // Check for existing NSS or CURP - const existingNss = await this.derechohabienteRepository.findOne({ - where: { nss: dto.nss, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (existingNss) { - throw new Error('A worker with this NSS already exists'); - } - - const existingCurp = await this.derechohabienteRepository.findOne({ - where: { curp: dto.curp, tenantId: ctx.tenantId } as FindOptionsWhere, - }); - if (existingCurp) { - throw new Error('A worker with this CURP already exists'); - } - - const derechohabiente = this.derechohabienteRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - nss: dto.nss, - curp: dto.curp.toUpperCase(), - rfc: dto.rfc?.toUpperCase(), - firstName: dto.firstName, - lastName: dto.lastName, - secondLastName: dto.secondLastName, - phone: dto.phone, - email: dto.email, - address: dto.address, - creditPoints: dto.creditPoints || 0, - notes: dto.notes, - status: 'active', - }); - - const saved = await this.derechohabienteRepository.save(derechohabiente); - - // Record initial points if provided - if (dto.creditPoints && dto.creditPoints > 0) { - const historico = this.historicoPuntosRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - derechohabienteId: saved.id, - recordDate: new Date(), - previousPoints: 0, - newPoints: dto.creditPoints, - pointsChange: dto.creditPoints, - movementType: 'initial', - source: 'Registration', - }); - await this.historicoPuntosRepository.save(historico); - } - - return saved; - } - - async update(ctx: ServiceContext, id: string, dto: UpdateDerechohabienteDto): Promise { - const derechohabiente = await this.findById(ctx, id); - if (!derechohabiente) { - return null; - } - - Object.assign(derechohabiente, { - ...dto, - updatedById: ctx.userId || '', - }); - - return this.derechohabienteRepository.save(derechohabiente); - } - - async updatePoints( - ctx: ServiceContext, - id: string, - newPoints: number, - source: string, - notes?: string - ): Promise { - const derechohabiente = await this.findById(ctx, id); - if (!derechohabiente) { - return null; - } - - const previousPoints = derechohabiente.creditPoints; - - // Record points change - const historico = this.historicoPuntosRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - derechohabienteId: id, - recordDate: new Date(), - previousPoints, - newPoints, - pointsChange: newPoints - previousPoints, - movementType: 'update', - source, - notes, - }); - await this.historicoPuntosRepository.save(historico); - - // Update derechohabiente - derechohabiente.creditPoints = newPoints; - derechohabiente.updatedById = ctx.userId || ''; - - return this.derechohabienteRepository.save(derechohabiente); - } - - async precalify(ctx: ServiceContext, id: string, dto: PrecalificationDto): Promise { - const derechohabiente = await this.findById(ctx, id); - if (!derechohabiente) { - return null; - } - - if (derechohabiente.status !== 'active') { - throw new Error('Only active workers can be precalified'); - } - - const previousPoints = derechohabiente.creditPoints; - - // Record points change from precalification - const historico = this.historicoPuntosRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - derechohabienteId: id, - recordDate: dto.precalificationDate, - previousPoints, - newPoints: dto.creditPoints, - pointsChange: dto.creditPoints - previousPoints, - movementType: 'precalification', - source: 'INFONAVIT Precalification', - }); - await this.historicoPuntosRepository.save(historico); - - // Update derechohabiente - derechohabiente.creditPoints = dto.creditPoints; - derechohabiente.precalificationDate = dto.precalificationDate; - derechohabiente.precalificationAmount = dto.precalificationAmount; - derechohabiente.creditType = dto.creditType; - derechohabiente.status = 'pre_qualified'; - derechohabiente.updatedById = ctx.userId || ''; - - return this.derechohabienteRepository.save(derechohabiente); - } - - async qualify(ctx: ServiceContext, id: string): Promise { - const derechohabiente = await this.findById(ctx, id); - if (!derechohabiente) { - return null; - } - - if (derechohabiente.status !== 'pre_qualified') { - throw new Error('Only pre-qualified workers can be qualified'); - } - - derechohabiente.status = 'qualified'; - derechohabiente.updatedById = ctx.userId || ''; - - return this.derechohabienteRepository.save(derechohabiente); - } - - async softDelete(ctx: ServiceContext, id: string): Promise { - const derechohabiente = await this.findById(ctx, id); - if (!derechohabiente) { - return false; - } - - if (derechohabiente.status === 'assigned') { - throw new Error('Cannot delete workers with assigned housing'); - } - - await this.derechohabienteRepository.update( - { id, tenantId: ctx.tenantId } as FindOptionsWhere, - { deletedAt: new Date(), deletedById: ctx.userId || '' } - ); - - return true; - } - - async getPointsHistory(ctx: ServiceContext, derechohabienteId: string): Promise { - return this.historicoPuntosRepository.find({ - where: { - derechohabienteId, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - order: { recordDate: 'DESC' }, - relations: ['createdBy'], - }); - } -} diff --git a/projects/erp-construccion/backend/src/modules/infonavit/services/index.ts b/projects/erp-construccion/backend/src/modules/infonavit/services/index.ts deleted file mode 100644 index eb79b5477..000000000 --- a/projects/erp-construccion/backend/src/modules/infonavit/services/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Infonavit Services Index - * @module Infonavit - */ - -export * from './derechohabiente.service'; -export * from './asignacion.service'; diff --git a/projects/erp-construccion/backend/src/modules/inventory/controllers/consumo-obra.controller.ts b/projects/erp-construccion/backend/src/modules/inventory/controllers/consumo-obra.controller.ts deleted file mode 100644 index 91f5394b2..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/controllers/consumo-obra.controller.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * ConsumoObraController - Controller de consumos de materiales - * - * Endpoints REST para registro de consumos de materiales por obra. - * - * @module Inventory - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - ConsumoObraService, - CreateConsumoDto, - ConsumoFilters, -} from '../services/consumo-obra.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { ConsumoObra } from '../entities/consumo-obra.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de consumos - */ -export function createConsumoObraController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const consumoRepository = dataSource.getRepository(ConsumoObra); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const consumoService = new ConsumoObraService(consumoRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /consumos - * Listar consumos con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: ConsumoFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - loteId: req.query.loteId as string, - conceptoId: req.query.conceptoId as string, - productId: req.query.productId as string, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await consumoService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /consumos/stats/:fraccionamientoId - * Obtener estad铆sticas de consumos por fraccionamiento - */ - router.get('/stats/:fraccionamientoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await consumoService.getStats(getContext(req), req.params.fraccionamientoId); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /consumos/:id - * Obtener consumo por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const consumo = await consumoService.findById(getContext(req), req.params.id); - if (!consumo) { - res.status(404).json({ error: 'Not Found', message: 'Consumption record not found' }); - return; - } - - res.status(200).json({ success: true, data: consumo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /consumos - * Registrar consumo de material - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateConsumoDto = req.body; - - if (!dto.fraccionamientoId || !dto.productId || dto.quantity === undefined || !dto.consumptionDate) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, productId, quantity and consumptionDate are required', - }); - return; - } - - dto.consumptionDate = new Date(dto.consumptionDate); - - const consumo = await consumoService.create(getContext(req), dto); - res.status(201).json({ success: true, data: consumo }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /consumos/:id - * Eliminar registro de consumo (soft delete) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await consumoService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Consumption record not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Consumption record deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createConsumoObraController; diff --git a/projects/erp-construccion/backend/src/modules/inventory/controllers/index.ts b/projects/erp-construccion/backend/src/modules/inventory/controllers/index.ts deleted file mode 100644 index 90c1a07c5..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/controllers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Inventory Controllers Index - * @module Inventory - */ - -export { createRequisicionController } from './requisicion.controller'; -export { createConsumoObraController } from './consumo-obra.controller'; diff --git a/projects/erp-construccion/backend/src/modules/inventory/controllers/requisicion.controller.ts b/projects/erp-construccion/backend/src/modules/inventory/controllers/requisicion.controller.ts deleted file mode 100644 index e83d8ed91..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/controllers/requisicion.controller.ts +++ /dev/null @@ -1,363 +0,0 @@ -/** - * RequisicionController - Controller de requisiciones de obra - * - * Endpoints REST para gesti贸n de requisiciones de material. - * Workflow: draft -> submitted -> approved -> partially_served -> served - * - * @module Inventory - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - RequisicionService, - CreateRequisicionDto, - AddLineaDto, - RequisicionFilters, -} from '../services/requisicion.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { RequisicionObra } from '../entities/requisicion-obra.entity'; -import { RequisicionLinea } from '../entities/requisicion-linea.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de requisiciones - */ -export function createRequisicionController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const requisicionRepository = dataSource.getRepository(RequisicionObra); - const lineaRepository = dataSource.getRepository(RequisicionLinea); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const requisicionService = new RequisicionService(requisicionRepository, lineaRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /requisiciones - * Listar requisiciones con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: RequisicionFilters = { - fraccionamientoId: req.query.fraccionamientoId as string, - status: req.query.status as any, - priority: req.query.priority as string, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await requisicionService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /requisiciones/:id - * Obtener requisici贸n con detalles - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const requisicion = await requisicionService.findWithDetails(getContext(req), req.params.id); - if (!requisicion) { - res.status(404).json({ error: 'Not Found', message: 'Requisition not found' }); - return; - } - - res.status(200).json({ success: true, data: requisicion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /requisiciones - * Crear requisici贸n - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateRequisicionDto = req.body; - - if (!dto.fraccionamientoId || !dto.requisitionDate || !dto.requiredDate) { - res.status(400).json({ - error: 'Bad Request', - message: 'fraccionamientoId, requisitionDate and requiredDate are required', - }); - return; - } - - dto.requisitionDate = new Date(dto.requisitionDate); - dto.requiredDate = new Date(dto.requiredDate); - - const requisicion = await requisicionService.create(getContext(req), dto); - res.status(201).json({ success: true, data: requisicion }); - } catch (error) { - next(error); - } - }); - - /** - * POST /requisiciones/:id/lineas - * Agregar l铆nea a requisici贸n - */ - router.post('/:id/lineas', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: AddLineaDto = req.body; - - if (!dto.productId || dto.quantityRequested === undefined) { - res.status(400).json({ - error: 'Bad Request', - message: 'productId and quantityRequested are required', - }); - return; - } - - const linea = await requisicionService.addLinea(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: linea }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Requisicion not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message.includes('draft')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * DELETE /requisiciones/:id/lineas/:lineaId - * Eliminar l铆nea de requisici贸n - */ - router.delete('/:id/lineas/:lineaId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const removed = await requisicionService.removeLinea(getContext(req), req.params.id, req.params.lineaId); - if (!removed) { - res.status(404).json({ error: 'Not Found', message: 'Line not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Line removed' }); - } catch (error) { - if (error instanceof Error && error.message.includes('draft')) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /requisiciones/:id/submit - * Enviar requisici贸n para aprobaci贸n - */ - router.post('/:id/submit', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const requisicion = await requisicionService.submit(getContext(req), req.params.id); - if (!requisicion) { - res.status(404).json({ error: 'Not Found', message: 'Requisition not found' }); - return; - } - - res.status(200).json({ success: true, data: requisicion, message: 'Requisition submitted' }); - } catch (error) { - if (error instanceof Error) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /requisiciones/:id/approve - * Aprobar requisici贸n - */ - router.post('/:id/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const requisicion = await requisicionService.approve(getContext(req), req.params.id); - if (!requisicion) { - res.status(404).json({ error: 'Not Found', message: 'Requisition not found' }); - return; - } - - res.status(200).json({ success: true, data: requisicion, message: 'Requisition approved' }); - } catch (error) { - if (error instanceof Error) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /requisiciones/:id/reject - * Rechazar requisici贸n - */ - router.post('/:id/reject', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { reason } = req.body; - if (!reason) { - res.status(400).json({ error: 'Bad Request', message: 'reason is required' }); - return; - } - - const requisicion = await requisicionService.reject(getContext(req), req.params.id, reason); - if (!requisicion) { - res.status(404).json({ error: 'Not Found', message: 'Requisition not found' }); - return; - } - - res.status(200).json({ success: true, data: requisicion, message: 'Requisition rejected' }); - } catch (error) { - if (error instanceof Error) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /requisiciones/:id/cancel - * Cancelar requisici贸n - */ - router.post('/:id/cancel', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const requisicion = await requisicionService.cancel(getContext(req), req.params.id); - if (!requisicion) { - res.status(404).json({ error: 'Not Found', message: 'Requisition not found' }); - return; - } - - res.status(200).json({ success: true, data: requisicion, message: 'Requisition cancelled' }); - } catch (error) { - if (error instanceof Error) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * DELETE /requisiciones/:id - * Eliminar requisici贸n (soft delete) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await requisicionService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Requisition not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Requisition deleted' }); - } catch (error) { - if (error instanceof Error) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - return router; -} - -export default createRequisicionController; diff --git a/projects/erp-construccion/backend/src/modules/inventory/entities/almacen-proyecto.entity.ts b/projects/erp-construccion/backend/src/modules/inventory/entities/almacen-proyecto.entity.ts deleted file mode 100644 index 21849769b..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/entities/almacen-proyecto.entity.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * AlmacenProyecto Entity - * Almacenes por proyecto/obra - * - * @module Inventory - * @table inventory.almacenes_proyecto - * @ddl schemas/06-inventory-ext-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -export type WarehouseTypeConstruction = 'central' | 'obra' | 'temporal' | 'transito'; - -@Entity({ schema: 'inventory', name: 'almacenes_proyecto' }) -@Index(['warehouseId'], { unique: true }) -export class AlmacenProyecto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'warehouse_id', type: 'uuid' }) - warehouseId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ - name: 'warehouse_type', - type: 'varchar', - length: 20, - default: 'obra', - }) - warehouseType: WarehouseTypeConstruction; - - @Column({ name: 'location_description', type: 'text', nullable: true }) - locationDescription: string; - - @Column({ name: 'responsible_id', type: 'uuid', nullable: true }) - responsibleId: string; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => User) - @JoinColumn({ name: 'responsible_id' }) - responsible: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/inventory/entities/consumo-obra.entity.ts b/projects/erp-construccion/backend/src/modules/inventory/entities/consumo-obra.entity.ts deleted file mode 100644 index 8d257f16b..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/entities/consumo-obra.entity.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * ConsumoObra Entity - * Consumos de materiales por obra/lote - * - * @module Inventory - * @table inventory.consumos_obra - * @ddl schemas/06-inventory-ext-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { Lote } from '../../construction/entities/lote.entity'; -import { Concepto } from '../../budgets/entities/concepto.entity'; - -@Entity({ schema: 'inventory', name: 'consumos_obra' }) -export class ConsumoObra { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'stock_move_id', type: 'uuid', nullable: true }) - stockMoveId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'lote_id', type: 'uuid', nullable: true }) - loteId: string; - - @Column({ name: 'departamento_id', type: 'uuid', nullable: true }) - departamentoId: string; - - @Column({ name: 'concepto_id', type: 'uuid', nullable: true }) - conceptoId: string; - - @Column({ name: 'product_id', type: 'uuid' }) - productId: string; - - @Column({ type: 'decimal', precision: 12, scale: 4 }) - quantity: number; - - @Column({ name: 'unit_cost', type: 'decimal', precision: 12, scale: 4, nullable: true }) - unitCost: number; - - @Column({ name: 'consumption_date', type: 'date' }) - consumptionDate: Date; - - @Column({ name: 'registered_by', type: 'uuid' }) - registeredById: string; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Computed property (in DB is GENERATED ALWAYS AS) - get totalCost(): number { - return this.quantity * (this.unitCost || 0); - } - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => Lote) - @JoinColumn({ name: 'lote_id' }) - lote: Lote; - - @ManyToOne(() => Concepto) - @JoinColumn({ name: 'concepto_id' }) - concepto: Concepto; - - @ManyToOne(() => User) - @JoinColumn({ name: 'registered_by' }) - registeredBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/inventory/entities/index.ts b/projects/erp-construccion/backend/src/modules/inventory/entities/index.ts deleted file mode 100644 index 818b04331..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/entities/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Inventory Entities Index - * @module Inventory - * - * Extensiones de inventario para construcci贸n (MAI-004) - */ - -export * from './almacen-proyecto.entity'; -export * from './requisicion-obra.entity'; -export * from './requisicion-linea.entity'; -export * from './consumo-obra.entity'; diff --git a/projects/erp-construccion/backend/src/modules/inventory/entities/requisicion-linea.entity.ts b/projects/erp-construccion/backend/src/modules/inventory/entities/requisicion-linea.entity.ts deleted file mode 100644 index 126826c53..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/entities/requisicion-linea.entity.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * RequisicionLinea Entity - * L铆neas de requisici贸n de obra - * - * @module Inventory - * @table inventory.requisicion_lineas - * @ddl schemas/06-inventory-ext-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { RequisicionObra } from './requisicion-obra.entity'; -import { Concepto } from '../../budgets/entities/concepto.entity'; -import { Lote } from '../../construction/entities/lote.entity'; - -@Entity({ schema: 'inventory', name: 'requisicion_lineas' }) -export class RequisicionLinea { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'requisicion_id', type: 'uuid' }) - requisicionId: string; - - @Column({ name: 'product_id', type: 'uuid' }) - productId: string; - - @Column({ name: 'concepto_id', type: 'uuid', nullable: true }) - conceptoId: string; - - @Column({ name: 'lote_id', type: 'uuid', nullable: true }) - loteId: string; - - @Column({ - name: 'quantity_requested', - type: 'decimal', - precision: 12, - scale: 4, - }) - quantityRequested: number; - - @Column({ - name: 'quantity_approved', - type: 'decimal', - precision: 12, - scale: 4, - nullable: true, - }) - quantityApproved: number; - - @Column({ - name: 'quantity_served', - type: 'decimal', - precision: 12, - scale: 4, - default: 0, - }) - quantityServed: number; - - @Column({ name: 'unit_id', type: 'uuid', nullable: true }) - unitId: string; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => RequisicionObra, (req) => req.lineas, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'requisicion_id' }) - requisicion: RequisicionObra; - - @ManyToOne(() => Concepto) - @JoinColumn({ name: 'concepto_id' }) - concepto: Concepto; - - @ManyToOne(() => Lote) - @JoinColumn({ name: 'lote_id' }) - lote: Lote; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/inventory/entities/requisicion-obra.entity.ts b/projects/erp-construccion/backend/src/modules/inventory/entities/requisicion-obra.entity.ts deleted file mode 100644 index c939fd784..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/entities/requisicion-obra.entity.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * RequisicionObra Entity - * Requisiciones de material desde obra - * - * @module Inventory - * @table inventory.requisiciones_obra - * @ddl schemas/06-inventory-ext-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { RequisicionLinea } from './requisicion-linea.entity'; - -export type RequisitionStatus = 'draft' | 'submitted' | 'approved' | 'partially_served' | 'served' | 'cancelled'; - -@Entity({ schema: 'inventory', name: 'requisiciones_obra' }) -@Index(['tenantId', 'requisitionNumber'], { unique: true }) -export class RequisicionObra { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'requisition_number', type: 'varchar', length: 30 }) - requisitionNumber: string; - - @Column({ name: 'requisition_date', type: 'date' }) - requisitionDate: Date; - - @Column({ name: 'required_date', type: 'date' }) - requiredDate: Date; - - @Column({ type: 'varchar', length: 20, default: 'draft' }) - status: RequisitionStatus; - - @Column({ type: 'varchar', length: 20, default: 'medium' }) - priority: string; - - @Column({ name: 'requested_by', type: 'uuid' }) - requestedById: string; - - @Column({ name: 'destination_warehouse_id', type: 'uuid', nullable: true }) - destinationWarehouseId: string; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date; - - @Column({ name: 'rejection_reason', type: 'text', nullable: true }) - rejectionReason: string; - - @Column({ name: 'purchase_order_id', type: 'uuid', nullable: true }) - purchaseOrderId: string; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => User) - @JoinColumn({ name: 'requested_by' }) - requestedBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => RequisicionLinea, (linea) => linea.requisicion) - lineas: RequisicionLinea[]; -} diff --git a/projects/erp-construccion/backend/src/modules/inventory/services/consumo-obra.service.ts b/projects/erp-construccion/backend/src/modules/inventory/services/consumo-obra.service.ts deleted file mode 100644 index ca1aedccd..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/services/consumo-obra.service.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * ConsumoObraService - Servicio de consumos de materiales - * - * Gesti贸n de consumos de materiales por obra/lote. - * - * @module Inventory - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { ConsumoObra } from '../entities/consumo-obra.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateConsumoDto { - fraccionamientoId: string; - loteId?: string; - departamentoId?: string; - conceptoId?: string; - productId: string; - quantity: number; - unitCost?: number; - consumptionDate: Date; - notes?: string; -} - -export interface ConsumoFilters { - fraccionamientoId?: string; - loteId?: string; - conceptoId?: string; - productId?: string; - dateFrom?: Date; - dateTo?: Date; -} - -export interface ConsumoStats { - totalConsumos: number; - totalQuantity: number; - totalCost: number; - byProduct: { productId: string; quantity: number; cost: number }[]; - byConcepto: { conceptoId: string; quantity: number; cost: number }[]; -} - -export class ConsumoObraService { - constructor(private readonly repository: Repository) {} - - async findWithFilters( - ctx: ServiceContext, - filters: ConsumoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.repository - .createQueryBuilder('consumo') - .leftJoinAndSelect('consumo.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('consumo.lote', 'lote') - .leftJoinAndSelect('consumo.concepto', 'concepto') - .leftJoinAndSelect('consumo.registeredBy', 'registeredBy') - .where('consumo.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('consumo.deleted_at IS NULL'); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('consumo.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.loteId) { - queryBuilder.andWhere('consumo.lote_id = :loteId', { loteId: filters.loteId }); - } - - if (filters.conceptoId) { - queryBuilder.andWhere('consumo.concepto_id = :conceptoId', { conceptoId: filters.conceptoId }); - } - - if (filters.productId) { - queryBuilder.andWhere('consumo.product_id = :productId', { productId: filters.productId }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('consumo.consumption_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('consumo.consumption_date <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('consumo.consumption_date', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - relations: ['fraccionamiento', 'lote', 'concepto', 'registeredBy'], - }); - } - - async create(ctx: ServiceContext, dto: CreateConsumoDto): Promise { - const consumo = this.repository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - registeredById: ctx.userId, - fraccionamientoId: dto.fraccionamientoId, - loteId: dto.loteId, - departamentoId: dto.departamentoId, - conceptoId: dto.conceptoId, - productId: dto.productId, - quantity: dto.quantity, - unitCost: dto.unitCost, - consumptionDate: dto.consumptionDate, - notes: dto.notes, - }); - - return this.repository.save(consumo); - } - - async getStats(ctx: ServiceContext, fraccionamientoId: string): Promise { - const baseQuery = this.repository - .createQueryBuilder('consumo') - .where('consumo.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('consumo.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }) - .andWhere('consumo.deleted_at IS NULL'); - - // Get totals - const totals = await baseQuery - .clone() - .select('COUNT(*)', 'total') - .addSelect('SUM(consumo.quantity)', 'totalQuantity') - .addSelect('SUM(consumo.quantity * COALESCE(consumo.unit_cost, 0))', 'totalCost') - .getRawOne(); - - // Get by product - const byProduct = await baseQuery - .clone() - .select('consumo.product_id', 'productId') - .addSelect('SUM(consumo.quantity)', 'quantity') - .addSelect('SUM(consumo.quantity * COALESCE(consumo.unit_cost, 0))', 'cost') - .groupBy('consumo.product_id') - .getRawMany(); - - // Get by concepto - const byConcepto = await baseQuery - .clone() - .select('consumo.concepto_id', 'conceptoId') - .addSelect('SUM(consumo.quantity)', 'quantity') - .addSelect('SUM(consumo.quantity * COALESCE(consumo.unit_cost, 0))', 'cost') - .where('consumo.concepto_id IS NOT NULL') - .groupBy('consumo.concepto_id') - .getRawMany(); - - return { - totalConsumos: parseInt(totals.total || '0', 10), - totalQuantity: parseFloat(totals.totalQuantity || '0'), - totalCost: parseFloat(totals.totalCost || '0'), - byProduct: byProduct.map((p) => ({ - productId: p.productId, - quantity: parseFloat(p.quantity || '0'), - cost: parseFloat(p.cost || '0'), - })), - byConcepto: byConcepto.map((c) => ({ - conceptoId: c.conceptoId, - quantity: parseFloat(c.quantity || '0'), - cost: parseFloat(c.cost || '0'), - })), - }; - } - - async softDelete(ctx: ServiceContext, id: string): Promise { - const consumo = await this.findById(ctx, id); - if (!consumo) { - return false; - } - - await this.repository.update( - { id, tenantId: ctx.tenantId } as unknown as FindOptionsWhere, - { deletedAt: new Date(), deletedById: ctx.userId } - ); - - return true; - } -} diff --git a/projects/erp-construccion/backend/src/modules/inventory/services/index.ts b/projects/erp-construccion/backend/src/modules/inventory/services/index.ts deleted file mode 100644 index af14d2de1..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/services/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Inventory Services Index - * @module Inventory - */ - -export * from './requisicion.service'; -export * from './consumo-obra.service'; diff --git a/projects/erp-construccion/backend/src/modules/inventory/services/requisicion.service.ts b/projects/erp-construccion/backend/src/modules/inventory/services/requisicion.service.ts deleted file mode 100644 index bc8a3128a..000000000 --- a/projects/erp-construccion/backend/src/modules/inventory/services/requisicion.service.ts +++ /dev/null @@ -1,339 +0,0 @@ -/** - * RequisicionService - Servicio de requisiciones de obra - * - * Gesti贸n de requisiciones de material con workflow: - * draft -> submitted -> approved -> partially_served -> served - * - * @module Inventory - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { RequisicionObra, RequisitionStatus } from '../entities/requisicion-obra.entity'; -import { RequisicionLinea } from '../entities/requisicion-linea.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateRequisicionDto { - fraccionamientoId: string; - requisitionDate: Date; - requiredDate: Date; - priority?: string; - destinationWarehouseId?: string; - notes?: string; -} - -export interface AddLineaDto { - productId: string; - conceptoId?: string; - loteId?: string; - quantityRequested: number; - unitId?: string; - notes?: string; -} - -export interface RequisicionFilters { - fraccionamientoId?: string; - status?: RequisitionStatus; - priority?: string; - dateFrom?: Date; - dateTo?: Date; -} - -export class RequisicionService { - constructor( - private readonly requisicionRepository: Repository, - private readonly lineaRepository: Repository - ) {} - - private generateNumber(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - return `REQ-${year}${month}-${random}`; - } - - async findWithFilters( - ctx: ServiceContext, - filters: RequisicionFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.requisicionRepository - .createQueryBuilder('req') - .leftJoinAndSelect('req.fraccionamiento', 'fraccionamiento') - .leftJoinAndSelect('req.requestedBy', 'requestedBy') - .where('req.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('req.deleted_at IS NULL'); - - if (filters.fraccionamientoId) { - queryBuilder.andWhere('req.fraccionamiento_id = :fraccionamientoId', { - fraccionamientoId: filters.fraccionamientoId, - }); - } - - if (filters.status) { - queryBuilder.andWhere('req.status = :status', { status: filters.status }); - } - - if (filters.priority) { - queryBuilder.andWhere('req.priority = :priority', { priority: filters.priority }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('req.requisition_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('req.requisition_date <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('req.requisition_date', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.requisicionRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - } - - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.requisicionRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - relations: [ - 'fraccionamiento', - 'requestedBy', - 'approvedBy', - 'lineas', - 'lineas.concepto', - 'lineas.lote', - ], - }); - } - - async create(ctx: ServiceContext, dto: CreateRequisicionDto): Promise { - if (!ctx.userId) { - throw new Error('User ID is required to create a requisition'); - } - - const requisicion = this.requisicionRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - requestedById: ctx.userId, - requisitionNumber: this.generateNumber(), - fraccionamientoId: dto.fraccionamientoId, - requisitionDate: dto.requisitionDate, - requiredDate: dto.requiredDate, - priority: dto.priority || 'medium', - destinationWarehouseId: dto.destinationWarehouseId, - notes: dto.notes, - status: 'draft', - }); - - return this.requisicionRepository.save(requisicion); - } - - async addLinea(ctx: ServiceContext, requisicionId: string, dto: AddLineaDto): Promise { - const requisicion = await this.findById(ctx, requisicionId); - if (!requisicion) { - throw new Error('Requisicion not found'); - } - - if (requisicion.status !== 'draft') { - throw new Error('Can only add lines to draft requisitions'); - } - - const linea = this.lineaRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - requisicionId, - productId: dto.productId, - conceptoId: dto.conceptoId, - loteId: dto.loteId, - quantityRequested: dto.quantityRequested, - unitId: dto.unitId, - notes: dto.notes, - }); - - return this.lineaRepository.save(linea); - } - - async removeLinea(ctx: ServiceContext, requisicionId: string, lineaId: string): Promise { - const requisicion = await this.findById(ctx, requisicionId); - if (!requisicion) { - return false; - } - - if (requisicion.status !== 'draft') { - throw new Error('Can only remove lines from draft requisitions'); - } - - const result = await this.lineaRepository.delete({ - id: lineaId, - requisicionId, - tenantId: ctx.tenantId, - } as FindOptionsWhere); - - return (result.affected ?? 0) > 0; - } - - async submit(ctx: ServiceContext, id: string): Promise { - const requisicion = await this.findWithDetails(ctx, id); - if (!requisicion) { - return null; - } - - if (requisicion.status !== 'draft') { - throw new Error('Can only submit draft requisitions'); - } - - if (!requisicion.lineas || requisicion.lineas.length === 0) { - throw new Error('Cannot submit requisition without lines'); - } - - requisicion.status = 'submitted'; - return this.requisicionRepository.save(requisicion); - } - - async approve(ctx: ServiceContext, id: string): Promise { - const requisicion = await this.findWithDetails(ctx, id); - if (!requisicion) { - return null; - } - - if (requisicion.status !== 'submitted') { - throw new Error('Can only approve submitted requisitions'); - } - - requisicion.status = 'approved'; - requisicion.approvedById = ctx.userId || ''; - requisicion.approvedAt = new Date(); - - // Set approved quantities to requested quantities by default - for (const linea of requisicion.lineas) { - linea.quantityApproved = linea.quantityRequested; - await this.lineaRepository.save(linea); - } - - return this.requisicionRepository.save(requisicion); - } - - async reject(ctx: ServiceContext, id: string, reason: string): Promise { - const requisicion = await this.findById(ctx, id); - if (!requisicion) { - return null; - } - - if (requisicion.status !== 'submitted') { - throw new Error('Can only reject submitted requisitions'); - } - - requisicion.status = 'cancelled'; - requisicion.rejectionReason = reason; - return this.requisicionRepository.save(requisicion); - } - - async cancel(ctx: ServiceContext, id: string): Promise { - const requisicion = await this.findById(ctx, id); - if (!requisicion) { - return null; - } - - if (requisicion.status === 'served' || requisicion.status === 'cancelled') { - throw new Error('Cannot cancel served or already cancelled requisitions'); - } - - requisicion.status = 'cancelled'; - return this.requisicionRepository.save(requisicion); - } - - async updateServedQuantity( - ctx: ServiceContext, - lineaId: string, - quantityServed: number - ): Promise { - const linea = await this.lineaRepository.findOne({ - where: { id: lineaId, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['requisicion'], - }); - - if (!linea || !linea.requisicion) { - return null; - } - - if (linea.requisicion.status !== 'approved' && linea.requisicion.status !== 'partially_served') { - throw new Error('Can only serve approved or partially served requisitions'); - } - - linea.quantityServed = quantityServed; - await this.lineaRepository.save(linea); - - // Update requisition status based on all lines - await this.updateRequisitionStatus(ctx, linea.requisicion.id); - - return linea; - } - - private async updateRequisitionStatus(ctx: ServiceContext, requisicionId: string): Promise { - const requisicion = await this.findWithDetails(ctx, requisicionId); - if (!requisicion || !requisicion.lineas) { - return; - } - - const allServed = requisicion.lineas.every( - (l) => l.quantityServed >= (l.quantityApproved || l.quantityRequested) - ); - - const someServed = requisicion.lineas.some((l) => l.quantityServed > 0); - - if (allServed) { - requisicion.status = 'served'; - } else if (someServed) { - requisicion.status = 'partially_served'; - } - - await this.requisicionRepository.save(requisicion); - } - - async softDelete(ctx: ServiceContext, id: string): Promise { - const requisicion = await this.findById(ctx, id); - if (!requisicion) { - return false; - } - - if (requisicion.status !== 'draft' && requisicion.status !== 'cancelled') { - throw new Error('Can only delete draft or cancelled requisitions'); - } - - await this.requisicionRepository.update( - { id, tenantId: ctx.tenantId } as unknown as FindOptionsWhere, - { deletedAt: new Date(), deletedById: ctx.userId } - ); - - return true; - } -} diff --git a/projects/erp-construccion/backend/src/modules/progress/controllers/avance-obra.controller.ts b/projects/erp-construccion/backend/src/modules/progress/controllers/avance-obra.controller.ts deleted file mode 100644 index 602b8b8c0..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/controllers/avance-obra.controller.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * AvanceObraController - Controller de avances de obra - * - * Endpoints REST para gesti贸n de avances f铆sicos de obra. - * Incluye workflow de captura -> revisi贸n -> aprobaci贸n. - * - * @module Progress - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { AvanceObraService, CreateAvanceDto, AddFotoDto, AvanceFilters } from '../services/avance-obra.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { AvanceObra } from '../entities/avance-obra.entity'; -import { FotoAvance } from '../entities/foto-avance.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de avances de obra - */ -export function createAvanceObraController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const avanceRepository = dataSource.getRepository(AvanceObra); - const fotoRepository = dataSource.getRepository(FotoAvance); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const avanceService = new AvanceObraService(avanceRepository, fotoRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /avances - * Listar avances con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: AvanceFilters = { - loteId: req.query.loteId as string, - departamentoId: req.query.departamentoId as string, - conceptoId: req.query.conceptoId as string, - status: req.query.status as any, - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - }; - - const result = await avanceService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /avances/accumulated - * Obtener avance acumulado por concepto - */ - router.get('/accumulated', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const loteId = req.query.loteId as string; - const departamentoId = req.query.departamentoId as string; - - const progress = await avanceService.getAccumulatedProgress(getContext(req), loteId, departamentoId); - res.status(200).json({ success: true, data: progress }); - } catch (error) { - next(error); - } - }); - - /** - * GET /avances/:id - * Obtener avance por ID con fotos - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const avance = await avanceService.findWithFotos(getContext(req), req.params.id); - if (!avance) { - res.status(404).json({ error: 'Not Found', message: 'Progress record not found' }); - return; - } - - res.status(200).json({ success: true, data: avance }); - } catch (error) { - next(error); - } - }); - - /** - * POST /avances - * Crear avance (captura) - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateAvanceDto = req.body; - - if (!dto.conceptoId) { - res.status(400).json({ error: 'Bad Request', message: 'conceptoId is required' }); - return; - } - - if (!dto.loteId && !dto.departamentoId) { - res.status(400).json({ error: 'Bad Request', message: 'Either loteId or departamentoId is required' }); - return; - } - - const avance = await avanceService.createAvance(getContext(req), dto); - res.status(201).json({ success: true, data: avance }); - } catch (error) { - if (error instanceof Error) { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /avances/:id/fotos - * Agregar foto al avance - */ - router.post('/:id/fotos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: AddFotoDto = req.body; - - if (!dto.fileUrl) { - res.status(400).json({ error: 'Bad Request', message: 'fileUrl is required' }); - return; - } - - const foto = await avanceService.addFoto(getContext(req), req.params.id, dto); - res.status(201).json({ success: true, data: foto }); - } catch (error) { - if (error instanceof Error && error.message === 'Avance not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /avances/:id/review - * Revisar avance - */ - router.post('/:id/review', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const avance = await avanceService.review(getContext(req), req.params.id); - if (!avance) { - res.status(400).json({ error: 'Bad Request', message: 'Cannot review this progress record. It may not exist or is not in captured status.' }); - return; - } - - res.status(200).json({ success: true, data: avance, message: 'Progress reviewed' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /avances/:id/approve - * Aprobar avance - */ - router.post('/:id/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const avance = await avanceService.approve(getContext(req), req.params.id); - if (!avance) { - res.status(400).json({ error: 'Bad Request', message: 'Cannot approve this progress record. It may not exist or is not in reviewed status.' }); - return; - } - - res.status(200).json({ success: true, data: avance, message: 'Progress approved' }); - } catch (error) { - next(error); - } - }); - - /** - * POST /avances/:id/reject - * Rechazar avance - */ - router.post('/:id/reject', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { reason } = req.body; - if (!reason) { - res.status(400).json({ error: 'Bad Request', message: 'reason is required' }); - return; - } - - const avance = await avanceService.reject(getContext(req), req.params.id, reason); - if (!avance) { - res.status(400).json({ error: 'Bad Request', message: 'Cannot reject this progress record.' }); - return; - } - - res.status(200).json({ success: true, data: avance, message: 'Progress rejected' }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /avances/:id - * Eliminar avance (soft delete) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await avanceService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Progress record not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Progress record deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createAvanceObraController; diff --git a/projects/erp-construccion/backend/src/modules/progress/controllers/bitacora-obra.controller.ts b/projects/erp-construccion/backend/src/modules/progress/controllers/bitacora-obra.controller.ts deleted file mode 100644 index 520e4f5bf..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/controllers/bitacora-obra.controller.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * BitacoraObraController - Controller de bit谩cora de obra - * - * Endpoints REST para gesti贸n de bit谩cora diaria de obra. - * - * @module Progress - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { BitacoraObraService, CreateBitacoraDto, UpdateBitacoraDto, BitacoraFilters } from '../services/bitacora-obra.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { BitacoraObra } from '../entities/bitacora-obra.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -/** - * Crear router de bit谩cora de obra - */ -export function createBitacoraObraController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const bitacoraRepository = dataSource.getRepository(BitacoraObra); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const bitacoraService = new BitacoraObraService(bitacoraRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto de servicio - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /bitacora - * Listar entradas de bit谩cora por fraccionamiento - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - if (!fraccionamientoId) { - res.status(400).json({ error: 'Bad Request', message: 'fraccionamientoId is required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: BitacoraFilters = { - dateFrom: req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined, - dateTo: req.query.dateTo ? new Date(req.query.dateTo as string) : undefined, - hasIncidents: req.query.hasIncidents === 'true' ? true : req.query.hasIncidents === 'false' ? false : undefined, - }; - - const result = await bitacoraService.findWithFilters(getContext(req), fraccionamientoId, filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bitacora/stats - * Obtener estad铆sticas de bit谩cora - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - if (!fraccionamientoId) { - res.status(400).json({ error: 'Bad Request', message: 'fraccionamientoId is required' }); - return; - } - - const stats = await bitacoraService.getStats(getContext(req), fraccionamientoId); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bitacora/latest - * Obtener 煤ltima entrada de bit谩cora - */ - router.get('/latest', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string; - if (!fraccionamientoId) { - res.status(400).json({ error: 'Bad Request', message: 'fraccionamientoId is required' }); - return; - } - - const entry = await bitacoraService.findLatest(getContext(req), fraccionamientoId); - if (!entry) { - res.status(404).json({ error: 'Not Found', message: 'No log entries found' }); - return; - } - - res.status(200).json({ success: true, data: entry }); - } catch (error) { - next(error); - } - }); - - /** - * GET /bitacora/:id - * Obtener entrada por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const entry = await bitacoraService.findById(getContext(req), req.params.id); - if (!entry) { - res.status(404).json({ error: 'Not Found', message: 'Log entry not found' }); - return; - } - - res.status(200).json({ success: true, data: entry }); - } catch (error) { - next(error); - } - }); - - /** - * POST /bitacora - * Crear entrada de bit谩cora - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateBitacoraDto = req.body; - - if (!dto.fraccionamientoId || !dto.entryDate || !dto.description) { - res.status(400).json({ error: 'Bad Request', message: 'fraccionamientoId, entryDate and description are required' }); - return; - } - - const entry = await bitacoraService.createEntry(getContext(req), dto); - res.status(201).json({ success: true, data: entry }); - } catch (error) { - next(error); - } - }); - - /** - * PATCH /bitacora/:id - * Actualizar entrada de bit谩cora - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'engineer', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateBitacoraDto = req.body; - const entry = await bitacoraService.update(getContext(req), req.params.id, dto); - - if (!entry) { - res.status(404).json({ error: 'Not Found', message: 'Log entry not found' }); - return; - } - - res.status(200).json({ success: true, data: entry }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /bitacora/:id - * Eliminar entrada de bit谩cora (soft delete) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await bitacoraService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Log entry not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Log entry deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createBitacoraObraController; diff --git a/projects/erp-construccion/backend/src/modules/progress/controllers/index.ts b/projects/erp-construccion/backend/src/modules/progress/controllers/index.ts deleted file mode 100644 index 66326c151..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/controllers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Progress Controllers Index - * @module Progress - */ - -export { createAvanceObraController } from './avance-obra.controller'; -export { createBitacoraObraController } from './bitacora-obra.controller'; diff --git a/projects/erp-construccion/backend/src/modules/progress/entities/avance-obra.entity.ts b/projects/erp-construccion/backend/src/modules/progress/entities/avance-obra.entity.ts deleted file mode 100644 index 3e112a9fb..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/entities/avance-obra.entity.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * AvanceObra Entity - * Registro de avances fisicos de obra por lote/departamento - * - * @module Progress - * @table construction.avances_obra - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, - Check, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Concepto } from '../../budgets/entities/concepto.entity'; -import { FotoAvance } from './foto-avance.entity'; - -export type AdvanceStatus = 'pending' | 'captured' | 'reviewed' | 'approved' | 'rejected'; - -@Entity({ schema: 'construction', name: 'avances_obra' }) -@Index(['tenantId']) -@Index(['loteId']) -@Index(['conceptoId']) -@Index(['captureDate']) -@Check(`"lote_id" IS NOT NULL AND "departamento_id" IS NULL OR "lote_id" IS NULL AND "departamento_id" IS NOT NULL`) -export class AvanceObra { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'lote_id', type: 'uuid', nullable: true }) - loteId: string | null; - - @Column({ name: 'departamento_id', type: 'uuid', nullable: true }) - departamentoId: string | null; - - @Column({ name: 'concepto_id', type: 'uuid' }) - conceptoId: string; - - @Column({ name: 'capture_date', type: 'date' }) - captureDate: Date; - - @Column({ name: 'quantity_executed', type: 'decimal', precision: 12, scale: 4, default: 0 }) - quantityExecuted: number; - - @Column({ name: 'percentage_executed', type: 'decimal', precision: 5, scale: 2, default: 0 }) - percentageExecuted: number; - - @Column({ - type: 'enum', - enum: ['pending', 'captured', 'reviewed', 'approved', 'rejected'], - enumName: 'construction.advance_status', - default: 'pending', - }) - status: AdvanceStatus; - - @Column({ type: 'text', nullable: true }) - notes: string | null; - - @Column({ name: 'captured_by', type: 'uuid' }) - capturedById: string; - - @Column({ name: 'reviewed_by', type: 'uuid', nullable: true }) - reviewedById: string | null; - - @Column({ name: 'reviewed_at', type: 'timestamptz', nullable: true }) - reviewedAt: Date | null; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string | null; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Concepto) - @JoinColumn({ name: 'concepto_id' }) - concepto: Concepto; - - @ManyToOne(() => User) - @JoinColumn({ name: 'captured_by' }) - capturedBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'reviewed_by' }) - reviewedBy: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User | null; - - @OneToMany(() => FotoAvance, (f) => f.avance) - fotos: FotoAvance[]; -} diff --git a/projects/erp-construccion/backend/src/modules/progress/entities/bitacora-obra.entity.ts b/projects/erp-construccion/backend/src/modules/progress/entities/bitacora-obra.entity.ts deleted file mode 100644 index 5dae0f80d..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/entities/bitacora-obra.entity.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * BitacoraObra Entity - * Registro diario de bitacora de obra - * - * @module Progress - * @table construction.bitacora_obra - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; - -@Entity({ schema: 'construction', name: 'bitacora_obra' }) -@Index(['fraccionamientoId', 'entryNumber'], { unique: true }) -@Index(['tenantId']) -@Index(['fraccionamientoId']) -export class BitacoraObra { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ name: 'entry_date', type: 'date' }) - entryDate: Date; - - @Column({ name: 'entry_number', type: 'integer' }) - entryNumber: number; - - @Column({ type: 'varchar', length: 50, nullable: true }) - weather: string | null; - - @Column({ name: 'temperature_max', type: 'decimal', precision: 4, scale: 1, nullable: true }) - temperatureMax: number | null; - - @Column({ name: 'temperature_min', type: 'decimal', precision: 4, scale: 1, nullable: true }) - temperatureMin: number | null; - - @Column({ name: 'workers_count', type: 'integer', default: 0 }) - workersCount: number; - - @Column({ type: 'text' }) - description: string; - - @Column({ type: 'text', nullable: true }) - observations: string | null; - - @Column({ type: 'text', nullable: true }) - incidents: string | null; - - @Column({ name: 'registered_by', type: 'uuid' }) - registeredById: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => User) - @JoinColumn({ name: 'registered_by' }) - registeredBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/progress/entities/foto-avance.entity.ts b/projects/erp-construccion/backend/src/modules/progress/entities/foto-avance.entity.ts deleted file mode 100644 index 652a0d3ba..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/entities/foto-avance.entity.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * FotoAvance Entity - * Evidencia fotografica de avances de obra - * - * @module Progress - * @table construction.fotos_avance - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { AvanceObra } from './avance-obra.entity'; - -@Entity({ schema: 'construction', name: 'fotos_avance' }) -@Index(['tenantId']) -@Index(['avanceId']) -export class FotoAvance { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'avance_id', type: 'uuid' }) - avanceId: string; - - @Column({ name: 'file_url', type: 'varchar', length: 500 }) - fileUrl: string; - - @Column({ name: 'file_name', type: 'varchar', length: 255, nullable: true }) - fileName: string | null; - - @Column({ name: 'file_size', type: 'integer', nullable: true }) - fileSize: number | null; - - @Column({ name: 'mime_type', type: 'varchar', length: 50, nullable: true }) - mimeType: string | null; - - @Column({ type: 'text', nullable: true }) - description: string | null; - - // PostGIS Point para ubicacion GPS - @Column({ - type: 'geometry', - spatialFeatureType: 'Point', - srid: 4326, - nullable: true, - }) - location: string | null; - - @Column({ name: 'captured_at', type: 'timestamptz', default: () => 'NOW()' }) - capturedAt: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => AvanceObra, (a) => a.fotos) - @JoinColumn({ name: 'avance_id' }) - avance: AvanceObra; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/progress/entities/index.ts b/projects/erp-construccion/backend/src/modules/progress/entities/index.ts deleted file mode 100644 index d7cc1a8c8..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/entities/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Progress Module - Entity Exports - * MAI-005: Control de Obra - */ - -export * from './avance-obra.entity'; -export * from './foto-avance.entity'; -export * from './bitacora-obra.entity'; -export * from './programa-obra.entity'; -export * from './programa-actividad.entity'; diff --git a/projects/erp-construccion/backend/src/modules/progress/entities/programa-actividad.entity.ts b/projects/erp-construccion/backend/src/modules/progress/entities/programa-actividad.entity.ts deleted file mode 100644 index 7973d18ce..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/entities/programa-actividad.entity.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * ProgramaActividad Entity - * Actividades del programa de obra (WBS) - * - * @module Progress - * @table construction.programa_actividades - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Concepto } from '../../budgets/entities/concepto.entity'; -import { ProgramaObra } from './programa-obra.entity'; - -@Entity({ schema: 'construction', name: 'programa_actividades' }) -@Index(['tenantId']) -@Index(['programaId']) -export class ProgramaActividad { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'programa_id', type: 'uuid' }) - programaId: string; - - @Column({ name: 'concepto_id', type: 'uuid', nullable: true }) - conceptoId: string | null; - - @Column({ name: 'parent_id', type: 'uuid', nullable: true }) - parentId: string | null; - - @Column({ type: 'varchar', length: 255 }) - name: string; - - @Column({ type: 'integer', default: 0 }) - sequence: number; - - @Column({ name: 'planned_start', type: 'date', nullable: true }) - plannedStart: Date | null; - - @Column({ name: 'planned_end', type: 'date', nullable: true }) - plannedEnd: Date | null; - - @Column({ name: 'planned_quantity', type: 'decimal', precision: 12, scale: 4, default: 0 }) - plannedQuantity: number; - - @Column({ name: 'planned_weight', type: 'decimal', precision: 8, scale: 4, default: 0 }) - plannedWeight: number; - - @Column({ name: 'wbs_code', type: 'varchar', length: 50, nullable: true }) - wbsCode: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => ProgramaObra, (p) => p.actividades) - @JoinColumn({ name: 'programa_id' }) - programa: ProgramaObra; - - @ManyToOne(() => Concepto, { nullable: true }) - @JoinColumn({ name: 'concepto_id' }) - concepto: Concepto | null; - - @ManyToOne(() => ProgramaActividad, (a) => a.children, { nullable: true }) - @JoinColumn({ name: 'parent_id' }) - parent: ProgramaActividad | null; - - @OneToMany(() => ProgramaActividad, (a) => a.parent) - children: ProgramaActividad[]; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/progress/entities/programa-obra.entity.ts b/projects/erp-construccion/backend/src/modules/progress/entities/programa-obra.entity.ts deleted file mode 100644 index 171d0d7dc..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/entities/programa-obra.entity.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * ProgramaObra Entity - * Programa maestro de obra (planificacion) - * - * @module Progress - * @table construction.programa_obra - * @ddl schemas/01-construction-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity'; -import { ProgramaActividad } from './programa-actividad.entity'; - -@Entity({ schema: 'construction', name: 'programa_obra' }) -@Index(['tenantId', 'code', 'version'], { unique: true }) -@Index(['tenantId']) -@Index(['fraccionamientoId']) -export class ProgramaObra { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'fraccionamiento_id', type: 'uuid' }) - fraccionamientoId: string; - - @Column({ type: 'varchar', length: 30 }) - code: string; - - @Column({ type: 'varchar', length: 255 }) - name: string; - - @Column({ type: 'integer', default: 1 }) - version: number; - - @Column({ name: 'start_date', type: 'date' }) - startDate: Date; - - @Column({ name: 'end_date', type: 'date' }) - endDate: Date; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Fraccionamiento) - @JoinColumn({ name: 'fraccionamiento_id' }) - fraccionamiento: Fraccionamiento; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @OneToMany(() => ProgramaActividad, (a) => a.programa) - actividades: ProgramaActividad[]; -} diff --git a/projects/erp-construccion/backend/src/modules/progress/services/avance-obra.service.ts b/projects/erp-construccion/backend/src/modules/progress/services/avance-obra.service.ts deleted file mode 100644 index 266a7c220..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/services/avance-obra.service.ts +++ /dev/null @@ -1,284 +0,0 @@ -/** - * AvanceObraService - Gesti贸n de Avances de Obra - * - * Gestiona el registro y aprobaci贸n de avances f铆sicos de obra. - * Incluye workflow de captura -> revisi贸n -> aprobaci贸n. - * - * @module Progress - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { AvanceObra, AdvanceStatus } from '../entities/avance-obra.entity'; -import { FotoAvance } from '../entities/foto-avance.entity'; - -export interface CreateAvanceDto { - loteId?: string; - departamentoId?: string; - conceptoId: string; - captureDate: Date; - quantityExecuted: number; - percentageExecuted?: number; - notes?: string; -} - -export interface AddFotoDto { - fileUrl: string; - fileName?: string; - fileSize?: number; - mimeType?: string; - description?: string; - location?: { lat: number; lng: number }; -} - -export interface AvanceFilters { - loteId?: string; - departamentoId?: string; - conceptoId?: string; - status?: AdvanceStatus; - dateFrom?: Date; - dateTo?: Date; -} - -export class AvanceObraService extends BaseService { - constructor( - repository: Repository, - private readonly fotoRepository: Repository - ) { - super(repository); - } - - /** - * Crear nuevo avance (captura) - */ - async createAvance( - ctx: ServiceContext, - data: CreateAvanceDto - ): Promise { - if (!data.loteId && !data.departamentoId) { - throw new Error('Either loteId or departamentoId is required'); - } - - if (data.loteId && data.departamentoId) { - throw new Error('Cannot specify both loteId and departamentoId'); - } - - return this.create(ctx, { - ...data, - status: 'captured', - capturedById: ctx.userId, - }); - } - - /** - * Obtener avances por lote - */ - async findByLote( - ctx: ServiceContext, - loteId: string, - page = 1, - limit = 20 - ): Promise> { - return this.findAll(ctx, { - page, - limit, - where: { loteId } as any, - }); - } - - /** - * Obtener avances por departamento - */ - async findByDepartamento( - ctx: ServiceContext, - departamentoId: string, - page = 1, - limit = 20 - ): Promise> { - return this.findAll(ctx, { - page, - limit, - where: { departamentoId } as any, - }); - } - - /** - * Obtener avances con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: AvanceFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('a') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.deleted_at IS NULL'); - - if (filters.loteId) { - qb.andWhere('a.lote_id = :loteId', { loteId: filters.loteId }); - } - if (filters.departamentoId) { - qb.andWhere('a.departamento_id = :departamentoId', { departamentoId: filters.departamentoId }); - } - if (filters.conceptoId) { - qb.andWhere('a.concepto_id = :conceptoId', { conceptoId: filters.conceptoId }); - } - if (filters.status) { - qb.andWhere('a.status = :status', { status: filters.status }); - } - if (filters.dateFrom) { - qb.andWhere('a.capture_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - if (filters.dateTo) { - qb.andWhere('a.capture_date <= :dateTo', { dateTo: filters.dateTo }); - } - - const skip = (page - 1) * limit; - qb.orderBy('a.capture_date', 'DESC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Obtener avance con fotos - */ - async findWithFotos(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - relations: ['fotos', 'concepto', 'capturedBy'], - }); - } - - /** - * Agregar foto al avance - */ - async addFoto( - ctx: ServiceContext, - avanceId: string, - data: AddFotoDto - ): Promise { - const avance = await this.findById(ctx, avanceId); - if (!avance) { - throw new Error('Avance not found'); - } - - const location = data.location - ? `POINT(${data.location.lng} ${data.location.lat})` - : null; - - const foto = this.fotoRepository.create({ - tenantId: ctx.tenantId, - avanceId, - fileUrl: data.fileUrl, - fileName: data.fileName, - fileSize: data.fileSize, - mimeType: data.mimeType, - description: data.description, - location, - createdById: ctx.userId, - }); - - return this.fotoRepository.save(foto); - } - - /** - * Revisar avance - */ - async review(ctx: ServiceContext, avanceId: string): Promise { - const avance = await this.findById(ctx, avanceId); - if (!avance || avance.status !== 'captured') { - return null; - } - - return this.update(ctx, avanceId, { - status: 'reviewed', - reviewedById: ctx.userId, - reviewedAt: new Date(), - }); - } - - /** - * Aprobar avance - */ - async approve(ctx: ServiceContext, avanceId: string): Promise { - const avance = await this.findById(ctx, avanceId); - if (!avance || avance.status !== 'reviewed') { - return null; - } - - return this.update(ctx, avanceId, { - status: 'approved', - approvedById: ctx.userId, - approvedAt: new Date(), - }); - } - - /** - * Rechazar avance - */ - async reject( - ctx: ServiceContext, - avanceId: string, - reason: string - ): Promise { - const avance = await this.findById(ctx, avanceId); - if (!avance || !['captured', 'reviewed'].includes(avance.status)) { - return null; - } - - return this.update(ctx, avanceId, { - status: 'rejected', - notes: reason, - }); - } - - /** - * Calcular avance acumulado por concepto - */ - async getAccumulatedProgress( - ctx: ServiceContext, - loteId?: string, - departamentoId?: string - ): Promise { - const qb = this.repository - .createQueryBuilder('a') - .select('a.concepto_id', 'conceptoId') - .addSelect('SUM(a.quantity_executed)', 'totalQuantity') - .addSelect('AVG(a.percentage_executed)', 'avgPercentage') - .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('a.deleted_at IS NULL') - .andWhere('a.status = :status', { status: 'approved' }) - .groupBy('a.concepto_id'); - - if (loteId) { - qb.andWhere('a.lote_id = :loteId', { loteId }); - } - if (departamentoId) { - qb.andWhere('a.departamento_id = :departamentoId', { departamentoId }); - } - - return qb.getRawMany(); - } -} - -interface ConceptProgress { - conceptoId: string; - totalQuantity: number; - avgPercentage: number; -} diff --git a/projects/erp-construccion/backend/src/modules/progress/services/bitacora-obra.service.ts b/projects/erp-construccion/backend/src/modules/progress/services/bitacora-obra.service.ts deleted file mode 100644 index 676de6881..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/services/bitacora-obra.service.ts +++ /dev/null @@ -1,209 +0,0 @@ -/** - * BitacoraObraService - Bit谩cora de Obra - * - * Gestiona el registro diario de bit谩cora de obra. - * Genera autom谩ticamente el n煤mero de entrada secuencial. - * - * @module Progress - */ - -import { Repository } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { BitacoraObra } from '../entities/bitacora-obra.entity'; - -export interface CreateBitacoraDto { - fraccionamientoId: string; - entryDate: Date; - weather?: string; - temperatureMax?: number; - temperatureMin?: number; - workersCount?: number; - description: string; - observations?: string; - incidents?: string; -} - -export interface UpdateBitacoraDto { - weather?: string; - temperatureMax?: number; - temperatureMin?: number; - workersCount?: number; - description?: string; - observations?: string; - incidents?: string; -} - -export interface BitacoraFilters { - dateFrom?: Date; - dateTo?: Date; - hasIncidents?: boolean; -} - -export class BitacoraObraService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Crear nueva entrada de bit谩cora - */ - async createEntry( - ctx: ServiceContext, - data: CreateBitacoraDto - ): Promise { - const entryNumber = await this.getNextEntryNumber(ctx, data.fraccionamientoId); - - return this.create(ctx, { - ...data, - entryNumber, - registeredById: ctx.userId, - }); - } - - /** - * Obtener siguiente n煤mero de entrada - */ - private async getNextEntryNumber( - ctx: ServiceContext, - fraccionamientoId: string - ): Promise { - const result = await this.repository - .createQueryBuilder('b') - .select('MAX(b.entry_number)', 'maxNumber') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }) - .getRawOne(); - - return (result?.maxNumber || 0) + 1; - } - - /** - * Obtener bit谩cora por fraccionamiento - */ - async findByFraccionamiento( - ctx: ServiceContext, - fraccionamientoId: string, - page = 1, - limit = 20 - ): Promise> { - return this.findAll(ctx, { - page, - limit, - where: { fraccionamientoId } as any, - }); - } - - /** - * Obtener bit谩cora con filtros - */ - async findWithFilters( - ctx: ServiceContext, - fraccionamientoId: string, - filters: BitacoraFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('b') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }) - .andWhere('b.deleted_at IS NULL'); - - if (filters.dateFrom) { - qb.andWhere('b.entry_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - if (filters.dateTo) { - qb.andWhere('b.entry_date <= :dateTo', { dateTo: filters.dateTo }); - } - if (filters.hasIncidents !== undefined) { - if (filters.hasIncidents) { - qb.andWhere('b.incidents IS NOT NULL'); - } else { - qb.andWhere('b.incidents IS NULL'); - } - } - - const skip = (page - 1) * limit; - qb.orderBy('b.entry_date', 'DESC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Obtener entrada por fecha - */ - async findByDate( - ctx: ServiceContext, - fraccionamientoId: string, - date: Date - ): Promise { - return this.findOne(ctx, { - fraccionamientoId, - entryDate: date, - } as any); - } - - /** - * Obtener 煤ltima entrada - */ - async findLatest( - ctx: ServiceContext, - fraccionamientoId: string - ): Promise { - const entries = await this.find(ctx, { - where: { fraccionamientoId } as any, - order: { entryNumber: 'DESC' }, - take: 1, - }); - - return entries[0] || null; - } - - /** - * Obtener estad铆sticas de bit谩cora - */ - async getStats( - ctx: ServiceContext, - fraccionamientoId: string - ): Promise { - const totalEntries = await this.count(ctx, { fraccionamientoId } as any); - - const incidentsCount = await this.repository - .createQueryBuilder('b') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }) - .andWhere('b.deleted_at IS NULL') - .andWhere('b.incidents IS NOT NULL') - .getCount(); - - const avgWorkers = await this.repository - .createQueryBuilder('b') - .select('AVG(b.workers_count)', 'avg') - .where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('b.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }) - .andWhere('b.deleted_at IS NULL') - .getRawOne(); - - return { - totalEntries, - entriesWithIncidents: incidentsCount, - avgWorkersCount: parseFloat(avgWorkers?.avg || '0'), - }; - } -} - -interface BitacoraStats { - totalEntries: number; - entriesWithIncidents: number; - avgWorkersCount: number; -} diff --git a/projects/erp-construccion/backend/src/modules/progress/services/index.ts b/projects/erp-construccion/backend/src/modules/progress/services/index.ts deleted file mode 100644 index 89c3b16df..000000000 --- a/projects/erp-construccion/backend/src/modules/progress/services/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Progress Module - Service Exports - * MAI-005: Control de Obra - */ - -export * from './avance-obra.service'; -export * from './bitacora-obra.service'; diff --git a/projects/erp-construccion/backend/src/modules/purchase/controllers/comparativo.controller.ts b/projects/erp-construccion/backend/src/modules/purchase/controllers/comparativo.controller.ts deleted file mode 100644 index 7062c2956..000000000 --- a/projects/erp-construccion/backend/src/modules/purchase/controllers/comparativo.controller.ts +++ /dev/null @@ -1,308 +0,0 @@ -/** - * ComparativoController - REST API for quotation comparisons - * - * Endpoints para gesti贸n de cuadros comparativos de cotizaciones. - * - * @module Purchase - * @routes /api/comparativos - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { ComparativoService, ComparativoFilters } from '../services/comparativo.service'; -import { ComparativoCotizaciones } from '../entities/comparativo-cotizaciones.entity'; -import { ComparativoProveedor } from '../entities/comparativo-proveedor.entity'; -import { ComparativoProducto } from '../entities/comparativo-producto.entity'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createComparativoController(dataSource: DataSource): Router { - const router = Router(); - - // Repositories - const comparativoRepo = dataSource.getRepository(ComparativoCotizaciones); - const proveedorRepo = dataSource.getRepository(ComparativoProveedor); - const productoRepo = dataSource.getRepository(ComparativoProducto); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Services - const service = new ComparativoService(comparativoRepo, proveedorRepo, productoRepo); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper for service context - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /api/comparativos - * List comparisons with filters - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: ComparativoFilters = {}; - if (req.query.requisicionId) { - filters.requisicionId = req.query.requisicionId as string; - } - if (req.query.status) { - filters.status = req.query.status as ComparativoFilters['status']; - } - if (req.query.dateFrom) { - filters.dateFrom = new Date(req.query.dateFrom as string); - } - if (req.query.dateTo) { - filters.dateTo = new Date(req.query.dateTo as string); - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/comparativos/:id - * Get comparison with full details - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const comparativo = await service.findWithDetails(getContext(req), req.params.id); - if (!comparativo) { - res.status(404).json({ error: 'Not Found', message: 'Comparison not found' }); - return; - } - - res.status(200).json({ success: true, data: comparativo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/comparativos - * Create new comparison - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'compras'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const comparativo = await service.create(getContext(req), { - requisicionId: req.body.requisicionId, - name: req.body.name, - comparisonDate: new Date(req.body.comparisonDate), - notes: req.body.notes, - }); - - res.status(201).json({ success: true, data: comparativo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/comparativos/:id/proveedores - * Add supplier to comparison - */ - router.post('/:id/proveedores', authMiddleware.authenticate, authMiddleware.authorize('admin', 'compras'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const proveedor = await service.addProveedor(getContext(req), req.params.id, { - supplierId: req.body.supplierId, - quotationNumber: req.body.quotationNumber, - quotationDate: req.body.quotationDate ? new Date(req.body.quotationDate) : undefined, - deliveryDays: req.body.deliveryDays, - paymentConditions: req.body.paymentConditions, - evaluationNotes: req.body.evaluationNotes, - }); - - res.status(201).json({ success: true, data: proveedor }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/comparativos/proveedores/:proveedorId/productos - * Add product to supplier entry - */ - router.post('/proveedores/:proveedorId/productos', authMiddleware.authenticate, authMiddleware.authorize('admin', 'compras'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const producto = await service.addProducto(getContext(req), req.params.proveedorId, { - productId: req.body.productId, - quantity: parseFloat(req.body.quantity), - unitPrice: parseFloat(req.body.unitPrice), - notes: req.body.notes, - }); - - res.status(201).json({ success: true, data: producto }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /api/comparativos/proveedores/:proveedorId/total - * Recalculate supplier total - */ - router.put('/proveedores/:proveedorId/total', authMiddleware.authenticate, authMiddleware.authorize('admin', 'compras'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const proveedor = await service.updateProveedorTotal(getContext(req), req.params.proveedorId); - if (!proveedor) { - res.status(404).json({ error: 'Not Found', message: 'Supplier entry not found' }); - return; - } - - res.status(200).json({ success: true, data: proveedor }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/comparativos/:id/start-evaluation - * Start evaluation process - */ - router.post('/:id/start-evaluation', authMiddleware.authenticate, authMiddleware.authorize('admin', 'compras'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const comparativo = await service.startEvaluation(getContext(req), req.params.id); - if (!comparativo) { - res.status(404).json({ error: 'Not Found', message: 'Comparison not found' }); - return; - } - - res.status(200).json({ success: true, data: comparativo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/comparativos/:id/select-winner - * Select winning supplier - */ - router.post('/:id/select-winner', authMiddleware.authenticate, authMiddleware.authorize('admin', 'compras'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const comparativo = await service.selectWinner(getContext(req), req.params.id, req.body.supplierId); - if (!comparativo) { - res.status(404).json({ error: 'Not Found', message: 'Comparison not found' }); - return; - } - - res.status(200).json({ success: true, data: comparativo }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/comparativos/:id/cancel - * Cancel comparison - */ - router.post('/:id/cancel', authMiddleware.authenticate, authMiddleware.authorize('admin', 'compras'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const comparativo = await service.cancel(getContext(req), req.params.id); - if (!comparativo) { - res.status(404).json({ error: 'Not Found', message: 'Comparison not found' }); - return; - } - - res.status(200).json({ success: true, data: comparativo }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /api/comparativos/:id - * Soft delete comparison - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await service.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Comparison not found' }); - return; - } - - res.status(204).send(); - } catch (error) { - next(error); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/purchase/controllers/index.ts b/projects/erp-construccion/backend/src/modules/purchase/controllers/index.ts deleted file mode 100644 index 22393fbad..000000000 --- a/projects/erp-construccion/backend/src/modules/purchase/controllers/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Purchase Controllers Index - * @module Purchase - */ - -export * from './comparativo.controller'; diff --git a/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-cotizaciones.entity.ts b/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-cotizaciones.entity.ts deleted file mode 100644 index 5e989bf67..000000000 --- a/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-cotizaciones.entity.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * ComparativoCotizaciones Entity - * Cuadro comparativo de cotizaciones - * - * @module Purchase - * @table purchase.comparativo_cotizaciones - * @ddl schemas/07-purchase-ext-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { RequisicionObra } from '../../inventory/entities/requisicion-obra.entity'; -import { ComparativoProveedor } from './comparativo-proveedor.entity'; - -export type ComparativoStatus = 'draft' | 'in_evaluation' | 'approved' | 'cancelled'; - -@Entity({ schema: 'purchase', name: 'comparativo_cotizaciones' }) -@Index(['tenantId', 'code'], { unique: true }) -export class ComparativoCotizaciones { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'requisicion_id', type: 'uuid', nullable: true }) - requisicionId: string; - - @Column({ type: 'varchar', length: 30 }) - code: string; - - @Column({ type: 'varchar', length: 255 }) - name: string; - - @Column({ name: 'comparison_date', type: 'date' }) - comparisonDate: Date; - - @Column({ type: 'varchar', length: 20, default: 'draft' }) - status: ComparativoStatus; - - @Column({ name: 'winner_supplier_id', type: 'uuid', nullable: true }) - winnerSupplierId: string; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => RequisicionObra) - @JoinColumn({ name: 'requisicion_id' }) - requisicion: RequisicionObra; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => ComparativoProveedor, (cp) => cp.comparativo) - proveedores: ComparativoProveedor[]; -} diff --git a/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-producto.entity.ts b/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-producto.entity.ts deleted file mode 100644 index f6e9640c5..000000000 --- a/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-producto.entity.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * ComparativoProducto Entity - * Productos cotizados por proveedor en comparativo - * - * @module Purchase - * @table purchase.comparativo_productos - * @ddl schemas/07-purchase-ext-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { ComparativoProveedor } from './comparativo-proveedor.entity'; - -@Entity({ schema: 'purchase', name: 'comparativo_productos' }) -export class ComparativoProducto { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'comparativo_proveedor_id', type: 'uuid' }) - comparativoProveedorId: string; - - @Column({ name: 'product_id', type: 'uuid' }) - productId: string; - - @Column({ type: 'decimal', precision: 12, scale: 4 }) - quantity: number; - - @Column({ name: 'unit_price', type: 'decimal', precision: 12, scale: 4 }) - unitPrice: number; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Computed property (in DB is GENERATED ALWAYS AS) - get totalPrice(): number { - return this.quantity * this.unitPrice; - } - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => ComparativoProveedor, (cp) => cp.productos, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'comparativo_proveedor_id' }) - comparativoProveedor: ComparativoProveedor; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-proveedor.entity.ts b/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-proveedor.entity.ts deleted file mode 100644 index 8a001049d..000000000 --- a/projects/erp-construccion/backend/src/modules/purchase/entities/comparativo-proveedor.entity.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * ComparativoProveedor Entity - * Proveedores participantes en comparativo - * - * @module Purchase - * @table purchase.comparativo_proveedores - * @ddl schemas/07-purchase-ext-schema-ddl.sql - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { ComparativoCotizaciones } from './comparativo-cotizaciones.entity'; -import { ComparativoProducto } from './comparativo-producto.entity'; - -@Entity({ schema: 'purchase', name: 'comparativo_proveedores' }) -export class ComparativoProveedor { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'comparativo_id', type: 'uuid' }) - comparativoId: string; - - @Column({ name: 'supplier_id', type: 'uuid' }) - supplierId: string; - - @Column({ name: 'quotation_number', type: 'varchar', length: 50, nullable: true }) - quotationNumber: string; - - @Column({ name: 'quotation_date', type: 'date', nullable: true }) - quotationDate: Date; - - @Column({ name: 'delivery_days', type: 'integer', nullable: true }) - deliveryDays: number; - - @Column({ name: 'payment_conditions', type: 'varchar', length: 100, nullable: true }) - paymentConditions: string; - - @Column({ name: 'total_amount', type: 'decimal', precision: 16, scale: 2, nullable: true }) - totalAmount: number; - - @Column({ name: 'is_selected', type: 'boolean', default: false }) - isSelected: boolean; - - @Column({ name: 'evaluation_notes', type: 'text', nullable: true }) - evaluationNotes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => ComparativoCotizaciones, (c) => c.proveedores, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'comparativo_id' }) - comparativo: ComparativoCotizaciones; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => ComparativoProducto, (cp) => cp.comparativoProveedor) - productos: ComparativoProducto[]; -} diff --git a/projects/erp-construccion/backend/src/modules/purchase/entities/index.ts b/projects/erp-construccion/backend/src/modules/purchase/entities/index.ts deleted file mode 100644 index 9a3adf541..000000000 --- a/projects/erp-construccion/backend/src/modules/purchase/entities/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Purchase Entities Index - * @module Purchase - * - * Extensiones de compras para construcci贸n (MAI-004) - */ - -export * from './comparativo-cotizaciones.entity'; -export * from './comparativo-proveedor.entity'; -export * from './comparativo-producto.entity'; diff --git a/projects/erp-construccion/backend/src/modules/purchase/services/comparativo.service.ts b/projects/erp-construccion/backend/src/modules/purchase/services/comparativo.service.ts deleted file mode 100644 index f43b4fa11..000000000 --- a/projects/erp-construccion/backend/src/modules/purchase/services/comparativo.service.ts +++ /dev/null @@ -1,311 +0,0 @@ -/** - * ComparativoService - Servicio de comparativos de cotizaciones - * - * Gesti贸n de cuadros comparativos para selecci贸n de proveedores. - * - * @module Purchase - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { ComparativoCotizaciones, ComparativoStatus } from '../entities/comparativo-cotizaciones.entity'; -import { ComparativoProveedor } from '../entities/comparativo-proveedor.entity'; -import { ComparativoProducto } from '../entities/comparativo-producto.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateComparativoDto { - requisicionId?: string; - name: string; - comparisonDate: Date; - notes?: string; -} - -export interface AddProveedorDto { - supplierId: string; - quotationNumber?: string; - quotationDate?: Date; - deliveryDays?: number; - paymentConditions?: string; - evaluationNotes?: string; -} - -export interface AddProductoDto { - productId: string; - quantity: number; - unitPrice: number; - notes?: string; -} - -export interface ComparativoFilters { - requisicionId?: string; - status?: ComparativoStatus; - dateFrom?: Date; - dateTo?: Date; -} - -export class ComparativoService { - constructor( - private readonly comparativoRepository: Repository, - private readonly proveedorRepository: Repository, - private readonly productoRepository: Repository - ) {} - - private generateCode(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - return `CMP-${year}${month}-${random}`; - } - - async findWithFilters( - ctx: ServiceContext, - filters: ComparativoFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.comparativoRepository - .createQueryBuilder('comp') - .leftJoinAndSelect('comp.requisicion', 'requisicion') - .leftJoinAndSelect('comp.createdBy', 'createdBy') - .where('comp.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('comp.deleted_at IS NULL'); - - if (filters.requisicionId) { - queryBuilder.andWhere('comp.requisicion_id = :requisicionId', { - requisicionId: filters.requisicionId, - }); - } - - if (filters.status) { - queryBuilder.andWhere('comp.status = :status', { status: filters.status }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('comp.comparison_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('comp.comparison_date <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('comp.comparison_date', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.comparativoRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - } - - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.comparativoRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - relations: [ - 'requisicion', - 'createdBy', - 'approvedBy', - 'proveedores', - 'proveedores.productos', - ], - }); - } - - async create(ctx: ServiceContext, dto: CreateComparativoDto): Promise { - const comparativo = this.comparativoRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - code: this.generateCode(), - requisicionId: dto.requisicionId, - name: dto.name, - comparisonDate: dto.comparisonDate, - notes: dto.notes, - status: 'draft', - }); - - return this.comparativoRepository.save(comparativo); - } - - async addProveedor( - ctx: ServiceContext, - comparativoId: string, - dto: AddProveedorDto - ): Promise { - const comparativo = await this.findById(ctx, comparativoId); - if (!comparativo) { - throw new Error('Comparativo not found'); - } - - if (comparativo.status !== 'draft' && comparativo.status !== 'in_evaluation') { - throw new Error('Cannot add suppliers to approved or cancelled comparisons'); - } - - const proveedor = this.proveedorRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - comparativoId, - supplierId: dto.supplierId, - quotationNumber: dto.quotationNumber, - quotationDate: dto.quotationDate, - deliveryDays: dto.deliveryDays, - paymentConditions: dto.paymentConditions, - evaluationNotes: dto.evaluationNotes, - }); - - return this.proveedorRepository.save(proveedor); - } - - async addProducto( - ctx: ServiceContext, - proveedorId: string, - dto: AddProductoDto - ): Promise { - const proveedor = await this.proveedorRepository.findOne({ - where: { id: proveedorId, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['comparativo'], - }); - - if (!proveedor) { - throw new Error('Supplier entry not found'); - } - - if (proveedor.comparativo.status !== 'draft' && proveedor.comparativo.status !== 'in_evaluation') { - throw new Error('Cannot add products to approved or cancelled comparisons'); - } - - const producto = this.productoRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - comparativoProveedorId: proveedorId, - productId: dto.productId, - quantity: dto.quantity, - unitPrice: dto.unitPrice, - notes: dto.notes, - }); - - return this.productoRepository.save(producto); - } - - async startEvaluation(ctx: ServiceContext, id: string): Promise { - const comparativo = await this.findWithDetails(ctx, id); - if (!comparativo) { - return null; - } - - if (comparativo.status !== 'draft') { - throw new Error('Can only start evaluation on draft comparisons'); - } - - if (!comparativo.proveedores || comparativo.proveedores.length < 2) { - throw new Error('Need at least 2 suppliers to start evaluation'); - } - - comparativo.status = 'in_evaluation'; - return this.comparativoRepository.save(comparativo); - } - - async selectWinner(ctx: ServiceContext, id: string, supplierId: string): Promise { - const comparativo = await this.findWithDetails(ctx, id); - if (!comparativo) { - return null; - } - - if (comparativo.status !== 'in_evaluation') { - throw new Error('Can only select winner during evaluation'); - } - - // Verify supplier is in the comparison - const proveedor = comparativo.proveedores?.find((p) => p.supplierId === supplierId); - if (!proveedor) { - throw new Error('Supplier not found in this comparison'); - } - - // Mark all as not selected, then mark winner - for (const p of comparativo.proveedores || []) { - p.isSelected = p.supplierId === supplierId; - await this.proveedorRepository.save(p); - } - - comparativo.winnerSupplierId = supplierId; - comparativo.status = 'approved'; - comparativo.approvedById = ctx.userId || ''; - comparativo.approvedAt = new Date(); - - return this.comparativoRepository.save(comparativo); - } - - async cancel(ctx: ServiceContext, id: string): Promise { - const comparativo = await this.findById(ctx, id); - if (!comparativo) { - return null; - } - - if (comparativo.status === 'approved') { - throw new Error('Cannot cancel approved comparisons'); - } - - comparativo.status = 'cancelled'; - return this.comparativoRepository.save(comparativo); - } - - async updateProveedorTotal(ctx: ServiceContext, proveedorId: string): Promise { - const proveedor = await this.proveedorRepository.findOne({ - where: { id: proveedorId, tenantId: ctx.tenantId } as FindOptionsWhere, - relations: ['productos'], - }); - - if (!proveedor) { - return null; - } - - const total = proveedor.productos?.reduce( - (sum, p) => sum + (p.quantity * p.unitPrice), - 0 - ) || 0; - - proveedor.totalAmount = total; - return this.proveedorRepository.save(proveedor); - } - - async softDelete(ctx: ServiceContext, id: string): Promise { - const comparativo = await this.findById(ctx, id); - if (!comparativo) { - return false; - } - - if (comparativo.status === 'approved') { - throw new Error('Cannot delete approved comparisons'); - } - - await this.comparativoRepository.update( - { id, tenantId: ctx.tenantId } as unknown as FindOptionsWhere, - { deletedAt: new Date(), deletedById: ctx.userId } - ); - - return true; - } -} diff --git a/projects/erp-construccion/backend/src/modules/purchase/services/index.ts b/projects/erp-construccion/backend/src/modules/purchase/services/index.ts deleted file mode 100644 index 96280d7d9..000000000 --- a/projects/erp-construccion/backend/src/modules/purchase/services/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Purchase Services Index - * @module Purchase - */ - -export * from './comparativo.service'; diff --git a/projects/erp-construccion/backend/src/modules/quality/controllers/index.ts b/projects/erp-construccion/backend/src/modules/quality/controllers/index.ts deleted file mode 100644 index 3ece966e0..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/controllers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Quality Controllers Index - * @module Quality - */ - -export * from './inspection.controller'; -export * from './ticket.controller'; diff --git a/projects/erp-construccion/backend/src/modules/quality/controllers/inspection.controller.ts b/projects/erp-construccion/backend/src/modules/quality/controllers/inspection.controller.ts deleted file mode 100644 index d98ba8258..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/controllers/inspection.controller.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * InspectionController - REST API for quality inspections - * - * Endpoints para gesti贸n de inspecciones de calidad. - * - * @module Quality - * @routes /api/inspections - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { InspectionService, InspectionFilters } from '../services/inspection.service'; -import { Inspection } from '../entities/inspection.entity'; -import { InspectionResult } from '../entities/inspection-result.entity'; -import { NonConformity } from '../entities/non-conformity.entity'; -import { Checklist } from '../entities/checklist.entity'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createInspectionController(dataSource: DataSource): Router { - const router = Router(); - - // Repositories - const inspectionRepo = dataSource.getRepository(Inspection); - const resultRepo = dataSource.getRepository(InspectionResult); - const ncRepo = dataSource.getRepository(NonConformity); - const checklistRepo = dataSource.getRepository(Checklist); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Services - const service = new InspectionService(inspectionRepo, resultRepo, ncRepo, checklistRepo); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper for service context - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /api/inspections - * List inspections with filters - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: InspectionFilters = {}; - if (req.query.checklistId) filters.checklistId = req.query.checklistId as string; - if (req.query.loteId) filters.loteId = req.query.loteId as string; - if (req.query.inspectorId) filters.inspectorId = req.query.inspectorId as string; - if (req.query.status) filters.status = req.query.status as InspectionFilters['status']; - if (req.query.dateFrom) filters.dateFrom = new Date(req.query.dateFrom as string); - if (req.query.dateTo) filters.dateTo = new Date(req.query.dateTo as string); - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/inspections/:id - * Get inspection with details - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const inspection = await service.findWithDetails(getContext(req), req.params.id); - if (!inspection) { - res.status(404).json({ error: 'Not Found', message: 'Inspection not found' }); - return; - } - - res.status(200).json({ success: true, data: inspection }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/inspections - * Create new inspection - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const inspection = await service.create(getContext(req), { - checklistId: req.body.checklistId, - loteId: req.body.loteId, - inspectionDate: new Date(req.body.inspectionDate), - inspectorId: req.body.inspectorId, - notes: req.body.notes, - }); - - res.status(201).json({ success: true, data: inspection }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/inspections/:id/start - * Start inspection - */ - router.post('/:id/start', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const inspection = await service.startInspection(getContext(req), req.params.id); - if (!inspection) { - res.status(404).json({ error: 'Not Found', message: 'Inspection not found' }); - return; - } - - res.status(200).json({ success: true, data: inspection }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/inspections/:id/results - * Record inspection result - */ - router.post('/:id/results', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const result = await service.recordResult(getContext(req), req.params.id, { - checklistItemId: req.body.checklistItemId, - result: req.body.result, - observations: req.body.observations, - photoUrl: req.body.photoUrl, - }); - - res.status(201).json({ success: true, data: result }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/inspections/:id/complete - * Complete inspection - */ - router.post('/:id/complete', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const inspection = await service.completeInspection(getContext(req), req.params.id); - if (!inspection) { - res.status(404).json({ error: 'Not Found', message: 'Inspection not found' }); - return; - } - - res.status(200).json({ success: true, data: inspection }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/inspections/:id/approve - * Approve inspection - */ - router.post('/:id/approve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const inspection = await service.approveInspection(getContext(req), req.params.id); - if (!inspection) { - res.status(404).json({ error: 'Not Found', message: 'Inspection not found' }); - return; - } - - res.status(200).json({ success: true, data: inspection }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/inspections/:id/reject - * Reject inspection - */ - router.post('/:id/reject', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const inspection = await service.rejectInspection(getContext(req), req.params.id, req.body.reason); - if (!inspection) { - res.status(404).json({ error: 'Not Found', message: 'Inspection not found' }); - return; - } - - res.status(200).json({ success: true, data: inspection }); - } catch (error) { - next(error); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/controllers/ticket.controller.ts b/projects/erp-construccion/backend/src/modules/quality/controllers/ticket.controller.ts deleted file mode 100644 index 2ad536490..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/controllers/ticket.controller.ts +++ /dev/null @@ -1,308 +0,0 @@ -/** - * TicketController - REST API for post-sale tickets - * - * Endpoints para gesti贸n de tickets de garant铆a. - * - * @module Quality - * @routes /api/tickets - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { TicketService, TicketFilters } from '../services/ticket.service'; -import { PostSaleTicket } from '../entities/post-sale-ticket.entity'; -import { TicketAssignment } from '../entities/ticket-assignment.entity'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createTicketController(dataSource: DataSource): Router { - const router = Router(); - - // Repositories - const ticketRepo = dataSource.getRepository(PostSaleTicket); - const assignmentRepo = dataSource.getRepository(TicketAssignment); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Services - const service = new TicketService(ticketRepo, assignmentRepo); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper for service context - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /api/tickets - * List tickets with filters - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const filters: TicketFilters = {}; - if (req.query.loteId) filters.loteId = req.query.loteId as string; - if (req.query.derechohabienteId) filters.derechohabienteId = req.query.derechohabienteId as string; - if (req.query.category) filters.category = req.query.category as TicketFilters['category']; - if (req.query.priority) filters.priority = req.query.priority as TicketFilters['priority']; - if (req.query.status) filters.status = req.query.status as TicketFilters['status']; - if (req.query.slaBreached) filters.slaBreached = req.query.slaBreached === 'true'; - if (req.query.assignedTo) filters.assignedTo = req.query.assignedTo as string; - if (req.query.dateFrom) filters.dateFrom = new Date(req.query.dateFrom as string); - if (req.query.dateTo) filters.dateTo = new Date(req.query.dateTo as string); - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await service.findWithFilters(getContext(req), filters, page, limit); - res.status(200).json({ success: true, data: result.data, pagination: result.meta }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/tickets/sla-stats - * Get SLA statistics - */ - router.get('/sla-stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await service.getSlaStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /api/tickets/:id - * Get ticket with details - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const ticket = await service.findWithDetails(getContext(req), req.params.id); - if (!ticket) { - res.status(404).json({ error: 'Not Found', message: 'Ticket not found' }); - return; - } - - res.status(200).json({ success: true, data: ticket }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/tickets - * Create new ticket - */ - router.post('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const ticket = await service.create(getContext(req), { - loteId: req.body.loteId, - derechohabienteId: req.body.derechohabienteId, - category: req.body.category, - title: req.body.title, - description: req.body.description, - photoUrl: req.body.photoUrl, - contactName: req.body.contactName, - contactPhone: req.body.contactPhone, - }); - - res.status(201).json({ success: true, data: ticket }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/tickets/:id/assign - * Assign ticket to technician - */ - router.post('/:id/assign', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'postventa'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const ticket = await service.assign(getContext(req), req.params.id, { - technicianId: req.body.technicianId, - scheduledDate: req.body.scheduledDate ? new Date(req.body.scheduledDate) : undefined, - scheduledTime: req.body.scheduledTime, - }); - - if (!ticket) { - res.status(404).json({ error: 'Not Found', message: 'Ticket not found' }); - return; - } - - res.status(200).json({ success: true, data: ticket }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/tickets/:id/start - * Start work on ticket - */ - router.post('/:id/start', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'postventa', 'technician'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const ticket = await service.startWork(getContext(req), req.params.id); - if (!ticket) { - res.status(404).json({ error: 'Not Found', message: 'Ticket not found' }); - return; - } - - res.status(200).json({ success: true, data: ticket }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/tickets/:id/resolve - * Resolve ticket - */ - router.post('/:id/resolve', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'postventa', 'technician'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const ticket = await service.resolve(getContext(req), req.params.id, { - resolutionNotes: req.body.resolutionNotes, - resolutionPhotoUrl: req.body.resolutionPhotoUrl, - }); - - if (!ticket) { - res.status(404).json({ error: 'Not Found', message: 'Ticket not found' }); - return; - } - - res.status(200).json({ success: true, data: ticket }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/tickets/:id/close - * Close ticket with satisfaction rating - */ - router.post('/:id/close', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const ticket = await service.close( - getContext(req), - req.params.id, - req.body.rating, - req.body.comment - ); - - if (!ticket) { - res.status(404).json({ error: 'Not Found', message: 'Ticket not found' }); - return; - } - - res.status(200).json({ success: true, data: ticket }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/tickets/:id/cancel - * Cancel ticket - */ - router.post('/:id/cancel', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'postventa'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const ticket = await service.cancel(getContext(req), req.params.id, req.body.reason); - if (!ticket) { - res.status(404).json({ error: 'Not Found', message: 'Ticket not found' }); - return; - } - - res.status(200).json({ success: true, data: ticket }); - } catch (error) { - next(error); - } - }); - - /** - * POST /api/tickets/check-sla - * Check and update SLA breaches - */ - router.post('/check-sla', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const breached = await service.checkSlaBreaches(getContext(req)); - res.status(200).json({ success: true, data: { breachedTickets: breached } }); - } catch (error) { - next(error); - } - }); - - return router; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/checklist-item.entity.ts b/projects/erp-construccion/backend/src/modules/quality/entities/checklist-item.entity.ts deleted file mode 100644 index bb68941dc..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/checklist-item.entity.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * ChecklistItem Entity - * Items de verificaci贸n en checklists - * - * @module Quality - * @table quality.checklist_items - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Checklist } from './checklist.entity'; - -@Entity({ schema: 'quality', name: 'checklist_items' }) -@Index(['tenantId', 'checklistId', 'sequenceNumber']) -export class ChecklistItem { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'checklist_id', type: 'uuid' }) - checklistId: string; - - @Column({ name: 'sequence_number', type: 'integer' }) - sequenceNumber: number; - - @Column({ type: 'varchar', length: 100 }) - category: string; - - @Column({ type: 'text' }) - description: string; - - @Column({ name: 'is_critical', type: 'boolean', default: false }) - isCritical: boolean; - - @Column({ name: 'requires_photo', type: 'boolean', default: false }) - requiresPhoto: boolean; - - @Column({ name: 'acceptance_criteria', type: 'text', nullable: true }) - acceptanceCriteria: string; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Checklist, (c) => c.items, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'checklist_id' }) - checklist: Checklist; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/checklist.entity.ts b/projects/erp-construccion/backend/src/modules/quality/entities/checklist.entity.ts deleted file mode 100644 index acefd6f3a..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/checklist.entity.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Checklist Entity - * Templates de inspecci贸n de calidad - * - * @module Quality - * @table quality.checklists - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { ChecklistItem } from './checklist-item.entity'; -import { Inspection } from './inspection.entity'; - -export type ChecklistStage = 'foundation' | 'structure' | 'installations' | 'finishes' | 'delivery' | 'custom'; - -@Entity({ schema: 'quality', name: 'checklists' }) -@Index(['tenantId', 'code'], { unique: true }) -export class Checklist { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 30 }) - code: string; - - @Column({ type: 'varchar', length: 255 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string; - - @Column({ type: 'varchar', length: 30 }) - stage: ChecklistStage; - - @Column({ name: 'prototype_id', type: 'uuid', nullable: true }) - prototypeId: string; - - @Column({ name: 'is_active', type: 'boolean', default: true }) - isActive: boolean; - - @Column({ type: 'integer', default: 0 }) - version: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => ChecklistItem, (item) => item.checklist) - items: ChecklistItem[]; - - @OneToMany(() => Inspection, (insp) => insp.checklist) - inspections: Inspection[]; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/corrective-action.entity.ts b/projects/erp-construccion/backend/src/modules/quality/entities/corrective-action.entity.ts deleted file mode 100644 index 9afb8799a..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/corrective-action.entity.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * CorrectiveAction Entity - * Acciones correctivas (CAPA) - * - * @module Quality - * @table quality.corrective_actions - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { NonConformity } from './non-conformity.entity'; - -export type ActionType = 'corrective' | 'preventive' | 'improvement'; -export type ActionStatus = 'pending' | 'in_progress' | 'completed' | 'verified'; - -@Entity({ schema: 'quality', name: 'corrective_actions' }) -@Index(['tenantId', 'nonConformityId']) -export class CorrectiveAction { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'non_conformity_id', type: 'uuid' }) - nonConformityId: string; - - @Column({ name: 'action_type', type: 'varchar', length: 20 }) - actionType: ActionType; - - @Column({ type: 'text' }) - description: string; - - @Column({ name: 'responsible_id', type: 'uuid' }) - responsibleId: string; - - @Column({ name: 'due_date', type: 'date' }) - dueDate: Date; - - @Column({ type: 'varchar', length: 20, default: 'pending' }) - status: ActionStatus; - - @Column({ name: 'completed_at', type: 'timestamptz', nullable: true }) - completedAt: Date; - - @Column({ name: 'completion_notes', type: 'text', nullable: true }) - completionNotes: string; - - @Column({ name: 'verified_at', type: 'timestamptz', nullable: true }) - verifiedAt: Date; - - @Column({ name: 'verified_by', type: 'uuid', nullable: true }) - verifiedById: string; - - @Column({ name: 'effectiveness_verified', type: 'boolean', default: false }) - effectivenessVerified: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => NonConformity, (nc) => nc.correctiveActions, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'non_conformity_id' }) - nonConformity: NonConformity; - - @ManyToOne(() => User) - @JoinColumn({ name: 'responsible_id' }) - responsible: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'verified_by' }) - verifiedBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/index.ts b/projects/erp-construccion/backend/src/modules/quality/entities/index.ts deleted file mode 100644 index 5bf0124f2..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Quality Entities Index - * @module Quality - * - * Control de calidad y postventa (MAI-009) - */ - -export * from './checklist.entity'; -export * from './checklist-item.entity'; -export * from './inspection.entity'; -export * from './inspection-result.entity'; -export * from './non-conformity.entity'; -export * from './corrective-action.entity'; -export * from './post-sale-ticket.entity'; -export * from './ticket-assignment.entity'; diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/inspection-result.entity.ts b/projects/erp-construccion/backend/src/modules/quality/entities/inspection-result.entity.ts deleted file mode 100644 index 79a938fd9..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/inspection-result.entity.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * InspectionResult Entity - * Resultados por item de inspecci贸n - * - * @module Quality - * @table quality.inspection_results - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Inspection } from './inspection.entity'; -import { ChecklistItem } from './checklist-item.entity'; - -export type InspectionResultStatus = 'pending' | 'passed' | 'failed' | 'not_applicable'; - -@Entity({ schema: 'quality', name: 'inspection_results' }) -@Index(['tenantId', 'inspectionId', 'checklistItemId'], { unique: true }) -export class InspectionResult { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'inspection_id', type: 'uuid' }) - inspectionId: string; - - @Column({ name: 'checklist_item_id', type: 'uuid' }) - checklistItemId: string; - - @Column({ type: 'varchar', length: 20, default: 'pending' }) - result: InspectionResultStatus; - - @Column({ type: 'text', nullable: true }) - observations: string; - - @Column({ name: 'photo_url', type: 'varchar', length: 500, nullable: true }) - photoUrl: string; - - @Column({ name: 'inspected_at', type: 'timestamptz', nullable: true }) - inspectedAt: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Inspection, (i) => i.results, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'inspection_id' }) - inspection: Inspection; - - @ManyToOne(() => ChecklistItem) - @JoinColumn({ name: 'checklist_item_id' }) - checklistItem: ChecklistItem; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/inspection.entity.ts b/projects/erp-construccion/backend/src/modules/quality/entities/inspection.entity.ts deleted file mode 100644 index a0c9c30b1..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/inspection.entity.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Inspection Entity - * Inspecciones de calidad realizadas - * - * @module Quality - * @table quality.inspections - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Checklist } from './checklist.entity'; -import { InspectionResult } from './inspection-result.entity'; -import { NonConformity } from './non-conformity.entity'; - -export type InspectionStatus = 'pending' | 'in_progress' | 'completed' | 'approved' | 'rejected'; - -@Entity({ schema: 'quality', name: 'inspections' }) -@Index(['tenantId', 'inspectionNumber'], { unique: true }) -export class Inspection { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'checklist_id', type: 'uuid' }) - checklistId: string; - - @Column({ name: 'lote_id', type: 'uuid' }) - loteId: string; - - @Column({ name: 'inspection_number', type: 'varchar', length: 50 }) - inspectionNumber: string; - - @Column({ name: 'inspection_date', type: 'date' }) - inspectionDate: Date; - - @Column({ name: 'inspector_id', type: 'uuid' }) - inspectorId: string; - - @Column({ type: 'varchar', length: 20, default: 'pending' }) - status: InspectionStatus; - - @Column({ name: 'total_items', type: 'integer', default: 0 }) - totalItems: number; - - @Column({ name: 'passed_items', type: 'integer', default: 0 }) - passedItems: number; - - @Column({ name: 'failed_items', type: 'integer', default: 0 }) - failedItems: number; - - @Column({ name: 'pass_rate', type: 'decimal', precision: 5, scale: 2, nullable: true }) - passRate: number; - - @Column({ name: 'completed_at', type: 'timestamptz', nullable: true }) - completedAt: Date; - - @Column({ name: 'approved_by', type: 'uuid', nullable: true }) - approvedById: string; - - @Column({ name: 'approved_at', type: 'timestamptz', nullable: true }) - approvedAt: Date; - - @Column({ type: 'text', nullable: true }) - notes: string; - - @Column({ name: 'rejection_reason', type: 'text', nullable: true }) - rejectionReason: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Checklist, (c) => c.inspections) - @JoinColumn({ name: 'checklist_id' }) - checklist: Checklist; - - @ManyToOne(() => User) - @JoinColumn({ name: 'inspector_id' }) - inspector: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'approved_by' }) - approvedBy: User; - - @OneToMany(() => InspectionResult, (r) => r.inspection) - results: InspectionResult[]; - - @OneToMany(() => NonConformity, (nc) => nc.inspection) - nonConformities: NonConformity[]; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/non-conformity.entity.ts b/projects/erp-construccion/backend/src/modules/quality/entities/non-conformity.entity.ts deleted file mode 100644 index fac8d63ff..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/non-conformity.entity.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * NonConformity Entity - * No conformidades detectadas en inspecciones - * - * @module Quality - * @table quality.non_conformities - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Inspection } from './inspection.entity'; -import { CorrectiveAction } from './corrective-action.entity'; - -export type NCSeverity = 'minor' | 'major' | 'critical'; -export type NCStatus = 'open' | 'in_progress' | 'closed' | 'verified'; - -@Entity({ schema: 'quality', name: 'non_conformities' }) -@Index(['tenantId', 'ncNumber'], { unique: true }) -export class NonConformity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'inspection_id', type: 'uuid', nullable: true }) - inspectionId: string; - - @Column({ name: 'lote_id', type: 'uuid' }) - loteId: string; - - @Column({ name: 'nc_number', type: 'varchar', length: 50 }) - ncNumber: string; - - @Column({ name: 'detection_date', type: 'date' }) - detectionDate: Date; - - @Column({ type: 'varchar', length: 100 }) - category: string; - - @Column({ type: 'varchar', length: 20 }) - severity: NCSeverity; - - @Column({ type: 'text' }) - description: string; - - @Column({ name: 'root_cause', type: 'text', nullable: true }) - rootCause: string; - - @Column({ name: 'photo_url', type: 'varchar', length: 500, nullable: true }) - photoUrl: string; - - @Column({ name: 'contractor_id', type: 'uuid', nullable: true }) - contractorId: string; - - @Column({ type: 'varchar', length: 20, default: 'open' }) - status: NCStatus; - - @Column({ name: 'due_date', type: 'date', nullable: true }) - dueDate: Date; - - @Column({ name: 'closed_at', type: 'timestamptz', nullable: true }) - closedAt: Date; - - @Column({ name: 'closed_by', type: 'uuid', nullable: true }) - closedById: string; - - @Column({ name: 'verified_at', type: 'timestamptz', nullable: true }) - verifiedAt: Date; - - @Column({ name: 'verified_by', type: 'uuid', nullable: true }) - verifiedById: string; - - @Column({ name: 'closure_photo_url', type: 'varchar', length: 500, nullable: true }) - closurePhotoUrl: string; - - @Column({ name: 'closure_notes', type: 'text', nullable: true }) - closureNotes: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Inspection, (i) => i.nonConformities) - @JoinColumn({ name: 'inspection_id' }) - inspection: Inspection; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'closed_by' }) - closedBy: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'verified_by' }) - verifiedBy: User; - - @OneToMany(() => CorrectiveAction, (ca) => ca.nonConformity) - correctiveActions: CorrectiveAction[]; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/post-sale-ticket.entity.ts b/projects/erp-construccion/backend/src/modules/quality/entities/post-sale-ticket.entity.ts deleted file mode 100644 index 127ad69f2..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/post-sale-ticket.entity.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * PostSaleTicket Entity - * Tickets de garant铆a postventa - * - * @module Quality - * @table quality.post_sale_tickets - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { TicketAssignment } from './ticket-assignment.entity'; - -export type TicketPriority = 'urgent' | 'high' | 'medium' | 'low'; -export type TicketStatus = 'created' | 'assigned' | 'in_progress' | 'resolved' | 'closed' | 'cancelled'; -export type TicketCategory = 'plumbing' | 'electrical' | 'finishes' | 'carpentry' | 'structural' | 'other'; - -@Entity({ schema: 'quality', name: 'post_sale_tickets' }) -@Index(['tenantId', 'ticketNumber'], { unique: true }) -export class PostSaleTicket { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'lote_id', type: 'uuid' }) - loteId: string; - - @Column({ name: 'derechohabiente_id', type: 'uuid', nullable: true }) - derechohabienteId: string; - - @Column({ name: 'ticket_number', type: 'varchar', length: 50 }) - ticketNumber: string; - - @Column({ type: 'varchar', length: 30 }) - category: TicketCategory; - - @Column({ type: 'varchar', length: 20 }) - priority: TicketPriority; - - @Column({ type: 'varchar', length: 255 }) - title: string; - - @Column({ type: 'text' }) - description: string; - - @Column({ name: 'photo_url', type: 'varchar', length: 500, nullable: true }) - photoUrl: string; - - @Column({ type: 'varchar', length: 20, default: 'created' }) - status: TicketStatus; - - @Column({ name: 'sla_hours', type: 'integer' }) - slaHours: number; - - @Column({ name: 'sla_due_at', type: 'timestamptz' }) - slaDueAt: Date; - - @Column({ name: 'sla_breached', type: 'boolean', default: false }) - slaBreached: boolean; - - @Column({ name: 'assigned_at', type: 'timestamptz', nullable: true }) - assignedAt: Date; - - @Column({ name: 'resolved_at', type: 'timestamptz', nullable: true }) - resolvedAt: Date; - - @Column({ name: 'closed_at', type: 'timestamptz', nullable: true }) - closedAt: Date; - - @Column({ name: 'resolution_notes', type: 'text', nullable: true }) - resolutionNotes: string; - - @Column({ name: 'resolution_photo_url', type: 'varchar', length: 500, nullable: true }) - resolutionPhotoUrl: string; - - @Column({ name: 'satisfaction_rating', type: 'integer', nullable: true }) - satisfactionRating: number; - - @Column({ name: 'satisfaction_comment', type: 'text', nullable: true }) - satisfactionComment: string; - - @Column({ name: 'contact_name', type: 'varchar', length: 200, nullable: true }) - contactName: string; - - @Column({ name: 'contact_phone', type: 'varchar', length: 20, nullable: true }) - contactPhone: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User; - - @OneToMany(() => TicketAssignment, (ta) => ta.ticket) - assignments: TicketAssignment[]; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/entities/ticket-assignment.entity.ts b/projects/erp-construccion/backend/src/modules/quality/entities/ticket-assignment.entity.ts deleted file mode 100644 index 0086afe23..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/entities/ticket-assignment.entity.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * TicketAssignment Entity - * Asignaciones de tickets a t茅cnicos - * - * @module Quality - * @table quality.ticket_assignments - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { PostSaleTicket } from './post-sale-ticket.entity'; - -export type AssignmentStatus = 'assigned' | 'accepted' | 'in_progress' | 'completed' | 'reassigned'; - -@Entity({ schema: 'quality', name: 'ticket_assignments' }) -@Index(['tenantId', 'ticketId', 'technicianId']) -export class TicketAssignment { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'ticket_id', type: 'uuid' }) - ticketId: string; - - @Column({ name: 'technician_id', type: 'uuid' }) - technicianId: string; - - @Column({ name: 'assigned_at', type: 'timestamptz' }) - assignedAt: Date; - - @Column({ name: 'assigned_by', type: 'uuid' }) - assignedById: string; - - @Column({ type: 'varchar', length: 20, default: 'assigned' }) - status: AssignmentStatus; - - @Column({ name: 'accepted_at', type: 'timestamptz', nullable: true }) - acceptedAt: Date; - - @Column({ name: 'scheduled_date', type: 'date', nullable: true }) - scheduledDate: Date; - - @Column({ name: 'scheduled_time', type: 'time', nullable: true }) - scheduledTime: string; - - @Column({ name: 'completed_at', type: 'timestamptz', nullable: true }) - completedAt: Date; - - @Column({ name: 'work_notes', type: 'text', nullable: true }) - workNotes: string; - - @Column({ name: 'reassignment_reason', type: 'text', nullable: true }) - reassignmentReason: string; - - @Column({ name: 'is_current', type: 'boolean', default: true }) - isCurrent: boolean; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) - updatedAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => PostSaleTicket, (t) => t.assignments, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'ticket_id' }) - ticket: PostSaleTicket; - - @ManyToOne(() => User) - @JoinColumn({ name: 'technician_id' }) - technician: User; - - @ManyToOne(() => User) - @JoinColumn({ name: 'assigned_by' }) - assignedBy: User; -} diff --git a/projects/erp-construccion/backend/src/modules/quality/services/index.ts b/projects/erp-construccion/backend/src/modules/quality/services/index.ts deleted file mode 100644 index c401230b6..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/services/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Quality Services Index - * @module Quality - */ - -export * from './inspection.service'; -export * from './ticket.service'; diff --git a/projects/erp-construccion/backend/src/modules/quality/services/inspection.service.ts b/projects/erp-construccion/backend/src/modules/quality/services/inspection.service.ts deleted file mode 100644 index d10f5d0e2..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/services/inspection.service.ts +++ /dev/null @@ -1,317 +0,0 @@ -/** - * InspectionService - Servicio de inspecciones de calidad - * - * Gesti贸n de inspecciones con checklists y resultados. - * - * @module Quality - */ - -import { Repository, FindOptionsWhere } from 'typeorm'; -import { Inspection, InspectionStatus } from '../entities/inspection.entity'; -import { InspectionResult } from '../entities/inspection-result.entity'; -import { NonConformity } from '../entities/non-conformity.entity'; -import { Checklist } from '../entities/checklist.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateInspectionDto { - checklistId: string; - loteId: string; - inspectionDate: Date; - inspectorId: string; - notes?: string; -} - -export interface RecordResultDto { - checklistItemId: string; - result: 'passed' | 'failed' | 'not_applicable'; - observations?: string; - photoUrl?: string; -} - -export interface InspectionFilters { - checklistId?: string; - loteId?: string; - inspectorId?: string; - status?: InspectionStatus; - dateFrom?: Date; - dateTo?: Date; -} - -export class InspectionService { - constructor( - private readonly inspectionRepository: Repository, - private readonly resultRepository: Repository, - private readonly nonConformityRepository: Repository, - private readonly checklistRepository: Repository - ) {} - - private generateInspectionNumber(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 8).toUpperCase(); - return `INS-${year}${month}-${random}`; - } - - private generateNCNumber(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 6).toUpperCase(); - return `NC-${year}${month}-${random}`; - } - - async findWithFilters( - ctx: ServiceContext, - filters: InspectionFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.inspectionRepository - .createQueryBuilder('insp') - .leftJoinAndSelect('insp.checklist', 'checklist') - .leftJoinAndSelect('insp.inspector', 'inspector') - .where('insp.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.checklistId) { - queryBuilder.andWhere('insp.checklist_id = :checklistId', { checklistId: filters.checklistId }); - } - - if (filters.loteId) { - queryBuilder.andWhere('insp.lote_id = :loteId', { loteId: filters.loteId }); - } - - if (filters.inspectorId) { - queryBuilder.andWhere('insp.inspector_id = :inspectorId', { inspectorId: filters.inspectorId }); - } - - if (filters.status) { - queryBuilder.andWhere('insp.status = :status', { status: filters.status }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('insp.inspection_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('insp.inspection_date <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('insp.inspection_date', 'DESC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.inspectionRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as unknown as FindOptionsWhere, - }); - } - - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.inspectionRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as unknown as FindOptionsWhere, - relations: ['checklist', 'checklist.items', 'inspector', 'results', 'results.checklistItem', 'nonConformities'], - }); - } - - async create(ctx: ServiceContext, dto: CreateInspectionDto): Promise { - // Validate checklist exists - const checklist = await this.checklistRepository.findOne({ - where: { - id: dto.checklistId, - tenantId: ctx.tenantId, - isActive: true, - deletedAt: null, - } as unknown as FindOptionsWhere, - relations: ['items'], - }); - - if (!checklist) { - throw new Error('Checklist not found or inactive'); - } - - const inspection = this.inspectionRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - checklistId: dto.checklistId, - loteId: dto.loteId, - inspectionNumber: this.generateInspectionNumber(), - inspectionDate: dto.inspectionDate, - inspectorId: dto.inspectorId, - notes: dto.notes, - status: 'pending', - totalItems: checklist.items?.filter(i => i.isActive).length || 0, - }); - - return this.inspectionRepository.save(inspection); - } - - async startInspection(ctx: ServiceContext, id: string): Promise { - const inspection = await this.findById(ctx, id); - if (!inspection) { - return null; - } - - if (inspection.status !== 'pending') { - throw new Error('Can only start pending inspections'); - } - - inspection.status = 'in_progress'; - inspection.updatedById = ctx.userId || ''; - - return this.inspectionRepository.save(inspection); - } - - async recordResult(ctx: ServiceContext, inspectionId: string, dto: RecordResultDto): Promise { - const inspection = await this.findById(ctx, inspectionId); - if (!inspection) { - throw new Error('Inspection not found'); - } - - if (inspection.status !== 'in_progress') { - throw new Error('Can only record results for in-progress inspections'); - } - - // Check if result already exists - let result = await this.resultRepository.findOne({ - where: { - inspectionId, - checklistItemId: dto.checklistItemId, - tenantId: ctx.tenantId, - } as FindOptionsWhere, - }); - - if (result) { - // Update existing - result.result = dto.result; - result.observations = dto.observations || result.observations; - result.photoUrl = dto.photoUrl || result.photoUrl; - result.inspectedAt = new Date(); - } else { - // Create new - result = this.resultRepository.create({ - tenantId: ctx.tenantId, - inspectionId, - checklistItemId: dto.checklistItemId, - result: dto.result, - observations: dto.observations, - photoUrl: dto.photoUrl, - inspectedAt: new Date(), - }); - } - - const savedResult = await this.resultRepository.save(result); - - // If failed, create non-conformity automatically - if (dto.result === 'failed') { - const nc = this.nonConformityRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - inspectionId, - loteId: inspection.loteId, - ncNumber: this.generateNCNumber(), - detectionDate: new Date(), - category: 'Inspection Finding', - severity: 'minor', - description: dto.observations || 'Failed inspection item', - photoUrl: dto.photoUrl, - status: 'open', - }); - await this.nonConformityRepository.save(nc); - } - - return savedResult; - } - - async completeInspection(ctx: ServiceContext, id: string): Promise { - const inspection = await this.findWithDetails(ctx, id); - if (!inspection) { - return null; - } - - if (inspection.status !== 'in_progress') { - throw new Error('Can only complete in-progress inspections'); - } - - // Calculate pass/fail counts - const results = inspection.results || []; - const passed = results.filter(r => r.result === 'passed').length; - const failed = results.filter(r => r.result === 'failed').length; - const total = results.filter(r => r.result !== 'not_applicable').length; - - inspection.passedItems = passed; - inspection.failedItems = failed; - inspection.passRate = total > 0 ? (passed / total) * 100 : 0; - inspection.status = 'completed'; - inspection.completedAt = new Date(); - inspection.updatedById = ctx.userId || ''; - - return this.inspectionRepository.save(inspection); - } - - async approveInspection(ctx: ServiceContext, id: string): Promise { - const inspection = await this.findWithDetails(ctx, id); - if (!inspection) { - return null; - } - - if (inspection.status !== 'completed') { - throw new Error('Can only approve completed inspections'); - } - - // Check for open critical non-conformities - const criticalNCs = inspection.nonConformities?.filter( - nc => nc.severity === 'critical' && nc.status !== 'verified' - ); - - if (criticalNCs && criticalNCs.length > 0) { - throw new Error('Cannot approve inspection with open critical non-conformities'); - } - - inspection.status = 'approved'; - inspection.approvedById = ctx.userId || ''; - inspection.approvedAt = new Date(); - inspection.updatedById = ctx.userId || ''; - - return this.inspectionRepository.save(inspection); - } - - async rejectInspection(ctx: ServiceContext, id: string, reason: string): Promise { - const inspection = await this.findById(ctx, id); - if (!inspection) { - return null; - } - - if (inspection.status !== 'completed') { - throw new Error('Can only reject completed inspections'); - } - - inspection.status = 'rejected'; - inspection.rejectionReason = reason; - inspection.updatedById = ctx.userId || ''; - - return this.inspectionRepository.save(inspection); - } -} diff --git a/projects/erp-construccion/backend/src/modules/quality/services/ticket.service.ts b/projects/erp-construccion/backend/src/modules/quality/services/ticket.service.ts deleted file mode 100644 index 48466f833..000000000 --- a/projects/erp-construccion/backend/src/modules/quality/services/ticket.service.ts +++ /dev/null @@ -1,395 +0,0 @@ -/** - * TicketService - Servicio de tickets postventa - * - * Gesti贸n de tickets de garant铆a con SLA. - * - * @module Quality - */ - -import { Repository, FindOptionsWhere, LessThan } from 'typeorm'; -import { PostSaleTicket, TicketStatus, TicketPriority, TicketCategory } from '../entities/post-sale-ticket.entity'; -import { TicketAssignment } from '../entities/ticket-assignment.entity'; -import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; - -export interface CreateTicketDto { - loteId: string; - derechohabienteId?: string; - category: TicketCategory; - title: string; - description: string; - photoUrl?: string; - contactName?: string; - contactPhone?: string; -} - -export interface AssignTicketDto { - technicianId: string; - scheduledDate?: Date; - scheduledTime?: string; -} - -export interface ResolveTicketDto { - resolutionNotes: string; - resolutionPhotoUrl?: string; -} - -export interface TicketFilters { - loteId?: string; - derechohabienteId?: string; - category?: TicketCategory; - priority?: TicketPriority; - status?: TicketStatus; - slaBreached?: boolean; - assignedTo?: string; - dateFrom?: Date; - dateTo?: Date; -} - -// SLA hours by priority -const SLA_HOURS: Record = { - urgent: 24, - high: 48, - medium: 168, // 7 days - low: 360, // 15 days -}; - -// Auto-priority by category -const CATEGORY_PRIORITY: Record = { - plumbing: 'high', - electrical: 'high', - structural: 'urgent', - finishes: 'medium', - carpentry: 'medium', - other: 'low', -}; - -export class TicketService { - constructor( - private readonly ticketRepository: Repository, - private readonly assignmentRepository: Repository - ) {} - - private generateTicketNumber(): string { - const now = new Date(); - const year = now.getFullYear().toString().slice(-2); - const month = (now.getMonth() + 1).toString().padStart(2, '0'); - const random = Math.random().toString(36).substring(2, 8).toUpperCase(); - return `TKT-${year}${month}-${random}`; - } - - async findWithFilters( - ctx: ServiceContext, - filters: TicketFilters = {}, - page: number = 1, - limit: number = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const queryBuilder = this.ticketRepository - .createQueryBuilder('tkt') - .leftJoinAndSelect('tkt.assignments', 'assignments', 'assignments.is_current = true') - .leftJoinAndSelect('assignments.technician', 'technician') - .where('tkt.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.loteId) { - queryBuilder.andWhere('tkt.lote_id = :loteId', { loteId: filters.loteId }); - } - - if (filters.derechohabienteId) { - queryBuilder.andWhere('tkt.derechohabiente_id = :derechohabienteId', { - derechohabienteId: filters.derechohabienteId, - }); - } - - if (filters.category) { - queryBuilder.andWhere('tkt.category = :category', { category: filters.category }); - } - - if (filters.priority) { - queryBuilder.andWhere('tkt.priority = :priority', { priority: filters.priority }); - } - - if (filters.status) { - queryBuilder.andWhere('tkt.status = :status', { status: filters.status }); - } - - if (filters.slaBreached !== undefined) { - queryBuilder.andWhere('tkt.sla_breached = :slaBreached', { slaBreached: filters.slaBreached }); - } - - if (filters.assignedTo) { - queryBuilder.andWhere('assignments.technician_id = :technicianId', { technicianId: filters.assignedTo }); - } - - if (filters.dateFrom) { - queryBuilder.andWhere('tkt.created_at >= :dateFrom', { dateFrom: filters.dateFrom }); - } - - if (filters.dateTo) { - queryBuilder.andWhere('tkt.created_at <= :dateTo', { dateTo: filters.dateTo }); - } - - queryBuilder - .orderBy('tkt.priority', 'ASC') - .addOrderBy('tkt.sla_due_at', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await queryBuilder.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async findById(ctx: ServiceContext, id: string): Promise { - return this.ticketRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as unknown as FindOptionsWhere, - }); - } - - async findWithDetails(ctx: ServiceContext, id: string): Promise { - return this.ticketRepository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - } as unknown as FindOptionsWhere, - relations: ['assignments', 'assignments.technician', 'assignments.assignedBy', 'createdBy'], - }); - } - - async create(ctx: ServiceContext, dto: CreateTicketDto): Promise { - // Determine priority based on category - const priority = CATEGORY_PRIORITY[dto.category]; - const slaHours = SLA_HOURS[priority]; - const slaDueAt = new Date(); - slaDueAt.setHours(slaDueAt.getHours() + slaHours); - - const ticket = this.ticketRepository.create({ - tenantId: ctx.tenantId, - createdById: ctx.userId, - loteId: dto.loteId, - derechohabienteId: dto.derechohabienteId, - ticketNumber: this.generateTicketNumber(), - category: dto.category, - priority, - title: dto.title, - description: dto.description, - photoUrl: dto.photoUrl, - contactName: dto.contactName, - contactPhone: dto.contactPhone, - status: 'created', - slaHours, - slaDueAt, - }); - - return this.ticketRepository.save(ticket); - } - - async assign(ctx: ServiceContext, id: string, dto: AssignTicketDto): Promise { - const ticket = await this.findWithDetails(ctx, id); - if (!ticket) { - return null; - } - - if (ticket.status === 'closed' || ticket.status === 'cancelled') { - throw new Error('Cannot assign closed or cancelled tickets'); - } - - // Mark previous assignments as not current - if (ticket.assignments && ticket.assignments.length > 0) { - for (const assignment of ticket.assignments) { - if (assignment.isCurrent) { - assignment.isCurrent = false; - assignment.status = 'reassigned'; - await this.assignmentRepository.save(assignment); - } - } - } - - // Create new assignment - const assignment = this.assignmentRepository.create({ - tenantId: ctx.tenantId, - ticketId: id, - technicianId: dto.technicianId, - assignedAt: new Date(), - assignedById: ctx.userId || '', - scheduledDate: dto.scheduledDate, - scheduledTime: dto.scheduledTime, - status: 'assigned', - isCurrent: true, - }); - await this.assignmentRepository.save(assignment); - - // Update ticket status - ticket.status = 'assigned'; - ticket.assignedAt = new Date(); - ticket.updatedById = ctx.userId || ''; - - return this.ticketRepository.save(ticket); - } - - async startWork(ctx: ServiceContext, id: string): Promise { - const ticket = await this.findWithDetails(ctx, id); - if (!ticket) { - return null; - } - - if (ticket.status !== 'assigned') { - throw new Error('Can only start work on assigned tickets'); - } - - // Update current assignment - const currentAssignment = ticket.assignments?.find(a => a.isCurrent); - if (currentAssignment) { - currentAssignment.status = 'in_progress'; - currentAssignment.acceptedAt = new Date(); - await this.assignmentRepository.save(currentAssignment); - } - - ticket.status = 'in_progress'; - ticket.updatedById = ctx.userId || ''; - - return this.ticketRepository.save(ticket); - } - - async resolve(ctx: ServiceContext, id: string, dto: ResolveTicketDto): Promise { - const ticket = await this.findWithDetails(ctx, id); - if (!ticket) { - return null; - } - - if (ticket.status !== 'in_progress') { - throw new Error('Can only resolve in-progress tickets'); - } - - // Update current assignment - const currentAssignment = ticket.assignments?.find(a => a.isCurrent); - if (currentAssignment) { - currentAssignment.status = 'completed'; - currentAssignment.completedAt = new Date(); - currentAssignment.workNotes = dto.resolutionNotes; - await this.assignmentRepository.save(currentAssignment); - } - - ticket.status = 'resolved'; - ticket.resolvedAt = new Date(); - ticket.resolutionNotes = dto.resolutionNotes; - if (dto.resolutionPhotoUrl) { - ticket.resolutionPhotoUrl = dto.resolutionPhotoUrl; - } - ticket.updatedById = ctx.userId || ''; - - // Check if SLA was breached - if (new Date() > ticket.slaDueAt) { - ticket.slaBreached = true; - } - - return this.ticketRepository.save(ticket); - } - - async close(ctx: ServiceContext, id: string, rating?: number, comment?: string): Promise { - const ticket = await this.findById(ctx, id); - if (!ticket) { - return null; - } - - if (ticket.status !== 'resolved') { - throw new Error('Can only close resolved tickets'); - } - - ticket.status = 'closed'; - ticket.closedAt = new Date(); - if (rating !== undefined) { - ticket.satisfactionRating = rating; - } - if (comment) { - ticket.satisfactionComment = comment; - } - ticket.updatedById = ctx.userId || ''; - - return this.ticketRepository.save(ticket); - } - - async cancel(ctx: ServiceContext, id: string, reason: string): Promise { - const ticket = await this.findById(ctx, id); - if (!ticket) { - return null; - } - - if (ticket.status === 'closed') { - throw new Error('Cannot cancel closed tickets'); - } - - ticket.status = 'cancelled'; - ticket.resolutionNotes = `[CANCELLED] ${reason}`; - ticket.updatedById = ctx.userId || ''; - - return this.ticketRepository.save(ticket); - } - - async checkSlaBreaches(ctx: ServiceContext): Promise { - const result = await this.ticketRepository.update( - { - tenantId: ctx.tenantId, - slaBreached: false, - slaDueAt: LessThan(new Date()), - status: 'created' as TicketStatus, - } as FindOptionsWhere, - { slaBreached: true } - ); - - // Also check assigned and in_progress - await this.ticketRepository.update( - { - tenantId: ctx.tenantId, - slaBreached: false, - slaDueAt: LessThan(new Date()), - status: 'assigned' as TicketStatus, - } as FindOptionsWhere, - { slaBreached: true } - ); - - await this.ticketRepository.update( - { - tenantId: ctx.tenantId, - slaBreached: false, - slaDueAt: LessThan(new Date()), - status: 'in_progress' as TicketStatus, - } as FindOptionsWhere, - { slaBreached: true } - ); - - return result.affected || 0; - } - - async getSlaStats(ctx: ServiceContext): Promise<{ total: number; breached: number; complianceRate: number }> { - const total = await this.ticketRepository.count({ - where: { - tenantId: ctx.tenantId, - status: 'closed' as TicketStatus, - } as FindOptionsWhere, - }); - - const breached = await this.ticketRepository.count({ - where: { - tenantId: ctx.tenantId, - status: 'closed' as TicketStatus, - slaBreached: true, - } as FindOptionsWhere, - }); - - const complianceRate = total > 0 ? ((total - breached) / total) * 100 : 100; - - return { total, breached, complianceRate }; - } -} diff --git a/projects/erp-construccion/backend/src/modules/reports/controllers/dashboard.controller.ts b/projects/erp-construccion/backend/src/modules/reports/controllers/dashboard.controller.ts deleted file mode 100644 index 1710a6d36..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/controllers/dashboard.controller.ts +++ /dev/null @@ -1,504 +0,0 @@ -/** - * DashboardController - Controller de Dashboards - * - * Endpoints REST para gesti贸n de dashboards y widgets. - * - * @module Reports - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { - DashboardService, - CreateDashboardDto, - UpdateDashboardDto, - DashboardFilters, - CreateWidgetDto, - UpdateWidgetDto, -} from '../services/dashboard.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Dashboard } from '../entities/dashboard.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createDashboardController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const dashboardRepository = dataSource.getRepository(Dashboard); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const dashboardService = new DashboardService(dashboardRepository, dataSource); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /dashboards - * Listar dashboards con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: DashboardFilters = {}; - if (req.query.dashboardType) filters.dashboardType = req.query.dashboardType as any; - if (req.query.visibility) filters.visibility = req.query.visibility as any; - if (req.query.ownerId) filters.ownerId = req.query.ownerId as string; - if (req.query.fraccionamientoId) filters.fraccionamientoId = req.query.fraccionamientoId as string; - if (req.query.isActive !== undefined) filters.isActive = req.query.isActive === 'true'; - if (req.query.search) filters.search = req.query.search as string; - - const result = await dashboardService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /dashboards/stats - * Estad铆sticas de dashboards - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await dashboardService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /dashboards/my - * Mis dashboards - */ - router.get('/my', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const userId = req.user?.sub; - if (!userId) { - res.status(400).json({ error: 'Bad Request', message: 'User ID required' }); - return; - } - - const dashboards = await dashboardService.findByOwner(getContext(req), userId); - res.status(200).json({ success: true, data: dashboards }); - } catch (error) { - next(error); - } - }); - - /** - * GET /dashboards/type/:type - * Dashboards por tipo - */ - router.get('/type/:type', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dashboards = await dashboardService.findByType(getContext(req), req.params.type as any); - res.status(200).json({ success: true, data: dashboards }); - } catch (error) { - next(error); - } - }); - - /** - * GET /dashboards/type/:type/default - * Dashboard por defecto para un tipo - */ - router.get('/type/:type/default', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dashboard = await dashboardService.findDefault(getContext(req), req.params.type as any); - if (!dashboard) { - res.status(404).json({ error: 'Not Found', message: 'No default dashboard found' }); - return; - } - - // Registrar vista - await dashboardService.recordView(getContext(req), dashboard.id); - - res.status(200).json({ success: true, data: dashboard }); - } catch (error) { - next(error); - } - }); - - /** - * GET /dashboards/code/:code - * Dashboard por c贸digo - */ - router.get('/code/:code', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dashboard = await dashboardService.findByCode(getContext(req), req.params.code); - if (!dashboard) { - res.status(404).json({ error: 'Not Found', message: 'Dashboard not found' }); - return; - } - - // Registrar vista - await dashboardService.recordView(getContext(req), dashboard.id); - - res.status(200).json({ success: true, data: dashboard }); - } catch (error) { - next(error); - } - }); - - /** - * GET /dashboards/:id - * Obtener dashboard con widgets - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dashboard = await dashboardService.findByIdWithWidgets(getContext(req), req.params.id); - if (!dashboard) { - res.status(404).json({ error: 'Not Found', message: 'Dashboard not found' }); - return; - } - - // Registrar vista - await dashboardService.recordView(getContext(req), dashboard.id); - - res.status(200).json({ success: true, data: dashboard }); - } catch (error) { - next(error); - } - }); - - /** - * POST /dashboards - * Crear dashboard - */ - router.post('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateDashboardDto = req.body; - if (!dto.code || !dto.name) { - res.status(400).json({ - error: 'Bad Request', - message: 'code and name are required', - }); - return; - } - - const dashboard = await dashboardService.createDashboard(getContext(req), dto); - res.status(201).json({ success: true, data: dashboard }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PUT /dashboards/:id - * Actualizar dashboard - */ - router.put('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateDashboardDto = req.body; - const dashboard = await dashboardService.update(getContext(req), req.params.id, dto); - - if (!dashboard) { - res.status(404).json({ error: 'Not Found', message: 'Dashboard not found' }); - return; - } - - res.status(200).json({ success: true, data: dashboard }); - } catch (error) { - next(error); - } - }); - - /** - * POST /dashboards/:id/set-default - * Establecer como dashboard por defecto - */ - router.post('/:id/set-default', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dashboard = await dashboardService.setDefault(getContext(req), req.params.id); - if (!dashboard) { - res.status(404).json({ error: 'Not Found', message: 'Dashboard not found' }); - return; - } - - res.status(200).json({ success: true, data: dashboard }); - } catch (error) { - next(error); - } - }); - - /** - * POST /dashboards/:id/duplicate - * Duplicar dashboard - */ - router.post('/:id/duplicate', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { code, name } = req.body; - if (!code || !name) { - res.status(400).json({ - error: 'Bad Request', - message: 'code and name are required for duplication', - }); - return; - } - - const dashboard = await dashboardService.duplicate(getContext(req), req.params.id, code, name); - res.status(201).json({ success: true, data: dashboard }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Dashboard not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * DELETE /dashboards/:id - * Eliminar dashboard - */ - router.delete('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - // Verificar que no sea un dashboard de sistema - const dashboard = await dashboardService.findById(getContext(req), req.params.id); - if (!dashboard) { - res.status(404).json({ error: 'Not Found', message: 'Dashboard not found' }); - return; - } - - if (dashboard.isSystem) { - res.status(403).json({ error: 'Forbidden', message: 'Cannot delete system dashboards' }); - return; - } - - const deleted = await dashboardService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Dashboard not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Dashboard deleted' }); - } catch (error) { - next(error); - } - }); - - // ==================== Widget Endpoints ==================== - - /** - * GET /dashboards/:id/widgets - * Obtener widgets de un dashboard - */ - router.get('/:id/widgets', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const widgets = await dashboardService.getWidgets(getContext(req), req.params.id); - res.status(200).json({ success: true, data: widgets }); - } catch (error) { - next(error); - } - }); - - /** - * POST /dashboards/:id/widgets - * Crear widget en dashboard - */ - router.post('/:id/widgets', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateWidgetDto = { - ...req.body, - dashboardId: req.params.id, - }; - - if (!dto.title || !dto.widgetType) { - res.status(400).json({ - error: 'Bad Request', - message: 'title and widgetType are required', - }); - return; - } - - const widget = await dashboardService.createWidget(getContext(req), dto); - res.status(201).json({ success: true, data: widget }); - } catch (error) { - if (error instanceof Error && error.message === 'Dashboard not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PUT /dashboards/:dashboardId/widgets/:widgetId - * Actualizar widget - */ - router.put('/:dashboardId/widgets/:widgetId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateWidgetDto = req.body; - const widget = await dashboardService.updateWidget(getContext(req), req.params.widgetId, dto); - - if (!widget) { - res.status(404).json({ error: 'Not Found', message: 'Widget not found' }); - return; - } - - res.status(200).json({ success: true, data: widget }); - } catch (error) { - next(error); - } - }); - - /** - * PUT /dashboards/:id/widgets/positions - * Actualizar posiciones de widgets - */ - router.put('/:id/widgets/positions', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { positions } = req.body; - if (!Array.isArray(positions)) { - res.status(400).json({ - error: 'Bad Request', - message: 'positions array is required', - }); - return; - } - - await dashboardService.updateWidgetPositions(getContext(req), req.params.id, positions); - res.status(200).json({ success: true, message: 'Widget positions updated' }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /dashboards/:dashboardId/widgets/:widgetId - * Eliminar widget - */ - router.delete('/:dashboardId/widgets/:widgetId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const deleted = await dashboardService.deleteWidget(getContext(req), req.params.widgetId); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Widget not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Widget deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createDashboardController; diff --git a/projects/erp-construccion/backend/src/modules/reports/controllers/index.ts b/projects/erp-construccion/backend/src/modules/reports/controllers/index.ts deleted file mode 100644 index e2047cd82..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/controllers/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Reports Controllers Index - * @module Reports - */ - -export { createReportController } from './report.controller'; -export { createDashboardController } from './dashboard.controller'; -export { createKpiController } from './kpi.controller'; diff --git a/projects/erp-construccion/backend/src/modules/reports/controllers/kpi.controller.ts b/projects/erp-construccion/backend/src/modules/reports/controllers/kpi.controller.ts deleted file mode 100644 index 0fb4d4fee..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/controllers/kpi.controller.ts +++ /dev/null @@ -1,349 +0,0 @@ -/** - * KpiController - Controller de KPIs - * - * Endpoints REST para gesti贸n de KPIs y analytics. - * - * @module Reports - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { KpiService, CreateKpiSnapshotDto, KpiFilters } from '../services/kpi.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { KpiSnapshot } from '../entities/kpi-snapshot.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createKpiController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const kpiRepository = dataSource.getRepository(KpiSnapshot); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const kpiService = new KpiService(kpiRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /kpis - * Listar snapshots de KPIs con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: KpiFilters = {}; - if (req.query.kpiCode) filters.kpiCode = req.query.kpiCode as string; - if (req.query.category) filters.category = req.query.category as any; - if (req.query.periodType) filters.periodType = req.query.periodType as any; - if (req.query.fraccionamientoId) filters.fraccionamientoId = req.query.fraccionamientoId as string; - if (req.query.dateFrom) filters.dateFrom = new Date(req.query.dateFrom as string); - if (req.query.dateTo) filters.dateTo = new Date(req.query.dateTo as string); - - const result = await kpiService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /kpis/stats - * Estad铆sticas de KPIs - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await kpiService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /kpis/dashboard - * KPIs para dashboard agrupados por categor铆a - */ - router.get('/dashboard', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string | undefined; - const dashboard = await kpiService.getDashboardKpis(getContext(req), fraccionamientoId); - - res.status(200).json({ success: true, data: dashboard }); - } catch (error) { - next(error); - } - }); - - /** - * GET /kpis/category/:category - * Resumen de KPIs por categor铆a - */ - router.get('/category/:category', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string | undefined; - const summary = await kpiService.getSummaryByCategory( - getContext(req), - req.params.category as any, - fraccionamientoId - ); - - res.status(200).json({ success: true, data: summary }); - } catch (error) { - next(error); - } - }); - - /** - * GET /kpis/code/:code/latest - * 脷ltimo snapshot de un KPI - */ - router.get('/code/:code/latest', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoId = req.query.fraccionamientoId as string | undefined; - const snapshot = await kpiService.getLatestSnapshot( - getContext(req), - req.params.code, - fraccionamientoId - ); - - if (!snapshot) { - res.status(404).json({ error: 'Not Found', message: 'KPI snapshot not found' }); - return; - } - - res.status(200).json({ success: true, data: snapshot }); - } catch (error) { - next(error); - } - }); - - /** - * GET /kpis/code/:code/trend - * Tendencia de un KPI - */ - router.get('/code/:code/trend', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const days = parseInt(req.query.days as string) || 30; - const fraccionamientoId = req.query.fraccionamientoId as string | undefined; - - const trend = await kpiService.getKpiTrend( - getContext(req), - req.params.code, - days, - fraccionamientoId - ); - - if (!trend) { - res.status(404).json({ error: 'Not Found', message: 'KPI data not found' }); - return; - } - - res.status(200).json({ success: true, data: trend }); - } catch (error) { - next(error); - } - }); - - /** - * GET /kpis/code/:code/history - * Historial de un KPI - */ - router.get('/code/:code/history', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dateFrom = req.query.dateFrom ? new Date(req.query.dateFrom as string) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - const dateTo = req.query.dateTo ? new Date(req.query.dateTo as string) : new Date(); - const fraccionamientoId = req.query.fraccionamientoId as string | undefined; - - const history = await kpiService.getKpiHistory( - getContext(req), - req.params.code, - dateFrom, - dateTo, - fraccionamientoId - ); - - res.status(200).json({ success: true, data: history }); - } catch (error) { - next(error); - } - }); - - /** - * GET /kpis/code/:code/compare - * Comparar KPI entre proyectos - */ - router.get('/code/:code/compare', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const fraccionamientoIds = (req.query.fraccionamientoIds as string)?.split(',') || []; - if (fraccionamientoIds.length < 2) { - res.status(400).json({ - error: 'Bad Request', - message: 'At least 2 fraccionamientoIds are required for comparison', - }); - return; - } - - const snapshotDate = req.query.date ? new Date(req.query.date as string) : undefined; - const comparison = await kpiService.compareProjects( - getContext(req), - req.params.code, - fraccionamientoIds, - snapshotDate - ); - - res.status(200).json({ success: true, data: comparison }); - } catch (error) { - next(error); - } - }); - - /** - * POST /kpis - * Crear snapshot de KPI - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'analyst', 'system'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateKpiSnapshotDto = req.body; - if (!dto.kpiCode || !dto.kpiName || !dto.category || !dto.snapshotDate || dto.value === undefined) { - res.status(400).json({ - error: 'Bad Request', - message: 'kpiCode, kpiName, category, snapshotDate, and value are required', - }); - return; - } - - const snapshot = await kpiService.createSnapshot(getContext(req), dto); - res.status(201).json({ success: true, data: snapshot }); - } catch (error) { - next(error); - } - }); - - /** - * POST /kpis/bulk - * Crear m煤ltiples snapshots (para jobs) - */ - router.post('/bulk', authMiddleware.authenticate, authMiddleware.authorize('admin', 'system'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { snapshots } = req.body; - if (!Array.isArray(snapshots) || snapshots.length === 0) { - res.status(400).json({ - error: 'Bad Request', - message: 'snapshots array is required and must not be empty', - }); - return; - } - - const result = await kpiService.bulkCreateSnapshots(getContext(req), snapshots); - res.status(201).json({ - success: true, - data: result, - message: `Created ${result.created} snapshots, ${result.errors} errors`, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /kpis/:id - * Obtener snapshot por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const snapshot = await kpiService.findById(getContext(req), req.params.id); - if (!snapshot) { - res.status(404).json({ error: 'Not Found', message: 'KPI snapshot not found' }); - return; - } - - res.status(200).json({ success: true, data: snapshot }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createKpiController; diff --git a/projects/erp-construccion/backend/src/modules/reports/controllers/report.controller.ts b/projects/erp-construccion/backend/src/modules/reports/controllers/report.controller.ts deleted file mode 100644 index 79fdd10f6..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/controllers/report.controller.ts +++ /dev/null @@ -1,373 +0,0 @@ -/** - * ReportController - Controller de Reportes - * - * Endpoints REST para gesti贸n de reportes y ejecuciones. - * - * @module Reports - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { ReportService, CreateReportDto, UpdateReportDto, ReportFilters, ExecuteReportDto } from '../services/report.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { Report } from '../entities/report.entity'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; -import { ServiceContext } from '../../../shared/services/base.service'; - -export function createReportController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const reportRepository = dataSource.getRepository(Report); - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const reportService = new ReportService(reportRepository, dataSource); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - // Helper para crear contexto - const getContext = (req: Request): ServiceContext => { - if (!req.tenantId) { - throw new Error('Tenant ID is required'); - } - return { - tenantId: req.tenantId, - userId: req.user?.sub, - }; - }; - - /** - * GET /reports - * Listar reportes con filtros - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const filters: ReportFilters = {}; - if (req.query.reportType) filters.reportType = req.query.reportType as any; - if (req.query.isActive !== undefined) filters.isActive = req.query.isActive === 'true'; - if (req.query.isScheduled !== undefined) filters.isScheduled = req.query.isScheduled === 'true'; - if (req.query.search) filters.search = req.query.search as string; - - const result = await reportService.findWithFilters(getContext(req), filters, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /reports/stats - * Estad铆sticas de reportes - */ - router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const stats = await reportService.getStats(getContext(req)); - res.status(200).json({ success: true, data: stats }); - } catch (error) { - next(error); - } - }); - - /** - * GET /reports/scheduled - * Obtener reportes programados - */ - router.get('/scheduled', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const reports = await reportService.findScheduled(getContext(req)); - res.status(200).json({ success: true, data: reports }); - } catch (error) { - next(error); - } - }); - - /** - * GET /reports/type/:type - * Obtener reportes por tipo - */ - router.get('/type/:type', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const reports = await reportService.findByType(getContext(req), req.params.type as any); - res.status(200).json({ success: true, data: reports }); - } catch (error) { - next(error); - } - }); - - /** - * GET /reports/code/:code - * Obtener reporte por c贸digo - */ - router.get('/code/:code', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const report = await reportService.findByCode(getContext(req), req.params.code); - if (!report) { - res.status(404).json({ error: 'Not Found', message: 'Report not found' }); - return; - } - - res.status(200).json({ success: true, data: report }); - } catch (error) { - next(error); - } - }); - - /** - * GET /reports/:id - * Obtener reporte por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const report = await reportService.findById(getContext(req), req.params.id); - if (!report) { - res.status(404).json({ error: 'Not Found', message: 'Report not found' }); - return; - } - - res.status(200).json({ success: true, data: report }); - } catch (error) { - next(error); - } - }); - - /** - * POST /reports - * Crear reporte - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'analyst'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateReportDto = req.body; - if (!dto.code || !dto.name || !dto.reportType) { - res.status(400).json({ - error: 'Bad Request', - message: 'code, name, and reportType are required', - }); - return; - } - - const report = await reportService.createReport(getContext(req), dto); - res.status(201).json({ success: true, data: report }); - } catch (error) { - if (error instanceof Error && error.message.includes('already exists')) { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PUT /reports/:id - * Actualizar reporte - */ - router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director', 'analyst'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateReportDto = req.body; - const report = await reportService.update(getContext(req), req.params.id, dto); - - if (!report) { - res.status(404).json({ error: 'Not Found', message: 'Report not found' }); - return; - } - - res.status(200).json({ success: true, data: report }); - } catch (error) { - next(error); - } - }); - - /** - * POST /reports/:id/execute - * Ejecutar reporte - */ - router.post('/:id/execute', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const options: ExecuteReportDto = req.body; - const execution = await reportService.executeReport(getContext(req), req.params.id, options); - - res.status(200).json({ success: true, data: execution }); - } catch (error) { - if (error instanceof Error) { - if (error.message === 'Report not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - if (error.message === 'Report is not active') { - res.status(400).json({ error: 'Bad Request', message: error.message }); - return; - } - } - next(error); - } - }); - - /** - * GET /reports/:id/executions - * Historial de ejecuciones - */ - router.get('/:id/executions', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - - const result = await reportService.getExecutionHistory(getContext(req), req.params.id, page, limit); - - res.status(200).json({ - success: true, - data: result.data, - pagination: result.meta, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /reports/:id/executions/latest - * 脷ltima ejecuci贸n - */ - router.get('/:id/executions/latest', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const execution = await reportService.getLastExecution(getContext(req), req.params.id); - if (!execution) { - res.status(404).json({ error: 'Not Found', message: 'No executions found' }); - return; - } - - res.status(200).json({ success: true, data: execution }); - } catch (error) { - next(error); - } - }); - - /** - * GET /reports/executions/:executionId - * Obtener ejecuci贸n espec铆fica - */ - router.get('/executions/:executionId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const execution = await reportService.getExecution(getContext(req), req.params.executionId); - if (!execution) { - res.status(404).json({ error: 'Not Found', message: 'Execution not found' }); - return; - } - - res.status(200).json({ success: true, data: execution }); - } catch (error) { - next(error); - } - }); - - /** - * DELETE /reports/:id - * Eliminar reporte (soft delete) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'director'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - if (!req.tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - // Verificar que no sea un reporte de sistema - const report = await reportService.findById(getContext(req), req.params.id); - if (!report) { - res.status(404).json({ error: 'Not Found', message: 'Report not found' }); - return; - } - - if (report.isSystem) { - res.status(403).json({ error: 'Forbidden', message: 'Cannot delete system reports' }); - return; - } - - const deleted = await reportService.softDelete(getContext(req), req.params.id); - if (!deleted) { - res.status(404).json({ error: 'Not Found', message: 'Report not found' }); - return; - } - - res.status(200).json({ success: true, message: 'Report deleted' }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createReportController; diff --git a/projects/erp-construccion/backend/src/modules/reports/entities/dashboard-widget.entity.ts b/projects/erp-construccion/backend/src/modules/reports/entities/dashboard-widget.entity.ts deleted file mode 100644 index 0fa425730..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/entities/dashboard-widget.entity.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * DashboardWidget Entity - * Configuraci贸n de widgets de dashboard - * - * @module Reports - * @table reports.dashboard_widgets - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Dashboard } from './dashboard.entity'; - -export type WidgetType = - | 'kpi_card' - | 'line_chart' - | 'bar_chart' - | 'pie_chart' - | 'donut_chart' - | 'area_chart' - | 'gauge' - | 'table' - | 'heatmap' - | 'map' - | 'timeline' - | 'progress' - | 'list' - | 'text' - | 'image' - | 'custom'; - -export type DataSourceType = 'query' | 'api' | 'static' | 'kpi' | 'report'; - -@Entity({ schema: 'reports', name: 'dashboard_widgets' }) -@Index(['tenantId']) -@Index(['dashboardId']) -@Index(['widgetType']) -@Index(['isActive']) -export class DashboardWidget { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'dashboard_id', type: 'uuid' }) - dashboardId: string; - - @Column({ type: 'varchar', length: 200 }) - title: string; - - @Column({ type: 'text', nullable: true }) - subtitle: string | null; - - @Column({ - name: 'widget_type', - type: 'varchar', - length: 30, - }) - widgetType: WidgetType; - - @Column({ - name: 'data_source_type', - type: 'varchar', - length: 20, - default: 'query', - }) - dataSourceType: DataSourceType; - - @Column({ - name: 'data_source', - type: 'jsonb', - nullable: true, - comment: 'Query, API endpoint, or KPI code', - }) - dataSource: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Widget-specific configuration', - }) - config: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Chart options (colors, legend, etc)', - }) - chartOptions: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Threshold/alert configuration', - }) - thresholds: Record | null; - - @Column({ - name: 'grid_x', - type: 'integer', - default: 0, - comment: 'Grid position X', - }) - gridX: number; - - @Column({ - name: 'grid_y', - type: 'integer', - default: 0, - comment: 'Grid position Y', - }) - gridY: number; - - @Column({ - name: 'grid_width', - type: 'integer', - default: 4, - comment: 'Width in grid units', - }) - gridWidth: number; - - @Column({ - name: 'grid_height', - type: 'integer', - default: 2, - comment: 'Height in grid units', - }) - gridHeight: number; - - @Column({ - name: 'min_width', - type: 'integer', - default: 2, - }) - minWidth: number; - - @Column({ - name: 'min_height', - type: 'integer', - default: 1, - }) - minHeight: number; - - @Column({ - name: 'refresh_interval', - type: 'integer', - nullable: true, - comment: 'Override dashboard refresh (seconds)', - }) - refreshInterval: number | null; - - @Column({ - name: 'cache_duration', - type: 'integer', - default: 60, - comment: 'Cache duration in seconds', - }) - cacheDuration: number; - - @Column({ - name: 'drill_down_config', - type: 'jsonb', - nullable: true, - comment: 'Drill-down navigation config', - }) - drillDownConfig: Record | null; - - @Column({ - name: 'click_action', - type: 'jsonb', - nullable: true, - comment: 'Action on click (navigate, filter, etc)', - }) - clickAction: Record | null; - - @Column({ - name: 'is_active', - type: 'boolean', - default: true, - }) - isActive: boolean; - - @Column({ - name: 'sort_order', - type: 'integer', - default: 0, - }) - sortOrder: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Dashboard) - @JoinColumn({ name: 'dashboard_id' }) - dashboard: Dashboard; -} diff --git a/projects/erp-construccion/backend/src/modules/reports/entities/dashboard.entity.ts b/projects/erp-construccion/backend/src/modules/reports/entities/dashboard.entity.ts deleted file mode 100644 index ca109beb6..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/entities/dashboard.entity.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Dashboard Entity - * Configuraci贸n de dashboards personalizables - * - * @module Reports - * @table reports.dashboards - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { DashboardWidget } from './dashboard-widget.entity'; - -export type DashboardType = 'corporate' | 'project' | 'department' | 'personal' | 'custom'; - -export type DashboardVisibility = 'private' | 'team' | 'department' | 'company'; - -@Entity({ schema: 'reports', name: 'dashboards' }) -@Index(['tenantId', 'code'], { unique: true }) -@Index(['tenantId']) -@Index(['dashboardType']) -@Index(['ownerId']) -@Index(['isActive']) -export class Dashboard { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 50 }) - code: string; - - @Column({ type: 'varchar', length: 200 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string | null; - - @Column({ - name: 'dashboard_type', - type: 'varchar', - length: 30, - default: 'custom', - }) - dashboardType: DashboardType; - - @Column({ - type: 'varchar', - length: 20, - default: 'private', - }) - visibility: DashboardVisibility; - - @Column({ - name: 'owner_id', - type: 'uuid', - nullable: true, - comment: 'User who owns this dashboard', - }) - ownerId: string | null; - - @Column({ - name: 'fraccionamiento_id', - type: 'uuid', - nullable: true, - comment: 'Project-specific dashboard', - }) - fraccionamientoId: string | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Layout configuration (grid positions)', - }) - layout: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Theme and styling configuration', - }) - theme: Record | null; - - @Column({ - name: 'refresh_interval', - type: 'integer', - default: 300, - comment: 'Auto-refresh interval in seconds', - }) - refreshInterval: number; - - @Column({ - name: 'default_date_range', - type: 'varchar', - length: 30, - default: 'last_30_days', - comment: 'Default date range filter', - }) - defaultDateRange: string; - - @Column({ - name: 'default_filters', - type: 'jsonb', - nullable: true, - }) - defaultFilters: Record | null; - - @Column({ - name: 'allowed_roles', - type: 'varchar', - array: true, - nullable: true, - comment: 'Roles that can view this dashboard', - }) - allowedRoles: string[] | null; - - @Column({ - name: 'is_default', - type: 'boolean', - default: false, - comment: 'Default dashboard for type', - }) - isDefault: boolean; - - @Column({ - name: 'is_active', - type: 'boolean', - default: true, - }) - isActive: boolean; - - @Column({ - name: 'is_system', - type: 'boolean', - default: false, - comment: 'System dashboard cannot be deleted', - }) - isSystem: boolean; - - @Column({ - name: 'sort_order', - type: 'integer', - default: 0, - }) - sortOrder: number; - - @Column({ - name: 'view_count', - type: 'integer', - default: 0, - }) - viewCount: number; - - @Column({ - name: 'last_viewed_at', - type: 'timestamptz', - nullable: true, - }) - lastViewedAt: Date | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'owner_id' }) - owner: User | null; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @OneToMany(() => DashboardWidget, (w) => w.dashboard) - widgets: DashboardWidget[]; -} diff --git a/projects/erp-construccion/backend/src/modules/reports/entities/index.ts b/projects/erp-construccion/backend/src/modules/reports/entities/index.ts deleted file mode 100644 index baef64233..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/entities/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Reports Module - Entity Exports - * MAI-006: Reportes y Analytics - */ - -export * from './report.entity'; -export * from './report-execution.entity'; -export * from './dashboard.entity'; -export * from './dashboard-widget.entity'; -export * from './kpi-snapshot.entity'; diff --git a/projects/erp-construccion/backend/src/modules/reports/entities/kpi-snapshot.entity.ts b/projects/erp-construccion/backend/src/modules/reports/entities/kpi-snapshot.entity.ts deleted file mode 100644 index 82c7e5906..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/entities/kpi-snapshot.entity.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * KpiSnapshot Entity - * Snapshots hist贸ricos de KPIs para an谩lisis - * - * @module Reports - * @table reports.kpi_snapshots - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; - -export type KpiCategory = - | 'financial' - | 'progress' - | 'quality' - | 'hse' - | 'hr' - | 'inventory' - | 'sales' - | 'operational'; - -export type KpiPeriodType = 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'; - -export type TrendDirection = 'up' | 'down' | 'stable'; - -@Entity({ schema: 'reports', name: 'kpi_snapshots' }) -@Index(['tenantId', 'kpiCode', 'snapshotDate']) -@Index(['tenantId']) -@Index(['kpiCode']) -@Index(['category']) -@Index(['snapshotDate']) -@Index(['fraccionamientoId']) -export class KpiSnapshot { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ - name: 'kpi_code', - type: 'varchar', - length: 50, - comment: 'Unique KPI identifier', - }) - kpiCode: string; - - @Column({ - name: 'kpi_name', - type: 'varchar', - length: 200, - }) - kpiName: string; - - @Column({ - type: 'varchar', - length: 30, - }) - category: KpiCategory; - - @Column({ - name: 'snapshot_date', - type: 'date', - }) - snapshotDate: Date; - - @Column({ - name: 'period_type', - type: 'varchar', - length: 20, - default: 'daily', - }) - periodType: KpiPeriodType; - - @Column({ - name: 'period_start', - type: 'date', - nullable: true, - }) - periodStart: Date | null; - - @Column({ - name: 'period_end', - type: 'date', - nullable: true, - }) - periodEnd: Date | null; - - @Column({ - name: 'fraccionamiento_id', - type: 'uuid', - nullable: true, - comment: 'Project-specific KPI, null for global', - }) - fraccionamientoId: string | null; - - @Column({ - type: 'decimal', - precision: 18, - scale: 4, - }) - value: number; - - @Column({ - name: 'previous_value', - type: 'decimal', - precision: 18, - scale: 4, - nullable: true, - }) - previousValue: number | null; - - @Column({ - name: 'target_value', - type: 'decimal', - precision: 18, - scale: 4, - nullable: true, - }) - targetValue: number | null; - - @Column({ - name: 'min_value', - type: 'decimal', - precision: 18, - scale: 4, - nullable: true, - comment: 'Minimum acceptable value', - }) - minValue: number | null; - - @Column({ - name: 'max_value', - type: 'decimal', - precision: 18, - scale: 4, - nullable: true, - comment: 'Maximum acceptable value', - }) - maxValue: number | null; - - @Column({ - type: 'varchar', - length: 20, - nullable: true, - comment: 'Unit of measurement', - }) - unit: string | null; - - @Column({ - name: 'change_percentage', - type: 'decimal', - precision: 8, - scale: 2, - nullable: true, - comment: 'Percentage change from previous', - }) - changePercentage: number | null; - - @Column({ - name: 'trend_direction', - type: 'varchar', - length: 10, - nullable: true, - }) - trendDirection: TrendDirection | null; - - @Column({ - name: 'is_on_target', - type: 'boolean', - nullable: true, - }) - isOnTarget: boolean | null; - - @Column({ - name: 'status_color', - type: 'varchar', - length: 20, - nullable: true, - comment: 'green, yellow, red based on thresholds', - }) - statusColor: string | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Additional breakdown data', - }) - breakdown: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Source data references', - }) - metadata: Record | null; - - @Column({ - name: 'calculated_at', - type: 'timestamptz', - default: () => 'CURRENT_TIMESTAMP', - }) - calculatedAt: Date; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} diff --git a/projects/erp-construccion/backend/src/modules/reports/entities/report-execution.entity.ts b/projects/erp-construccion/backend/src/modules/reports/entities/report-execution.entity.ts deleted file mode 100644 index 57fa312c9..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/entities/report-execution.entity.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * ReportExecution Entity - * Historial de ejecuciones de reportes - * - * @module Reports - * @table reports.report_executions - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { Report, ReportFormat } from './report.entity'; - -export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; - -@Entity({ schema: 'reports', name: 'report_executions' }) -@Index(['tenantId']) -@Index(['reportId']) -@Index(['status']) -@Index(['executedAt']) -@Index(['executedById']) -export class ReportExecution { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ name: 'report_id', type: 'uuid' }) - reportId: string; - - @Column({ - type: 'varchar', - length: 20, - default: 'pending', - }) - status: ExecutionStatus; - - @Column({ - type: 'varchar', - length: 20, - }) - format: ReportFormat; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Parameters used for this execution', - }) - parameters: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Filters applied', - }) - filters: Record | null; - - @Column({ - name: 'started_at', - type: 'timestamptz', - nullable: true, - }) - startedAt: Date | null; - - @Column({ - name: 'completed_at', - type: 'timestamptz', - nullable: true, - }) - completedAt: Date | null; - - @Column({ - name: 'duration_ms', - type: 'integer', - nullable: true, - comment: 'Execution duration in milliseconds', - }) - durationMs: number | null; - - @Column({ - name: 'row_count', - type: 'integer', - nullable: true, - }) - rowCount: number | null; - - @Column({ - name: 'file_path', - type: 'varchar', - length: 500, - nullable: true, - }) - filePath: string | null; - - @Column({ - name: 'file_size', - type: 'integer', - nullable: true, - comment: 'File size in bytes', - }) - fileSize: number | null; - - @Column({ - name: 'file_url', - type: 'varchar', - length: 1000, - nullable: true, - comment: 'Presigned URL or download URL', - }) - fileUrl: string | null; - - @Column({ - name: 'url_expires_at', - type: 'timestamptz', - nullable: true, - }) - urlExpiresAt: Date | null; - - @Column({ - name: 'error_message', - type: 'text', - nullable: true, - }) - errorMessage: string | null; - - @Column({ - name: 'error_stack', - type: 'text', - nullable: true, - }) - errorStack: string | null; - - @Column({ - name: 'is_scheduled', - type: 'boolean', - default: false, - comment: 'Was this a scheduled execution?', - }) - isScheduled: boolean; - - @Column({ - name: 'distributed_to', - type: 'varchar', - array: true, - nullable: true, - comment: 'Email addresses report was sent to', - }) - distributedTo: string[] | null; - - @Column({ - name: 'distributed_at', - type: 'timestamptz', - nullable: true, - }) - distributedAt: Date | null; - - @Column({ - name: 'executed_at', - type: 'timestamptz', - default: () => 'CURRENT_TIMESTAMP', - }) - executedAt: Date; - - @Column({ name: 'executed_by', type: 'uuid', nullable: true }) - executedById: string | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => Report) - @JoinColumn({ name: 'report_id' }) - report: Report; - - @ManyToOne(() => User) - @JoinColumn({ name: 'executed_by' }) - executedBy: User | null; -} diff --git a/projects/erp-construccion/backend/src/modules/reports/entities/report.entity.ts b/projects/erp-construccion/backend/src/modules/reports/entities/report.entity.ts deleted file mode 100644 index 7117fef65..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/entities/report.entity.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Report Entity - * Definici贸n de reportes configurables - * - * @module Reports - * @table reports.report_definitions - */ - -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, - Index, -} from 'typeorm'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { User } from '../../core/entities/user.entity'; -import { ReportExecution } from './report-execution.entity'; - -export type ReportType = - | 'financial' - | 'progress' - | 'quality' - | 'hse' - | 'hr' - | 'inventory' - | 'contracts' - | 'executive' - | 'custom'; - -export type ReportFormat = 'pdf' | 'excel' | 'csv' | 'html' | 'json'; - -export type ReportFrequency = 'once' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'; - -@Entity({ schema: 'reports', name: 'report_definitions' }) -@Index(['tenantId', 'code'], { unique: true }) -@Index(['tenantId']) -@Index(['reportType']) -@Index(['isActive']) -export class Report { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ name: 'tenant_id', type: 'uuid' }) - tenantId: string; - - @Column({ type: 'varchar', length: 50 }) - code: string; - - @Column({ type: 'varchar', length: 200 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string | null; - - @Column({ - name: 'report_type', - type: 'varchar', - length: 30, - }) - reportType: ReportType; - - @Column({ - name: 'default_format', - type: 'varchar', - length: 20, - default: 'pdf', - }) - defaultFormat: ReportFormat; - - @Column({ - name: 'available_formats', - type: 'varchar', - array: true, - default: ['pdf', 'excel'], - }) - availableFormats: ReportFormat[]; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'SQL query or data source configuration', - }) - query: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Report parameters definition', - }) - parameters: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Column/field definitions', - }) - columns: Record[] | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Grouping and aggregation config', - }) - grouping: Record | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Sorting configuration', - }) - sorting: Record[] | null; - - @Column({ - type: 'jsonb', - nullable: true, - comment: 'Default filters', - }) - filters: Record | null; - - @Column({ - name: 'template_path', - type: 'varchar', - length: 500, - nullable: true, - }) - templatePath: string | null; - - @Column({ - name: 'is_scheduled', - type: 'boolean', - default: false, - }) - isScheduled: boolean; - - @Column({ - type: 'varchar', - length: 20, - nullable: true, - }) - frequency: ReportFrequency | null; - - @Column({ - name: 'schedule_config', - type: 'jsonb', - nullable: true, - comment: 'Cron or schedule configuration', - }) - scheduleConfig: Record | null; - - @Column({ - name: 'distribution_list', - type: 'varchar', - array: true, - nullable: true, - comment: 'Email addresses for distribution', - }) - distributionList: string[] | null; - - @Column({ - name: 'is_active', - type: 'boolean', - default: true, - }) - isActive: boolean; - - @Column({ - name: 'is_system', - type: 'boolean', - default: false, - comment: 'System report cannot be deleted', - }) - isSystem: boolean; - - @Column({ - name: 'execution_count', - type: 'integer', - default: 0, - }) - executionCount: number; - - @Column({ - name: 'last_executed_at', - type: 'timestamptz', - nullable: true, - }) - lastExecutedAt: Date | null; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - createdAt: Date; - - @Column({ name: 'created_by', type: 'uuid', nullable: true }) - createdById: string | null; - - @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) - updatedAt: Date | null; - - @Column({ name: 'updated_by', type: 'uuid', nullable: true }) - updatedById: string | null; - - @Column({ name: 'deleted_at', type: 'timestamptz', nullable: true }) - deletedAt: Date | null; - - @Column({ name: 'deleted_by', type: 'uuid', nullable: true }) - deletedById: string | null; - - // Relations - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @ManyToOne(() => User) - @JoinColumn({ name: 'created_by' }) - createdBy: User | null; - - @OneToMany(() => ReportExecution, (e) => e.report) - executions: ReportExecution[]; -} diff --git a/projects/erp-construccion/backend/src/modules/reports/services/dashboard.service.ts b/projects/erp-construccion/backend/src/modules/reports/services/dashboard.service.ts deleted file mode 100644 index 6103fab1d..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/services/dashboard.service.ts +++ /dev/null @@ -1,471 +0,0 @@ -/** - * DashboardService - Gesti贸n de Dashboards - * - * Administra dashboards personalizables y sus widgets. - * - * @module Reports - */ - -import { Repository, DataSource } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Dashboard, DashboardType, DashboardVisibility } from '../entities/dashboard.entity'; -import { DashboardWidget, WidgetType, DataSourceType } from '../entities/dashboard-widget.entity'; - -export interface CreateDashboardDto { - code: string; - name: string; - description?: string; - dashboardType?: DashboardType; - visibility?: DashboardVisibility; - fraccionamientoId?: string; - layout?: Record; - theme?: Record; - refreshInterval?: number; - defaultDateRange?: string; - defaultFilters?: Record; - allowedRoles?: string[]; -} - -export interface UpdateDashboardDto extends Partial { - isDefault?: boolean; - isActive?: boolean; - sortOrder?: number; -} - -export interface CreateWidgetDto { - dashboardId: string; - title: string; - subtitle?: string; - widgetType: WidgetType; - dataSourceType?: DataSourceType; - dataSource?: Record; - config?: Record; - chartOptions?: Record; - thresholds?: Record; - gridX?: number; - gridY?: number; - gridWidth?: number; - gridHeight?: number; - refreshInterval?: number; - cacheDuration?: number; - drillDownConfig?: Record; - clickAction?: Record; -} - -export interface UpdateWidgetDto extends Partial> { - isActive?: boolean; - sortOrder?: number; -} - -export interface DashboardFilters { - dashboardType?: DashboardType; - visibility?: DashboardVisibility; - ownerId?: string; - fraccionamientoId?: string; - isActive?: boolean; - search?: string; -} - -export class DashboardService extends BaseService { - private widgetRepository: Repository; - - constructor( - repository: Repository, - dataSource: DataSource - ) { - super(repository); - this.widgetRepository = dataSource.getRepository(DashboardWidget); - } - - /** - * Buscar dashboards con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: DashboardFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('d') - .leftJoinAndSelect('d.widgets', 'w', 'w.deleted_at IS NULL AND w.is_active = true') - .where('d.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('d.deleted_at IS NULL'); - - if (filters.dashboardType) { - qb.andWhere('d.dashboard_type = :dashboardType', { dashboardType: filters.dashboardType }); - } - if (filters.visibility) { - qb.andWhere('d.visibility = :visibility', { visibility: filters.visibility }); - } - if (filters.ownerId) { - qb.andWhere('d.owner_id = :ownerId', { ownerId: filters.ownerId }); - } - if (filters.fraccionamientoId) { - qb.andWhere('d.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId: filters.fraccionamientoId }); - } - if (filters.isActive !== undefined) { - qb.andWhere('d.is_active = :isActive', { isActive: filters.isActive }); - } - if (filters.search) { - qb.andWhere('(d.name ILIKE :search OR d.code ILIKE :search OR d.description ILIKE :search)', { - search: `%${filters.search}%`, - }); - } - - const skip = (page - 1) * limit; - qb.orderBy('d.sort_order', 'ASC') - .addOrderBy('d.name', 'ASC') - .skip(skip) - .take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Buscar dashboard por c贸digo - */ - async findByCode(ctx: ServiceContext, code: string): Promise { - return this.repository.findOne({ - where: { - tenantId: ctx.tenantId, - code, - deletedAt: null, - } as any, - relations: ['widgets'], - }); - } - - /** - * Obtener dashboard con widgets - */ - async findByIdWithWidgets(ctx: ServiceContext, id: string): Promise { - return this.repository - .createQueryBuilder('d') - .leftJoinAndSelect('d.widgets', 'w', 'w.deleted_at IS NULL') - .where('d.id = :id', { id }) - .andWhere('d.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('d.deleted_at IS NULL') - .orderBy('w.sort_order', 'ASC') - .getOne(); - } - - /** - * Obtener dashboards por tipo - */ - async findByType(ctx: ServiceContext, dashboardType: DashboardType): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - dashboardType, - isActive: true, - deletedAt: null, - } as any, - order: { sortOrder: 'ASC', name: 'ASC' }, - }); - } - - /** - * Obtener dashboards del usuario - */ - async findByOwner(ctx: ServiceContext, ownerId: string): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - ownerId, - deletedAt: null, - } as any, - order: { sortOrder: 'ASC', name: 'ASC' }, - }); - } - - /** - * Obtener dashboard por defecto por tipo - */ - async findDefault(ctx: ServiceContext, dashboardType: DashboardType): Promise { - return this.repository.findOne({ - where: { - tenantId: ctx.tenantId, - dashboardType, - isDefault: true, - isActive: true, - deletedAt: null, - } as any, - relations: ['widgets'], - }); - } - - /** - * Crear dashboard - */ - async createDashboard(ctx: ServiceContext, data: CreateDashboardDto): Promise { - const existing = await this.findByCode(ctx, data.code); - if (existing) { - throw new Error(`Dashboard with code ${data.code} already exists`); - } - - return this.create(ctx, { - ...data, - ownerId: ctx.userId, - isActive: true, - isSystem: false, - isDefault: false, - viewCount: 0, - }); - } - - /** - * Establecer dashboard por defecto - */ - async setDefault(ctx: ServiceContext, id: string): Promise { - const dashboard = await this.findById(ctx, id); - if (!dashboard) { - return null; - } - - // Quitar default de otros dashboards del mismo tipo - await this.repository.update( - { - tenantId: ctx.tenantId, - dashboardType: dashboard.dashboardType, - isDefault: true, - } as any, - { isDefault: false } as any - ); - - return this.update(ctx, id, { isDefault: true }); - } - - /** - * Registrar vista de dashboard - */ - async recordView(ctx: ServiceContext, id: string): Promise { - await this.repository.update( - { id, tenantId: ctx.tenantId } as any, - { - viewCount: () => 'view_count + 1', - lastViewedAt: new Date(), - } as any - ); - } - - /** - * Duplicar dashboard - */ - async duplicate( - ctx: ServiceContext, - id: string, - newCode: string, - newName: string - ): Promise { - const original = await this.findByIdWithWidgets(ctx, id); - if (!original) { - throw new Error('Dashboard not found'); - } - - // Crear copia del dashboard - const newDashboard = await this.create(ctx, { - code: newCode, - name: newName, - description: original.description, - dashboardType: original.dashboardType, - visibility: 'private', - layout: original.layout, - theme: original.theme, - refreshInterval: original.refreshInterval, - defaultDateRange: original.defaultDateRange, - defaultFilters: original.defaultFilters, - ownerId: ctx.userId, - isDefault: false, - isSystem: false, - isActive: true, - }); - - // Copiar widgets - if (original.widgets?.length) { - for (const widget of original.widgets) { - await this.createWidget(ctx, { - dashboardId: newDashboard.id, - title: widget.title, - subtitle: widget.subtitle || undefined, - widgetType: widget.widgetType, - dataSourceType: widget.dataSourceType, - dataSource: widget.dataSource || undefined, - config: widget.config || undefined, - chartOptions: widget.chartOptions || undefined, - thresholds: widget.thresholds || undefined, - gridX: widget.gridX, - gridY: widget.gridY, - gridWidth: widget.gridWidth, - gridHeight: widget.gridHeight, - refreshInterval: widget.refreshInterval || undefined, - cacheDuration: widget.cacheDuration, - drillDownConfig: widget.drillDownConfig || undefined, - clickAction: widget.clickAction || undefined, - }); - } - } - - return this.findByIdWithWidgets(ctx, newDashboard.id) as Promise; - } - - // ==================== Widget Methods ==================== - - /** - * Crear widget - */ - async createWidget(ctx: ServiceContext, data: CreateWidgetDto): Promise { - const dashboard = await this.findById(ctx, data.dashboardId); - if (!dashboard) { - throw new Error('Dashboard not found'); - } - - const widget = this.widgetRepository.create({ - tenantId: ctx.tenantId, - ...data, - isActive: true, - createdById: ctx.userId, - }); - - return this.widgetRepository.save(widget); - } - - /** - * Actualizar widget - */ - async updateWidget( - ctx: ServiceContext, - widgetId: string, - data: UpdateWidgetDto - ): Promise { - const widget = await this.widgetRepository.findOne({ - where: { - id: widgetId, - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - }); - - if (!widget) { - return null; - } - - const updated = this.widgetRepository.merge(widget, { - ...data, - updatedById: ctx.userId, - }); - - return this.widgetRepository.save(updated); - } - - /** - * Eliminar widget (soft delete) - */ - async deleteWidget(ctx: ServiceContext, widgetId: string): Promise { - const result = await this.widgetRepository.update( - { - id: widgetId, - tenantId: ctx.tenantId, - } as any, - { - deletedAt: new Date(), - } as any - ); - - return (result.affected ?? 0) > 0; - } - - /** - * Obtener widgets de un dashboard - */ - async getWidgets(ctx: ServiceContext, dashboardId: string): Promise { - return this.widgetRepository.find({ - where: { - tenantId: ctx.tenantId, - dashboardId, - deletedAt: null, - } as any, - order: { sortOrder: 'ASC' }, - }); - } - - /** - * Actualizar posiciones de widgets - */ - async updateWidgetPositions( - ctx: ServiceContext, - dashboardId: string, - positions: { id: string; gridX: number; gridY: number; gridWidth: number; gridHeight: number }[] - ): Promise { - for (const pos of positions) { - await this.widgetRepository.update( - { - id: pos.id, - dashboardId, - tenantId: ctx.tenantId, - } as any, - { - gridX: pos.gridX, - gridY: pos.gridY, - gridWidth: pos.gridWidth, - gridHeight: pos.gridHeight, - } as any - ); - } - } - - /** - * Estad铆sticas de dashboards - */ - async getStats(ctx: ServiceContext): Promise { - const dashboards = await this.repository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - }); - - const byType = new Map(); - let activeCount = 0; - let totalViews = 0; - - dashboards.forEach((d) => { - byType.set(d.dashboardType, (byType.get(d.dashboardType) || 0) + 1); - if (d.isActive) activeCount++; - totalViews += d.viewCount; - }); - - const widgetCount = await this.widgetRepository.count({ - where: { - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - }); - - return { - totalDashboards: dashboards.length, - activeDashboards: activeCount, - totalWidgets: widgetCount, - totalViews, - byType: Array.from(byType.entries()).map(([type, count]) => ({ type, count })), - }; - } -} - -export interface DashboardStats { - totalDashboards: number; - activeDashboards: number; - totalWidgets: number; - totalViews: number; - byType: { type: DashboardType; count: number }[]; -} diff --git a/projects/erp-construccion/backend/src/modules/reports/services/index.ts b/projects/erp-construccion/backend/src/modules/reports/services/index.ts deleted file mode 100644 index ddefa06da..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/services/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Reports Module - Service Exports - * MAI-006: Reportes y Analytics - */ - -export * from './report.service'; -export * from './dashboard.service'; -export * from './kpi.service'; diff --git a/projects/erp-construccion/backend/src/modules/reports/services/kpi.service.ts b/projects/erp-construccion/backend/src/modules/reports/services/kpi.service.ts deleted file mode 100644 index 5e5ad0ebf..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/services/kpi.service.ts +++ /dev/null @@ -1,425 +0,0 @@ -/** - * KpiService - Gesti贸n de KPIs y Analytics - * - * Administra snapshots de KPIs, c谩lculos y tendencias. - * - * @module Reports - */ - -import { Repository, LessThanOrEqual } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { KpiSnapshot, KpiCategory, KpiPeriodType, TrendDirection } from '../entities/kpi-snapshot.entity'; - -export interface CreateKpiSnapshotDto { - kpiCode: string; - kpiName: string; - category: KpiCategory; - snapshotDate: Date; - periodType?: KpiPeriodType; - periodStart?: Date; - periodEnd?: Date; - fraccionamientoId?: string; - value: number; - previousValue?: number; - targetValue?: number; - minValue?: number; - maxValue?: number; - unit?: string; - breakdown?: Record; - metadata?: Record; -} - -export interface KpiFilters { - kpiCode?: string; - category?: KpiCategory; - periodType?: KpiPeriodType; - fraccionamientoId?: string; - dateFrom?: Date; - dateTo?: Date; -} - -export interface KpiTrendData { - kpiCode: string; - kpiName: string; - category: KpiCategory; - currentValue: number; - previousValue: number | null; - targetValue: number | null; - changePercentage: number | null; - trend: TrendDirection; - isOnTarget: boolean; - statusColor: string; - unit: string | null; - history: { date: Date; value: number }[]; -} - -export interface KpiSummary { - category: KpiCategory; - kpis: { - code: string; - name: string; - value: number; - target: number | null; - trend: TrendDirection; - statusColor: string; - unit: string | null; - }[]; -} - -export class KpiService extends BaseService { - constructor(repository: Repository) { - super(repository); - } - - /** - * Buscar snapshots con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: KpiFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('k') - .where('k.tenant_id = :tenantId', { tenantId: ctx.tenantId }); - - if (filters.kpiCode) { - qb.andWhere('k.kpi_code = :kpiCode', { kpiCode: filters.kpiCode }); - } - if (filters.category) { - qb.andWhere('k.category = :category', { category: filters.category }); - } - if (filters.periodType) { - qb.andWhere('k.period_type = :periodType', { periodType: filters.periodType }); - } - if (filters.fraccionamientoId) { - qb.andWhere('k.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId: filters.fraccionamientoId }); - } - if (filters.dateFrom) { - qb.andWhere('k.snapshot_date >= :dateFrom', { dateFrom: filters.dateFrom }); - } - if (filters.dateTo) { - qb.andWhere('k.snapshot_date <= :dateTo', { dateTo: filters.dateTo }); - } - - const skip = (page - 1) * limit; - qb.orderBy('k.snapshot_date', 'DESC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Crear snapshot de KPI - */ - async createSnapshot(ctx: ServiceContext, data: CreateKpiSnapshotDto): Promise { - // Calcular cambio porcentual y tendencia - const changePercentage = data.previousValue - ? ((data.value - data.previousValue) / Math.abs(data.previousValue)) * 100 - : null; - - let trendDirection: TrendDirection = 'stable'; - if (changePercentage !== null) { - if (changePercentage > 1) trendDirection = 'up'; - else if (changePercentage < -1) trendDirection = 'down'; - } - - // Determinar si est谩 en objetivo - let isOnTarget: boolean | null = null; - let statusColor = 'gray'; - if (data.targetValue !== undefined) { - isOnTarget = data.value >= data.targetValue; - if (isOnTarget) { - statusColor = 'green'; - } else if (data.value >= data.targetValue * 0.9) { - statusColor = 'yellow'; - } else { - statusColor = 'red'; - } - } - - return this.create(ctx, { - ...data, - changePercentage, - trendDirection, - isOnTarget, - statusColor, - }); - } - - /** - * Obtener 煤ltimo snapshot de un KPI - */ - async getLatestSnapshot( - ctx: ServiceContext, - kpiCode: string, - fraccionamientoId?: string - ): Promise { - const where: any = { - tenantId: ctx.tenantId, - kpiCode, - }; - if (fraccionamientoId) { - where.fraccionamientoId = fraccionamientoId; - } - - return this.repository.findOne({ - where, - order: { snapshotDate: 'DESC' }, - }); - } - - /** - * Obtener historial de un KPI - */ - async getKpiHistory( - ctx: ServiceContext, - kpiCode: string, - dateFrom: Date, - dateTo: Date, - fraccionamientoId?: string - ): Promise { - const qb = this.repository - .createQueryBuilder('k') - .where('k.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('k.kpi_code = :kpiCode', { kpiCode }) - .andWhere('k.snapshot_date BETWEEN :dateFrom AND :dateTo', { dateFrom, dateTo }); - - if (fraccionamientoId) { - qb.andWhere('k.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - return qb.orderBy('k.snapshot_date', 'ASC').getMany(); - } - - /** - * Obtener tendencia de KPI con historial - */ - async getKpiTrend( - ctx: ServiceContext, - kpiCode: string, - days = 30, - fraccionamientoId?: string - ): Promise { - const dateTo = new Date(); - const dateFrom = new Date(); - dateFrom.setDate(dateFrom.getDate() - days); - - const history = await this.getKpiHistory(ctx, kpiCode, dateFrom, dateTo, fraccionamientoId); - if (!history.length) { - return null; - } - - const latest = history[history.length - 1]; - const previous = history.length > 1 ? history[history.length - 2] : null; - - return { - kpiCode: latest.kpiCode, - kpiName: latest.kpiName, - category: latest.category, - currentValue: Number(latest.value), - previousValue: previous ? Number(previous.value) : null, - targetValue: latest.targetValue ? Number(latest.targetValue) : null, - changePercentage: latest.changePercentage ? Number(latest.changePercentage) : null, - trend: latest.trendDirection || 'stable', - isOnTarget: latest.isOnTarget || false, - statusColor: latest.statusColor || 'gray', - unit: latest.unit, - history: history.map((h) => ({ - date: h.snapshotDate, - value: Number(h.value), - })), - }; - } - - /** - * Obtener resumen de KPIs por categor铆a - */ - async getSummaryByCategory( - ctx: ServiceContext, - category: KpiCategory, - fraccionamientoId?: string - ): Promise { - // Obtener los KPIs 煤nicos de la categor铆a - const qb = this.repository - .createQueryBuilder('k') - .select('DISTINCT k.kpi_code', 'kpiCode') - .addSelect('k.kpi_name', 'kpiName') - .where('k.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('k.category = :category', { category }); - - if (fraccionamientoId) { - qb.andWhere('k.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId }); - } - - const kpiCodes = await qb.getRawMany(); - - const kpis = []; - for (const kpi of kpiCodes) { - const latest = await this.getLatestSnapshot(ctx, kpi.kpiCode, fraccionamientoId); - if (latest) { - kpis.push({ - code: latest.kpiCode, - name: latest.kpiName, - value: Number(latest.value), - target: latest.targetValue ? Number(latest.targetValue) : null, - trend: latest.trendDirection || 'stable', - statusColor: latest.statusColor || 'gray', - unit: latest.unit, - }); - } - } - - return { - category, - kpis, - }; - } - - /** - * Obtener dashboard de KPIs - */ - async getDashboardKpis( - ctx: ServiceContext, - fraccionamientoId?: string - ): Promise<{ [category: string]: KpiSummary }> { - const categories: KpiCategory[] = [ - 'financial', - 'progress', - 'quality', - 'hse', - 'hr', - 'inventory', - 'operational', - ]; - - const result: { [category: string]: KpiSummary } = {}; - - for (const category of categories) { - const summary = await this.getSummaryByCategory(ctx, category, fraccionamientoId); - if (summary.kpis.length > 0) { - result[category] = summary; - } - } - - return result; - } - - /** - * Comparar KPIs entre proyectos - */ - async compareProjects( - ctx: ServiceContext, - kpiCode: string, - fraccionamientoIds: string[], - snapshotDate?: Date - ): Promise<{ fraccionamientoId: string; value: number; target: number | null }[]> { - const date = snapshotDate || new Date(); - - const results = []; - for (const fraccionamientoId of fraccionamientoIds) { - const snapshot = await this.repository.findOne({ - where: { - tenantId: ctx.tenantId, - kpiCode, - fraccionamientoId, - snapshotDate: LessThanOrEqual(date), - } as any, - order: { snapshotDate: 'DESC' }, - }); - - if (snapshot) { - results.push({ - fraccionamientoId, - value: Number(snapshot.value), - target: snapshot.targetValue ? Number(snapshot.targetValue) : null, - }); - } - } - - return results; - } - - /** - * Calcular y guardar snapshot masivo (para jobs) - */ - async bulkCreateSnapshots( - ctx: ServiceContext, - snapshots: CreateKpiSnapshotDto[] - ): Promise<{ created: number; errors: number }> { - let created = 0; - let errors = 0; - - for (const data of snapshots) { - try { - await this.createSnapshot(ctx, data); - created++; - } catch { - errors++; - } - } - - return { created, errors }; - } - - /** - * Estad铆sticas de KPIs - */ - async getStats(ctx: ServiceContext): Promise { - const qb = this.repository - .createQueryBuilder('k') - .select('k.category', 'category') - .addSelect('COUNT(DISTINCT k.kpi_code)', 'uniqueKpis') - .addSelect('COUNT(*)', 'totalSnapshots') - .where('k.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .groupBy('k.category'); - - const categoryStats = await qb.getRawMany(); - - const onTarget = await this.repository.count({ - where: { - tenantId: ctx.tenantId, - isOnTarget: true, - } as any, - }); - - const offTarget = await this.repository.count({ - where: { - tenantId: ctx.tenantId, - isOnTarget: false, - } as any, - }); - - return { - byCategory: categoryStats.map((s) => ({ - category: s.category as KpiCategory, - uniqueKpis: parseInt(s.uniqueKpis), - totalSnapshots: parseInt(s.totalSnapshots), - })), - onTargetCount: onTarget, - offTargetCount: offTarget, - healthPercentage: onTarget + offTarget > 0 ? (onTarget / (onTarget + offTarget)) * 100 : 0, - }; - } -} - -export interface KpiStats { - byCategory: { - category: KpiCategory; - uniqueKpis: number; - totalSnapshots: number; - }[]; - onTargetCount: number; - offTargetCount: number; - healthPercentage: number; -} diff --git a/projects/erp-construccion/backend/src/modules/reports/services/report.service.ts b/projects/erp-construccion/backend/src/modules/reports/services/report.service.ts deleted file mode 100644 index 12cc4653e..000000000 --- a/projects/erp-construccion/backend/src/modules/reports/services/report.service.ts +++ /dev/null @@ -1,364 +0,0 @@ -/** - * ReportService - Gesti贸n de Reportes - * - * Administra definiciones de reportes, ejecuciones y distribuci贸n. - * - * @module Reports - */ - -import { Repository, DataSource } from 'typeorm'; -import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service'; -import { Report, ReportType, ReportFormat, ReportFrequency } from '../entities/report.entity'; -import { ReportExecution, ExecutionStatus } from '../entities/report-execution.entity'; - -export interface CreateReportDto { - code: string; - name: string; - description?: string; - reportType: ReportType; - defaultFormat?: ReportFormat; - availableFormats?: ReportFormat[]; - query?: Record; - parameters?: Record; - columns?: Record[]; - grouping?: Record; - sorting?: Record[]; - filters?: Record; - templatePath?: string; - isScheduled?: boolean; - frequency?: ReportFrequency; - scheduleConfig?: Record; - distributionList?: string[]; -} - -export interface UpdateReportDto extends Partial { - isActive?: boolean; -} - -export interface ReportFilters { - reportType?: ReportType; - isActive?: boolean; - isScheduled?: boolean; - search?: string; -} - -export interface ExecuteReportDto { - format?: ReportFormat; - parameters?: Record; - filters?: Record; - distribute?: boolean; -} - -export class ReportService extends BaseService { - private executionRepository: Repository; - - constructor( - repository: Repository, - dataSource: DataSource - ) { - super(repository); - this.executionRepository = dataSource.getRepository(ReportExecution); - } - - /** - * Buscar reportes con filtros - */ - async findWithFilters( - ctx: ServiceContext, - filters: ReportFilters, - page = 1, - limit = 20 - ): Promise> { - const qb = this.repository - .createQueryBuilder('r') - .where('r.tenant_id = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('r.deleted_at IS NULL'); - - if (filters.reportType) { - qb.andWhere('r.report_type = :reportType', { reportType: filters.reportType }); - } - if (filters.isActive !== undefined) { - qb.andWhere('r.is_active = :isActive', { isActive: filters.isActive }); - } - if (filters.isScheduled !== undefined) { - qb.andWhere('r.is_scheduled = :isScheduled', { isScheduled: filters.isScheduled }); - } - if (filters.search) { - qb.andWhere('(r.name ILIKE :search OR r.code ILIKE :search OR r.description ILIKE :search)', { - search: `%${filters.search}%`, - }); - } - - const skip = (page - 1) * limit; - qb.orderBy('r.name', 'ASC').skip(skip).take(limit); - - const [data, total] = await qb.getManyAndCount(); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Buscar reporte por c贸digo - */ - async findByCode(ctx: ServiceContext, code: string): Promise { - return this.repository.findOne({ - where: { - tenantId: ctx.tenantId, - code, - deletedAt: null, - } as any, - }); - } - - /** - * Obtener reportes por tipo - */ - async findByType(ctx: ServiceContext, reportType: ReportType): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - reportType, - isActive: true, - deletedAt: null, - } as any, - order: { name: 'ASC' }, - }); - } - - /** - * Obtener reportes programados - */ - async findScheduled(ctx: ServiceContext): Promise { - return this.repository.find({ - where: { - tenantId: ctx.tenantId, - isScheduled: true, - isActive: true, - deletedAt: null, - } as any, - order: { name: 'ASC' }, - }); - } - - /** - * Crear reporte - */ - async createReport(ctx: ServiceContext, data: CreateReportDto): Promise { - const existing = await this.findByCode(ctx, data.code); - if (existing) { - throw new Error(`Report with code ${data.code} already exists`); - } - - return this.create(ctx, { - ...data, - isActive: true, - isSystem: false, - executionCount: 0, - }); - } - - /** - * Ejecutar reporte - */ - async executeReport( - ctx: ServiceContext, - reportId: string, - options: ExecuteReportDto - ): Promise { - const report = await this.findById(ctx, reportId); - if (!report) { - throw new Error('Report not found'); - } - - if (!report.isActive) { - throw new Error('Report is not active'); - } - - // Crear registro de ejecuci贸n - const execution = this.executionRepository.create({ - tenantId: ctx.tenantId, - reportId, - status: 'pending' as ExecutionStatus, - format: options.format || report.defaultFormat, - parameters: options.parameters || report.parameters, - filters: options.filters || report.filters, - isScheduled: false, - executedById: ctx.userId, - }); - - const savedExecution = await this.executionRepository.save(execution); - - // En una implementaci贸n real, aqu铆 se ejecutar铆a el reporte de forma as铆ncrona - // Por ahora, simulamos el proceso - try { - await this.processReportExecution(ctx, savedExecution, report, options); - } catch (error) { - await this.markExecutionFailed(savedExecution.id, error as Error); - throw error; - } - - // Actualizar contador de ejecuciones - await this.repository.update( - { id: reportId } as any, - { - executionCount: () => 'execution_count + 1', - lastExecutedAt: new Date(), - } as any - ); - - return this.executionRepository.findOne({ - where: { id: savedExecution.id } as any, - }) as Promise; - } - - /** - * Procesar ejecuci贸n del reporte - */ - private async processReportExecution( - _ctx: ServiceContext, - execution: ReportExecution, - _report: Report, - _options: ExecuteReportDto - ): Promise { - const startTime = Date.now(); - - await this.executionRepository.update( - { id: execution.id } as any, - { status: 'running', startedAt: new Date() } as any - ); - - // Aqu铆 ir铆a la l贸gica real de generaci贸n del reporte - // Por ahora simulamos una ejecuci贸n exitosa - const duration = Date.now() - startTime; - - await this.executionRepository.update( - { id: execution.id } as any, - { - status: 'completed', - completedAt: new Date(), - durationMs: duration, - rowCount: 0, // Se calcular铆a del resultado real - } as any - ); - } - - /** - * Marcar ejecuci贸n como fallida - */ - private async markExecutionFailed(executionId: string, error: Error): Promise { - await this.executionRepository.update( - { id: executionId } as any, - { - status: 'failed', - completedAt: new Date(), - errorMessage: error.message, - errorStack: error.stack, - } as any - ); - } - - /** - * Obtener historial de ejecuciones - */ - async getExecutionHistory( - ctx: ServiceContext, - reportId: string, - page = 1, - limit = 20 - ): Promise> { - const skip = (page - 1) * limit; - - const [data, total] = await this.executionRepository.findAndCount({ - where: { - tenantId: ctx.tenantId, - reportId, - } as any, - order: { executedAt: 'DESC' }, - skip, - take: limit, - }); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Obtener 煤ltima ejecuci贸n - */ - async getLastExecution(ctx: ServiceContext, reportId: string): Promise { - return this.executionRepository.findOne({ - where: { - tenantId: ctx.tenantId, - reportId, - } as any, - order: { executedAt: 'DESC' }, - }); - } - - /** - * Obtener ejecuci贸n por ID - */ - async getExecution(ctx: ServiceContext, executionId: string): Promise { - return this.executionRepository.findOne({ - where: { - tenantId: ctx.tenantId, - id: executionId, - } as any, - }); - } - - /** - * Estad铆sticas de reportes - */ - async getStats(ctx: ServiceContext): Promise { - const reports = await this.repository.find({ - where: { - tenantId: ctx.tenantId, - deletedAt: null, - } as any, - }); - - const byType = new Map(); - let activeCount = 0; - let scheduledCount = 0; - let totalExecutions = 0; - - reports.forEach((r) => { - byType.set(r.reportType, (byType.get(r.reportType) || 0) + 1); - if (r.isActive) activeCount++; - if (r.isScheduled) scheduledCount++; - totalExecutions += r.executionCount; - }); - - return { - totalReports: reports.length, - activeReports: activeCount, - scheduledReports: scheduledCount, - totalExecutions, - byType: Array.from(byType.entries()).map(([type, count]) => ({ type, count })), - }; - } -} - -export interface ReportStats { - totalReports: number; - activeReports: number; - scheduledReports: number; - totalExecutions: number; - byType: { type: ReportType; count: number }[]; -} diff --git a/projects/erp-construccion/backend/src/modules/users/controllers/index.ts b/projects/erp-construccion/backend/src/modules/users/controllers/index.ts deleted file mode 100644 index 01880acbd..000000000 --- a/projects/erp-construccion/backend/src/modules/users/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './users.controller'; diff --git a/projects/erp-construccion/backend/src/modules/users/controllers/users.controller.ts b/projects/erp-construccion/backend/src/modules/users/controllers/users.controller.ts deleted file mode 100644 index 7bb5194a5..000000000 --- a/projects/erp-construccion/backend/src/modules/users/controllers/users.controller.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * UsersController - Controlador de usuarios - * - * Endpoints REST para CRUD de usuarios y asignaci贸n de roles. - * - * @module Users - */ - -import { Router, Request, Response, NextFunction } from 'express'; -import { DataSource } from 'typeorm'; -import { UsersService, CreateUserDto, UpdateUserDto } from '../services/users.service'; -import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; -import { AuthService } from '../../auth/services/auth.service'; -import { User } from '../../core/entities/user.entity'; -import { Tenant } from '../../core/entities/tenant.entity'; -import { Role } from '../../auth/entities/role.entity'; -import { UserRole } from '../../auth/entities/user-role.entity'; -import { RefreshToken } from '../../auth/entities/refresh-token.entity'; - -/** - * Crear router de usuarios - */ -export function createUsersController(dataSource: DataSource): Router { - const router = Router(); - - // Repositorios - const userRepository = dataSource.getRepository(User); - const tenantRepository = dataSource.getRepository(Tenant); - const roleRepository = dataSource.getRepository(Role); - const userRoleRepository = dataSource.getRepository(UserRole); - const refreshTokenRepository = dataSource.getRepository(RefreshToken); - - // Servicios - const usersService = new UsersService(userRepository, roleRepository, userRoleRepository); - const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); - const authMiddleware = new AuthMiddleware(authService, dataSource); - - /** - * GET /users - * Listar usuarios del tenant - */ - router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const page = parseInt(req.query.page as string) || 1; - const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); - const search = req.query.search as string; - const isActive = req.query.isActive === 'true' ? true : req.query.isActive === 'false' ? false : undefined; - - const result = await usersService.findAll({ tenantId, page, limit, search, isActive }); - - res.status(200).json({ - success: true, - data: result.users, - pagination: { - page, - limit, - total: result.total, - totalPages: Math.ceil(result.total / limit), - }, - }); - } catch (error) { - next(error); - } - }); - - /** - * GET /users/roles - * Listar roles disponibles - */ - router.get('/roles', authMiddleware.authenticate, async (_req: Request, res: Response, next: NextFunction): Promise => { - try { - const roles = await usersService.listRoles(); - res.status(200).json({ success: true, data: roles }); - } catch (error) { - next(error); - } - }); - - /** - * GET /users/:id - * Obtener usuario por ID - */ - router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const user = await usersService.findById(req.params.id, tenantId); - if (!user) { - res.status(404).json({ error: 'Not Found', message: 'User not found' }); - return; - } - - const roles = await usersService.getUserRoles(user.id, tenantId); - - res.status(200).json({ - success: true, - data: { ...user, assignedRoles: roles }, - }); - } catch (error) { - next(error); - } - }); - - /** - * POST /users - * Crear usuario - */ - router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'super_admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: CreateUserDto = { - ...req.body, - tenantId, - }; - - if (!dto.email || !dto.password || !dto.firstName || !dto.lastName) { - res.status(400).json({ - error: 'Bad Request', - message: 'Email, password, firstName and lastName are required', - }); - return; - } - - const user = await usersService.create(dto, req.user?.sub); - res.status(201).json({ success: true, data: user }); - } catch (error) { - if (error instanceof Error && error.message === 'Email already exists in this tenant') { - res.status(409).json({ error: 'Conflict', message: error.message }); - return; - } - next(error); - } - }); - - /** - * PATCH /users/:id - * Actualizar usuario - */ - router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'super_admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const dto: UpdateUserDto = req.body; - const user = await usersService.update(req.params.id, tenantId, dto); - res.status(200).json({ success: true, data: user }); - } catch (error) { - if (error instanceof Error && error.message === 'User not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * DELETE /users/:id - * Eliminar usuario (soft delete) - */ - router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'super_admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - await usersService.delete(req.params.id, tenantId, req.user?.sub); - res.status(200).json({ success: true, message: 'User deleted' }); - } catch (error) { - if (error instanceof Error && error.message === 'User not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * POST /users/:id/roles - * Asignar rol a usuario - */ - router.post('/:id/roles', authMiddleware.authenticate, authMiddleware.authorize('admin', 'super_admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const { roleCode } = req.body; - if (!roleCode) { - res.status(400).json({ error: 'Bad Request', message: 'roleCode is required' }); - return; - } - - const userRole = await usersService.assignRole( - { userId: req.params.id, roleCode, tenantId }, - req.user?.sub - ); - res.status(200).json({ success: true, data: userRole }); - } catch (error) { - if (error instanceof Error && error.message === 'Role not found') { - res.status(404).json({ error: 'Not Found', message: error.message }); - return; - } - next(error); - } - }); - - /** - * DELETE /users/:id/roles/:roleCode - * Remover rol de usuario - */ - router.delete('/:id/roles/:roleCode', authMiddleware.authenticate, authMiddleware.authorize('admin', 'super_admin'), async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - await usersService.removeRole(req.params.id, req.params.roleCode, tenantId); - res.status(200).json({ success: true, message: 'Role removed' }); - } catch (error) { - next(error); - } - }); - - /** - * GET /users/:id/roles - * Obtener roles de usuario - */ - router.get('/:id/roles', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const tenantId = req.tenantId; - if (!tenantId) { - res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); - return; - } - - const roles = await usersService.getUserRoles(req.params.id, tenantId); - res.status(200).json({ success: true, data: roles }); - } catch (error) { - next(error); - } - }); - - return router; -} - -export default createUsersController; diff --git a/projects/erp-construccion/backend/src/modules/users/index.ts b/projects/erp-construccion/backend/src/modules/users/index.ts deleted file mode 100644 index f9202e67b..000000000 --- a/projects/erp-construccion/backend/src/modules/users/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Users Module - Main Exports - * - * Gesti贸n de usuarios y roles RBAC. - * - * @module Users - */ - -export * from './services/users.service'; -export * from './controllers/users.controller'; diff --git a/projects/erp-construccion/backend/src/modules/users/services/index.ts b/projects/erp-construccion/backend/src/modules/users/services/index.ts deleted file mode 100644 index be64bcaa5..000000000 --- a/projects/erp-construccion/backend/src/modules/users/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './users.service'; diff --git a/projects/erp-construccion/backend/src/modules/users/services/users.service.ts b/projects/erp-construccion/backend/src/modules/users/services/users.service.ts deleted file mode 100644 index bdb43f486..000000000 --- a/projects/erp-construccion/backend/src/modules/users/services/users.service.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * UsersService - Gesti贸n de usuarios - * - * CRUD de usuarios con soporte multi-tenant y RBAC. - * - * @module Users - */ - -import { Repository, IsNull } from 'typeorm'; -import * as bcrypt from 'bcryptjs'; -import { User } from '../../core/entities/user.entity'; -import { Role } from '../../auth/entities/role.entity'; -import { UserRole } from '../../auth/entities/user-role.entity'; - -export interface CreateUserDto { - email: string; - password: string; - firstName: string; - lastName: string; - tenantId: string; - roles?: string[]; -} - -export interface UpdateUserDto { - firstName?: string; - lastName?: string; - isActive?: boolean; -} - -export interface AssignRoleDto { - userId: string; - roleCode: string; - tenantId: string; -} - -export interface UserListOptions { - tenantId: string; - page?: number; - limit?: number; - search?: string; - isActive?: boolean; -} - -export class UsersService { - constructor( - private readonly userRepository: Repository, - private readonly roleRepository: Repository, - private readonly userRoleRepository: Repository - ) {} - - /** - * Listar usuarios de un tenant - */ - async findAll(options: UserListOptions): Promise<{ users: User[]; total: number }> { - const { tenantId, page = 1, limit = 20, search, isActive } = options; - - const query = this.userRepository - .createQueryBuilder('user') - .leftJoinAndSelect('user.tenant', 'tenant') - .where('user.tenant_id = :tenantId', { tenantId }) - .andWhere('user.deleted_at IS NULL'); - - if (search) { - query.andWhere( - '(user.email ILIKE :search OR user.first_name ILIKE :search OR user.last_name ILIKE :search)', - { search: `%${search}%` } - ); - } - - if (isActive !== undefined) { - query.andWhere('user.is_active = :isActive', { isActive }); - } - - const total = await query.getCount(); - const users = await query - .skip((page - 1) * limit) - .take(limit) - .orderBy('user.created_at', 'DESC') - .getMany(); - - return { users, total }; - } - - /** - * Obtener usuario por ID - */ - async findById(id: string, tenantId: string): Promise { - return this.userRepository.findOne({ - where: { id, tenantId, deletedAt: IsNull() } as any, - relations: ['tenant'], - }); - } - - /** - * Obtener usuario por email - */ - async findByEmail(email: string, tenantId: string): Promise { - return this.userRepository.findOne({ - where: { email, tenantId, deletedAt: IsNull() } as any, - }); - } - - /** - * Crear usuario - */ - async create(dto: CreateUserDto, createdBy?: string): Promise { - // Verificar email 煤nico en tenant - const existing = await this.findByEmail(dto.email, dto.tenantId); - if (existing) { - throw new Error('Email already exists in this tenant'); - } - - const passwordHash = await bcrypt.hash(dto.password, 12); - - const user = await this.userRepository.save( - this.userRepository.create({ - email: dto.email, - passwordHash, - firstName: dto.firstName, - lastName: dto.lastName, - tenantId: dto.tenantId, - defaultTenantId: dto.tenantId, - isActive: true, - roles: dto.roles || ['viewer'], - }) - ); - - // Asignar roles si se especificaron - if (dto.roles && dto.roles.length > 0) { - for (const roleCode of dto.roles) { - await this.assignRole({ userId: user.id, roleCode, tenantId: dto.tenantId }, createdBy); - } - } - - return user; - } - - /** - * Actualizar usuario - */ - async update(id: string, tenantId: string, dto: UpdateUserDto): Promise { - const user = await this.findById(id, tenantId); - if (!user) { - throw new Error('User not found'); - } - - await this.userRepository.update(id, { - ...dto, - updatedAt: new Date(), - }); - - return this.findById(id, tenantId) as Promise; - } - - /** - * Soft delete de usuario - */ - async delete(id: string, tenantId: string, _deletedBy?: string): Promise { - const user = await this.findById(id, tenantId); - if (!user) { - throw new Error('User not found'); - } - - await this.userRepository.update(id, { - deletedAt: new Date(), - isActive: false, - }); - } - - /** - * Activar/Desactivar usuario - */ - async setActive(id: string, tenantId: string, isActive: boolean): Promise { - const user = await this.findById(id, tenantId); - if (!user) { - throw new Error('User not found'); - } - - await this.userRepository.update(id, { isActive }); - return this.findById(id, tenantId) as Promise; - } - - /** - * Asignar rol a usuario - */ - async assignRole(dto: AssignRoleDto, assignedBy?: string): Promise { - const role = await this.roleRepository.findOne({ - where: { code: dto.roleCode, isActive: true }, - }); - - if (!role) { - throw new Error('Role not found'); - } - - // Verificar si ya tiene el rol - const existing = await this.userRoleRepository.findOne({ - where: { userId: dto.userId, roleId: role.id, tenantId: dto.tenantId }, - }); - - if (existing) { - return existing; - } - - return this.userRoleRepository.save( - this.userRoleRepository.create({ - userId: dto.userId, - roleId: role.id, - tenantId: dto.tenantId, - assignedBy, - }) - ); - } - - /** - * Remover rol de usuario - */ - async removeRole(userId: string, roleCode: string, tenantId: string): Promise { - const role = await this.roleRepository.findOne({ - where: { code: roleCode }, - }); - - if (!role) { - throw new Error('Role not found'); - } - - await this.userRoleRepository.delete({ - userId, - roleId: role.id, - tenantId, - }); - } - - /** - * Obtener roles de usuario - */ - async getUserRoles(userId: string, tenantId: string): Promise { - const userRoles = await this.userRoleRepository.find({ - where: { userId, tenantId }, - relations: ['role'], - }); - - return userRoles.map((ur) => ur.role); - } - - /** - * Listar todos los roles disponibles - */ - async listRoles(): Promise { - return this.roleRepository.find({ - where: { isActive: true }, - order: { name: 'ASC' }, - }); - } -} diff --git a/projects/erp-construccion/backend/src/server.ts b/projects/erp-construccion/backend/src/server.ts deleted file mode 100644 index a78e577f9..000000000 --- a/projects/erp-construccion/backend/src/server.ts +++ /dev/null @@ -1,364 +0,0 @@ -/** - * Server Entry Point - * MVP Sistema Administraci贸n de Obra e INFONAVIT - * - * @author Backend-Agent - * @date 2025-11-20 - */ - -import 'reflect-metadata'; -import express, { Application } from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import morgan from 'morgan'; -import dotenv from 'dotenv'; -import { AppDataSource } from './shared/database/typeorm.config'; - -// Cargar variables de entorno -dotenv.config(); - -const app: Application = express(); -const PORT = process.env.APP_PORT || 3000; -const API_VERSION = process.env.API_VERSION || 'v1'; - -/** - * Middlewares - */ -app.use(helmet()); // Seguridad HTTP headers -app.use(cors({ - origin: process.env.CORS_ORIGIN?.split(',') || '*', - credentials: process.env.CORS_CREDENTIALS === 'true', -})); -app.use(morgan(process.env.LOG_FORMAT || 'dev')); // Logging -app.use(express.json()); // Parse JSON bodies -app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies - -/** - * Health Check - */ -app.get('/health', (_req, res) => { - res.status(200).json({ - status: 'ok', - timestamp: new Date().toISOString(), - environment: process.env.NODE_ENV || 'development', - version: API_VERSION, - }); -}); - -/** - * API Routes - */ -import { proyectoController, fraccionamientoController, createEtapaController, createManzanaController, createLoteController, createPrototipoController } from './modules/construction/controllers'; -import { createAuthController } from './modules/auth/controllers/auth.controller'; -import { createUsersController } from './modules/users/controllers/users.controller'; -import { createConceptoController, createPresupuestoController } from './modules/budgets/controllers'; -import { createAvanceObraController, createBitacoraObraController } from './modules/progress/controllers'; -import { createEstimacionController, createAnticipoController, createFondoGarantiaController, createRetencionController } from './modules/estimates/controllers'; -import { createPuestoController, createEmployeeController } from './modules/hr/controllers'; -import { createCapacitacionController, createIncidenteController } from './modules/hse/controllers'; -import { createRequisicionController, createConsumoObraController } from './modules/inventory/controllers'; -import { createComparativoController } from './modules/purchase/controllers'; -import { createDerechohabienteController, createAsignacionController } from './modules/infonavit/controllers'; -import { createInspectionController, createTicketController } from './modules/quality/controllers'; -import { createContractController, createSubcontractorController } from './modules/contracts/controllers'; -import { createReportController, createDashboardController, createKpiController } from './modules/reports/controllers'; -import { createCostCenterController, createAuditLogController, createSystemSettingController, createBackupController } from './modules/admin/controllers'; -import { createOpportunityController, createBidController, createBidBudgetController, createBidAnalyticsController } from './modules/bidding/controllers'; -import { createAccountingController, createAPController, createARController, createCashFlowController, createBankReconciliationController, createReportsController } from './modules/finance/controllers'; - -// Root API info -app.get(`/api/${API_VERSION}`, (_req, res) => { - res.status(200).json({ - message: 'API MVP Sistema Administraci贸n de Obra', - version: API_VERSION, - endpoints: { - health: '/health', - docs: `/api/${API_VERSION}/docs`, - auth: `/api/${API_VERSION}/auth`, - users: `/api/${API_VERSION}/users`, - proyectos: `/api/${API_VERSION}/proyectos`, - fraccionamientos: `/api/${API_VERSION}/fraccionamientos`, - etapas: `/api/${API_VERSION}/etapas`, - manzanas: `/api/${API_VERSION}/manzanas`, - lotes: `/api/${API_VERSION}/lotes`, - prototipos: `/api/${API_VERSION}/prototipos`, - conceptos: `/api/${API_VERSION}/conceptos`, - presupuestos: `/api/${API_VERSION}/presupuestos`, - avances: `/api/${API_VERSION}/avances`, - bitacora: `/api/${API_VERSION}/bitacora`, - estimaciones: `/api/${API_VERSION}/estimaciones`, - anticipos: `/api/${API_VERSION}/anticipos`, - 'fondos-garantia': `/api/${API_VERSION}/fondos-garantia`, - retenciones: `/api/${API_VERSION}/retenciones`, - puestos: `/api/${API_VERSION}/puestos`, - empleados: `/api/${API_VERSION}/empleados`, - capacitaciones: `/api/${API_VERSION}/capacitaciones`, - incidentes: `/api/${API_VERSION}/incidentes`, - requisiciones: `/api/${API_VERSION}/requisiciones`, - consumos: `/api/${API_VERSION}/consumos`, - comparativos: `/api/${API_VERSION}/comparativos`, - derechohabientes: `/api/${API_VERSION}/derechohabientes`, - asignaciones: `/api/${API_VERSION}/asignaciones`, - inspections: `/api/${API_VERSION}/inspections`, - tickets: `/api/${API_VERSION}/tickets`, - contracts: `/api/${API_VERSION}/contracts`, - subcontractors: `/api/${API_VERSION}/subcontractors`, - reports: `/api/${API_VERSION}/reports`, - dashboards: `/api/${API_VERSION}/dashboards`, - kpis: `/api/${API_VERSION}/kpis`, - 'cost-centers': `/api/${API_VERSION}/cost-centers`, - 'audit-logs': `/api/${API_VERSION}/audit-logs`, - settings: `/api/${API_VERSION}/settings`, - backups: `/api/${API_VERSION}/backups`, - opportunities: `/api/${API_VERSION}/opportunities`, - bids: `/api/${API_VERSION}/bids`, - 'bid-budgets': `/api/${API_VERSION}/bid-budgets`, - 'bid-analytics': `/api/${API_VERSION}/bid-analytics`, - accounting: `/api/${API_VERSION}/accounting`, - 'accounts-payable': `/api/${API_VERSION}/accounts-payable`, - 'accounts-receivable': `/api/${API_VERSION}/accounts-receivable`, - 'cash-flow': `/api/${API_VERSION}/cash-flow`, - 'bank-reconciliation': `/api/${API_VERSION}/bank-reconciliation`, - 'financial-reports': `/api/${API_VERSION}/financial-reports`, - }, - }); -}); - -// Construction Module Routes -app.use(`/api/${API_VERSION}/proyectos`, proyectoController); -app.use(`/api/${API_VERSION}/fraccionamientos`, fraccionamientoController); - -// Auth y Users Module Routes - Se inicializan despu茅s de la conexi贸n a BD -let authController: ReturnType; -let usersController: ReturnType; - -/** - * 404 Handler - */ -app.use((req, res) => { - res.status(404).json({ - error: 'Not Found', - message: `Cannot ${req.method} ${req.path}`, - }); -}); - -/** - * Error Handler - */ -app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { - console.error('Error:', err); - res.status(500).json({ - error: 'Internal Server Error', - message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong', - }); -}); - -/** - * Inicializar Base de Datos y Servidor - */ -async function bootstrap() { - try { - // Conectar a base de datos - console.log('馃攲 Conectando a base de datos...'); - await AppDataSource.initialize(); - console.log('鉁 Base de datos conectada'); - - // Inicializar Auth Controller (requiere DataSource) - authController = createAuthController(AppDataSource); - app.use(`/api/${API_VERSION}/auth`, authController); - console.log('馃攼 Auth module inicializado'); - - // Inicializar Users Controller (requiere DataSource) - usersController = createUsersController(AppDataSource); - app.use(`/api/${API_VERSION}/users`, usersController); - console.log('馃懃 Users module inicializado'); - - // Inicializar Construction Controllers (requieren DataSource para auth) - const etapaController = createEtapaController(AppDataSource); - app.use(`/api/${API_VERSION}/etapas`, etapaController); - - const manzanaController = createManzanaController(AppDataSource); - app.use(`/api/${API_VERSION}/manzanas`, manzanaController); - - const loteController = createLoteController(AppDataSource); - app.use(`/api/${API_VERSION}/lotes`, loteController); - - const prototipoController = createPrototipoController(AppDataSource); - app.use(`/api/${API_VERSION}/prototipos`, prototipoController); - - console.log('馃彈锔 Construction module inicializado'); - - // Inicializar Budgets Controllers (requieren DataSource para auth) - const conceptoController = createConceptoController(AppDataSource); - app.use(`/api/${API_VERSION}/conceptos`, conceptoController); - - const presupuestoController = createPresupuestoController(AppDataSource); - app.use(`/api/${API_VERSION}/presupuestos`, presupuestoController); - - console.log('馃挵 Budgets module inicializado'); - - // Inicializar Progress Controllers (requieren DataSource para auth) - const avanceObraController = createAvanceObraController(AppDataSource); - app.use(`/api/${API_VERSION}/avances`, avanceObraController); - - const bitacoraObraController = createBitacoraObraController(AppDataSource); - app.use(`/api/${API_VERSION}/bitacora`, bitacoraObraController); - - console.log('馃搳 Progress module inicializado'); - - // Inicializar Estimates Controllers (requieren DataSource para auth) - const estimacionController = createEstimacionController(AppDataSource); - app.use(`/api/${API_VERSION}/estimaciones`, estimacionController); - - const anticipoController = createAnticipoController(AppDataSource); - app.use(`/api/${API_VERSION}/anticipos`, anticipoController); - - const fondoGarantiaController = createFondoGarantiaController(AppDataSource); - app.use(`/api/${API_VERSION}/fondos-garantia`, fondoGarantiaController); - - const retencionController = createRetencionController(AppDataSource); - app.use(`/api/${API_VERSION}/retenciones`, retencionController); - - console.log('馃摑 Estimates module inicializado'); - - // Inicializar HR Controllers (requieren DataSource para auth) - const puestoController = createPuestoController(AppDataSource); - app.use(`/api/${API_VERSION}/puestos`, puestoController); - - const employeeController = createEmployeeController(AppDataSource); - app.use(`/api/${API_VERSION}/empleados`, employeeController); - - console.log('馃懛 HR module inicializado'); - - // Inicializar HSE Controllers (requieren DataSource para auth) - const capacitacionController = createCapacitacionController(AppDataSource); - app.use(`/api/${API_VERSION}/capacitaciones`, capacitacionController); - - const incidenteController = createIncidenteController(AppDataSource); - app.use(`/api/${API_VERSION}/incidentes`, incidenteController); - - console.log('馃 HSE module inicializado'); - - // Inicializar Inventory Controllers (requieren DataSource para auth) - const requisicionController = createRequisicionController(AppDataSource); - app.use(`/api/${API_VERSION}/requisiciones`, requisicionController); - - const consumoObraController = createConsumoObraController(AppDataSource); - app.use(`/api/${API_VERSION}/consumos`, consumoObraController); - - console.log('馃摝 Inventory module inicializado'); - - // Inicializar Purchase Controllers (requieren DataSource para auth) - const comparativoController = createComparativoController(AppDataSource); - app.use(`/api/${API_VERSION}/comparativos`, comparativoController); - - console.log('馃洅 Purchase module inicializado'); - - // Inicializar Infonavit Controllers (requieren DataSource para auth) - const derechohabienteController = createDerechohabienteController(AppDataSource); - app.use(`/api/${API_VERSION}/derechohabientes`, derechohabienteController); - - const asignacionController = createAsignacionController(AppDataSource); - app.use(`/api/${API_VERSION}/asignaciones`, asignacionController); - - console.log('馃彔 Infonavit module inicializado'); - - // Inicializar Quality Controllers (requieren DataSource para auth) - const inspectionController = createInspectionController(AppDataSource); - app.use(`/api/${API_VERSION}/inspections`, inspectionController); - - const ticketController = createTicketController(AppDataSource); - app.use(`/api/${API_VERSION}/tickets`, ticketController); - - console.log('鉁 Quality module inicializado'); - - // Inicializar Contracts Controllers (requieren DataSource para auth) - const contractController = createContractController(AppDataSource); - app.use(`/api/${API_VERSION}/contracts`, contractController); - - const subcontractorController = createSubcontractorController(AppDataSource); - app.use(`/api/${API_VERSION}/subcontractors`, subcontractorController); - - console.log('馃搫 Contracts module inicializado'); - - // Inicializar Reports Controllers (requieren DataSource para auth) - const reportController = createReportController(AppDataSource); - app.use(`/api/${API_VERSION}/reports`, reportController); - - const dashboardController = createDashboardController(AppDataSource); - app.use(`/api/${API_VERSION}/dashboards`, dashboardController); - - const kpiController = createKpiController(AppDataSource); - app.use(`/api/${API_VERSION}/kpis`, kpiController); - - console.log('馃搱 Reports module inicializado'); - - // Inicializar Admin Controllers (requieren DataSource para auth) - const costCenterController = createCostCenterController(AppDataSource); - app.use(`/api/${API_VERSION}/cost-centers`, costCenterController); - - const auditLogController = createAuditLogController(AppDataSource); - app.use(`/api/${API_VERSION}/audit-logs`, auditLogController); - - const systemSettingController = createSystemSettingController(AppDataSource); - app.use(`/api/${API_VERSION}/settings`, systemSettingController); - - const backupController = createBackupController(AppDataSource); - app.use(`/api/${API_VERSION}/backups`, backupController); - - console.log('鈿欙笍 Admin module inicializado'); - - // Inicializar Bidding Controllers (requieren DataSource para auth) - const opportunityController = createOpportunityController(AppDataSource); - app.use(`/api/${API_VERSION}/opportunities`, opportunityController); - - const bidController = createBidController(AppDataSource); - app.use(`/api/${API_VERSION}/bids`, bidController); - - const bidBudgetController = createBidBudgetController(AppDataSource); - app.use(`/api/${API_VERSION}/bid-budgets`, bidBudgetController); - - const bidAnalyticsController = createBidAnalyticsController(AppDataSource); - app.use(`/api/${API_VERSION}/bid-analytics`, bidAnalyticsController); - - console.log('馃搵 Bidding module inicializado'); - - // Inicializar Finance Controllers (requieren DataSource para auth) - const accountingController = createAccountingController(AppDataSource); - app.use(`/api/${API_VERSION}/accounting`, accountingController); - - const apController = createAPController(AppDataSource); - app.use(`/api/${API_VERSION}/accounts-payable`, apController); - - const arController = createARController(AppDataSource); - app.use(`/api/${API_VERSION}/accounts-receivable`, arController); - - const cashFlowController = createCashFlowController(AppDataSource); - app.use(`/api/${API_VERSION}/cash-flow`, cashFlowController); - - const bankReconciliationController = createBankReconciliationController(AppDataSource); - app.use(`/api/${API_VERSION}/bank-reconciliation`, bankReconciliationController); - - const financialReportsController = createReportsController(AppDataSource); - app.use(`/api/${API_VERSION}/financial-reports`, financialReportsController); - - console.log('馃挼 Finance module inicializado'); - - // Iniciar servidor - app.listen(PORT, () => { - console.log('馃殌 Servidor iniciado'); - console.log(`馃搷 URL: http://localhost:${PORT}`); - console.log(`馃搷 API: http://localhost:${PORT}/api/${API_VERSION}`); - console.log(`馃搷 Health: http://localhost:${PORT}/health`); - console.log(`馃實 Entorno: ${process.env.NODE_ENV || 'development'}`); - }); - } catch (error) { - console.error('鉂 Error al iniciar servidor:', error); - process.exit(1); - } -} - -// Iniciar aplicaci贸n -bootstrap(); - -export default app; diff --git a/projects/erp-construccion/backend/src/shared/constants/api.constants.ts b/projects/erp-construccion/backend/src/shared/constants/api.constants.ts deleted file mode 100644 index b8d04af5e..000000000 --- a/projects/erp-construccion/backend/src/shared/constants/api.constants.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * API Constants - SSOT (Single Source of Truth) - * - * Todas las rutas de API, versiones y endpoints. - * NO hardcodear rutas en controllers o frontend. - * - * @module @shared/constants/api - */ - -/** - * API Version - */ -export const API_VERSION = 'v1'; -export const API_PREFIX = `/api/${API_VERSION}`; - -/** - * API Routes organized by Module - */ -export const API_ROUTES = { - // Base - ROOT: '/', - HEALTH: '/health', - DOCS: `${API_PREFIX}/docs`, - - // Auth Module - AUTH: { - BASE: `${API_PREFIX}/auth`, - LOGIN: `${API_PREFIX}/auth/login`, - LOGOUT: `${API_PREFIX}/auth/logout`, - REFRESH: `${API_PREFIX}/auth/refresh`, - REGISTER: `${API_PREFIX}/auth/register`, - FORGOT_PASSWORD: `${API_PREFIX}/auth/forgot-password`, - RESET_PASSWORD: `${API_PREFIX}/auth/reset-password`, - ME: `${API_PREFIX}/auth/me`, - CHANGE_PASSWORD: `${API_PREFIX}/auth/change-password`, - }, - - // Users Module - USERS: { - BASE: `${API_PREFIX}/users`, - BY_ID: (id: string) => `${API_PREFIX}/users/${id}`, - ROLES: (id: string) => `${API_PREFIX}/users/${id}/roles`, - }, - - // Tenants Module - TENANTS: { - BASE: `${API_PREFIX}/tenants`, - BY_ID: (id: string) => `${API_PREFIX}/tenants/${id}`, - CURRENT: `${API_PREFIX}/tenants/current`, - }, - - // Construction Module - PROYECTOS: { - BASE: `${API_PREFIX}/proyectos`, - BY_ID: (id: string) => `${API_PREFIX}/proyectos/${id}`, - FRACCIONAMIENTOS: (id: string) => `${API_PREFIX}/proyectos/${id}/fraccionamientos`, - DASHBOARD: (id: string) => `${API_PREFIX}/proyectos/${id}/dashboard`, - PROGRESS: (id: string) => `${API_PREFIX}/proyectos/${id}/progress`, - }, - - FRACCIONAMIENTOS: { - BASE: `${API_PREFIX}/fraccionamientos`, - BY_ID: (id: string) => `${API_PREFIX}/fraccionamientos/${id}`, - ETAPAS: (id: string) => `${API_PREFIX}/fraccionamientos/${id}/etapas`, - MANZANAS: (id: string) => `${API_PREFIX}/fraccionamientos/${id}/manzanas`, - LOTES: (id: string) => `${API_PREFIX}/fraccionamientos/${id}/lotes`, - }, - - PRESUPUESTOS: { - BASE: `${API_PREFIX}/presupuestos`, - BY_ID: (id: string) => `${API_PREFIX}/presupuestos/${id}`, - PARTIDAS: (id: string) => `${API_PREFIX}/presupuestos/${id}/partidas`, - COMPARE: (id: string) => `${API_PREFIX}/presupuestos/${id}/compare`, - VERSIONS: (id: string) => `${API_PREFIX}/presupuestos/${id}/versions`, - }, - - AVANCES: { - BASE: `${API_PREFIX}/avances`, - BY_ID: (id: string) => `${API_PREFIX}/avances/${id}`, - BY_PROYECTO: (proyectoId: string) => `${API_PREFIX}/proyectos/${proyectoId}/avances`, - CURVA_S: (proyectoId: string) => `${API_PREFIX}/proyectos/${proyectoId}/curva-s`, - FOTOS: (id: string) => `${API_PREFIX}/avances/${id}/fotos`, - }, - - // HR Module - EMPLOYEES: { - BASE: `${API_PREFIX}/employees`, - BY_ID: (id: string) => `${API_PREFIX}/employees/${id}`, - ASISTENCIAS: (id: string) => `${API_PREFIX}/employees/${id}/asistencias`, - CAPACITACIONES: (id: string) => `${API_PREFIX}/employees/${id}/capacitaciones`, - }, - - ASISTENCIAS: { - BASE: `${API_PREFIX}/asistencias`, - BY_ID: (id: string) => `${API_PREFIX}/asistencias/${id}`, - CHECK_IN: `${API_PREFIX}/asistencias/check-in`, - CHECK_OUT: `${API_PREFIX}/asistencias/check-out`, - BY_PROYECTO: (proyectoId: string) => `${API_PREFIX}/proyectos/${proyectoId}/asistencias`, - }, - - CUADRILLAS: { - BASE: `${API_PREFIX}/cuadrillas`, - BY_ID: (id: string) => `${API_PREFIX}/cuadrillas/${id}`, - MIEMBROS: (id: string) => `${API_PREFIX}/cuadrillas/${id}/miembros`, - }, - - // HSE Module - INCIDENTES: { - BASE: `${API_PREFIX}/incidentes`, - BY_ID: (id: string) => `${API_PREFIX}/incidentes/${id}`, - INVOLUCRADOS: (id: string) => `${API_PREFIX}/incidentes/${id}/involucrados`, - ACCIONES: (id: string) => `${API_PREFIX}/incidentes/${id}/acciones`, - BY_PROYECTO: (proyectoId: string) => `${API_PREFIX}/proyectos/${proyectoId}/incidentes`, - }, - - CAPACITACIONES: { - BASE: `${API_PREFIX}/capacitaciones`, - BY_ID: (id: string) => `${API_PREFIX}/capacitaciones/${id}`, - PARTICIPANTES: (id: string) => `${API_PREFIX}/capacitaciones/${id}/participantes`, - CERTIFICADOS: (id: string) => `${API_PREFIX}/capacitaciones/${id}/certificados`, - }, - - INSPECCIONES: { - BASE: `${API_PREFIX}/inspecciones`, - BY_ID: (id: string) => `${API_PREFIX}/inspecciones/${id}`, - HALLAZGOS: (id: string) => `${API_PREFIX}/inspecciones/${id}/hallazgos`, - }, - - EPP: { - BASE: `${API_PREFIX}/epp`, - BY_ID: (id: string) => `${API_PREFIX}/epp/${id}`, - ASIGNACIONES: `${API_PREFIX}/epp/asignaciones`, - ENTREGAS: `${API_PREFIX}/epp/entregas`, - STOCK: `${API_PREFIX}/epp/stock`, - }, - - // Estimates Module - ESTIMACIONES: { - BASE: `${API_PREFIX}/estimaciones`, - BY_ID: (id: string) => `${API_PREFIX}/estimaciones/${id}`, - CONCEPTOS: (id: string) => `${API_PREFIX}/estimaciones/${id}/conceptos`, - GENERADORES: (id: string) => `${API_PREFIX}/estimaciones/${id}/generadores`, - WORKFLOW: (id: string) => `${API_PREFIX}/estimaciones/${id}/workflow`, - SUBMIT: (id: string) => `${API_PREFIX}/estimaciones/${id}/submit`, - APPROVE: (id: string) => `${API_PREFIX}/estimaciones/${id}/approve`, - REJECT: (id: string) => `${API_PREFIX}/estimaciones/${id}/reject`, - }, - - // INFONAVIT Module - INFONAVIT: { - BASE: `${API_PREFIX}/infonavit`, - REGISTRO: `${API_PREFIX}/infonavit/registro`, - OFERTA: `${API_PREFIX}/infonavit/oferta`, - DERECHOHABIENTES: `${API_PREFIX}/infonavit/derechohabientes`, - ASIGNACIONES: `${API_PREFIX}/infonavit/asignaciones`, - ACTAS: `${API_PREFIX}/infonavit/actas`, - REPORTES: `${API_PREFIX}/infonavit/reportes`, - }, - - // Inventory Module - ALMACENES: { - BASE: `${API_PREFIX}/almacenes`, - BY_ID: (id: string) => `${API_PREFIX}/almacenes/${id}`, - BY_PROYECTO: (proyectoId: string) => `${API_PREFIX}/proyectos/${proyectoId}/almacenes`, - STOCK: (id: string) => `${API_PREFIX}/almacenes/${id}/stock`, - }, - - REQUISICIONES: { - BASE: `${API_PREFIX}/requisiciones`, - BY_ID: (id: string) => `${API_PREFIX}/requisiciones/${id}`, - LINEAS: (id: string) => `${API_PREFIX}/requisiciones/${id}/lineas`, - SUBMIT: (id: string) => `${API_PREFIX}/requisiciones/${id}/submit`, - APPROVE: (id: string) => `${API_PREFIX}/requisiciones/${id}/approve`, - }, - - // Purchase Module - COMPRAS: { - BASE: `${API_PREFIX}/compras`, - BY_ID: (id: string) => `${API_PREFIX}/compras/${id}`, - LINEAS: (id: string) => `${API_PREFIX}/compras/${id}/lineas`, - COMPARATIVO: `${API_PREFIX}/compras/comparativo`, - RECEPCIONES: (id: string) => `${API_PREFIX}/compras/${id}/recepciones`, - }, - - PROVEEDORES: { - BASE: `${API_PREFIX}/proveedores`, - BY_ID: (id: string) => `${API_PREFIX}/proveedores/${id}`, - COTIZACIONES: (id: string) => `${API_PREFIX}/proveedores/${id}/cotizaciones`, - }, - - // Contracts Module - CONTRATOS: { - BASE: `${API_PREFIX}/contratos`, - BY_ID: (id: string) => `${API_PREFIX}/contratos/${id}`, - PARTIDAS: (id: string) => `${API_PREFIX}/contratos/${id}/partidas`, - ESTIMACIONES: (id: string) => `${API_PREFIX}/contratos/${id}/estimaciones`, - }, - - // Reports Module - REPORTS: { - BASE: `${API_PREFIX}/reports`, - DASHBOARD: `${API_PREFIX}/reports/dashboard`, - AVANCE_FISICO: `${API_PREFIX}/reports/avance-fisico`, - AVANCE_FINANCIERO: `${API_PREFIX}/reports/avance-financiero`, - CURVA_S: `${API_PREFIX}/reports/curva-s`, - PRESUPUESTO_VS_REAL: `${API_PREFIX}/reports/presupuesto-vs-real`, - KPI_HSE: `${API_PREFIX}/reports/kpi-hse`, - EXPORT: `${API_PREFIX}/reports/export`, - }, -} as const; - -/** - * HTTP Methods - */ -export const HTTP_METHODS = { - GET: 'GET', - POST: 'POST', - PUT: 'PUT', - PATCH: 'PATCH', - DELETE: 'DELETE', -} as const; - -/** - * HTTP Status Codes - */ -export const HTTP_STATUS = { - OK: 200, - CREATED: 201, - NO_CONTENT: 204, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - CONFLICT: 409, - UNPROCESSABLE_ENTITY: 422, - INTERNAL_SERVER_ERROR: 500, - SERVICE_UNAVAILABLE: 503, -} as const; - -/** - * Content Types - */ -export const CONTENT_TYPES = { - JSON: 'application/json', - FORM_URLENCODED: 'application/x-www-form-urlencoded', - MULTIPART: 'multipart/form-data', - PDF: 'application/pdf', - EXCEL: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', -} as const; diff --git a/projects/erp-construccion/backend/src/shared/constants/database.constants.ts b/projects/erp-construccion/backend/src/shared/constants/database.constants.ts deleted file mode 100644 index 14c4d7839..000000000 --- a/projects/erp-construccion/backend/src/shared/constants/database.constants.ts +++ /dev/null @@ -1,315 +0,0 @@ -/** - * Database Constants - SSOT (Single Source of Truth) - * - * IMPORTANTE: Este archivo es la UNICA fuente de verdad para nombres de - * schemas, tablas y columnas. Cualquier hardcoding sera detectado por - * el script validate-constants-usage.ts - * - * @module @shared/constants/database - */ - -/** - * Database Schemas - * Todos los schemas de la base de datos PostgreSQL - */ -export const DB_SCHEMAS = { - // Auth & Core - AUTH: 'auth', - CORE: 'core', - - // Domain Schemas - CONSTRUCTION: 'construction', - HR: 'hr', - HSE: 'hse', - ESTIMATES: 'estimates', - INFONAVIT: 'infonavit', - INVENTORY: 'inventory', - PURCHASE: 'purchase', - - // System Schemas - FINANCIAL: 'financial', - ANALYTICS: 'analytics', - AUDIT: 'audit', - SYSTEM: 'system', -} as const; - -export type DBSchema = typeof DB_SCHEMAS[keyof typeof DB_SCHEMAS]; - -/** - * Database Tables organized by Schema - */ -export const DB_TABLES = { - // Auth Schema - [DB_SCHEMAS.AUTH]: { - USERS: 'users', - ROLES: 'roles', - PERMISSIONS: 'permissions', - ROLE_PERMISSIONS: 'role_permissions', - USER_ROLES: 'user_roles', - SESSIONS: 'sessions', - REFRESH_TOKENS: 'refresh_tokens', - TENANTS: 'tenants', - TENANT_USERS: 'tenant_users', - PASSWORD_RESETS: 'password_resets', - }, - - // Core Schema - [DB_SCHEMAS.CORE]: { - COMPANIES: 'companies', - PARTNERS: 'partners', - CURRENCIES: 'currencies', - COUNTRIES: 'countries', - STATES: 'states', - CITIES: 'cities', - UOM: 'units_of_measure', - UOM_CATEGORIES: 'uom_categories', - SEQUENCES: 'sequences', - ATTACHMENTS: 'attachments', - }, - - // Construction Schema (24 tables) - [DB_SCHEMAS.CONSTRUCTION]: { - // Project Structure (8) - PROYECTOS: 'proyectos', - FRACCIONAMIENTOS: 'fraccionamientos', - ETAPAS: 'etapas', - MANZANAS: 'manzanas', - LOTES: 'lotes', - TORRES: 'torres', - NIVELES: 'niveles', - DEPARTAMENTOS: 'departamentos', - PROTOTIPOS: 'prototipos', - - // Budget & Concepts (3) - CONCEPTOS: 'conceptos', - PRESUPUESTOS: 'presupuestos', - PRESUPUESTO_PARTIDAS: 'presupuesto_partidas', - - // Schedule & Progress (5) - PROGRAMA_OBRA: 'programa_obra', - PROGRAMA_ACTIVIDADES: 'programa_actividades', - AVANCES_OBRA: 'avances_obra', - FOTOS_AVANCE: 'fotos_avance', - BITACORA_OBRA: 'bitacora_obra', - - // Quality (5) - CHECKLISTS: 'checklists', - CHECKLIST_ITEMS: 'checklist_items', - INSPECCIONES: 'inspecciones', - INSPECCION_RESULTADOS: 'inspeccion_resultados', - TICKETS_POSTVENTA: 'tickets_postventa', - - // Contracts (3) - SUBCONTRATISTAS: 'subcontratistas', - CONTRATOS: 'contratos', - CONTRATO_PARTIDAS: 'contrato_partidas', - }, - - // HR Schema (8 tables) - [DB_SCHEMAS.HR]: { - EMPLOYEES: 'employees', - EMPLOYEE_CONSTRUCTION: 'employee_construction', - PUESTOS: 'puestos', - ASISTENCIAS: 'asistencias', - ASISTENCIA_BIOMETRICO: 'asistencia_biometrico', - GEOCERCAS: 'geocercas', - DESTAJO: 'destajo', - DESTAJO_DETALLE: 'destajo_detalle', - CUADRILLAS: 'cuadrillas', - CUADRILLA_MIEMBROS: 'cuadrilla_miembros', - EMPLOYEE_FRACCIONAMIENTOS: 'employee_fraccionamientos', - }, - - // HSE Schema (58 tables - main groups) - [DB_SCHEMAS.HSE]: { - // Incidents (5) - INCIDENTES: 'incidentes', - INCIDENTE_INVOLUCRADOS: 'incidente_involucrados', - INCIDENTE_ACCIONES: 'incidente_acciones', - INCIDENTE_EVIDENCIAS: 'incidente_evidencias', - INCIDENTE_CAUSAS: 'incidente_causas', - - // Training (6) - CAPACITACIONES: 'capacitaciones', - CAPACITACION_PARTICIPANTES: 'capacitacion_participantes', - CAPACITACION_MATERIALES: 'capacitacion_materiales', - CERTIFICACIONES: 'certificaciones', - CERTIFICACION_EMPLEADOS: 'certificacion_empleados', - PLAN_CAPACITACION: 'plan_capacitacion', - - // Inspections (7) - INSPECCIONES_SEGURIDAD: 'inspecciones_seguridad', - INSPECCION_HALLAZGOS: 'inspeccion_hallazgos', - CHECKLIST_SEGURIDAD: 'checklist_seguridad', - CHECKLIST_SEGURIDAD_ITEMS: 'checklist_seguridad_items', - AREAS_RIESGO: 'areas_riesgo', - RONDAS_SEGURIDAD: 'rondas_seguridad', - RONDA_PUNTOS: 'ronda_puntos', - - // EPP (7) - EPP_CATALOGO: 'epp_catalogo', - EPP_ASIGNACIONES: 'epp_asignaciones', - EPP_ENTREGAS: 'epp_entregas', - EPP_DEVOLUCIONES: 'epp_devoluciones', - EPP_INSPECCIONES: 'epp_inspecciones', - EPP_VIDA_UTIL: 'epp_vida_util', - EPP_STOCK: 'epp_stock', - - // STPS Compliance (11) - NORMAS_STPS: 'normas_stps', - REQUISITOS_NORMA: 'requisitos_norma', - CUMPLIMIENTO_NORMA: 'cumplimiento_norma', - AUDITORIAS_STPS: 'auditorias_stps', - AUDITORIA_HALLAZGOS: 'auditoria_hallazgos', - PLANES_ACCION: 'planes_accion', - ACCIONES_CORRECTIVAS: 'acciones_correctivas', - COMISION_SEGURIDAD: 'comision_seguridad', - COMISION_MIEMBROS: 'comision_miembros', - RECORRIDOS_COMISION: 'recorridos_comision', - ACTAS_COMISION: 'actas_comision', - - // Environmental (9) - IMPACTOS_AMBIENTALES: 'impactos_ambientales', - RESIDUOS: 'residuos', - RESIDUO_MOVIMIENTOS: 'residuo_movimientos', - MANIFIESTOS_RESIDUOS: 'manifiestos_residuos', - MONITOREO_AMBIENTAL: 'monitoreo_ambiental', - PERMISOS_AMBIENTALES: 'permisos_ambientales', - PROGRAMAS_AMBIENTALES: 'programas_ambientales', - INDICADORES_AMBIENTALES: 'indicadores_ambientales', - EVENTOS_AMBIENTALES: 'eventos_ambientales', - - // Work Permits (8) - PERMISOS_TRABAJO: 'permisos_trabajo', - PERMISO_RIESGOS: 'permiso_riesgos', - PERMISO_AUTORIZACIONES: 'permiso_autorizaciones', - PERMISOS_ALTURA: 'permisos_altura', - PERMISOS_CALIENTE: 'permisos_caliente', - PERMISOS_CONFINADO: 'permisos_confinado', - PERMISOS_ELECTRICO: 'permisos_electrico', - PERMISOS_EXCAVACION: 'permisos_excavacion', - - // KPIs (7) - KPI_CONFIGURACION: 'kpi_configuracion', - KPI_VALORES: 'kpi_valores', - KPI_METAS: 'kpi_metas', - DASHBOARDS_HSE: 'dashboards_hse', - ALERTAS_HSE: 'alertas_hse', - REPORTES_HSE: 'reportes_hse', - ESTADISTICAS_PERIODO: 'estadisticas_periodo', - }, - - // Estimates Schema (8 tables) - [DB_SCHEMAS.ESTIMATES]: { - ESTIMACIONES: 'estimaciones', - ESTIMACION_CONCEPTOS: 'estimacion_conceptos', - GENERADORES: 'generadores', - ANTICIPOS: 'anticipos', - AMORTIZACIONES: 'amortizaciones', - RETENCIONES: 'retenciones', - FONDO_GARANTIA: 'fondo_garantia', - ESTIMACION_WORKFLOW: 'estimacion_workflow', - }, - - // INFONAVIT Schema (8 tables) - [DB_SCHEMAS.INFONAVIT]: { - REGISTRO_INFONAVIT: 'registro_infonavit', - OFERTA_VIVIENDA: 'oferta_vivienda', - DERECHOHABIENTES: 'derechohabientes', - ASIGNACION_VIVIENDA: 'asignacion_vivienda', - ACTAS: 'actas', - ACTA_VIVIENDAS: 'acta_viviendas', - REPORTES_INFONAVIT: 'reportes_infonavit', - HISTORICO_PUNTOS: 'historico_puntos', - }, - - // Inventory Extension Schema (4 tables) - [DB_SCHEMAS.INVENTORY]: { - ALMACENES_PROYECTO: 'almacenes_proyecto', - REQUISICIONES_OBRA: 'requisiciones_obra', - REQUISICION_LINEAS: 'requisicion_lineas', - CONSUMOS_OBRA: 'consumos_obra', - // Base tables (reference) - PRODUCTS: 'products', - LOCATIONS: 'locations', - STOCK_MOVES: 'stock_moves', - STOCK_QUANTS: 'stock_quants', - }, - - // Purchase Extension Schema (5 tables) - [DB_SCHEMAS.PURCHASE]: { - PURCHASE_ORDER_CONSTRUCTION: 'purchase_order_construction', - SUPPLIER_CONSTRUCTION: 'supplier_construction', - COMPARATIVO_COTIZACIONES: 'comparativo_cotizaciones', - COMPARATIVO_PROVEEDORES: 'comparativo_proveedores', - COMPARATIVO_PRODUCTOS: 'comparativo_productos', - // Base tables (reference) - PURCHASE_ORDERS: 'purchase_orders', - PURCHASE_ORDER_LINES: 'purchase_order_lines', - SUPPLIERS: 'suppliers', - }, - - // Audit Schema - [DB_SCHEMAS.AUDIT]: { - AUDIT_LOG: 'audit_log', - CHANGE_HISTORY: 'change_history', - USER_ACTIVITY: 'user_activity', - }, -} as const; - -/** - * Common Column Names (to avoid hardcoding) - */ -export const DB_COLUMNS = { - // Audit columns - ID: 'id', - CREATED_AT: 'created_at', - UPDATED_AT: 'updated_at', - CREATED_BY: 'created_by', - UPDATED_BY: 'updated_by', - DELETED_AT: 'deleted_at', - - // Multi-tenant columns - TENANT_ID: 'tenant_id', - - // Common FK columns - USER_ID: 'user_id', - PROJECT_ID: 'proyecto_id', - FRACCIONAMIENTO_ID: 'fraccionamiento_id', - EMPLOYEE_ID: 'employee_id', - - // Status columns - STATUS: 'status', - IS_ACTIVE: 'is_active', - - // Analytic columns (Odoo pattern) - ANALYTIC_ACCOUNT_ID: 'analytic_account_id', -} as const; - -/** - * Helper function to get full table name - */ -export function getFullTableName(schema: DBSchema, table: string): string { - return `${schema}.${table}`; -} - -/** - * Get schema.table reference - */ -export const TABLE_REFS = { - // Auth - USERS: getFullTableName(DB_SCHEMAS.AUTH, DB_TABLES[DB_SCHEMAS.AUTH].USERS), - TENANTS: getFullTableName(DB_SCHEMAS.AUTH, DB_TABLES[DB_SCHEMAS.AUTH].TENANTS), - ROLES: getFullTableName(DB_SCHEMAS.AUTH, DB_TABLES[DB_SCHEMAS.AUTH].ROLES), - - // Construction - PROYECTOS: getFullTableName(DB_SCHEMAS.CONSTRUCTION, DB_TABLES[DB_SCHEMAS.CONSTRUCTION].PROYECTOS), - FRACCIONAMIENTOS: getFullTableName(DB_SCHEMAS.CONSTRUCTION, DB_TABLES[DB_SCHEMAS.CONSTRUCTION].FRACCIONAMIENTOS), - - // HR - EMPLOYEES: getFullTableName(DB_SCHEMAS.HR, DB_TABLES[DB_SCHEMAS.HR].EMPLOYEES), - - // HSE - INCIDENTES: getFullTableName(DB_SCHEMAS.HSE, DB_TABLES[DB_SCHEMAS.HSE].INCIDENTES), - CAPACITACIONES: getFullTableName(DB_SCHEMAS.HSE, DB_TABLES[DB_SCHEMAS.HSE].CAPACITACIONES), -} as const; diff --git a/projects/erp-construccion/backend/src/shared/constants/enums.constants.ts b/projects/erp-construccion/backend/src/shared/constants/enums.constants.ts deleted file mode 100644 index 03c4330a4..000000000 --- a/projects/erp-construccion/backend/src/shared/constants/enums.constants.ts +++ /dev/null @@ -1,494 +0,0 @@ -/** - * Enums Constants - SSOT (Single Source of Truth) - * - * Todos los enums del sistema. Estos se sincronizan automaticamente - * al frontend usando el script sync-enums.ts - * - * @module @shared/constants/enums - */ - -// ============================================================================ -// AUTH & USERS -// ============================================================================ - -/** - * Roles del sistema de construccion - */ -export const ROLES = { - // Super Admin (Plataforma) - SUPER_ADMIN: 'super_admin', - - // Tenant Admin - ADMIN: 'admin', - - // Direccion - DIRECTOR_GENERAL: 'director_general', - DIRECTOR_PROYECTOS: 'director_proyectos', - DIRECTOR_CONSTRUCCION: 'director_construccion', - - // Gerencias - GERENTE_ADMINISTRATIVO: 'gerente_administrativo', - GERENTE_OPERACIONES: 'gerente_operaciones', - - // Ingenieria y Control - INGENIERO_RESIDENTE: 'ingeniero_residente', - INGENIERO_COSTOS: 'ingeniero_costos', - CONTROL_OBRA: 'control_obra', - PLANEADOR: 'planeador', - - // Supervisores - SUPERVISOR_OBRA: 'supervisor_obra', - SUPERVISOR_HSE: 'supervisor_hse', - SUPERVISOR_CALIDAD: 'supervisor_calidad', - - // Compras y Almacen - COMPRAS: 'compras', - ALMACENISTA: 'almacenista', - - // RRHH - RRHH: 'rrhh', - NOMINA: 'nomina', - - // Finanzas - CONTADOR: 'contador', - TESORERO: 'tesorero', - - // Postventa - POSTVENTA: 'postventa', - - // Externos - SUBCONTRATISTA: 'subcontratista', - PROVEEDOR: 'proveedor', - DERECHOHABIENTE: 'derechohabiente', - - // Solo lectura - VIEWER: 'viewer', -} as const; - -export type Role = typeof ROLES[keyof typeof ROLES]; - -/** - * Estados de cuenta de usuario - */ -export const USER_STATUS = { - ACTIVE: 'active', - INACTIVE: 'inactive', - PENDING: 'pending', - SUSPENDED: 'suspended', - BLOCKED: 'blocked', -} as const; - -export type UserStatus = typeof USER_STATUS[keyof typeof USER_STATUS]; - -// ============================================================================ -// PROYECTOS Y ESTRUCTURA -// ============================================================================ - -/** - * Estados de proyecto - */ -export const PROJECT_STATUS = { - DRAFT: 'draft', - PLANNING: 'planning', - BIDDING: 'bidding', - AWARDED: 'awarded', - ACTIVE: 'active', - PAUSED: 'paused', - COMPLETED: 'completed', - CANCELLED: 'cancelled', -} as const; - -export type ProjectStatus = typeof PROJECT_STATUS[keyof typeof PROJECT_STATUS]; - -/** - * Tipos de proyecto - */ -export const PROJECT_TYPE = { - HORIZONTAL: 'horizontal', // Casas individuales - VERTICAL: 'vertical', // Edificios/Torres - MIXED: 'mixed', // Combinado - INFRASTRUCTURE: 'infrastructure', // Infraestructura -} as const; - -export type ProjectType = typeof PROJECT_TYPE[keyof typeof PROJECT_TYPE]; - -/** - * Estados de fraccionamiento - */ -export const FRACCIONAMIENTO_STATUS = { - ACTIVE: 'activo', - PAUSED: 'pausado', - COMPLETED: 'completado', - CANCELLED: 'cancelado', -} as const; - -export type FraccionamientoStatus = typeof FRACCIONAMIENTO_STATUS[keyof typeof FRACCIONAMIENTO_STATUS]; - -/** - * Estados de lote - */ -export const LOT_STATUS = { - AVAILABLE: 'disponible', - RESERVED: 'apartado', - SOLD: 'vendido', - IN_CONSTRUCTION: 'en_construccion', - DELIVERED: 'entregado', - WARRANTY: 'en_garantia', -} as const; - -export type LotStatus = typeof LOT_STATUS[keyof typeof LOT_STATUS]; - -// ============================================================================ -// PRESUPUESTOS Y COSTOS -// ============================================================================ - -/** - * Tipos de concepto de obra - */ -export const CONCEPT_TYPE = { - MATERIAL: 'material', - LABOR: 'mano_obra', - EQUIPMENT: 'equipo', - SUBCONTRACT: 'subcontrato', - INDIRECT: 'indirecto', - OVERHEAD: 'overhead', - UTILITY: 'utilidad', -} as const; - -export type ConceptType = typeof CONCEPT_TYPE[keyof typeof CONCEPT_TYPE]; - -/** - * Estados de presupuesto - */ -export const BUDGET_STATUS = { - DRAFT: 'borrador', - SUBMITTED: 'enviado', - APPROVED: 'aprobado', - CONTRACTED: 'contratado', - CLOSED: 'cerrado', -} as const; - -export type BudgetStatus = typeof BUDGET_STATUS[keyof typeof BUDGET_STATUS]; - -// ============================================================================ -// COMPRAS E INVENTARIOS -// ============================================================================ - -/** - * Estados de orden de compra - */ -export const PURCHASE_ORDER_STATUS = { - DRAFT: 'borrador', - SUBMITTED: 'enviado', - APPROVED: 'aprobado', - CONFIRMED: 'confirmado', - PARTIAL: 'parcial', - RECEIVED: 'recibido', - CANCELLED: 'cancelado', -} as const; - -export type PurchaseOrderStatus = typeof PURCHASE_ORDER_STATUS[keyof typeof PURCHASE_ORDER_STATUS]; - -/** - * Estados de requisicion - */ -export const REQUISITION_STATUS = { - DRAFT: 'borrador', - SUBMITTED: 'enviado', - APPROVED: 'aprobado', - REJECTED: 'rechazado', - ORDERED: 'ordenado', - CLOSED: 'cerrado', -} as const; - -export type RequisitionStatus = typeof REQUISITION_STATUS[keyof typeof REQUISITION_STATUS]; - -/** - * Tipos de movimiento de inventario - */ -export const STOCK_MOVE_TYPE = { - INCOMING: 'entrada', - OUTGOING: 'salida', - TRANSFER: 'traspaso', - ADJUSTMENT: 'ajuste', - RETURN: 'devolucion', - CONSUMPTION: 'consumo', -} as const; - -export type StockMoveType = typeof STOCK_MOVE_TYPE[keyof typeof STOCK_MOVE_TYPE]; - -// ============================================================================ -// ESTIMACIONES -// ============================================================================ - -/** - * Estados de estimacion - */ -export const ESTIMATION_STATUS = { - DRAFT: 'borrador', - IN_REVIEW: 'en_revision', - SUBMITTED: 'enviado', - CLIENT_REVIEW: 'revision_cliente', - APPROVED: 'aprobado', - REJECTED: 'rechazado', - PAID: 'pagado', - CANCELLED: 'cancelado', -} as const; - -export type EstimationStatus = typeof ESTIMATION_STATUS[keyof typeof ESTIMATION_STATUS]; - -/** - * Tipos de retencion - */ -export const RETENTION_TYPE = { - WARRANTY: 'garantia', - ADVANCE_AMORTIZATION: 'amortizacion_anticipo', - IMSS: 'imss', - ISR: 'isr', - OTHER: 'otro', -} as const; - -export type RetentionType = typeof RETENTION_TYPE[keyof typeof RETENTION_TYPE]; - -// ============================================================================ -// HSE (Health, Safety & Environment) -// ============================================================================ - -/** - * Severidad de incidente - */ -export const INCIDENT_SEVERITY = { - LOW: 'bajo', - MEDIUM: 'medio', - HIGH: 'alto', - CRITICAL: 'critico', - FATAL: 'fatal', -} as const; - -export type IncidentSeverity = typeof INCIDENT_SEVERITY[keyof typeof INCIDENT_SEVERITY]; - -/** - * Tipos de incidente - */ -export const INCIDENT_TYPE = { - ACCIDENT: 'accidente', - NEAR_MISS: 'casi_accidente', - UNSAFE_CONDITION: 'condicion_insegura', - UNSAFE_ACT: 'acto_inseguro', - FIRST_AID: 'primeros_auxilios', - ENVIRONMENTAL: 'ambiental', -} as const; - -export type IncidentType = typeof INCIDENT_TYPE[keyof typeof INCIDENT_TYPE]; - -/** - * Estados de incidente - */ -export const INCIDENT_STATUS = { - REPORTED: 'reportado', - UNDER_INVESTIGATION: 'en_investigacion', - PENDING_ACTIONS: 'pendiente_acciones', - ACTIONS_IN_PROGRESS: 'acciones_en_progreso', - CLOSED: 'cerrado', -} as const; - -export type IncidentStatus = typeof INCIDENT_STATUS[keyof typeof INCIDENT_STATUS]; - -/** - * Tipos de capacitacion - */ -export const TRAINING_TYPE = { - INDUCTION: 'induccion', - SAFETY: 'seguridad', - TECHNICAL: 'tecnico', - REGULATORY: 'normativo', - REFRESHER: 'actualizacion', - CERTIFICATION: 'certificacion', -} as const; - -export type TrainingType = typeof TRAINING_TYPE[keyof typeof TRAINING_TYPE]; - -/** - * Tipos de permiso de trabajo - */ -export const WORK_PERMIT_TYPE = { - HOT_WORK: 'trabajo_caliente', - CONFINED_SPACE: 'espacio_confinado', - HEIGHT_WORK: 'trabajo_altura', - ELECTRICAL: 'electrico', - EXCAVATION: 'excavacion', - LIFTING: 'izaje', -} as const; - -export type WorkPermitType = typeof WORK_PERMIT_TYPE[keyof typeof WORK_PERMIT_TYPE]; - -// ============================================================================ -// RRHH -// ============================================================================ - -/** - * Tipos de empleado - */ -export const EMPLOYEE_TYPE = { - PERMANENT: 'planta', - TEMPORARY: 'temporal', - CONTRACTOR: 'contratista', - INTERN: 'practicante', -} as const; - -export type EmployeeType = typeof EMPLOYEE_TYPE[keyof typeof EMPLOYEE_TYPE]; - -/** - * Tipos de asistencia - */ -export const ATTENDANCE_TYPE = { - CHECK_IN: 'entrada', - CHECK_OUT: 'salida', - BREAK_START: 'inicio_descanso', - BREAK_END: 'fin_descanso', -} as const; - -export type AttendanceType = typeof ATTENDANCE_TYPE[keyof typeof ATTENDANCE_TYPE]; - -/** - * Metodos de validacion de asistencia - */ -export const ATTENDANCE_VALIDATION = { - GPS: 'gps', - BIOMETRIC: 'biometrico', - QR: 'qr', - MANUAL: 'manual', - NFC: 'nfc', -} as const; - -export type AttendanceValidation = typeof ATTENDANCE_VALIDATION[keyof typeof ATTENDANCE_VALIDATION]; - -// ============================================================================ -// INFONAVIT -// ============================================================================ - -/** - * Estados de asignacion INFONAVIT - */ -export const INFONAVIT_ASSIGNMENT_STATUS = { - AVAILABLE: 'disponible', - IN_PROCESS: 'en_proceso', - ASSIGNED: 'asignado', - DOCUMENTED: 'documentado', - REGISTERED: 'registrado', - DELIVERED: 'entregado', -} as const; - -export type InfonavitAssignmentStatus = typeof INFONAVIT_ASSIGNMENT_STATUS[keyof typeof INFONAVIT_ASSIGNMENT_STATUS]; - -/** - * Programas INFONAVIT - */ -export const INFONAVIT_PROGRAM = { - TRADICIONAL: 'tradicional', - COFINAVIT: 'cofinavit', - APOYO_INFONAVIT: 'apoyo_infonavit', - UNAMOS_CREDITOS: 'unamos_creditos', - MEJORAVIT: 'mejoravit', -} as const; - -export type InfonavitProgram = typeof INFONAVIT_PROGRAM[keyof typeof INFONAVIT_PROGRAM]; - -// ============================================================================ -// CALIDAD Y POSTVENTA -// ============================================================================ - -/** - * Estados de ticket postventa - */ -export const TICKET_STATUS = { - OPEN: 'abierto', - IN_PROGRESS: 'en_proceso', - PENDING_CUSTOMER: 'pendiente_cliente', - PENDING_PARTS: 'pendiente_refacciones', - RESOLVED: 'resuelto', - CLOSED: 'cerrado', -} as const; - -export type TicketStatus = typeof TICKET_STATUS[keyof typeof TICKET_STATUS]; - -/** - * Prioridad de ticket - */ -export const TICKET_PRIORITY = { - LOW: 'baja', - MEDIUM: 'media', - HIGH: 'alta', - URGENT: 'urgente', -} as const; - -export type TicketPriority = typeof TICKET_PRIORITY[keyof typeof TICKET_PRIORITY]; - -// ============================================================================ -// DOCUMENTOS -// ============================================================================ - -/** - * Estados de documento - */ -export const DOCUMENT_STATUS = { - DRAFT: 'borrador', - PENDING_REVIEW: 'pendiente_revision', - APPROVED: 'aprobado', - REJECTED: 'rechazado', - OBSOLETE: 'obsoleto', -} as const; - -export type DocumentStatus = typeof DOCUMENT_STATUS[keyof typeof DOCUMENT_STATUS]; - -/** - * Tipos de documento - */ -export const DOCUMENT_TYPE = { - PLAN: 'plano', - CONTRACT: 'contrato', - PERMIT: 'permiso', - CERTIFICATE: 'certificado', - REPORT: 'reporte', - PHOTO: 'fotografia', - OTHER: 'otro', -} as const; - -export type DocumentType = typeof DOCUMENT_TYPE[keyof typeof DOCUMENT_TYPE]; - -// ============================================================================ -// WORKFLOW -// ============================================================================ - -/** - * Acciones de workflow - */ -export const WORKFLOW_ACTION = { - SUBMIT: 'submit', - APPROVE: 'approve', - REJECT: 'reject', - RETURN: 'return', - CANCEL: 'cancel', - REOPEN: 'reopen', -} as const; - -export type WorkflowAction = typeof WORKFLOW_ACTION[keyof typeof WORKFLOW_ACTION]; - -// ============================================================================ -// AUDIT -// ============================================================================ - -/** - * Tipos de accion de auditoria - */ -export const AUDIT_ACTION = { - CREATE: 'create', - UPDATE: 'update', - DELETE: 'delete', - VIEW: 'view', - EXPORT: 'export', - LOGIN: 'login', - LOGOUT: 'logout', -} as const; - -export type AuditAction = typeof AUDIT_ACTION[keyof typeof AUDIT_ACTION]; diff --git a/projects/erp-construccion/backend/src/shared/constants/index.ts b/projects/erp-construccion/backend/src/shared/constants/index.ts deleted file mode 100644 index c56cd3d9f..000000000 --- a/projects/erp-construccion/backend/src/shared/constants/index.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Constants - SSOT Entry Point - * - * Este archivo es el punto de entrada para todas las constantes del sistema. - * Exporta desde los modulos especializados para mantener SSOT. - * - * @module @shared/constants - */ - -// ============================================================================ -// DATABASE CONSTANTS -// ============================================================================ -export { - DB_SCHEMAS, - DB_TABLES, - DB_COLUMNS, - TABLE_REFS, - getFullTableName, - type DBSchema, -} from './database.constants'; - -// ============================================================================ -// API CONSTANTS -// ============================================================================ -export { - API_VERSION, - API_PREFIX, - API_ROUTES, - HTTP_METHODS, - HTTP_STATUS, - CONTENT_TYPES, -} from './api.constants'; - -// ============================================================================ -// ENUMS -// ============================================================================ -export { - // Auth - ROLES, - USER_STATUS, - type Role, - type UserStatus, - - // Projects - PROJECT_STATUS, - PROJECT_TYPE, - FRACCIONAMIENTO_STATUS, - LOT_STATUS, - type ProjectStatus, - type ProjectType, - type FraccionamientoStatus, - type LotStatus, - - // Budget - CONCEPT_TYPE, - BUDGET_STATUS, - type ConceptType, - type BudgetStatus, - - // Purchases - PURCHASE_ORDER_STATUS, - REQUISITION_STATUS, - STOCK_MOVE_TYPE, - type PurchaseOrderStatus, - type RequisitionStatus, - type StockMoveType, - - // Estimates - ESTIMATION_STATUS, - RETENTION_TYPE, - type EstimationStatus, - type RetentionType, - - // HSE - INCIDENT_SEVERITY, - INCIDENT_TYPE, - INCIDENT_STATUS, - TRAINING_TYPE, - WORK_PERMIT_TYPE, - type IncidentSeverity, - type IncidentType, - type IncidentStatus, - type TrainingType, - type WorkPermitType, - - // HR - EMPLOYEE_TYPE, - ATTENDANCE_TYPE, - ATTENDANCE_VALIDATION, - type EmployeeType, - type AttendanceType, - type AttendanceValidation, - - // INFONAVIT - INFONAVIT_ASSIGNMENT_STATUS, - INFONAVIT_PROGRAM, - type InfonavitAssignmentStatus, - type InfonavitProgram, - - // Quality - TICKET_STATUS, - TICKET_PRIORITY, - type TicketStatus, - type TicketPriority, - - // Documents - DOCUMENT_STATUS, - DOCUMENT_TYPE, - type DocumentStatus, - type DocumentType, - - // Workflow - WORKFLOW_ACTION, - type WorkflowAction, - - // Audit - AUDIT_ACTION, - type AuditAction, -} from './enums.constants'; - -// ============================================================================ -// APP CONFIG CONSTANTS -// ============================================================================ - -/** - * Application Context for PostgreSQL RLS - */ -export const APP_CONTEXT = { - TENANT_ID: 'app.current_tenant_id', - USER_ID: 'app.current_user_id', -} as const; - -/** - * Custom HTTP Headers - */ -export const CUSTOM_HEADERS = { - TENANT_ID: 'x-tenant-id', - CORRELATION_ID: 'x-correlation-id', - API_KEY: 'x-api-key', -} as const; - -/** - * Pagination Defaults - */ -export const PAGINATION = { - DEFAULT_PAGE: 1, - DEFAULT_LIMIT: 20, - MAX_LIMIT: 100, -} as const; - -/** - * Regex Patterns for Validation - */ -export const PATTERNS = { - UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, - RFC: /^[A-Z&脩]{3,4}[0-9]{6}[A-Z0-9]{3}$/, - CURP: /^[A-Z]{4}[0-9]{6}[HM][A-Z]{5}[0-9A-Z][0-9]$/, - NSS: /^[0-9]{11}$/, - EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - PHONE_MX: /^(\+52)?[0-9]{10}$/, - POSTAL_CODE_MX: /^[0-9]{5}$/, -} as const; - -/** - * File Upload Limits - */ -export const FILE_LIMITS = { - MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB - MAX_FILES: 10, - ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/webp'], - ALLOWED_DOC_TYPES: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], - ALLOWED_SPREADSHEET_TYPES: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], -} as const; - -/** - * Cache TTL (Time To Live) in seconds - */ -export const CACHE_TTL = { - SHORT: 60, // 1 minute - MEDIUM: 300, // 5 minutes - LONG: 3600, // 1 hour - DAY: 86400, // 24 hours -} as const; - -/** - * Date Formats - */ -export const DATE_FORMATS = { - ISO: 'YYYY-MM-DDTHH:mm:ss.sssZ', - DATE_ONLY: 'YYYY-MM-DD', - TIME_ONLY: 'HH:mm:ss', - DISPLAY_MX: 'DD/MM/YYYY', - DISPLAY_FULL_MX: 'DD/MM/YYYY HH:mm', -} as const; diff --git a/projects/erp-construccion/backend/src/shared/database/typeorm.config.ts b/projects/erp-construccion/backend/src/shared/database/typeorm.config.ts deleted file mode 100644 index 0b64b9ad8..000000000 --- a/projects/erp-construccion/backend/src/shared/database/typeorm.config.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * TypeORM Configuration - * Configuraci贸n de conexi贸n a PostgreSQL - * - * @see https://typeorm.io/data-source-options - */ - -import { DataSource } from 'typeorm'; -import dotenv from 'dotenv'; - -dotenv.config(); - -export const AppDataSource = new DataSource({ - type: 'postgres', - url: process.env.DATABASE_URL, - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), - username: process.env.DB_USER || 'erp_user', - password: process.env.DB_PASSWORD || 'erp_dev_password', - database: process.env.DB_NAME || 'erp_construccion', - synchronize: process.env.DB_SYNCHRONIZE === 'true', // 鈿狅笍 NUNCA en producci贸n - logging: process.env.DB_LOGGING === 'true', - entities: [ - __dirname + '/../../modules/**/entities/*.entity{.ts,.js}', - ], - migrations: [ - __dirname + '/../../../migrations/*{.ts,.js}', - ], - subscribers: [], - // Configuraci贸n de pool - extra: { - max: 20, // M谩ximo de conexiones - min: 5, // M铆nimo de conexiones - idleTimeoutMillis: 30000, - }, -}); - -/** - * Inicializar conexi贸n - */ -export async function initializeDatabase(): Promise { - try { - await AppDataSource.initialize(); - console.log('鉁 TypeORM conectado a PostgreSQL'); - } catch (error) { - console.error('鉂 Error al conectar TypeORM:', error); - throw error; - } -} - -/** - * Cerrar conexi贸n - */ -export async function closeDatabase(): Promise { - try { - await AppDataSource.destroy(); - console.log('鉁 Conexi贸n TypeORM cerrada'); - } catch (error) { - console.error('鉂 Error al cerrar TypeORM:', error); - throw error; - } -} diff --git a/projects/erp-construccion/backend/src/shared/interfaces/base.interface.ts b/projects/erp-construccion/backend/src/shared/interfaces/base.interface.ts deleted file mode 100644 index c6ff3ba90..000000000 --- a/projects/erp-construccion/backend/src/shared/interfaces/base.interface.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Base Interfaces - * Interfaces compartidas para todo el proyecto - */ - -import { Request } from 'express'; - -/** - * Entidad base con campos de auditoria - */ -export interface BaseEntity { - id: string; - createdAt: Date; - updatedAt: Date; - createdBy?: string; -} - -/** - * Entidad con tenant - */ -export interface TenantEntity extends BaseEntity { - tenantId: string; -} - -/** - * Request con contexto de tenant y usuario - */ -export interface AuthenticatedRequest extends Request { - user?: { - sub: string; - id: string; - email: string; - tenantId: string; - roles: string[]; - type: 'access' | 'refresh'; - }; - tenantId?: string; -} - -/** - * Respuesta paginada - */ -export interface PaginatedResponse { - data: T[]; - meta: { - total: number; - page: number; - limit: number; - totalPages: number; - hasNext: boolean; - hasPrev: boolean; - }; -} - -/** - * Query params para paginacion - */ -export interface PaginationQuery { - page?: number; - limit?: number; - sortBy?: string; - sortOrder?: 'ASC' | 'DESC'; - search?: string; -} - -/** - * Respuesta API estandar - */ -export interface ApiResponse { - success: boolean; - data?: T; - message?: string; - errors?: Array<{ - field?: string; - message: string; - code?: string; - }>; - meta?: Record; -} diff --git a/projects/erp-construccion/backend/src/shared/services/base.service.ts b/projects/erp-construccion/backend/src/shared/services/base.service.ts deleted file mode 100644 index 41a87ae43..000000000 --- a/projects/erp-construccion/backend/src/shared/services/base.service.ts +++ /dev/null @@ -1,217 +0,0 @@ -/** - * BaseService - Abstract Service with Common CRUD Operations - * - * Provides multi-tenant aware CRUD operations using TypeORM. - * All domain services should extend this base class. - * - * @module @shared/services - */ - -import { - Repository, - FindOptionsWhere, - FindManyOptions, - DeepPartial, - ObjectLiteral, -} from 'typeorm'; - -export interface PaginationOptions { - page?: number; - limit?: number; -} - -export interface PaginatedResult { - data: T[]; - meta: { - total: number; - page: number; - limit: number; - totalPages: number; - }; -} - -export interface ServiceContext { - tenantId: string; - userId?: string; -} - -export abstract class BaseService { - constructor(protected readonly repository: Repository) {} - - /** - * Find all records for a tenant with optional pagination - */ - async findAll( - ctx: ServiceContext, - options?: PaginationOptions & { where?: FindOptionsWhere } - ): Promise> { - const page = options?.page || 1; - const limit = options?.limit || 20; - const skip = (page - 1) * limit; - - const where = { - tenantId: ctx.tenantId, - deletedAt: null, - ...options?.where, - } as unknown as FindOptionsWhere; - - const [data, total] = await this.repository.findAndCount({ - where, - take: limit, - skip, - order: { createdAt: 'DESC' } as any, - }); - - return { - data, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - /** - * Find one record by ID for a tenant - */ - async findById(ctx: ServiceContext, id: string): Promise { - return this.repository.findOne({ - where: { - id, - tenantId: ctx.tenantId, - deletedAt: null, - } as unknown as FindOptionsWhere, - }); - } - - /** - * Find one record by criteria - */ - async findOne( - ctx: ServiceContext, - where: FindOptionsWhere - ): Promise { - return this.repository.findOne({ - where: { - tenantId: ctx.tenantId, - deletedAt: null, - ...where, - } as FindOptionsWhere, - }); - } - - /** - * Find records by custom options - */ - async find( - ctx: ServiceContext, - options: FindManyOptions - ): Promise { - return this.repository.find({ - ...options, - where: { - tenantId: ctx.tenantId, - deletedAt: null, - ...(options.where || {}), - } as FindOptionsWhere, - }); - } - - /** - * Create a new record - */ - async create( - ctx: ServiceContext, - data: DeepPartial - ): Promise { - const entity = this.repository.create({ - ...data, - tenantId: ctx.tenantId, - createdById: ctx.userId, - } as DeepPartial); - - return this.repository.save(entity); - } - - /** - * Update an existing record - */ - async update( - ctx: ServiceContext, - id: string, - data: DeepPartial - ): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return null; - } - - const updated = this.repository.merge(existing, { - ...data, - updatedById: ctx.userId, - } as DeepPartial); - - return this.repository.save(updated); - } - - /** - * Soft delete a record - */ - async softDelete(ctx: ServiceContext, id: string): Promise { - const existing = await this.findById(ctx, id); - if (!existing) { - return false; - } - - await this.repository.update( - { id, tenantId: ctx.tenantId } as unknown as FindOptionsWhere, - { - deletedAt: new Date(), - deletedById: ctx.userId, - } as any - ); - - return true; - } - - /** - * Hard delete a record (use with caution) - */ - async hardDelete(ctx: ServiceContext, id: string): Promise { - const result = await this.repository.delete({ - id, - tenantId: ctx.tenantId, - } as unknown as FindOptionsWhere); - - return (result.affected ?? 0) > 0; - } - - /** - * Count records - */ - async count( - ctx: ServiceContext, - where?: FindOptionsWhere - ): Promise { - return this.repository.count({ - where: { - tenantId: ctx.tenantId, - deletedAt: null, - ...where, - } as unknown as FindOptionsWhere, - }); - } - - /** - * Check if a record exists - */ - async exists( - ctx: ServiceContext, - where: FindOptionsWhere - ): Promise { - const count = await this.count(ctx, where); - return count > 0; - } -} diff --git a/projects/erp-construccion/backend/src/shared/services/index.ts b/projects/erp-construccion/backend/src/shared/services/index.ts deleted file mode 100644 index 74d25cf04..000000000 --- a/projects/erp-construccion/backend/src/shared/services/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Shared Services - Exports - */ - -export * from './base.service'; diff --git a/projects/erp-construccion/backend/tsconfig.json b/projects/erp-construccion/backend/tsconfig.json deleted file mode 100644 index aeeb34d22..000000000 --- a/projects/erp-construccion/backend/tsconfig.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "commonjs", - "lib": ["ES2022"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "moduleResolution": "node", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "strictPropertyInitialization": false, - "noImplicitAny": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "baseUrl": "./src", - "paths": { - "@shared/*": ["shared/*"], - "@modules/*": ["modules/*"], - "@config/*": ["shared/config/*"], - "@types/*": ["shared/types/*"], - "@utils/*": ["shared/utils/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] -} diff --git a/projects/erp-construccion/database/HERENCIA-ERP-CORE.md b/projects/erp-construccion/database/HERENCIA-ERP-CORE.md deleted file mode 100644 index 72e7a691d..000000000 --- a/projects/erp-construccion/database/HERENCIA-ERP-CORE.md +++ /dev/null @@ -1,362 +0,0 @@ -# Referencia de Base de Datos - ERP Construcci贸n - -**Fecha:** 2025-12-09 -**Versi贸n:** 1.2 -**Proyecto:** ERP Construcci贸n -**Nivel:** 2B.2 (Proyecto Independiente) - ---- - -## RESUMEN - -ERP Construcci贸n es un **proyecto independiente** que implementa y adapta patrones del ERP-Core para el dominio de construcci贸n de vivienda. No es una extensi贸n del core, sino un sistema aut贸nomo que: - -1. **Implementa** schemas propios basados en patrones del core -2. **Adapta** estructuras de datos al dominio de construcci贸n -3. **Reutiliza** c贸digo y patrones donde tiene sentido -4. **Opera independientemente** del ERP-Core - -**DDL de Referencia (Core):** `apps/erp-core/database/ddl/` -**DDL Propio:** `database/schemas/` - ---- - -## ARQUITECTURA DEL PROYECTO - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ERP CORE (Referencia) 鈹 -鈹 Patrones, specs y estructuras reutilizables 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 auth 鈹 鈹 core 鈹 鈹俰nventory鈹 鈹 financial鈹 鈹 -鈹 鈹 patrones鈹 鈹 patrones鈹 鈹 patrones鈹 鈹 patrones 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹 REFERENCIA / FORK - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ERP CONSTRUCCI脫N (Proyecto Independiente) 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹俢onstruction 鈹 鈹 hr 鈹 鈹 hse 鈹 鈹 -鈹 鈹 24 tbl 鈹 鈹 8 tbl 鈹 鈹 58 tbl 鈹 鈹 -鈹 鈹 (proyectos) 鈹 鈹 (empleados) 鈹 鈹 (seguridad) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 estimates 鈹 鈹 infonavit 鈹 鈹 inventory 鈹 鈹 -鈹 鈹 8 tbl 鈹 鈹 8 tbl 鈹 鈹 4 tbl 鈹 鈹 -鈹 鈹(estimaci贸n) 鈹 鈹 (ruv) 鈹 鈹 (ext) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 purchase 鈹 Schemas propios: 7 鈹 -鈹 鈹 5 tbl 鈹 Tablas propias: 110 鈹 -鈹 鈹 (ext) 鈹 Opera de forma INDEPENDIENTE 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## PATRONES REUTILIZADOS DEL CORE - -Los siguientes patrones del ERP-Core fueron **adaptados e implementados** en este proyecto: - -| Patr贸n del Core | Adaptaci贸n en Construcci贸n | -|-----------------|---------------------------| -| `auth.*` | Implementaci贸n propia de autenticaci贸n multi-tenant | -| `core.partners` | Contratistas, proveedores, clientes | -| `inventory.*` | Materiales de construcci贸n, almacenes de obra | -| `projects.*` | Obras, fraccionamientos, etapas | -| `hr.*` | Personal de obra, cuadrillas, asistencias | - -**Nota:** Este proyecto NO depende del ERP-Core para ejecutarse. Implementa sus propios schemas basados en los patrones de referencia. - ---- - -## SCHEMAS ESPEC脥FICOS DE CONSTRUCCI脫N - -### 1. Schema `construction` (24 tablas) - -**Prop贸sito:** Gesti贸n de proyectos de obra, estructura y avances - -```sql --- DDL: 01-construction-schema-ddl.sql --- Estructura de proyecto (8 tablas): --- fraccionamientos, etapas, manzanas, lotes, torres, niveles, departamentos, prototipos --- Presupuestos y Conceptos (3 tablas): --- conceptos, presupuestos, presupuesto_partidas --- Programaci贸n y Avances (5 tablas): --- programa_obra, programa_actividades, avances_obra, fotos_avance, bitacora_obra --- Calidad (5 tablas): --- checklists, checklist_items, inspecciones, inspeccion_resultados, tickets_postventa --- Contratos (3 tablas): --- subcontratistas, contratos, contrato_partidas -``` - -### 2. Schema `hr` extendido (8 tablas) - -**Prop贸sito:** Gesti贸n de personal de obra, asistencias, destajo - -```sql --- DDL: 02-hr-schema-ddl.sql --- Extiende: hr schema del core - -hr.employee_construction -- Extensi贸n empleados construcci贸n -hr.asistencias -- Registro con GPS/biom茅trico -hr.asistencia_biometrico -- Datos biom茅tricos -hr.geocercas -- Validaci贸n GPS (PostGIS) -hr.destajo -- Trabajo a destajo -hr.destajo_detalle -- Mediciones destajo -hr.cuadrillas -- Equipos de trabajo -hr.cuadrilla_miembros -- Miembros cuadrillas -``` - -### 3. Schema `hse` (58 tablas) - -**Prop贸sito:** Health, Safety & Environment - -```sql --- DDL: 03-hse-schema-ddl.sql --- Implementa 8 requerimientos funcionales (RF-MAA017-001 a 008) - -Grupos de tablas: -- Gesti贸n de Incidentes (5 tablas) -- Control de Capacitaciones (6 tablas) -- Inspecciones de Seguridad (7 tablas) -- Control de EPP (7 tablas) -- Cumplimiento STPS (11 tablas) -- Gesti贸n Ambiental (9 tablas) -- Permisos de Trabajo (8 tablas) -- Indicadores HSE (7 tablas) -``` - -### 4. Schema `estimates` (8 tablas) - -**Prop贸sito:** Estimaciones, anticipos, retenciones - -```sql --- DDL: 04-estimates-schema-ddl.sql --- M贸dulo: MAI-008 (Estimaciones y Facturaci贸n) - -estimates.estimaciones -- Estimaciones de obra -estimates.estimacion_conceptos -- Conceptos estimados -estimates.generadores -- N煤meros generadores -estimates.anticipos -- Anticipos de obra -estimates.amortizaciones -- Amortizaci贸n de anticipos -estimates.retenciones -- Retenciones (garant铆a, IMSS, ISR) -estimates.fondo_garantia -- Fondo de garant铆a -estimates.estimacion_workflow -- Workflow de aprobaci贸n -``` - -### 5. Schema `infonavit` (8 tablas) - -**Prop贸sito:** Integraci贸n INFONAVIT, RUV, derechohabientes - -```sql --- DDL: 05-infonavit-schema-ddl.sql --- M贸dulos: MAI-010/011 (CRM Derechohabientes, Integraci贸n INFONAVIT) - -infonavit.registro_infonavit -- Registro RUV -infonavit.oferta_vivienda -- Oferta registrada -infonavit.derechohabientes -- Derechohabientes -infonavit.asignacion_vivienda -- Asignaciones -infonavit.actas -- Actas de entrega -infonavit.acta_viviendas -- Viviendas en acta -infonavit.reportes_infonavit -- Reportes RUV -infonavit.historico_puntos -- Hist贸rico puntos ecol贸gicos -``` - -### 6. Schema `inventory` extensi贸n (4 tablas) - -**Prop贸sito:** Almacenes de proyecto, requisiciones de obra - -```sql --- DDL: 06-inventory-ext-schema-ddl.sql --- Extiende: inventory schema del core - -inventory.almacenes_proyecto -- Almacenes por obra -inventory.requisiciones_obra -- Requisiciones desde obra -inventory.requisicion_lineas -- L铆neas de requisici贸n -inventory.consumos_obra -- Consumos por lote/concepto -``` - -### 7. Schema `purchase` extensi贸n (5 tablas) - -**Prop贸sito:** 脫rdenes de compra construcci贸n, comparativos - -```sql --- DDL: 07-purchase-ext-schema-ddl.sql --- Extiende: purchase schema del core - -purchase.purchase_order_construction -- Extensi贸n OC -purchase.supplier_construction -- Extensi贸n proveedores -purchase.comparativo_cotizaciones -- Cuadro comparativo -purchase.comparativo_proveedores -- Proveedores en comparativo -purchase.comparativo_productos -- Productos cotizados -``` - ---- - -## ORDEN DE EJECUCI脫N DDL - -Para recrear la base de datos completa: - -```bash -# PASO 1: Cargar ERP Core (base) -cd apps/erp-core/database -./scripts/reset-database.sh --force - -# PASO 2: Cargar extensiones de Construcci贸n (orden importante) -cd apps/verticales/construccion/database -psql $DATABASE_URL -f schemas/01-construction-schema-ddl.sql # 24 tablas -psql $DATABASE_URL -f schemas/02-hr-schema-ddl.sql # 8 tablas -psql $DATABASE_URL -f schemas/03-hse-schema-ddl.sql # 58 tablas -psql $DATABASE_URL -f schemas/04-estimates-schema-ddl.sql # 8 tablas -psql $DATABASE_URL -f schemas/05-infonavit-schema-ddl.sql # 8 tablas -psql $DATABASE_URL -f schemas/06-inventory-ext-schema-ddl.sql # 4 tablas -psql $DATABASE_URL -f schemas/07-purchase-ext-schema-ddl.sql # 5 tablas -``` - -**Nota:** Los archivos 06 y 07 dependen de que 01-construction est茅 instalado. - ---- - -## DEPENDENCIAS CRUZADAS - -### Tablas de Construcci贸n que dependen del Core - -| Tabla Construcci贸n | Depende de (Core) | -|--------------------|-------------------| -| `construccion.proyectos` | `core.partners` (cliente) | -| `construccion.proyectos` | `auth.users` (created_by) | -| `construccion.fraccionamientos` | `construccion.proyectos` | -| `hr.employees` | `auth.users` | -| `hr.employee_fraccionamientos` | `construccion.fraccionamientos` | -| `hse.incidentes` | `construccion.fraccionamientos` | -| `hse.incidente_involucrados` | `hr.employees` | -| `hse.*` | `auth.users` (auditoria) | - ---- - -## SPECS DEL CORE IMPLEMENTADAS - -| Spec Core | Aplicaci贸n en Construcci贸n | Estado | -|-----------|---------------------------|--------| -| SPEC-VALORACION-INVENTARIO | Materiales de construcci贸n | 鉁 DDL LISTO | -| SPEC-TRAZABILIDAD-LOTES-SERIES | Lotes de concreto, acero | 鉁 DDL LISTO | -| SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN | Partidas de obra | PENDIENTE | -| SPEC-MAIL-THREAD-TRACKING | Historial de presupuestos | PENDIENTE | -| SPEC-WIZARD-TRANSIENT-MODEL | Asistentes de estimaciones | PENDIENTE | - -### Correcciones de DDL Core (2025-12-08) - -El DDL del ERP-Core fue corregido para resolver FK inv谩lidas: - -1. **stock_valuation_layers**: Campos `journal_entry_id` y `journal_entry_line_id` (antes `account_move_*`) -2. **stock_move_consume_rel**: Nueva tabla de trazabilidad (antes `move_line_consume_rel`) -3. **category_stock_accounts**: FK corregida a `core.product_categories` -4. **product_categories**: ALTERs ahora apuntan a schema `core` - -Estas correcciones permiten que el DDL de inventory se ejecute correctamente. - -### Correcciones de DDL Construcci贸n (2025-12-08) - -El DDL de la vertical Construcci贸n fue corregido para alinearse con ERP-Core: - -| Archivo | Correcciones | Detalle | -|---------|--------------|---------| -| `01-construction-schema-ddl.sql` | 4 FK | `core.tenants` 鈫 `auth.tenants`, `core.users` 鈫 `auth.users` | -| `02-hr-schema-ddl.sql` | 4 FK | Referencias corregidas a `auth.*` | -| `03-hse-schema-ddl.sql` | 42 FK | Todas las referencias corregidas | -| **Total** | **50 FK** | Ahora usa `auth.tenants` y `auth.users` correctamente | - -**Verificaciones de prerequisitos actualizadas:** -- Los DDL ahora validan que `auth.tenants` y `auth.users` existan antes de crear tablas -- ERP-Core debe estar instalado antes de ejecutar DDL de Construcci贸n - ---- - -## MAPEO DE NOMENCLATURA - -| Core | Construcci贸n | -|------|--------------| -| `core.partners` | Contratistas, proveedores | -| `inventory.products` | Materiales de construcci贸n | -| `inventory.locations` | Almacenes de obra | -| `projects.projects` | Base para `construccion.proyectos` | -| `hr.employees` | Personal de obra | -| `purchase.orders` | 脫rdenes de compra de materiales | - ---- - -## VALIDACI脫N DE HERENCIA - -### Verificar schemas heredados - -```sql --- Verificar que existen los schemas del core -SELECT schema_name -FROM information_schema.schemata -WHERE schema_name IN ('auth', 'core', 'financial', 'inventory', - 'purchase', 'projects', 'hr', 'analytics', 'system'); -``` - -### Verificar extensiones de construcci贸n - -```sql --- Verificar schemas espec铆ficos -SELECT schema_name -FROM information_schema.schemata -WHERE schema_name IN ('construccion', 'hse'); - --- Contar tablas por schema -SELECT schemaname, COUNT(*) as tables -FROM pg_tables -WHERE schemaname IN ('construccion', 'hr', 'hse') -GROUP BY schemaname; -``` - ---- - -## SPECS DEL CORE APLICABLES - -Seg煤n el [MAPEO-SPECS-VERTICALES.md](../../../../erp-core/docs/04-modelado/MAPEO-SPECS-VERTICALES.md): - -| Categor铆a | Total | Obligatorias | Opcionales | No Aplican | -|-----------|-------|--------------|------------|------------| -| **Construcci贸n** | 30 | 22 | 4 | 4 | - -### SPECS Cr铆ticas para Construcci贸n - -| SPEC | Aplicaci贸n | Estado DDL | -|------|------------|------------| -| SPEC-VALORACION-INVENTARIO | Costeo de materiales | 鉁 DDL LISTO | -| SPEC-TRAZABILIDAD-LOTES-SERIES | Lotes de concreto, acero | 鉁 DDL LISTO | -| SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN | Partidas de obra | PENDIENTE | -| SPEC-PRESUPUESTOS-REVISIONES | Control presupuestal | PENDIENTE | -| SPEC-RRHH-EVALUACIONES-SKILLS | Personal de obra | PENDIENTE | - -### SPECS No Aplicables - -- `SPEC-INTEGRACION-CALENDAR` - Sin necesidad de calendario externo -- `SPEC-OAUTH2-SOCIAL-LOGIN` - Opcional, no cr铆tico -- `SPEC-INVENTARIOS-CICLICOS` - Opcional para construcci贸n -- `SPEC-CONSOLIDACION-FINANCIERA` - Opcional para construcci贸n - ---- - -## REFERENCIAS - -- ERP Core DDL: `apps/erp-core/database/ddl/` -- ERP Core README: `apps/erp-core/database/README.md` -- MAPEO-SPECS-VERTICALES: `apps/erp-core/docs/04-modelado/MAPEO-SPECS-VERTICALES.md` -- DATABASE_INVENTORY.yml: `orchestration/inventarios/` - ---- - -**Documento de herencia oficial** -**脷ltima actualizaci贸n:** 2025-12-09 -**Total schemas:** 7 | **Total tablas:** 110 diff --git a/projects/erp-construccion/database/README.md b/projects/erp-construccion/database/README.md deleted file mode 100644 index c63e6e13f..000000000 --- a/projects/erp-construccion/database/README.md +++ /dev/null @@ -1,185 +0,0 @@ -# Database - MVP Sistema Administraci贸n de Obra - -**Stack:** PostgreSQL 15+ con PostGIS -**Versi贸n:** 1.0.0 -**Fecha:** 2025-11-20 - ---- - -## 馃搵 DESCRIPCI脫N - -Base de datos del sistema de administraci贸n de obra e INFONAVIT. - -**Schemas principales:** -- `auth_management` - Autenticaci贸n y usuarios -- `project_management` - Proyectos y estructura de obra -- `financial_management` - Presupuestos y control financiero -- `purchasing_management` - Compras e inventarios -- `construction_management` - Control de obra y avances -- `quality_management` - Calidad y postventa -- `infonavit_management` - Integraci贸n INFONAVIT - ---- - -## 馃彈锔 ESTRUCTURA - -``` -ddl/ -鈹溾攢鈹 00-init.sql # Inicializaci贸n + extensiones -鈹斺攢鈹 schemas/ # Schemas por contexto - 鈹溾攢鈹 auth_management/ - 鈹 鈹溾攢鈹 tables/ # Tablas (01-users.sql, 02-roles.sql, ...) - 鈹 鈹溾攢鈹 functions/ # Funciones SQL - 鈹 鈹溾攢鈹 triggers/ # Triggers - 鈹 鈹斺攢鈹 views/ # Vistas - 鈹溾攢鈹 project_management/ - 鈹 鈹斺攢鈹 ... - 鈹斺攢鈹 [otros schemas]/ - -seeds/ -鈹溾攢鈹 dev/ # Datos de desarrollo -鈹斺攢鈹 prod/ # Datos de producci贸n inicial - -migrations/ # Migraciones versionadas -scripts/ # Scripts de utilidad -``` - ---- - -## 馃殌 SETUP INICIAL - -### 1. Crear Base de Datos - -```bash -# Ejecutar script de creaci贸n -cd scripts -./create-database.sh -``` - -### 2. Ejecutar DDL - -```bash -# Ejecutar inicializaci贸n -psql $DATABASE_URL -f ddl/00-init.sql - -# Ejecutar schemas (en orden) -psql $DATABASE_URL -f ddl/schemas/auth_management/tables/01-users.sql -# ... etc -``` - -### 3. Cargar Seeds (desarrollo) - -```bash -psql $DATABASE_URL -f seeds/dev/01-users.sql -psql $DATABASE_URL -f seeds/dev/02-projects.sql -``` - ---- - -## 馃敡 SCRIPTS DISPONIBLES - -| Script | Descripci贸n | -|--------|-------------| -| `create-database.sh` | Crea la base de datos desde cero | -| `reset-database.sh` | Elimina y recrea la base de datos | -| `run-migrations.sh` | Ejecuta migraciones pendientes | -| `backup-database.sh` | Crea backup de la base de datos | - ---- - -## 馃搳 CONVENCIONES - -### Nomenclatura - -Seguir **ESTANDARES-NOMENCLATURA.md**: -- Schemas: `snake_case` + sufijo `_management` -- Tablas: `snake_case` plural -- Columnas: `snake_case` singular -- 脥ndices: `idx_{tabla}_{columnas}` -- Foreign Keys: `fk_{origen}_to_{destino}` -- Constraints: `chk_{tabla}_{columna}` - -### Estructura de Archivo DDL - -```sql --- ============================================================================ --- Tabla: nombre_tabla --- Schema: nombre_schema --- Descripci贸n: [descripci贸n] --- Autor: Database-Agent --- Fecha: YYYY-MM-DD --- ============================================================================ - -DROP TABLE IF EXISTS schema.tabla CASCADE; - -CREATE TABLE schema.tabla ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - -- columnas... - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -COMMENT ON TABLE schema.tabla IS '[descripci贸n]'; -COMMENT ON COLUMN schema.tabla.columna IS '[descripci贸n]'; - -CREATE INDEX idx_tabla_columna ON schema.tabla(columna); -``` - ---- - -## 馃攳 VALIDACI脫N - -### Verificar Schemas - -```sql -SELECT schema_name -FROM information_schema.schemata -WHERE schema_name LIKE '%_management'; -``` - -### Verificar Tablas - -```sql -SELECT table_schema, table_name, - (SELECT COUNT(*) FROM information_schema.columns - WHERE table_schema = t.table_schema - AND table_name = t.table_name) as column_count -FROM information_schema.tables t -WHERE table_schema LIKE '%_management' -ORDER BY table_schema, table_name; -``` - -### Verificar 脥ndices - -```sql -SELECT tablename, indexname -FROM pg_indexes -WHERE schemaname LIKE '%_management' -ORDER BY tablename; -``` - ---- - -## 馃摎 REFERENCIAS - -- [DIRECTIVA-DISENO-BASE-DATOS.md](../../orchestration/directivas/DIRECTIVA-DISENO-BASE-DATOS.md) -- [ESTANDARES-NOMENCLATURA.md](../../orchestration/directivas/ESTANDARES-NOMENCLATURA.md) -- [MVP-APP.md](../../docs/00-overview/MVP-APP.md) - ---- - -## 馃摑 VARIABLES DE ENTORNO - -```bash -DATABASE_URL=postgresql://usuario:password@localhost:5432/nombre_db -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=erp_construccion -DB_USER=postgres -DB_PASSWORD=password -``` - ---- - -**Mantenido por:** Database-Agent -**脷ltima actualizaci贸n:** 2025-11-20 diff --git a/projects/erp-construccion/database/VALIDACION-DDL-INVENTARIOS.md b/projects/erp-construccion/database/VALIDACION-DDL-INVENTARIOS.md deleted file mode 100644 index 4ee0bb54e..000000000 --- a/projects/erp-construccion/database/VALIDACION-DDL-INVENTARIOS.md +++ /dev/null @@ -1,323 +0,0 @@ -# VALIDACION DDL vs INVENTARIOS - ERP CONSTRUCCION -**Fecha:** 2025-12-06 -**Version:** 1.0.0 -**Generado por:** Requirements-Analyst - ---- - -## RESUMEN EJECUTIVO - -| Metrica | Inventarios | DDL Real | Estado | -|---------|-------------|----------|--------| -| **Schemas** | 6 (+ 3 pendientes) | 7 implementados | DISCREPANCIA | -| **Tablas declaradas** | 57 | 65 | DISCREPANCIA | -| **HSE Schema** | "pendiente" | 58 tablas implementadas | DESACTUALIZADO | -| **ENUMs** | 22 | 89 (22 base + 67 HSE) | DESACTUALIZADO | - -### Conclusion General -Los inventarios MASTER_INVENTORY.yml y DATABASE_INVENTORY.yml estan **DESACTUALIZADOS** respecto al DDL implementado. El schema HSE con 58 tablas y 67 ENUMs ya fue implementado pero los inventarios lo marcan como "pendiente". - ---- - -## 1. ANALISIS DE OBJETOS DDL IMPLEMENTADOS - -### 1.1 Schemas Creados (7 schemas de negocio) - -| Schema | Origen | Tablas | ENUMs | Estado | -|--------|--------|--------|-------|--------| -| core | construccion | 2 | 0 | Implementado | -| core_shared | construccion | 0 | 0 | Implementado (funciones) | -| construction | construccion | 2 | 0 | Implementado | -| hr | construccion | 3 | 0 | Implementado | -| hse | construccion | 58 | 67 | **IMPLEMENTADO** | -| estimates | construccion | 0 | 0 | Schema vacio | -| infonavit | construccion | 0 | 0 | Schema vacio | - -### 1.2 Tablas por Schema (Conteo Real del DDL) - -#### core (2 tablas) -- `core.tenants` - Multi-tenancy base -- `core.users` - Usuarios base - -#### construction (2 tablas) -- `construction.proyectos` - Proyectos de desarrollo -- `construction.fraccionamientos` - Obras/fraccionamientos - -#### hr (3 tablas) -- `hr.employees` - Empleados -- `hr.puestos` - Catalogo de puestos -- `hr.employee_fraccionamientos` - Asignacion empleados a obras - -#### hse (58 tablas) - RF-MAA017-001 a RF-MAA017-008 - -**RF-MAA017-001 Gestion de Incidentes (5 tablas):** -- `hse.incidentes` -- `hse.incidente_involucrados` -- `hse.incidente_investigacion` -- `hse.incidente_acciones` -- `hse.incidente_evidencias` - -**RF-MAA017-002 Control de Capacitaciones (6 tablas):** -- `hse.capacitaciones` -- `hse.capacitacion_matriz` -- `hse.instructores` -- `hse.capacitacion_sesiones` -- `hse.capacitacion_asistentes` -- `hse.constancias_dc3` - -**RF-MAA017-003 Inspecciones de Seguridad (7 tablas):** -- `hse.tipos_inspeccion` -- `hse.checklist_items` -- `hse.programa_inspecciones` -- `hse.inspecciones` -- `hse.inspeccion_evaluaciones` -- `hse.hallazgos` -- `hse.hallazgo_evidencias` - -**RF-MAA017-004 Control de EPP (7 tablas):** -- `hse.epp_catalogo` -- `hse.epp_matriz_puesto` -- `hse.epp_asignaciones` -- `hse.epp_inspecciones` -- `hse.epp_bajas` -- `hse.epp_inventario` -- `hse.epp_movimientos` - -**RF-MAA017-005 Cumplimiento STPS (11 tablas):** -- `hse.normas_stps` -- `hse.norma_requisitos` -- `hse.cumplimiento_obra` -- `hse.comision_seguridad` -- `hse.comision_integrantes` -- `hse.comision_recorridos` -- `hse.programa_seguridad` -- `hse.programa_actividades` -- `hse.documentos_stps` -- `hse.auditorias` - -**RF-MAA017-006 Gestion Ambiental (9 tablas):** -- `hse.residuos_catalogo` -- `hse.residuos_generacion` -- `hse.almacen_temporal` -- `hse.proveedores_ambientales` -- `hse.manifiestos_residuos` -- `hse.manifiesto_detalle` -- `hse.impacto_ambiental` -- `hse.quejas_ambientales` - -**RF-MAA017-007 Permisos de Trabajo (8 tablas):** -- `hse.tipos_permiso_trabajo` -- `hse.permisos_trabajo` -- `hse.permiso_personal` -- `hse.permiso_autorizaciones` -- `hse.permiso_checklist` -- `hse.permiso_monitoreos` -- `hse.permiso_eventos` -- `hse.permiso_documentos` - -**RF-MAA017-008 Indicadores HSE (6 tablas):** -- `hse.indicadores_config` -- `hse.indicadores_meta_obra` -- `hse.indicadores_valores` -- `hse.horas_trabajadas` -- `hse.dias_sin_accidente` -- `hse.reportes_programados` -- `hse.alertas_indicadores` - -### 1.3 ENUMs Implementados (89 total) - -**ENUMs HSE (67):** -- Incidentes: `tipo_incidente`, `gravedad_incidente`, `estado_incidente`, `rol_involucrado`, `factor_causa` -- Capacitaciones: `tipo_capacitacion`, `estado_sesion` -- Inspecciones: `frecuencia`, `estado_inspeccion`, `resultado_evaluacion`, `gravedad_hallazgo`, `estado_hallazgo`, `tipo_evidencia` -- EPP: `categoria_epp`, `estado_epp`, `estado_inspeccion_epp`, `motivo_baja_epp`, `tipo_movimiento_epp` -- STPS: `estado_comision`, `rol_comision`, `representacion`, `estado_recorrido`, `estado_programa`, `tipo_actividad_programa`, `estado_actividad`, `tipo_documento_stps`, `tipo_auditoria`, `resultado_auditoria`, `estado_cumplimiento` -- Ambiental: `categoria_residuo`, `unidad_residuo`, `estado_residuo`, `estado_almacen`, `tipo_proveedor_ambiental`, `estado_manifiesto`, `tipo_impacto`, `severidad`, `probabilidad`, `nivel_riesgo`, `estado_impacto`, `origen_queja`, `tipo_queja`, `estado_queja` -- Permisos: `estado_permiso`, `rol_permiso`, `decision_autorizacion`, `momento_checklist`, `tipo_evento_permiso` -- Indicadores: `tipo_indicador`, `frecuencia_calculo`, `periodo_tipo`, `estado_semaforo`, `fuente_horas`, `tipo_reporte_hse`, `formato_reporte`, `tipo_alerta_indicador` - ---- - -## 2. DISCREPANCIAS DETECTADAS - -### 2.1 MASTER_INVENTORY.yml - -| Campo | Valor Actual | Valor Correcto | Accion | -|-------|--------------|----------------|--------| -| `metricas.database.schemas` | 6 | 7 | Actualizar a 7 | -| `metricas.database.tablas` | 57 | 65 | Actualizar a 65 | -| `metricas.database.enums` | 22 | 89 | Actualizar a 89 | -| `schemas.hse.estado` | "pendiente" | "implementado" | Actualizar | -| `schemas.hse.tablas` | 0 | 58 | Actualizar a 58 | -| `schemas.hse.ddl` | "pendiente" | "03-hse-schema-ddl.sql" | Actualizar | -| `modulos_fase_3.MAA-017.tablas` | 11 items | 58 items | Actualizar lista completa | - -### 2.2 DATABASE_INVENTORY.yml - -| Campo | Valor Actual | Valor Correcto | Accion | -|-------|--------------|----------------|--------| -| `resumen.schemas` | 6 | 7 | Actualizar | -| `resumen.tablas` | 57 | 65 | Actualizar | -| `resumen.enums` | 22 | 89 | Actualizar | -| Schema `hse` | No existe | Agregar seccion completa | FALTA | -| Tablas HSE | 0 | 58 | Agregar todas | - -### 2.3 Tablas Faltantes en Inventarios - -Las siguientes 58 tablas HSE + 2 core + 2 construction + 3 hr = 65 tablas existen en DDL pero el inventario solo declara 57: - -**FALTANTES:** -- Todas las 58 tablas del schema `hse` -- Las 2 tablas minimas de `core` (tenants, users) -- La tabla `hr.puestos` -- La tabla `hr.employee_fraccionamientos` -- Las tablas de `construction` tienen nombres distintos en inventario vs DDL: - - Inventario: `fraccionamientos` (sin proyecto_id directo) - - DDL: `proyectos` + `fraccionamientos` (con proyecto_id) - ---- - -## 3. TRAZABILIDAD RF -> DDL - -### 3.1 Modulo MAA-017 Seguridad HSE - -| RF | Nombre | Tablas DDL | Trazabilidad | -|----|--------|------------|--------------| -| RF-MAA017-001 | Gestion de Incidentes | 5 tablas | COMPLETA | -| RF-MAA017-002 | Control de Capacitaciones | 6 tablas | COMPLETA | -| RF-MAA017-003 | Inspecciones de Seguridad | 7 tablas | COMPLETA | -| RF-MAA017-004 | Control de EPP | 7 tablas | COMPLETA | -| RF-MAA017-005 | Cumplimiento STPS | 11 tablas | COMPLETA | -| RF-MAA017-006 | Gestion Ambiental | 9 tablas | COMPLETA | -| RF-MAA017-007 | Permisos de Trabajo | 8 tablas | COMPLETA | -| RF-MAA017-008 | Indicadores HSE | 7 tablas | COMPLETA | - -**Total:** 8 RFs -> 58 tablas + 67 ENUMs - -### 3.2 Otros Modulos (Inventariados pero NO implementados en DDL) - -| Modulo | Tablas Inventario | Tablas DDL | Estado | -|--------|-------------------|------------|--------| -| MAI-002 | 8 | 2 (proyectos, fraccionamientos) | PARCIAL | -| MAI-003 | 3 | 0 | SIN DDL | -| MAI-004 | 9 | 0 | SIN DDL | -| MAI-005 | 5 | 0 | SIN DDL | -| MAI-007 | 8 | 3 (employees, puestos, employee_fracc) | PARCIAL | -| MAI-008 | 8 | 0 | SIN DDL | -| MAI-009 | 5 | 0 | SIN DDL | -| MAI-010 | 1 | 0 | SIN DDL | -| MAI-011 | 7 | 0 | SIN DDL | -| MAI-012 | 3 | 0 | SIN DDL | - ---- - -## 4. POLITICA DE CARGA LIMPIA - -### 4.1 Cumplimiento - -| Check | Estado | Detalle | -|-------|--------|---------| -| No carpeta migrations/ | OK | No existe | -| No archivos fix-*.sql | OK | No existen | -| No archivos migration-*.sql | OK | No existen | -| Existe drop-and-recreate-database.sh | OK | Existe y es ejecutable | -| DDL en schemas/ | OK | 3 archivos SQL | -| Archivo init existe | OK | init-scripts/01-init-database.sql | - -**Resultado:** POLITICA CUMPLIDA (6/6 checks) - -### 4.2 Archivos DDL Actuales - -``` -database/ -鈹溾攢鈹 init-scripts/ -鈹 鈹斺攢鈹 01-init-database.sql # Extensiones, schemas base, funciones core -鈹溾攢鈹 schemas/ -鈹 鈹溾攢鈹 01-construction-schema-ddl.sql # proyectos, fraccionamientos -鈹 鈹溾攢鈹 02-hr-schema-ddl.sql # employees, puestos, employee_fracc -鈹 鈹斺攢鈹 03-hse-schema-ddl.sql # 58 tablas HSE -鈹溾攢鈹 drop-and-recreate-database.sh # Script carga limpia -鈹斺攢鈹 validate-clean-load-policy.sh # Validador de politica -``` - ---- - -## 5. ACCIONES REQUERIDAS - -### 5.1 Prioridad ALTA (Inventarios Desactualizados) - -1. **Actualizar MASTER_INVENTORY.yml:** - - Cambiar `schemas.hse.estado` de "pendiente" a "implementado" - - Cambiar `schemas.hse.tablas` de 0 a 58 - - Agregar `schemas.hse.ddl: 03-hse-schema-ddl.sql` - - Actualizar conteos globales - -2. **Actualizar DATABASE_INVENTORY.yml:** - - Agregar seccion completa para schema `hse` con 58 tablas - - Agregar 67 ENUMs de HSE - - Actualizar conteos en resumen - -### 5.2 Prioridad MEDIA (Completar DDL Faltante) - -Los siguientes schemas tienen tablas inventariadas pero NO implementadas: - -| Schema | Tablas Faltantes | DDL Requerido | -|--------|------------------|---------------| -| construction | 22 tablas | construction-schema-ddl.sql (expandir) | -| estimates | 8 tablas | estimates-schema-ddl.sql (crear) | -| infonavit | 8 tablas | infonavit-schema-ddl.sql (crear) | -| hr | 5 tablas | hr-schema-ddl.sql (expandir) | -| inventory | 4 tablas | inventory-ext-schema-ddl.sql (crear) | -| purchase | 5 tablas | purchase-ext-schema-ddl.sql (crear) | - -### 5.3 Prioridad BAJA (Documentacion) - -- Crear TRACEABILITY.yml por modulo cuando se implemente -- Actualizar README de database/ con estructura actual - ---- - -## 6. RESUMEN FINAL - -### Estado Actual - -``` -DDL Implementado: -鈹溾攢鈹 core: 2 tablas (tenants, users) -鈹溾攢鈹 construction: 2 tablas -鈹溾攢鈹 hr: 3 tablas -鈹溾攢鈹 hse: 58 tablas + 67 ENUMs 鈫 IMPLEMENTADO (inventario dice "pendiente") -鈹溾攢鈹 estimates: schema vacio -鈹溾攢鈹 infonavit: schema vacio -鈹溾攢鈹 inventory: schema vacio -鈹斺攢鈹 purchase: schema vacio - -Total: 65 tablas, 89 ENUMs -``` - -### Inventarios Declaran - -``` -MASTER + DATABASE_INVENTORY: -鈹溾攢鈹 construction: 24 tablas (22 sin DDL) -鈹溾攢鈹 estimates: 8 tablas (sin DDL) -鈹溾攢鈹 infonavit: 8 tablas (sin DDL) -鈹溾攢鈹 hr: 8 tablas (5 sin DDL) -鈹溾攢鈹 inventory: 4 tablas (sin DDL) -鈹溾攢鈹 purchase: 5 tablas (sin DDL) -鈹斺攢鈹 hse: "pendiente" (INCORRECTO - tiene 58 tablas) - -Total declarado: 57 tablas, 22 ENUMs -``` - -### Gap Analysis - -| Categoria | Inventario | DDL Real | Diferencia | -|-----------|------------|----------|------------| -| Tablas | 57 | 65 | +8 (HSE +58, otros -50) | -| ENUMs | 22 | 89 | +67 (todos HSE) | -| Schemas implementados | 6 | 7 | +1 (hse) | - ---- - -**Documento generado automaticamente como parte de la validacion de Sprint 0.** diff --git a/projects/erp-construccion/database/_MAP.md b/projects/erp-construccion/database/_MAP.md deleted file mode 100644 index d0fb2981a..000000000 --- a/projects/erp-construccion/database/_MAP.md +++ /dev/null @@ -1,306 +0,0 @@ -# Database Map - ERP Construccion - -**Proyecto:** ERP Construccion -**Version:** 1.0 -**Ultima Actualizacion:** 2025-12-12 -**Total Schemas:** 7 -**Total Tablas:** 110 - ---- - -## NAVEGACION RAPIDA - -``` -database/ -鈹溾攢鈹 _MAP.md # Este archivo (indice maestro) -鈹溾攢鈹 schemas/ # DDL por schema -鈹 鈹溾攢鈹 01-construction-schema-ddl.sql # 24 tablas -鈹 鈹溾攢鈹 02-hr-schema-ddl.sql # 8 tablas -鈹 鈹溾攢鈹 03-hse-schema-ddl.sql # 58 tablas -鈹 鈹溾攢鈹 04-estimates-schema-ddl.sql # 8 tablas -鈹 鈹溾攢鈹 05-infonavit-schema-ddl.sql # 8 tablas -鈹 鈹溾攢鈹 06-inventory-ext-schema-ddl.sql # 4 tablas -鈹 鈹斺攢鈹 07-purchase-ext-schema-ddl.sql # 5 tablas -鈹溾攢鈹 init-scripts/ # Scripts de inicializacion -鈹 鈹斺攢鈹 01-init-database.sql -鈹溾攢鈹 migrations/ # Migraciones TypeORM -鈹溾攢鈹 seeds/ # Datos de prueba -鈹斺攢鈹 HERENCIA-ERP-CORE.md # Referencia a ERP-Core -``` - ---- - -## SCHEMAS OVERVIEW - -| # | Schema | Tablas | Descripcion | Estado | -|---|--------|--------|-------------|--------| -| 1 | `construction` | 24 | Proyectos, estructura, avances | 鉁 DDL | -| 2 | `hr` | 8 | RRHH, asistencias, cuadrillas | 鉁 DDL | -| 3 | `hse` | 58 | Seguridad, incidentes, EPP | 鉁 DDL | -| 4 | `estimates` | 8 | Estimaciones, anticipos | 鉁 DDL | -| 5 | `infonavit` | 8 | INFONAVIT, derechohabientes | 鉁 DDL | -| 6 | `inventory` | 4 | Extension inventario obra | 鉁 DDL | -| 7 | `purchase` | 5 | Extension compras obra | 鉁 DDL | - ---- - -## DETALLE POR SCHEMA - -### 1. Schema: `construction` (24 tablas) - -**DDL:** `schemas/01-construction-schema-ddl.sql` - -#### Estructura de Proyecto (8 tablas) - -| Tabla | Descripcion | FK Principales | -|-------|-------------|----------------| -| `proyectos` | Proyectos/obras | `auth.tenants`, `auth.users` | -| `fraccionamientos` | Fraccionamientos | `proyectos` | -| `etapas` | Etapas de construccion | `fraccionamientos` | -| `manzanas` | Manzanas | `etapas` | -| `lotes` | Lotes/unidades | `manzanas`, `prototipos` | -| `torres` | Torres (vertical) | `fraccionamientos` | -| `niveles` | Niveles/pisos | `torres` | -| `departamentos` | Departamentos | `niveles`, `prototipos` | -| `prototipos` | Tipos de vivienda | `fraccionamientos` | - -#### Presupuestos (3 tablas) - -| Tabla | Descripcion | -|-------|-------------| -| `conceptos` | Catalogo de conceptos de obra | -| `presupuestos` | Presupuestos maestros | -| `presupuesto_partidas` | Partidas presupuestales | - -#### Programacion y Avances (5 tablas) - -| Tabla | Descripcion | -|-------|-------------| -| `programa_obra` | Programa general de obra | -| `programa_actividades` | Actividades programadas | -| `avances_obra` | Registro de avances | -| `fotos_avance` | Evidencias fotograficas | -| `bitacora_obra` | Bitacora de obra | - -#### Calidad (5 tablas) - -| Tabla | Descripcion | -|-------|-------------| -| `checklists` | Checklists de calidad | -| `checklist_items` | Items de checklist | -| `inspecciones` | Inspecciones de calidad | -| `inspeccion_resultados` | Resultados | -| `tickets_postventa` | Tickets de postventa | - -#### Contratos (3 tablas) - -| Tabla | Descripcion | -|-------|-------------| -| `subcontratistas` | Catalogo subcontratistas | -| `contratos` | Contratos de obra | -| `contrato_partidas` | Partidas contratadas | - ---- - -### 2. Schema: `hr` (8 tablas) - -**DDL:** `schemas/02-hr-schema-ddl.sql` - -| Tabla | Descripcion | Caracteristicas | -|-------|-------------|-----------------| -| `employees` | Empleados base | Extension de core | -| `employee_construction` | Extension construccion | Campos especificos | -| `puestos` | Catalogo de puestos | - | -| `asistencias` | Registro asistencias | GPS, biometrico | -| `asistencia_biometrico` | Datos biometricos | - | -| `geocercas` | Geocercas para GPS | PostGIS | -| `destajo` | Trabajo a destajo | - | -| `destajo_detalle` | Mediciones destajo | - | -| `cuadrillas` | Cuadrillas de trabajo | - | -| `cuadrilla_miembros` | Miembros de cuadrilla | - | -| `employee_fraccionamientos` | Asignacion a fracc | - | - ---- - -### 3. Schema: `hse` (58 tablas) - -**DDL:** `schemas/03-hse-schema-ddl.sql` - -#### Gestion de Incidentes (5 tablas) -- `incidentes`, `incidente_involucrados`, `incidente_acciones` -- `incidente_evidencias`, `incidente_causas` - -#### Control de Capacitaciones (6 tablas) -- `capacitaciones`, `capacitacion_participantes`, `capacitacion_materiales` -- `certificaciones`, `certificacion_empleados`, `plan_capacitacion` - -#### Inspecciones de Seguridad (7 tablas) -- `inspecciones_seguridad`, `inspeccion_hallazgos` -- `checklist_seguridad`, `checklist_seguridad_items` -- `areas_riesgo`, `rondas_seguridad`, `ronda_puntos` - -#### Control de EPP (7 tablas) -- `epp_catalogo`, `epp_asignaciones`, `epp_entregas` -- `epp_devoluciones`, `epp_inspecciones` -- `epp_vida_util`, `epp_stock` - -#### Cumplimiento STPS (11 tablas) -- `normas_stps`, `requisitos_norma`, `cumplimiento_norma` -- `auditorias_stps`, `auditoria_hallazgos` -- `planes_accion`, `acciones_correctivas` -- `comision_seguridad`, `comision_miembros` -- `recorridos_comision`, `actas_comision` - -#### Gestion Ambiental (9 tablas) -- `impactos_ambientales`, `residuos`, `residuo_movimientos` -- `manifiestos_residuos`, `monitoreo_ambiental` -- `permisos_ambientales`, `programas_ambientales` -- `indicadores_ambientales`, `eventos_ambientales` - -#### Permisos de Trabajo (8 tablas) -- `permisos_trabajo`, `permiso_riesgos`, `permiso_autorizaciones` -- `permisos_altura`, `permisos_caliente`, `permisos_confinado` -- `permisos_electrico`, `permisos_excavacion` - -#### Indicadores HSE (7 tablas) -- `kpi_configuracion`, `kpi_valores`, `kpi_metas` -- `dashboards_hse`, `alertas_hse` -- `reportes_hse`, `estadisticas_periodo` - ---- - -### 4. Schema: `estimates` (8 tablas) - -**DDL:** `schemas/04-estimates-schema-ddl.sql` - -| Tabla | Descripcion | -|-------|-------------| -| `estimaciones` | Estimaciones de obra | -| `estimacion_conceptos` | Conceptos estimados | -| `generadores` | Numeros generadores | -| `anticipos` | Anticipos de obra | -| `amortizaciones` | Amortizacion de anticipos | -| `retenciones` | Retenciones (garantia, IMSS) | -| `fondo_garantia` | Fondo de garantia | -| `estimacion_workflow` | Workflow de aprobacion | - ---- - -### 5. Schema: `infonavit` (8 tablas) - -**DDL:** `schemas/05-infonavit-schema-ddl.sql` - -| Tabla | Descripcion | -|-------|-------------| -| `registro_infonavit` | Registro RUV | -| `oferta_vivienda` | Oferta registrada | -| `derechohabientes` | Derechohabientes | -| `asignacion_vivienda` | Asignaciones | -| `actas` | Actas de entrega | -| `acta_viviendas` | Viviendas en acta | -| `reportes_infonavit` | Reportes RUV | -| `historico_puntos` | Historico puntos ecologicos | - ---- - -### 6. Schema: `inventory` Extension (4 tablas) - -**DDL:** `schemas/06-inventory-ext-schema-ddl.sql` - -| Tabla | Descripcion | -|-------|-------------| -| `almacenes_proyecto` | Almacenes por obra | -| `requisiciones_obra` | Requisiciones desde obra | -| `requisicion_lineas` | Lineas de requisicion | -| `consumos_obra` | Consumos por lote/concepto | - ---- - -### 7. Schema: `purchase` Extension (5 tablas) - -**DDL:** `schemas/07-purchase-ext-schema-ddl.sql` - -| Tabla | Descripcion | -|-------|-------------| -| `purchase_order_construction` | Extension OC | -| `supplier_construction` | Extension proveedores | -| `comparativo_cotizaciones` | Cuadro comparativo | -| `comparativo_proveedores` | Proveedores en comparativo | -| `comparativo_productos` | Productos cotizados | - ---- - -## ORDEN DE EJECUCION DDL - -```bash -# Prerequisito: ERP-Core debe estar instalado -# Schema auth.* y core.* deben existir - -# 1. Construction (base) -psql $DATABASE_URL -f schemas/01-construction-schema-ddl.sql - -# 2. HR (depende de construction) -psql $DATABASE_URL -f schemas/02-hr-schema-ddl.sql - -# 3. HSE (depende de construction y hr) -psql $DATABASE_URL -f schemas/03-hse-schema-ddl.sql - -# 4. Estimates (depende de construction) -psql $DATABASE_URL -f schemas/04-estimates-schema-ddl.sql - -# 5. INFONAVIT (depende de construction) -psql $DATABASE_URL -f schemas/05-infonavit-schema-ddl.sql - -# 6. Inventory Extension (depende de construction) -psql $DATABASE_URL -f schemas/06-inventory-ext-schema-ddl.sql - -# 7. Purchase Extension (depende de construction) -psql $DATABASE_URL -f schemas/07-purchase-ext-schema-ddl.sql -``` - ---- - -## RELACIONES PRINCIPALES - -``` -auth.tenants - 鈹斺攢鈹 construction.proyectos - 鈹斺攢鈹 construction.fraccionamientos - 鈹溾攢鈹 construction.etapas - 鈹 鈹斺攢鈹 construction.manzanas - 鈹 鈹斺攢鈹 construction.lotes - 鈹溾攢鈹 construction.torres (vertical) - 鈹 鈹斺攢鈹 construction.niveles - 鈹 鈹斺攢鈹 construction.departamentos - 鈹溾攢鈹 hr.employee_fraccionamientos - 鈹 鈹斺攢鈹 hr.employees - 鈹斺攢鈹 hse.incidentes - 鈹斺攢鈹 hse.incidente_involucrados - 鈹斺攢鈹 hr.employees -``` - ---- - -## ENUMS UTILIZADOS - -Ver archivo: `backend/src/shared/constants/enums.constants.ts` - -Los principales enums estan definidos en: -- `PROJECT_STATUS` - Estados de proyecto -- `LOT_STATUS` - Estados de lote -- `INCIDENT_SEVERITY` - Severidad de incidentes -- `ESTIMATION_STATUS` - Estados de estimacion -- `INFONAVIT_ASSIGNMENT_STATUS` - Estados INFONAVIT - ---- - -## REFERENCIAS - -- **ERP-Core DDL:** `apps/erp-core/database/ddl/` -- **Herencia:** `HERENCIA-ERP-CORE.md` -- **Constantes SSOT:** `backend/src/shared/constants/database.constants.ts` - ---- - -**Mantenido por:** Architecture-Analyst -**Actualizacion:** Manual al agregar/modificar schemas diff --git a/projects/erp-construccion/database/drop-and-recreate-database.sh b/projects/erp-construccion/database/drop-and-recreate-database.sh deleted file mode 100755 index b6792658e..000000000 --- a/projects/erp-construccion/database/drop-and-recreate-database.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/bin/bash -# ============================================================================= -# DROP AND RECREATE DATABASE - ERP CONSTRUCCION -# ============================================================================= -# Script de carga limpia segun DIRECTIVA-POLITICA-CARGA-LIMPIA.md -# -# Uso: ./drop-and-recreate-database.sh [DATABASE_URL] -# Ejemplo: ./drop-and-recreate-database.sh "postgresql://user:pass@localhost:5433/erp_construccion" -# ============================================================================= - -set -e - -# Colores -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuracion -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DDL_DIR="$SCRIPT_DIR/ddl" -SCHEMAS_DIR="$SCRIPT_DIR/schemas" -INIT_SCRIPTS_DIR="$SCRIPT_DIR/init-scripts" - -# Database URL (por defecto desarrollo local) -DATABASE_URL="${1:-${DATABASE_URL:-postgresql://postgres:postgres@localhost:5433/erp_construccion}}" - -# Extraer parametros de conexion -DB_HOST=$(echo "$DATABASE_URL" | sed -E 's|.*@([^:]+):.*|\1|') -DB_PORT=$(echo "$DATABASE_URL" | sed -E 's|.*:([0-9]+)/.*|\1|') -DB_NAME=$(echo "$DATABASE_URL" | sed -E 's|.*/([^?]+).*|\1|') -DB_USER=$(echo "$DATABASE_URL" | sed -E 's|.*://([^:]+):.*|\1|') - -echo -e "${BLUE}=============================================================================${NC}" -echo -e "${BLUE} ERP CONSTRUCCION - Carga Limpia de Base de Datos${NC}" -echo -e "${BLUE}=============================================================================${NC}" -echo "" -echo -e "Host: ${YELLOW}$DB_HOST:$DB_PORT${NC}" -echo -e "Database: ${YELLOW}$DB_NAME${NC}" -echo -e "Usuario: ${YELLOW}$DB_USER${NC}" -echo "" - -# ============================================================================= -# PASO 1: Verificar conexion -# ============================================================================= -echo -e "${YELLOW}[1/6] Verificando conexion a PostgreSQL...${NC}" -if ! psql "$DATABASE_URL" -c "SELECT 1" > /dev/null 2>&1; then - echo -e "${RED}ERROR: No se puede conectar a PostgreSQL${NC}" - echo -e "${YELLOW}Verificar que PostgreSQL esta corriendo y las credenciales son correctas${NC}" - exit 1 -fi -echo -e "${GREEN}OK - Conexion establecida${NC}" -echo "" - -# ============================================================================= -# PASO 2: DROP schemas existentes (carga limpia) -# ============================================================================= -echo -e "${YELLOW}[2/6] Eliminando schemas existentes (carga limpia)...${NC}" - -# Lista de schemas a eliminar (orden inverso de dependencias) -SCHEMAS_TO_DROP=( - "hse" - "infonavit_management" - "safety_management" - "quality_management" - "construction_management" - "inventory_management" - "purchasing_management" - "financial_management" - "project_management" - "auth_management" - "core_shared" -) - -for schema in "${SCHEMAS_TO_DROP[@]}"; do - if psql "$DATABASE_URL" -t -c "SELECT 1 FROM pg_namespace WHERE nspname = '$schema'" 2>/dev/null | grep -q 1; then - psql "$DATABASE_URL" -c "DROP SCHEMA IF EXISTS $schema CASCADE" > /dev/null 2>&1 - echo -e " - Schema ${YELLOW}$schema${NC} eliminado" - fi -done -echo -e "${GREEN}OK - Schemas eliminados${NC}" -echo "" - -# ============================================================================= -# PASO 3: Ejecutar DDL inicial -# ============================================================================= -echo -e "${YELLOW}[3/6] Ejecutando DDL inicial (extensiones, schemas base)...${NC}" - -if [ -f "$INIT_SCRIPTS_DIR/01-init-database.sql" ]; then - psql "$DATABASE_URL" -f "$INIT_SCRIPTS_DIR/01-init-database.sql" > /dev/null 2>&1 - echo -e "${GREEN}OK - DDL inicial ejecutado${NC}" -elif [ -f "$DDL_DIR/00-init.sql" ]; then - psql "$DATABASE_URL" -f "$DDL_DIR/00-init.sql" > /dev/null 2>&1 - echo -e "${GREEN}OK - DDL inicial ejecutado (00-init.sql)${NC}" -else - echo -e "${RED}ERROR: No se encontro archivo de inicializacion${NC}" - exit 1 -fi -echo "" - -# ============================================================================= -# PASO 4: Ejecutar DDL de schemas modulares -# ============================================================================= -echo -e "${YELLOW}[4/6] Ejecutando DDL de schemas modulares...${NC}" - -# Buscar y ejecutar todos los archivos DDL en orden -DDL_FILES=$(find "$SCHEMAS_DIR" -name "*.sql" -type f 2>/dev/null | sort) - -if [ -z "$DDL_FILES" ]; then - echo -e "${YELLOW} No hay archivos DDL modulares adicionales${NC}" -else - for ddl_file in $DDL_FILES; do - filename=$(basename "$ddl_file") - echo -ne " - Ejecutando ${YELLOW}$filename${NC}..." - if psql "$DATABASE_URL" -f "$ddl_file" > /dev/null 2>&1; then - echo -e " ${GREEN}OK${NC}" - else - echo -e " ${RED}ERROR${NC}" - echo -e "${RED}Fallo al ejecutar: $ddl_file${NC}" - exit 1 - fi - done -fi -echo "" - -# ============================================================================= -# PASO 5: Ejecutar DDL legacy (si existe) -# ============================================================================= -echo -e "${YELLOW}[5/6] Ejecutando DDL de schemas legacy (si existen)...${NC}" - -LEGACY_DDL_DIR="$DDL_DIR/schemas" -if [ -d "$LEGACY_DDL_DIR" ]; then - LEGACY_FILES=$(find "$LEGACY_DDL_DIR" -name "*.sql" -type f 2>/dev/null | sort) - for ddl_file in $LEGACY_FILES; do - filename=$(basename "$ddl_file") - dirname=$(dirname "$ddl_file" | xargs basename) - echo -ne " - ${YELLOW}$dirname/$filename${NC}..." - if psql "$DATABASE_URL" -f "$ddl_file" > /dev/null 2>&1; then - echo -e " ${GREEN}OK${NC}" - else - echo -e " ${YELLOW}SKIP (puede requerir dependencias)${NC}" - fi - done -else - echo -e " No hay DDL legacy" -fi -echo "" - -# ============================================================================= -# PASO 6: Verificar resultado -# ============================================================================= -echo -e "${YELLOW}[6/6] Verificando resultado...${NC}" -echo "" - -# Contar schemas creados -SCHEMA_COUNT=$(psql "$DATABASE_URL" -t -c " -SELECT COUNT(*) FROM pg_namespace -WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'pg_temp_1', 'pg_toast_temp_1', 'public') -AND nspname NOT LIKE 'pg_%' -" | tr -d ' ') - -# Contar tablas totales -TABLE_COUNT=$(psql "$DATABASE_URL" -t -c " -SELECT COUNT(*) FROM pg_tables -WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'public') -" | tr -d ' ') - -# Mostrar resumen por schema -echo -e "${GREEN}=== RESUMEN DE CARGA LIMPIA ===${NC}" -echo "" -psql "$DATABASE_URL" -c " -SELECT - schemaname AS \"Schema\", - COUNT(*) AS \"Tablas\" -FROM pg_tables -WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'public') -GROUP BY schemaname -ORDER BY schemaname; -" - -echo "" -echo -e "${GREEN}=============================================================================${NC}" -echo -e "${GREEN} CARGA LIMPIA COMPLETADA EXITOSAMENTE${NC}" -echo -e "${GREEN}=============================================================================${NC}" -echo -e " Schemas creados: ${YELLOW}$SCHEMA_COUNT${NC}" -echo -e " Tablas creadas: ${YELLOW}$TABLE_COUNT${NC}" -echo -e "${GREEN}=============================================================================${NC}" diff --git a/projects/erp-construccion/database/init-scripts/01-init-database.sql b/projects/erp-construccion/database/init-scripts/01-init-database.sql deleted file mode 100644 index 48fa162f6..000000000 --- a/projects/erp-construccion/database/init-scripts/01-init-database.sql +++ /dev/null @@ -1,317 +0,0 @@ --- ============================================================================ --- Archivo: 01-init-database.sql --- Descripcion: Inicializacion completa de base de datos - Carga Limpia --- Proyecto: ERP Suite - Vertical Construccion --- Version: 2.0.0 --- Fecha: 2025-12-06 --- ============================================================================ --- POLITICA: CARGA LIMPIA (ver DIRECTIVA-POLITICA-CARGA-LIMPIA.md) --- Este archivo es parte de la fuente de verdad DDL. --- NO usar migrations, fixes o patches incrementales. --- ============================================================================ - --- ============================================================================ --- EXTENSIONES --- ============================================================================ - -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Generacion de UUIDs -CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- Funciones criptograficas -CREATE EXTENSION IF NOT EXISTS "postgis"; -- Geolocalizacion -CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Busqueda fuzzy -CREATE EXTENSION IF NOT EXISTS "btree_gist"; -- Indices GiST avanzados - --- ============================================================================ --- SCHEMA: core_shared (funciones compartidas) --- ============================================================================ - -CREATE SCHEMA IF NOT EXISTS core_shared; - -COMMENT ON SCHEMA core_shared IS 'Funciones, tipos y utilidades compartidas entre modulos'; - --- ============================================================================ --- FUNCIONES DE AUDITORIA --- ============================================================================ - --- Funcion para actualizar updated_at automaticamente -CREATE OR REPLACE FUNCTION core_shared.set_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -COMMENT ON FUNCTION core_shared.set_updated_at() IS -'Trigger function para actualizar automaticamente el campo updated_at en cada UPDATE'; - --- Funcion para establecer tenant_id desde contexto -CREATE OR REPLACE FUNCTION core_shared.set_tenant_id() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.tenant_id IS NULL THEN - NEW.tenant_id = current_setting('app.current_tenant_id', true)::uuid; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -COMMENT ON FUNCTION core_shared.set_tenant_id() IS -'Trigger function para establecer tenant_id automaticamente desde el contexto de sesion'; - --- Funcion para establecer created_by desde contexto -CREATE OR REPLACE FUNCTION core_shared.set_created_by() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.created_by IS NULL THEN - NEW.created_by = current_setting('app.current_user_id', true)::uuid; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -COMMENT ON FUNCTION core_shared.set_created_by() IS -'Trigger function para establecer created_by automaticamente desde el contexto de sesion'; - --- ============================================================================ --- FUNCIONES DE CONTEXTO --- ============================================================================ - -CREATE OR REPLACE FUNCTION core_shared.get_current_tenant_id() -RETURNS UUID AS $$ -BEGIN - RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID; -EXCEPTION - WHEN OTHERS THEN - RETURN NULL; -END; -$$ LANGUAGE plpgsql STABLE; - -COMMENT ON FUNCTION core_shared.get_current_tenant_id() IS -'Obtiene el ID del tenant actual desde el contexto de sesion'; - -CREATE OR REPLACE FUNCTION core_shared.get_current_user_id() -RETURNS UUID AS $$ -BEGIN - RETURN NULLIF(current_setting('app.current_user_id', true), '')::UUID; -EXCEPTION - WHEN OTHERS THEN - RETURN NULL; -END; -$$ LANGUAGE plpgsql STABLE; - -COMMENT ON FUNCTION core_shared.get_current_user_id() IS -'Obtiene el ID del usuario actual desde el contexto de sesion'; - --- ============================================================================ --- FUNCIONES DE UTILIDAD --- ============================================================================ - --- Generar slug desde texto -CREATE OR REPLACE FUNCTION core_shared.generate_slug(input_text TEXT) -RETURNS TEXT AS $$ -BEGIN - RETURN LOWER( - REGEXP_REPLACE( - REGEXP_REPLACE( - TRIM(input_text), - '[^a-zA-Z0-9\s-]', '', 'g' - ), - '\s+', '-', 'g' - ) - ); -END; -$$ LANGUAGE plpgsql IMMUTABLE; - --- Validar formato de email -CREATE OR REPLACE FUNCTION core_shared.is_valid_email(email TEXT) -RETURNS BOOLEAN AS $$ -BEGIN - RETURN email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'; -END; -$$ LANGUAGE plpgsql IMMUTABLE; - --- Validar formato de RFC mexicano -CREATE OR REPLACE FUNCTION core_shared.is_valid_rfc(rfc TEXT) -RETURNS BOOLEAN AS $$ -BEGIN - RETURN rfc ~* '^[A-Z&脩]{3,4}[0-9]{6}[A-Z0-9]{3}$'; -END; -$$ LANGUAGE plpgsql IMMUTABLE; - --- ============================================================================ --- PERMISOS --- ============================================================================ - -GRANT USAGE ON SCHEMA core_shared TO PUBLIC; -GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA core_shared TO PUBLIC; - --- ============================================================================ --- SCHEMAS DE NEGOCIO - NOMENCLATURA UNIFICADA --- ============================================================================ --- Segun NAMING-CONVENTIONS.md, usamos nombres cortos y descriptivos: --- - construction (antes project_management, construction_management) --- - estimates (antes financial_management) --- - infonavit (antes infonavit_management) --- - hr (extension de erp-core) --- - inventory (extension de erp-core) --- - purchase (extension de erp-core) --- - hse (nuevo: seguridad, salud, medio ambiente) --- ============================================================================ - --- Schemas propios de construccion -CREATE SCHEMA IF NOT EXISTS construction; -CREATE SCHEMA IF NOT EXISTS estimates; -CREATE SCHEMA IF NOT EXISTS infonavit; -CREATE SCHEMA IF NOT EXISTS hse; - --- Schemas de extension (extendemos modulos de erp-core) --- Estos pueden ya existir si erp-core esta instalado -CREATE SCHEMA IF NOT EXISTS hr; -CREATE SCHEMA IF NOT EXISTS inventory; -CREATE SCHEMA IF NOT EXISTS purchase; - --- Schemas legacy (compatibilidad temporal, marcar para deprecacion) --- NOTA: Estos seran eliminados en version futura -CREATE SCHEMA IF NOT EXISTS auth_management; -CREATE SCHEMA IF NOT EXISTS project_management; -CREATE SCHEMA IF NOT EXISTS financial_management; -CREATE SCHEMA IF NOT EXISTS purchasing_management; -CREATE SCHEMA IF NOT EXISTS inventory_management; -CREATE SCHEMA IF NOT EXISTS construction_management; -CREATE SCHEMA IF NOT EXISTS quality_management; -CREATE SCHEMA IF NOT EXISTS safety_management; -CREATE SCHEMA IF NOT EXISTS infonavit_management; - --- ============================================================================ --- COMENTARIOS EN SCHEMAS --- ============================================================================ - --- Schemas principales (nuevos) -COMMENT ON SCHEMA construction IS - 'Gestion de obras: proyectos, fraccionamientos, fases, viviendas, avances'; - -COMMENT ON SCHEMA estimates IS - 'Presupuestos, partidas, estimaciones, control de costos'; - -COMMENT ON SCHEMA infonavit IS - 'Integracion INFONAVIT: tramites, ECUVE, subsidios, normatividad'; - -COMMENT ON SCHEMA hse IS - 'Seguridad, Salud Ocupacional y Medio Ambiente (HSE/EHS)'; - -COMMENT ON SCHEMA hr IS - 'Extension de RRHH para construccion: cuadrillas, destajo, asistencia obra'; - -COMMENT ON SCHEMA inventory IS - 'Extension de inventarios: almacenes de obra, control de materiales'; - -COMMENT ON SCHEMA purchase IS - 'Extension de compras: proveedores de construccion, requisiciones de obra'; - --- Schemas legacy (deprecated) -COMMENT ON SCHEMA auth_management IS - 'DEPRECATED: Usar core.auth. Schema mantenido para compatibilidad'; - -COMMENT ON SCHEMA project_management IS - 'DEPRECATED: Usar construction. Schema mantenido para compatibilidad'; - -COMMENT ON SCHEMA financial_management IS - 'DEPRECATED: Usar estimates. Schema mantenido para compatibilidad'; - -COMMENT ON SCHEMA purchasing_management IS - 'DEPRECATED: Usar purchase. Schema mantenido para compatibilidad'; - -COMMENT ON SCHEMA inventory_management IS - 'DEPRECATED: Usar inventory. Schema mantenido para compatibilidad'; - -COMMENT ON SCHEMA construction_management IS - 'DEPRECATED: Usar construction. Schema mantenido para compatibilidad'; - -COMMENT ON SCHEMA quality_management IS - 'DEPRECATED: Funcionalidad movida a hse (inspecciones) y construction (calidad)'; - -COMMENT ON SCHEMA safety_management IS - 'DEPRECATED: Usar hse. Schema mantenido para compatibilidad'; - -COMMENT ON SCHEMA infonavit_management IS - 'DEPRECATED: Usar infonavit. Schema mantenido para compatibilidad'; - --- ============================================================================ --- FUNCION LEGACY (compatibilidad) --- ============================================================================ - -CREATE OR REPLACE FUNCTION update_updated_at_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = now(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -COMMENT ON FUNCTION update_updated_at_column() IS - 'LEGACY: Usar core_shared.set_updated_at() en nuevas tablas'; - --- ============================================================================ --- TABLAS CORE MINIMAS (si erp-core no esta instalado) --- ============================================================================ --- Estas tablas son requeridas como FK por los modulos de construccion --- Si erp-core esta instalado, estas ya existiran y el IF NOT EXISTS las omitira - --- Schema core para tablas compartidas -CREATE SCHEMA IF NOT EXISTS core; - --- Tabla de tenants (multi-tenancy) -CREATE TABLE IF NOT EXISTS core.tenants ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(50) NOT NULL UNIQUE, - name VARCHAR(200) NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT true, - settings JSONB DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla de usuarios -CREATE TABLE IF NOT EXISTS core.users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID REFERENCES core.tenants(id), - email VARCHAR(255) NOT NULL, - username VARCHAR(100), - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE(tenant_id, email) -); - --- ============================================================================ --- VERIFICACION --- ============================================================================ - -DO $$ -DECLARE - schema_count INTEGER; - ext_count INTEGER; -BEGIN - SELECT COUNT(*) INTO schema_count - FROM pg_namespace - WHERE nspname IN ('construction', 'estimates', 'infonavit', 'hse', 'hr', 'inventory', 'purchase', 'core', 'core_shared'); - - SELECT COUNT(*) INTO ext_count - FROM pg_extension - WHERE extname IN ('uuid-ossp', 'pgcrypto', 'postgis', 'pg_trgm', 'btree_gist'); - - RAISE NOTICE '============================================================'; - RAISE NOTICE 'ERP CONSTRUCCION - Base de datos inicializada'; - RAISE NOTICE '============================================================'; - RAISE NOTICE 'Extensiones instaladas: %', ext_count; - RAISE NOTICE 'Schemas principales creados: %', schema_count; - RAISE NOTICE '============================================================'; - RAISE NOTICE 'Schemas principales: construction, estimates, infonavit, hse'; - RAISE NOTICE 'Schemas extension: hr, inventory, purchase'; - RAISE NOTICE 'Schemas compartidos: core, core_shared'; - RAISE NOTICE '============================================================'; -END $$; - --- ============================================================================ --- FIN --- ============================================================================ diff --git a/projects/erp-construccion/database/schemas/01-construction-schema-ddl.sql b/projects/erp-construccion/database/schemas/01-construction-schema-ddl.sql deleted file mode 100644 index 470ebf9f5..000000000 --- a/projects/erp-construccion/database/schemas/01-construction-schema-ddl.sql +++ /dev/null @@ -1,903 +0,0 @@ --- ============================================================================ --- CONSTRUCTION Schema DDL - Gesti贸n de Obras (COMPLETO) --- Modulos: MAI-002, MAI-003, MAI-005, MAI-009, MAI-012 --- Version: 2.0.0 --- Fecha: 2025-12-08 --- ============================================================================ --- POLITICA: CARGA LIMPIA (ver DIRECTIVA-POLITICA-CARGA-LIMPIA.md) --- Este archivo es parte de la fuente de verdad DDL. --- ============================================================================ - --- Verificar que ERP-Core est谩 instalado -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN - RAISE EXCEPTION 'Schema auth no existe. Ejecutar primero ERP-Core DDL'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'tenants') THEN - RAISE EXCEPTION 'Tabla auth.tenants no existe. ERP-Core debe estar instalado'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users') THEN - RAISE EXCEPTION 'Tabla auth.users no existe. ERP-Core debe estar instalado'; - END IF; -END $$; - --- Crear schema si no existe -CREATE SCHEMA IF NOT EXISTS construction; - --- ============================================================================ --- TYPES (ENUMs) --- ============================================================================ - -DO $$ BEGIN - CREATE TYPE construction.project_status AS ENUM ( - 'draft', 'planning', 'in_progress', 'paused', 'completed', 'cancelled' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE construction.lot_status AS ENUM ( - 'available', 'reserved', 'sold', 'under_construction', 'delivered', 'warranty' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE construction.prototype_type AS ENUM ( - 'horizontal', 'vertical', 'commercial', 'mixed' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE construction.advance_status AS ENUM ( - 'pending', 'captured', 'reviewed', 'approved', 'rejected' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE construction.quality_status AS ENUM ( - 'pending', 'in_review', 'approved', 'rejected', 'rework' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE construction.contract_type AS ENUM ( - 'fixed_price', 'unit_price', 'cost_plus', 'mixed' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE construction.contract_status AS ENUM ( - 'draft', 'pending_approval', 'active', 'suspended', 'terminated', 'closed' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - --- ============================================================================ --- TABLES - ESTRUCTURA DE PROYECTO --- ============================================================================ - --- Tabla: fraccionamientos (desarrollo inmobiliario) -CREATE TABLE IF NOT EXISTS construction.fraccionamientos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - code VARCHAR(20) NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - address TEXT, - city VARCHAR(100), - state VARCHAR(100), - zip_code VARCHAR(10), - location GEOMETRY(POINT, 4326), - total_area_m2 DECIMAL(12,2), - buildable_area_m2 DECIMAL(12,2), - total_lots INTEGER DEFAULT 0, - status construction.project_status NOT NULL DEFAULT 'draft', - start_date DATE, - expected_end_date DATE, - actual_end_date DATE, - metadata JSONB DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_fraccionamientos_code_tenant UNIQUE (tenant_id, code) -); - --- Tabla: etapas (fases del fraccionamiento) -CREATE TABLE IF NOT EXISTS construction.etapas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id) ON DELETE CASCADE, - code VARCHAR(20) NOT NULL, - name VARCHAR(100) NOT NULL, - description TEXT, - sequence INTEGER NOT NULL DEFAULT 1, - total_lots INTEGER DEFAULT 0, - status construction.project_status NOT NULL DEFAULT 'draft', - start_date DATE, - expected_end_date DATE, - actual_end_date DATE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_etapas_code_fracc UNIQUE (fraccionamiento_id, code) -); - --- Tabla: manzanas (agrupaci贸n de lotes) -CREATE TABLE IF NOT EXISTS construction.manzanas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - etapa_id UUID NOT NULL REFERENCES construction.etapas(id) ON DELETE CASCADE, - code VARCHAR(20) NOT NULL, - name VARCHAR(100), - total_lots INTEGER DEFAULT 0, - polygon GEOMETRY(POLYGON, 4326), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_manzanas_code_etapa UNIQUE (etapa_id, code) -); - --- Tabla: prototipos (tipos de vivienda) - definida antes de lotes -CREATE TABLE IF NOT EXISTS construction.prototipos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - code VARCHAR(20) NOT NULL, - name VARCHAR(100) NOT NULL, - description TEXT, - type construction.prototype_type NOT NULL DEFAULT 'horizontal', - area_construction_m2 DECIMAL(10,2), - area_terrain_m2 DECIMAL(10,2), - bedrooms INTEGER DEFAULT 0, - bathrooms DECIMAL(3,1) DEFAULT 0, - parking_spaces INTEGER DEFAULT 0, - floors INTEGER DEFAULT 1, - base_price DECIMAL(14,2), - blueprint_url VARCHAR(500), - render_url VARCHAR(500), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - metadata JSONB DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_prototipos_code_tenant UNIQUE (tenant_id, code) -); - --- Tabla: lotes (unidades vendibles horizontal) -CREATE TABLE IF NOT EXISTS construction.lotes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - manzana_id UUID NOT NULL REFERENCES construction.manzanas(id) ON DELETE CASCADE, - prototipo_id UUID REFERENCES construction.prototipos(id), - code VARCHAR(30) NOT NULL, - official_number VARCHAR(50), - area_m2 DECIMAL(10,2), - front_m DECIMAL(8,2), - depth_m DECIMAL(8,2), - status construction.lot_status NOT NULL DEFAULT 'available', - location GEOMETRY(POINT, 4326), - polygon GEOMETRY(POLYGON, 4326), - price_base DECIMAL(14,2), - price_final DECIMAL(14,2), - buyer_id UUID, - sale_date DATE, - delivery_date DATE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_lotes_code_manzana UNIQUE (manzana_id, code) -); - --- ============================================================================ --- TABLES - ESTRUCTURA VERTICAL (TORRES) --- ============================================================================ - --- Tabla: torres (edificios verticales) -CREATE TABLE IF NOT EXISTS construction.torres ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - etapa_id UUID NOT NULL REFERENCES construction.etapas(id) ON DELETE CASCADE, - code VARCHAR(20) NOT NULL, - name VARCHAR(100) NOT NULL, - total_floors INTEGER NOT NULL DEFAULT 1, - total_units INTEGER DEFAULT 0, - status construction.project_status NOT NULL DEFAULT 'draft', - location GEOMETRY(POINT, 4326), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_torres_code_etapa UNIQUE (etapa_id, code) -); - --- Tabla: niveles (pisos de torre) -CREATE TABLE IF NOT EXISTS construction.niveles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - torre_id UUID NOT NULL REFERENCES construction.torres(id) ON DELETE CASCADE, - floor_number INTEGER NOT NULL, - name VARCHAR(50), - total_units INTEGER DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_niveles_floor_torre UNIQUE (torre_id, floor_number) -); - --- Tabla: departamentos (unidades en torre) -CREATE TABLE IF NOT EXISTS construction.departamentos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - nivel_id UUID NOT NULL REFERENCES construction.niveles(id) ON DELETE CASCADE, - prototipo_id UUID REFERENCES construction.prototipos(id), - code VARCHAR(30) NOT NULL, - unit_number VARCHAR(20) NOT NULL, - area_m2 DECIMAL(10,2), - status construction.lot_status NOT NULL DEFAULT 'available', - price_base DECIMAL(14,2), - price_final DECIMAL(14,2), - buyer_id UUID, - sale_date DATE, - delivery_date DATE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_departamentos_code_nivel UNIQUE (nivel_id, code) -); - --- ============================================================================ --- TABLES - CONCEPTOS Y PRESUPUESTOS --- ============================================================================ - --- Tabla: conceptos (cat谩logo de conceptos de obra) -CREATE TABLE IF NOT EXISTS construction.conceptos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - parent_id UUID REFERENCES construction.conceptos(id), - code VARCHAR(50) NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - unit_id UUID, - unit_price DECIMAL(12,4), - is_composite BOOLEAN NOT NULL DEFAULT FALSE, - level INTEGER NOT NULL DEFAULT 0, - path VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_conceptos_code_tenant UNIQUE (tenant_id, code) -); - --- Tabla: presupuestos (presupuesto por prototipo/obra) -CREATE TABLE IF NOT EXISTS construction.presupuestos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), - prototipo_id UUID REFERENCES construction.prototipos(id), - code VARCHAR(30) NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - version INTEGER NOT NULL DEFAULT 1, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - total_amount DECIMAL(16,2) DEFAULT 0, - currency_id UUID, - approved_at TIMESTAMPTZ, - approved_by UUID REFERENCES auth.users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_presupuestos_code_version UNIQUE (tenant_id, code, version) -); - --- Tabla: presupuesto_partidas (l铆neas del presupuesto) -CREATE TABLE IF NOT EXISTS construction.presupuesto_partidas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - presupuesto_id UUID NOT NULL REFERENCES construction.presupuestos(id) ON DELETE CASCADE, - concepto_id UUID NOT NULL REFERENCES construction.conceptos(id), - sequence INTEGER NOT NULL DEFAULT 0, - quantity DECIMAL(12,4) NOT NULL DEFAULT 0, - unit_price DECIMAL(12,4) NOT NULL DEFAULT 0, - total_amount DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_partidas_presupuesto_concepto UNIQUE (presupuesto_id, concepto_id) -); - --- ============================================================================ --- TABLES - AVANCES Y CONTROL DE OBRA --- ============================================================================ - --- Tabla: programa_obra (programa maestro) -CREATE TABLE IF NOT EXISTS construction.programa_obra ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - code VARCHAR(30) NOT NULL, - name VARCHAR(255) NOT NULL, - version INTEGER NOT NULL DEFAULT 1, - start_date DATE NOT NULL, - end_date DATE NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_programa_code_version UNIQUE (tenant_id, code, version) -); - --- Tabla: programa_actividades (actividades del programa) -CREATE TABLE IF NOT EXISTS construction.programa_actividades ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - programa_id UUID NOT NULL REFERENCES construction.programa_obra(id) ON DELETE CASCADE, - concepto_id UUID REFERENCES construction.conceptos(id), - parent_id UUID REFERENCES construction.programa_actividades(id), - name VARCHAR(255) NOT NULL, - sequence INTEGER NOT NULL DEFAULT 0, - planned_start DATE, - planned_end DATE, - planned_quantity DECIMAL(12,4) DEFAULT 0, - planned_weight DECIMAL(8,4) DEFAULT 0, - wbs_code VARCHAR(50), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- Tabla: avances_obra (captura de avances) -CREATE TABLE IF NOT EXISTS construction.avances_obra ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - lote_id UUID REFERENCES construction.lotes(id), - departamento_id UUID REFERENCES construction.departamentos(id), - concepto_id UUID NOT NULL REFERENCES construction.conceptos(id), - capture_date DATE NOT NULL, - quantity_executed DECIMAL(12,4) NOT NULL DEFAULT 0, - percentage_executed DECIMAL(5,2) DEFAULT 0, - status construction.advance_status NOT NULL DEFAULT 'pending', - notes TEXT, - captured_by UUID NOT NULL REFERENCES auth.users(id), - reviewed_by UUID REFERENCES auth.users(id), - reviewed_at TIMESTAMPTZ, - approved_by UUID REFERENCES auth.users(id), - approved_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT chk_avances_lote_or_depto CHECK ( - (lote_id IS NOT NULL AND departamento_id IS NULL) OR - (lote_id IS NULL AND departamento_id IS NOT NULL) - ) -); - --- Tabla: fotos_avance (evidencia fotogr谩fica) -CREATE TABLE IF NOT EXISTS construction.fotos_avance ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - avance_id UUID NOT NULL REFERENCES construction.avances_obra(id) ON DELETE CASCADE, - file_url VARCHAR(500) NOT NULL, - file_name VARCHAR(255), - file_size INTEGER, - mime_type VARCHAR(50), - description TEXT, - location GEOMETRY(POINT, 4326), - captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- Tabla: bitacora_obra (registro de bit谩cora) -CREATE TABLE IF NOT EXISTS construction.bitacora_obra ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - entry_date DATE NOT NULL, - entry_number INTEGER NOT NULL, - weather VARCHAR(50), - temperature_max DECIMAL(4,1), - temperature_min DECIMAL(4,1), - workers_count INTEGER DEFAULT 0, - description TEXT NOT NULL, - observations TEXT, - incidents TEXT, - registered_by UUID NOT NULL REFERENCES auth.users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_bitacora_fracc_number UNIQUE (fraccionamiento_id, entry_number) -); - --- ============================================================================ --- TABLES - CALIDAD Y POSTVENTA (MAI-009) --- ============================================================================ - --- Tabla: checklists (plantillas de verificaci贸n) -CREATE TABLE IF NOT EXISTS construction.checklists ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - code VARCHAR(30) NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - prototipo_id UUID REFERENCES construction.prototipos(id), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_checklists_code_tenant UNIQUE (tenant_id, code) -); - --- Tabla: checklist_items (items del checklist) -CREATE TABLE IF NOT EXISTS construction.checklist_items ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - checklist_id UUID NOT NULL REFERENCES construction.checklists(id) ON DELETE CASCADE, - sequence INTEGER NOT NULL DEFAULT 0, - name VARCHAR(255) NOT NULL, - description TEXT, - is_required BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- Tabla: inspecciones (inspecciones de calidad) -CREATE TABLE IF NOT EXISTS construction.inspecciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - checklist_id UUID NOT NULL REFERENCES construction.checklists(id), - lote_id UUID REFERENCES construction.lotes(id), - departamento_id UUID REFERENCES construction.departamentos(id), - inspection_date DATE NOT NULL, - status construction.quality_status NOT NULL DEFAULT 'pending', - inspector_id UUID NOT NULL REFERENCES auth.users(id), - notes TEXT, - approved_by UUID REFERENCES auth.users(id), - approved_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- Tabla: inspeccion_resultados (resultados por item) -CREATE TABLE IF NOT EXISTS construction.inspeccion_resultados ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - inspeccion_id UUID NOT NULL REFERENCES construction.inspecciones(id) ON DELETE CASCADE, - checklist_item_id UUID NOT NULL REFERENCES construction.checklist_items(id), - is_passed BOOLEAN, - notes TEXT, - photo_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id) -); - --- Tabla: tickets_postventa (tickets de garant铆a) -CREATE TABLE IF NOT EXISTS construction.tickets_postventa ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - lote_id UUID REFERENCES construction.lotes(id), - departamento_id UUID REFERENCES construction.departamentos(id), - ticket_number VARCHAR(30) NOT NULL, - reported_date DATE NOT NULL, - category VARCHAR(50), - description TEXT NOT NULL, - priority VARCHAR(20) DEFAULT 'medium', - status VARCHAR(20) NOT NULL DEFAULT 'open', - assigned_to UUID REFERENCES auth.users(id), - resolution TEXT, - resolved_at TIMESTAMPTZ, - resolved_by UUID REFERENCES auth.users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_tickets_number_tenant UNIQUE (tenant_id, ticket_number) -); - --- ============================================================================ --- TABLES - CONTRATOS Y SUBCONTRATOS (MAI-012) --- ============================================================================ - --- Tabla: subcontratistas -CREATE TABLE IF NOT EXISTS construction.subcontratistas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - partner_id UUID, - code VARCHAR(20) NOT NULL, - name VARCHAR(255) NOT NULL, - legal_name VARCHAR(255), - tax_id VARCHAR(20), - specialty VARCHAR(100), - contact_name VARCHAR(100), - contact_phone VARCHAR(20), - contact_email VARCHAR(100), - address TEXT, - rating DECIMAL(3,2), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_subcontratistas_code_tenant UNIQUE (tenant_id, code) -); - --- Tabla: contratos (contratos con subcontratistas) -CREATE TABLE IF NOT EXISTS construction.contratos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - subcontratista_id UUID NOT NULL REFERENCES construction.subcontratistas(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - contract_number VARCHAR(30) NOT NULL, - contract_type construction.contract_type NOT NULL DEFAULT 'unit_price', - name VARCHAR(255) NOT NULL, - description TEXT, - start_date DATE NOT NULL, - end_date DATE, - total_amount DECIMAL(16,2), - advance_percentage DECIMAL(5,2) DEFAULT 0, - retention_percentage DECIMAL(5,2) DEFAULT 5, - status construction.contract_status NOT NULL DEFAULT 'draft', - signed_at TIMESTAMPTZ, - signed_by UUID REFERENCES auth.users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_contratos_number_tenant UNIQUE (tenant_id, contract_number) -); - --- Tabla: contrato_partidas (l铆neas del contrato) -CREATE TABLE IF NOT EXISTS construction.contrato_partidas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - contrato_id UUID NOT NULL REFERENCES construction.contratos(id) ON DELETE CASCADE, - concepto_id UUID NOT NULL REFERENCES construction.conceptos(id), - quantity DECIMAL(12,4) NOT NULL DEFAULT 0, - unit_price DECIMAL(12,4) NOT NULL DEFAULT 0, - total_amount DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- INDICES --- ============================================================================ - --- Fraccionamientos -CREATE INDEX IF NOT EXISTS idx_fraccionamientos_tenant_id ON construction.fraccionamientos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_fraccionamientos_status ON construction.fraccionamientos(status); -CREATE INDEX IF NOT EXISTS idx_fraccionamientos_code ON construction.fraccionamientos(code); - --- Etapas -CREATE INDEX IF NOT EXISTS idx_etapas_tenant_id ON construction.etapas(tenant_id); -CREATE INDEX IF NOT EXISTS idx_etapas_fraccionamiento_id ON construction.etapas(fraccionamiento_id); - --- Manzanas -CREATE INDEX IF NOT EXISTS idx_manzanas_tenant_id ON construction.manzanas(tenant_id); -CREATE INDEX IF NOT EXISTS idx_manzanas_etapa_id ON construction.manzanas(etapa_id); - --- Lotes -CREATE INDEX IF NOT EXISTS idx_lotes_tenant_id ON construction.lotes(tenant_id); -CREATE INDEX IF NOT EXISTS idx_lotes_manzana_id ON construction.lotes(manzana_id); -CREATE INDEX IF NOT EXISTS idx_lotes_prototipo_id ON construction.lotes(prototipo_id); -CREATE INDEX IF NOT EXISTS idx_lotes_status ON construction.lotes(status); - --- Torres -CREATE INDEX IF NOT EXISTS idx_torres_tenant_id ON construction.torres(tenant_id); -CREATE INDEX IF NOT EXISTS idx_torres_etapa_id ON construction.torres(etapa_id); - --- Niveles -CREATE INDEX IF NOT EXISTS idx_niveles_tenant_id ON construction.niveles(tenant_id); -CREATE INDEX IF NOT EXISTS idx_niveles_torre_id ON construction.niveles(torre_id); - --- Departamentos -CREATE INDEX IF NOT EXISTS idx_departamentos_tenant_id ON construction.departamentos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_departamentos_nivel_id ON construction.departamentos(nivel_id); -CREATE INDEX IF NOT EXISTS idx_departamentos_status ON construction.departamentos(status); - --- Prototipos -CREATE INDEX IF NOT EXISTS idx_prototipos_tenant_id ON construction.prototipos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_prototipos_type ON construction.prototipos(type); - --- Conceptos -CREATE INDEX IF NOT EXISTS idx_conceptos_tenant_id ON construction.conceptos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_conceptos_parent_id ON construction.conceptos(parent_id); -CREATE INDEX IF NOT EXISTS idx_conceptos_code ON construction.conceptos(code); - --- Presupuestos -CREATE INDEX IF NOT EXISTS idx_presupuestos_tenant_id ON construction.presupuestos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_presupuestos_fraccionamiento_id ON construction.presupuestos(fraccionamiento_id); - --- Avances -CREATE INDEX IF NOT EXISTS idx_avances_tenant_id ON construction.avances_obra(tenant_id); -CREATE INDEX IF NOT EXISTS idx_avances_lote_id ON construction.avances_obra(lote_id); -CREATE INDEX IF NOT EXISTS idx_avances_concepto_id ON construction.avances_obra(concepto_id); -CREATE INDEX IF NOT EXISTS idx_avances_capture_date ON construction.avances_obra(capture_date); - --- Bitacora -CREATE INDEX IF NOT EXISTS idx_bitacora_tenant_id ON construction.bitacora_obra(tenant_id); -CREATE INDEX IF NOT EXISTS idx_bitacora_fraccionamiento_id ON construction.bitacora_obra(fraccionamiento_id); - --- Inspecciones -CREATE INDEX IF NOT EXISTS idx_inspecciones_tenant_id ON construction.inspecciones(tenant_id); -CREATE INDEX IF NOT EXISTS idx_inspecciones_status ON construction.inspecciones(status); - --- Tickets -CREATE INDEX IF NOT EXISTS idx_tickets_tenant_id ON construction.tickets_postventa(tenant_id); -CREATE INDEX IF NOT EXISTS idx_tickets_status ON construction.tickets_postventa(status); - --- Subcontratistas -CREATE INDEX IF NOT EXISTS idx_subcontratistas_tenant_id ON construction.subcontratistas(tenant_id); - --- Contratos -CREATE INDEX IF NOT EXISTS idx_contratos_tenant_id ON construction.contratos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_contratos_subcontratista_id ON construction.contratos(subcontratista_id); -CREATE INDEX IF NOT EXISTS idx_contratos_fraccionamiento_id ON construction.contratos(fraccionamiento_id); - --- ============================================================================ --- ROW LEVEL SECURITY (RLS) --- ============================================================================ - -ALTER TABLE construction.fraccionamientos ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.etapas ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.manzanas ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.lotes ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.torres ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.niveles ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.departamentos ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.prototipos ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.conceptos ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.presupuestos ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.presupuesto_partidas ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.programa_obra ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.programa_actividades ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.avances_obra ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.fotos_avance ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.bitacora_obra ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.checklists ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.checklist_items ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.inspecciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.inspeccion_resultados ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.tickets_postventa ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.subcontratistas ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.contratos ENABLE ROW LEVEL SECURITY; -ALTER TABLE construction.contrato_partidas ENABLE ROW LEVEL SECURITY; - --- Policies de tenant isolation usando current_setting -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_fraccionamientos ON construction.fraccionamientos; - CREATE POLICY tenant_isolation_fraccionamientos ON construction.fraccionamientos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_etapas ON construction.etapas; - CREATE POLICY tenant_isolation_etapas ON construction.etapas - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_manzanas ON construction.manzanas; - CREATE POLICY tenant_isolation_manzanas ON construction.manzanas - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_lotes ON construction.lotes; - CREATE POLICY tenant_isolation_lotes ON construction.lotes - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_torres ON construction.torres; - CREATE POLICY tenant_isolation_torres ON construction.torres - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_niveles ON construction.niveles; - CREATE POLICY tenant_isolation_niveles ON construction.niveles - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_departamentos ON construction.departamentos; - CREATE POLICY tenant_isolation_departamentos ON construction.departamentos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_prototipos ON construction.prototipos; - CREATE POLICY tenant_isolation_prototipos ON construction.prototipos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_conceptos ON construction.conceptos; - CREATE POLICY tenant_isolation_conceptos ON construction.conceptos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_presupuestos ON construction.presupuestos; - CREATE POLICY tenant_isolation_presupuestos ON construction.presupuestos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_presupuesto_partidas ON construction.presupuesto_partidas; - CREATE POLICY tenant_isolation_presupuesto_partidas ON construction.presupuesto_partidas - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_programa_obra ON construction.programa_obra; - CREATE POLICY tenant_isolation_programa_obra ON construction.programa_obra - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_programa_actividades ON construction.programa_actividades; - CREATE POLICY tenant_isolation_programa_actividades ON construction.programa_actividades - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_avances_obra ON construction.avances_obra; - CREATE POLICY tenant_isolation_avances_obra ON construction.avances_obra - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_fotos_avance ON construction.fotos_avance; - CREATE POLICY tenant_isolation_fotos_avance ON construction.fotos_avance - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_bitacora_obra ON construction.bitacora_obra; - CREATE POLICY tenant_isolation_bitacora_obra ON construction.bitacora_obra - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_checklists ON construction.checklists; - CREATE POLICY tenant_isolation_checklists ON construction.checklists - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_checklist_items ON construction.checklist_items; - CREATE POLICY tenant_isolation_checklist_items ON construction.checklist_items - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_inspecciones ON construction.inspecciones; - CREATE POLICY tenant_isolation_inspecciones ON construction.inspecciones - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_inspeccion_resultados ON construction.inspeccion_resultados; - CREATE POLICY tenant_isolation_inspeccion_resultados ON construction.inspeccion_resultados - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_tickets_postventa ON construction.tickets_postventa; - CREATE POLICY tenant_isolation_tickets_postventa ON construction.tickets_postventa - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_subcontratistas ON construction.subcontratistas; - CREATE POLICY tenant_isolation_subcontratistas ON construction.subcontratistas - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_contratos ON construction.contratos; - CREATE POLICY tenant_isolation_contratos ON construction.contratos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_contrato_partidas ON construction.contrato_partidas; - CREATE POLICY tenant_isolation_contrato_partidas ON construction.contrato_partidas - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - --- ============================================================================ --- COMENTARIOS --- ============================================================================ - -COMMENT ON SCHEMA construction IS 'Schema de construcci贸n: obras, lotes, avances, calidad, contratos'; -COMMENT ON TABLE construction.fraccionamientos IS 'Desarrollos inmobiliarios/fraccionamientos'; -COMMENT ON TABLE construction.etapas IS 'Etapas/fases de un fraccionamiento'; -COMMENT ON TABLE construction.manzanas IS 'Manzanas dentro de una etapa'; -COMMENT ON TABLE construction.lotes IS 'Lotes/terrenos vendibles (horizontal)'; -COMMENT ON TABLE construction.torres IS 'Torres/edificios (vertical)'; -COMMENT ON TABLE construction.niveles IS 'Pisos de una torre'; -COMMENT ON TABLE construction.departamentos IS 'Departamentos/unidades en torre'; -COMMENT ON TABLE construction.prototipos IS 'Tipos de vivienda/prototipos'; -COMMENT ON TABLE construction.conceptos IS 'Cat谩logo de conceptos de obra'; -COMMENT ON TABLE construction.presupuestos IS 'Presupuestos por prototipo u obra'; -COMMENT ON TABLE construction.avances_obra IS 'Captura de avances f铆sicos'; -COMMENT ON TABLE construction.bitacora_obra IS 'Bit谩cora diaria de obra'; -COMMENT ON TABLE construction.checklists IS 'Plantillas de verificaci贸n'; -COMMENT ON TABLE construction.inspecciones IS 'Inspecciones de calidad'; -COMMENT ON TABLE construction.tickets_postventa IS 'Tickets de garant铆a'; -COMMENT ON TABLE construction.subcontratistas IS 'Cat谩logo de subcontratistas'; -COMMENT ON TABLE construction.contratos IS 'Contratos con subcontratistas'; - --- ============================================================================ --- FIN DEL SCHEMA CONSTRUCTION --- Total tablas: 24 --- ============================================================================ diff --git a/projects/erp-construccion/database/schemas/02-hr-schema-ddl.sql b/projects/erp-construccion/database/schemas/02-hr-schema-ddl.sql deleted file mode 100644 index 2ce313756..000000000 --- a/projects/erp-construccion/database/schemas/02-hr-schema-ddl.sql +++ /dev/null @@ -1,156 +0,0 @@ --- ============================================================================ --- HR Schema DDL - Extension de RRHH para Construccion --- Modulo: MAI-007 RRHH y Asistencias --- Version: 1.0.0 --- Fecha: 2025-12-06 --- ============================================================================ --- POLITICA: CARGA LIMPIA (ver DIRECTIVA-POLITICA-CARGA-LIMPIA.md) --- Este archivo es parte de la fuente de verdad DDL. --- ============================================================================ - --- Verificar prerequisitos -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN - RAISE EXCEPTION 'Schema auth no existe. ERP-Core debe estar instalado'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'tenants') THEN - RAISE EXCEPTION 'Tabla auth.tenants no existe. ERP-Core debe estar instalado'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN - RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero 01-construction-schema-ddl.sql'; - END IF; -END $$; - --- Crear schema si no existe -CREATE SCHEMA IF NOT EXISTS hr; - --- Configurar search_path -SET search_path TO hr, construction, core, core_shared, public; - --- ============================================================================ --- TABLAS BASE (requeridas por HSE y otros modulos) --- ============================================================================ - --- Tabla: Empleados -CREATE TABLE IF NOT EXISTS hr.employees ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - codigo VARCHAR(20) NOT NULL, - nombre VARCHAR(100) NOT NULL, - apellido_paterno VARCHAR(100) NOT NULL, - apellido_materno VARCHAR(100), - curp VARCHAR(18), - rfc VARCHAR(13), - nss VARCHAR(11), - fecha_nacimiento DATE, - genero VARCHAR(1), - email VARCHAR(255), - telefono VARCHAR(20), - direccion TEXT, - fecha_ingreso DATE NOT NULL, - fecha_baja DATE, - puesto_id UUID, - departamento VARCHAR(100), - tipo_contrato VARCHAR(50), - salario_diario DECIMAL(10,2), - estado VARCHAR(20) NOT NULL DEFAULT 'activo', - foto_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - - CONSTRAINT uq_employees_codigo UNIQUE (tenant_id, codigo), - CONSTRAINT uq_employees_curp UNIQUE (tenant_id, curp) -); - --- Tabla: Puestos -CREATE TABLE IF NOT EXISTS hr.puestos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - codigo VARCHAR(20) NOT NULL, - nombre VARCHAR(100) NOT NULL, - descripcion TEXT, - nivel_riesgo VARCHAR(20), - requiere_capacitacion_especial BOOLEAN DEFAULT false, - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_puestos_codigo UNIQUE (tenant_id, codigo) -); - --- Agregar FK de puesto a empleados -ALTER TABLE hr.employees - ADD CONSTRAINT fk_employees_puesto - FOREIGN KEY (puesto_id) REFERENCES hr.puestos(id); - --- Tabla: Asignacion de empleados a obras -CREATE TABLE IF NOT EXISTS hr.employee_fraccionamientos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - employee_id UUID NOT NULL REFERENCES hr.employees(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - fecha_inicio DATE NOT NULL, - fecha_fin DATE, - rol VARCHAR(50), - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_employee_fraccionamiento UNIQUE (employee_id, fraccionamiento_id, fecha_inicio) -); - --- ============================================================================ --- INDICES --- ============================================================================ - -CREATE INDEX IF NOT EXISTS idx_employees_tenant ON hr.employees(tenant_id); -CREATE INDEX IF NOT EXISTS idx_employees_estado ON hr.employees(estado); -CREATE INDEX IF NOT EXISTS idx_employees_puesto ON hr.employees(puesto_id); -CREATE INDEX IF NOT EXISTS idx_puestos_tenant ON hr.puestos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_employee_fraccionamientos_employee ON hr.employee_fraccionamientos(employee_id); -CREATE INDEX IF NOT EXISTS idx_employee_fraccionamientos_fraccionamiento ON hr.employee_fraccionamientos(fraccionamiento_id); - --- ============================================================================ --- ROW LEVEL SECURITY --- ============================================================================ - -ALTER TABLE hr.employees ENABLE ROW LEVEL SECURITY; -ALTER TABLE hr.puestos ENABLE ROW LEVEL SECURITY; -ALTER TABLE hr.employee_fraccionamientos ENABLE ROW LEVEL SECURITY; - -CREATE POLICY tenant_isolation_employees ON hr.employees - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - -CREATE POLICY tenant_isolation_puestos ON hr.puestos - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - -CREATE POLICY tenant_isolation_employee_fraccionamientos ON hr.employee_fraccionamientos - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - --- ============================================================================ --- TRIGGERS --- ============================================================================ - -CREATE TRIGGER trg_employees_updated_at - BEFORE UPDATE ON hr.employees - FOR EACH ROW EXECUTE FUNCTION core_shared.set_updated_at(); - -CREATE TRIGGER trg_puestos_updated_at - BEFORE UPDATE ON hr.puestos - FOR EACH ROW EXECUTE FUNCTION core_shared.set_updated_at(); - --- ============================================================================ --- COMENTARIOS --- ============================================================================ - -COMMENT ON TABLE hr.employees IS 'Empleados de la empresa'; -COMMENT ON TABLE hr.puestos IS 'Catalogo de puestos de trabajo'; -COMMENT ON TABLE hr.employee_fraccionamientos IS 'Asignacion de empleados a obras/fraccionamientos'; - --- ============================================================================ --- FIN --- ============================================================================ diff --git a/projects/erp-construccion/database/schemas/03-hse-schema-ddl.sql b/projects/erp-construccion/database/schemas/03-hse-schema-ddl.sql deleted file mode 100644 index 4fe65cfee..000000000 --- a/projects/erp-construccion/database/schemas/03-hse-schema-ddl.sql +++ /dev/null @@ -1,1268 +0,0 @@ --- ============================================================================ --- HSE Schema DDL - Seguridad, Salud Ocupacional y Medio Ambiente --- Modulo: MAA-017 Seguridad HSE --- Version: 1.0.0 --- Fecha: 2025-12-06 --- ============================================================================ - --- Verificar prerequisitos -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'core') THEN - RAISE EXCEPTION 'Schema core no existe. Ejecutar primero erp-core DDL.'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN - RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction-schema-ddl.sql'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'hr') THEN - RAISE EXCEPTION 'Schema hr no existe. Ejecutar primero hr-schema-ddl.sql'; - END IF; -END $$; - --- Crear schema HSE -CREATE SCHEMA IF NOT EXISTS hse; - --- Configurar search_path -SET search_path TO hse, core, construction, hr, public; - --- ============================================================================ --- EXTENSION PostGIS para geolocalizacion --- ============================================================================ -CREATE EXTENSION IF NOT EXISTS postgis; - --- ============================================================================ --- TIPOS ENUMERADOS --- ============================================================================ - --- Tipos de incidentes -CREATE TYPE hse.tipo_incidente AS ENUM ('accidente', 'incidente', 'casi_accidente'); -CREATE TYPE hse.gravedad_incidente AS ENUM ('leve', 'moderado', 'grave', 'fatal'); -CREATE TYPE hse.estado_incidente AS ENUM ('abierto', 'en_investigacion', 'cerrado'); -CREATE TYPE hse.rol_involucrado AS ENUM ('lesionado', 'testigo', 'responsable'); -CREATE TYPE hse.factor_causa AS ENUM ('acto_inseguro', 'condicion_insegura'); - --- Tipos de capacitaciones -CREATE TYPE hse.tipo_capacitacion AS ENUM ('induccion', 'especifica', 'certificacion', 'reentrenamiento'); -CREATE TYPE hse.estado_sesion AS ENUM ('programada', 'en_curso', 'completada', 'cancelada'); - --- Tipos de inspecciones -CREATE TYPE hse.frecuencia AS ENUM ('diaria', 'semanal', 'quincenal', 'mensual', 'eventual'); -CREATE TYPE hse.estado_inspeccion AS ENUM ('programada', 'en_progreso', 'completada', 'cancelada', 'vencida'); -CREATE TYPE hse.resultado_evaluacion AS ENUM ('cumple', 'no_cumple', 'no_aplica'); -CREATE TYPE hse.gravedad_hallazgo AS ENUM ('critico', 'mayor', 'menor'); -CREATE TYPE hse.estado_hallazgo AS ENUM ('abierto', 'en_correccion', 'verificando', 'cerrado', 'reabierto'); -CREATE TYPE hse.tipo_evidencia AS ENUM ('hallazgo', 'correccion'); - --- Tipos de EPP -CREATE TYPE hse.categoria_epp AS ENUM ('cabeza', 'ojos', 'auditiva', 'respiratoria', 'manos', 'pies', 'caidas', 'ropa'); -CREATE TYPE hse.estado_epp AS ENUM ('activo', 'vencido', 'danado', 'perdido', 'devuelto'); -CREATE TYPE hse.estado_inspeccion_epp AS ENUM ('bueno', 'regular', 'malo', 'danado'); -CREATE TYPE hse.motivo_baja_epp AS ENUM ('vencimiento', 'danado', 'perdido', 'terminacion_laboral'); -CREATE TYPE hse.tipo_movimiento_epp AS ENUM ('entrada', 'salida', 'transferencia', 'ajuste'); - --- Tipos STPS -CREATE TYPE hse.estado_comision AS ENUM ('activa', 'vencida', 'renovada'); -CREATE TYPE hse.rol_comision AS ENUM ('presidente', 'secretario', 'vocal_patronal', 'vocal_trabajador'); -CREATE TYPE hse.representacion AS ENUM ('patronal', 'trabajadores'); -CREATE TYPE hse.estado_recorrido AS ENUM ('programado', 'realizado', 'cancelado', 'pendiente'); -CREATE TYPE hse.estado_programa AS ENUM ('borrador', 'activo', 'finalizado'); -CREATE TYPE hse.tipo_actividad_programa AS ENUM ('capacitacion', 'inspeccion', 'simulacro', 'campana', 'otro'); -CREATE TYPE hse.estado_actividad AS ENUM ('pendiente', 'en_progreso', 'completada', 'cancelada'); -CREATE TYPE hse.tipo_documento_stps AS ENUM ('dc1', 'dc2', 'dc3', 'dc4', 'st7', 'st9'); -CREATE TYPE hse.tipo_auditoria AS ENUM ('interna', 'simulada', 'stps', 'cliente', 'certificadora'); -CREATE TYPE hse.resultado_auditoria AS ENUM ('aprobada', 'aprobada_observaciones', 'no_aprobada'); -CREATE TYPE hse.estado_cumplimiento AS ENUM ('cumple', 'parcial', 'no_cumple', 'no_aplica'); - --- Tipos ambientales -CREATE TYPE hse.categoria_residuo AS ENUM ('peligroso', 'manejo_especial', 'urbano'); -CREATE TYPE hse.unidad_residuo AS ENUM ('kg', 'litros', 'm3', 'piezas'); -CREATE TYPE hse.estado_residuo AS ENUM ('almacenado', 'en_transito', 'dispuesto'); -CREATE TYPE hse.estado_almacen AS ENUM ('operativo', 'lleno', 'mantenimiento'); -CREATE TYPE hse.tipo_proveedor_ambiental AS ENUM ('transportista', 'reciclador', 'confinamiento'); -CREATE TYPE hse.estado_manifiesto AS ENUM ('emitido', 'en_transito', 'entregado', 'cerrado'); -CREATE TYPE hse.tipo_impacto AS ENUM ('ruido', 'polvo', 'vibraciones', 'agua', 'emision', 'vegetacion', 'otro'); -CREATE TYPE hse.severidad AS ENUM ('bajo', 'medio', 'alto'); -CREATE TYPE hse.probabilidad AS ENUM ('baja', 'media', 'alta'); -CREATE TYPE hse.nivel_riesgo AS ENUM ('tolerable', 'moderado', 'significativo'); -CREATE TYPE hse.estado_impacto AS ENUM ('identificado', 'mitigando', 'controlado'); -CREATE TYPE hse.origen_queja AS ENUM ('vecino', 'autoridad', 'interno', 'anonimo'); -CREATE TYPE hse.tipo_queja AS ENUM ('ruido', 'polvo', 'olores', 'agua', 'otro'); -CREATE TYPE hse.estado_queja AS ENUM ('recibida', 'atendiendo', 'cerrada'); - --- Tipos permisos de trabajo -CREATE TYPE hse.estado_permiso AS ENUM ('borrador', 'solicitado', 'aprobado_parcial', 'autorizado', 'en_ejecucion', 'suspendido', 'cerrado', 'rechazado', 'vencido'); -CREATE TYPE hse.rol_permiso AS ENUM ('ejecutor', 'supervisor', 'vigia', 'operador', 'senalero'); -CREATE TYPE hse.decision_autorizacion AS ENUM ('aprobado', 'rechazado'); -CREATE TYPE hse.momento_checklist AS ENUM ('pre_trabajo', 'durante', 'post_trabajo'); -CREATE TYPE hse.tipo_evento_permiso AS ENUM ('inicio', 'suspension', 'reanudacion', 'extension', 'anomalia', 'cierre'); - --- Tipos indicadores -CREATE TYPE hse.tipo_indicador AS ENUM ('reactivo', 'proactivo', 'ambiental'); -CREATE TYPE hse.frecuencia_calculo AS ENUM ('diario', 'semanal', 'mensual'); -CREATE TYPE hse.periodo_tipo AS ENUM ('diario', 'semanal', 'mensual', 'anual'); -CREATE TYPE hse.estado_semaforo AS ENUM ('verde', 'amarillo', 'rojo'); -CREATE TYPE hse.fuente_horas AS ENUM ('asistencia', 'manual'); -CREATE TYPE hse.tipo_reporte_hse AS ENUM ('semanal', 'mensual', 'trimestral', 'anual'); -CREATE TYPE hse.formato_reporte AS ENUM ('pdf', 'excel', 'ambos'); -CREATE TYPE hse.tipo_alerta_indicador AS ENUM ('meta_superada', 'tendencia_negativa', 'sin_datos'); - --- ============================================================================ --- RF-MAA017-001: GESTION DE INCIDENTES --- ============================================================================ - --- Tabla: Incidentes -CREATE TABLE hse.incidentes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - folio VARCHAR(20) NOT NULL, - fecha_hora TIMESTAMPTZ NOT NULL, - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - ubicacion_descripcion TEXT, - ubicacion_geo GEOMETRY(Point, 4326), - tipo hse.tipo_incidente NOT NULL, - gravedad hse.gravedad_incidente NOT NULL, - descripcion TEXT NOT NULL, - causa_inmediata TEXT, - causa_basica TEXT, - estado hse.estado_incidente NOT NULL DEFAULT 'abierto', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - - CONSTRAINT uq_incidentes_folio UNIQUE (tenant_id, folio) -); - --- Tabla: Involucrados en incidentes -CREATE TABLE hse.incidente_involucrados ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - incidente_id UUID NOT NULL REFERENCES hse.incidentes(id) ON DELETE CASCADE, - employee_id UUID NOT NULL REFERENCES hr.employees(id), - rol hse.rol_involucrado NOT NULL, - descripcion_lesion TEXT, - parte_cuerpo VARCHAR(100), - dias_incapacidad INTEGER DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Investigacion de incidentes -CREATE TABLE hse.incidente_investigacion ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - incidente_id UUID NOT NULL REFERENCES hse.incidentes(id) ON DELETE CASCADE, - fecha_inicio DATE NOT NULL, - fecha_cierre DATE, - investigador_id UUID REFERENCES hr.employees(id), - metodologia VARCHAR(100), - factor_causa hse.factor_causa, - analisis_causas TEXT, - conclusiones TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Acciones correctivas de incidentes -CREATE TABLE hse.incidente_acciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - incidente_id UUID NOT NULL REFERENCES hse.incidentes(id) ON DELETE CASCADE, - descripcion TEXT NOT NULL, - tipo VARCHAR(50) NOT NULL, - responsable_id UUID REFERENCES hr.employees(id), - fecha_compromiso DATE NOT NULL, - fecha_cierre DATE, - estado VARCHAR(20) NOT NULL DEFAULT 'pendiente', - evidencia_url VARCHAR(500), - observaciones TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Evidencias de incidentes (fotos) -CREATE TABLE hse.incidente_evidencias ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - incidente_id UUID NOT NULL REFERENCES hse.incidentes(id) ON DELETE CASCADE, - tipo VARCHAR(50) NOT NULL DEFAULT 'foto', - archivo_url VARCHAR(500) NOT NULL, - descripcion VARCHAR(200), - ubicacion_geo GEOMETRY(Point, 4326), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- RF-MAA017-002: CONTROL DE CAPACITACIONES --- ============================================================================ - --- Tabla: Catalogo de capacitaciones -CREATE TABLE hse.capacitaciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - codigo VARCHAR(20) NOT NULL, - nombre VARCHAR(200) NOT NULL, - descripcion TEXT, - tipo hse.tipo_capacitacion NOT NULL, - duracion_horas DECIMAL(4,1) NOT NULL, - validez_meses INTEGER, - norma_referencia VARCHAR(50), - requiere_evaluacion BOOLEAN NOT NULL DEFAULT false, - calificacion_minima INTEGER DEFAULT 70, - contenido_tematico TEXT, - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_capacitaciones_codigo UNIQUE (tenant_id, codigo) -); - --- Tabla: Matriz de capacitacion por puesto -CREATE TABLE hse.capacitacion_matriz ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - puesto_id UUID NOT NULL, - capacitacion_id UUID NOT NULL REFERENCES hse.capacitaciones(id), - es_obligatoria BOOLEAN NOT NULL DEFAULT true, - plazo_dias INTEGER DEFAULT 30, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Instructores -CREATE TABLE hse.instructores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - nombre VARCHAR(200) NOT NULL, - registro_stps VARCHAR(50), - especialidades TEXT, - es_interno BOOLEAN NOT NULL DEFAULT false, - employee_id UUID REFERENCES hr.employees(id), - contacto_telefono VARCHAR(20), - contacto_email VARCHAR(100), - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Sesiones de capacitacion -CREATE TABLE hse.capacitacion_sesiones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - capacitacion_id UUID NOT NULL REFERENCES hse.capacitaciones(id), - fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), - instructor_id UUID REFERENCES hse.instructores(id), - fecha_programada DATE NOT NULL, - hora_inicio TIME NOT NULL, - hora_fin TIME NOT NULL, - lugar VARCHAR(200), - cupo_maximo INTEGER, - estado hse.estado_sesion NOT NULL DEFAULT 'programada', - observaciones TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Asistencia a capacitaciones -CREATE TABLE hse.capacitacion_asistentes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - sesion_id UUID NOT NULL REFERENCES hse.capacitacion_sesiones(id) ON DELETE CASCADE, - employee_id UUID NOT NULL REFERENCES hr.employees(id), - asistio BOOLEAN DEFAULT false, - hora_entrada TIME, - hora_salida TIME, - calificacion INTEGER, - aprobado BOOLEAN, - observaciones TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Constancias DC-3 -CREATE TABLE hse.constancias_dc3 ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - folio VARCHAR(30) NOT NULL, - asistente_id UUID NOT NULL REFERENCES hse.capacitacion_asistentes(id), - employee_id UUID NOT NULL REFERENCES hr.employees(id), - capacitacion_id UUID NOT NULL REFERENCES hse.capacitaciones(id), - fecha_emision DATE NOT NULL, - fecha_vencimiento DATE, - documento_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_constancias_dc3_folio UNIQUE (tenant_id, folio) -); - --- ============================================================================ --- RF-MAA017-003: INSPECCIONES DE SEGURIDAD --- ============================================================================ - --- Tabla: Tipos de inspeccion -CREATE TABLE hse.tipos_inspeccion ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - codigo VARCHAR(20) NOT NULL, - nombre VARCHAR(200) NOT NULL, - descripcion TEXT, - frecuencia hse.frecuencia NOT NULL, - norma_referencia VARCHAR(50), - duracion_estimada_min INTEGER, - requiere_firma BOOLEAN NOT NULL DEFAULT true, - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_tipos_inspeccion_codigo UNIQUE (tenant_id, codigo) -); - --- Tabla: Items de checklist -CREATE TABLE hse.checklist_items ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tipo_inspeccion_id UUID NOT NULL REFERENCES hse.tipos_inspeccion(id) ON DELETE CASCADE, - numero_orden INTEGER NOT NULL, - categoria VARCHAR(100), - descripcion TEXT NOT NULL, - criterio_cumplimiento TEXT, - es_critico BOOLEAN NOT NULL DEFAULT false, - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Programa de inspecciones -CREATE TABLE hse.programa_inspecciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - tipo_inspeccion_id UUID NOT NULL REFERENCES hse.tipos_inspeccion(id), - inspector_id UUID REFERENCES hr.employees(id), - fecha_programada DATE NOT NULL, - hora_programada TIME, - zona_area VARCHAR(200), - estado hse.estado_inspeccion NOT NULL DEFAULT 'programada', - motivo_cancelacion TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Inspecciones ejecutadas -CREATE TABLE hse.inspecciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - programa_id UUID REFERENCES hse.programa_inspecciones(id), - tipo_inspeccion_id UUID NOT NULL REFERENCES hse.tipos_inspeccion(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - inspector_id UUID NOT NULL REFERENCES hr.employees(id), - fecha_inicio TIMESTAMPTZ NOT NULL, - fecha_fin TIMESTAMPTZ, - ubicacion_geo GEOMETRY(Point, 4326), - items_evaluados INTEGER DEFAULT 0, - items_cumple INTEGER DEFAULT 0, - items_no_cumple INTEGER DEFAULT 0, - items_no_aplica INTEGER DEFAULT 0, - porcentaje_cumplimiento DECIMAL(5,2), - observaciones_generales TEXT, - firma_inspector TEXT, - estado VARCHAR(20) NOT NULL DEFAULT 'borrador', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Evaluaciones de inspeccion -CREATE TABLE hse.inspeccion_evaluaciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - inspeccion_id UUID NOT NULL REFERENCES hse.inspecciones(id) ON DELETE CASCADE, - checklist_item_id UUID NOT NULL REFERENCES hse.checklist_items(id), - resultado hse.resultado_evaluacion NOT NULL, - observacion TEXT, - genera_hallazgo BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Hallazgos de inspeccion -CREATE TABLE hse.hallazgos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - inspeccion_id UUID NOT NULL REFERENCES hse.inspecciones(id), - evaluacion_id UUID REFERENCES hse.inspeccion_evaluaciones(id), - folio VARCHAR(20) NOT NULL, - gravedad hse.gravedad_hallazgo NOT NULL, - tipo hse.factor_causa NOT NULL, - descripcion TEXT NOT NULL, - ubicacion_descripcion VARCHAR(500), - ubicacion_geo GEOMETRY(Point, 4326), - responsable_correccion_id UUID REFERENCES hr.employees(id), - fecha_limite DATE NOT NULL, - estado hse.estado_hallazgo NOT NULL DEFAULT 'abierto', - fecha_correccion TIMESTAMPTZ, - descripcion_correccion TEXT, - verificador_id UUID REFERENCES hr.employees(id), - fecha_verificacion TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_hallazgos_folio UNIQUE (tenant_id, folio) -); - --- Tabla: Evidencias de hallazgos -CREATE TABLE hse.hallazgo_evidencias ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - hallazgo_id UUID NOT NULL REFERENCES hse.hallazgos(id) ON DELETE CASCADE, - tipo hse.tipo_evidencia NOT NULL, - archivo_url VARCHAR(500) NOT NULL, - descripcion VARCHAR(200), - ubicacion_geo GEOMETRY(Point, 4326), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- RF-MAA017-004: CONTROL DE EPP --- ============================================================================ - --- Tabla: Catalogo de EPP -CREATE TABLE hse.epp_catalogo ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - codigo VARCHAR(20) NOT NULL, - nombre VARCHAR(200) NOT NULL, - categoria hse.categoria_epp NOT NULL, - descripcion TEXT, - especificaciones TEXT, - vida_util_dias INTEGER NOT NULL, - norma_referencia VARCHAR(50), - requiere_certificacion BOOLEAN NOT NULL DEFAULT false, - requiere_inspeccion_periodica BOOLEAN NOT NULL DEFAULT false, - frecuencia_inspeccion_dias INTEGER, - alerta_dias_antes INTEGER DEFAULT 15, - imagen_url VARCHAR(500), - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_epp_catalogo_codigo UNIQUE (tenant_id, codigo) -); - --- Tabla: Matriz EPP por puesto -CREATE TABLE hse.epp_matriz_puesto ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - puesto_id UUID NOT NULL, - epp_id UUID NOT NULL REFERENCES hse.epp_catalogo(id), - es_obligatorio BOOLEAN NOT NULL DEFAULT true, - actividad_especifica VARCHAR(200), - observaciones TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Asignaciones de EPP -CREATE TABLE hse.epp_asignaciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - employee_id UUID NOT NULL REFERENCES hr.employees(id), - epp_id UUID NOT NULL REFERENCES hse.epp_catalogo(id), - fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), - fecha_entrega DATE NOT NULL, - fecha_vencimiento DATE NOT NULL, - numero_serie VARCHAR(100), - numero_lote VARCHAR(100), - firma_trabajador TEXT, - foto_entrega_url VARCHAR(500), - capacitacion_uso BOOLEAN NOT NULL DEFAULT false, - estado hse.estado_epp NOT NULL DEFAULT 'activo', - costo_unitario DECIMAL(10,2), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- Tabla: Inspecciones de EPP -CREATE TABLE hse.epp_inspecciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - asignacion_id UUID NOT NULL REFERENCES hse.epp_asignaciones(id) ON DELETE CASCADE, - inspector_id UUID NOT NULL REFERENCES hr.employees(id), - fecha_inspeccion DATE NOT NULL, - estado_epp hse.estado_inspeccion_epp NOT NULL, - observaciones TEXT, - requiere_reemplazo BOOLEAN NOT NULL DEFAULT false, - foto_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Bajas de EPP -CREATE TABLE hse.epp_bajas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - asignacion_id UUID NOT NULL REFERENCES hse.epp_asignaciones(id), - fecha_baja DATE NOT NULL, - motivo hse.motivo_baja_epp NOT NULL, - descripcion TEXT, - descuento_aplicado BOOLEAN NOT NULL DEFAULT false, - monto_descuento DECIMAL(10,2), - autorizado_por UUID REFERENCES hr.employees(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Inventario de EPP -CREATE TABLE hse.epp_inventario ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - epp_id UUID NOT NULL REFERENCES hse.epp_catalogo(id), - almacen_id UUID, - cantidad_disponible INTEGER NOT NULL DEFAULT 0, - cantidad_minima INTEGER DEFAULT 0, - cantidad_maxima INTEGER, - costo_promedio DECIMAL(10,2), - ultima_entrada DATE, - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Movimientos de EPP -CREATE TABLE hse.epp_movimientos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - epp_id UUID NOT NULL REFERENCES hse.epp_catalogo(id), - almacen_origen_id UUID, - almacen_destino_id UUID, - tipo hse.tipo_movimiento_epp NOT NULL, - cantidad INTEGER NOT NULL, - referencia VARCHAR(100), - observaciones TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- RF-MAA017-005: CUMPLIMIENTO STPS --- ============================================================================ - --- Tabla: Catalogo de normas STPS -CREATE TABLE hse.normas_stps ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - codigo VARCHAR(30) NOT NULL UNIQUE, - nombre VARCHAR(300) NOT NULL, - descripcion TEXT, - fecha_publicacion DATE, - ultima_actualizacion DATE, - aplica_construccion BOOLEAN NOT NULL DEFAULT true, - documento_url VARCHAR(500), - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Requisitos por norma -CREATE TABLE hse.norma_requisitos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - norma_id UUID NOT NULL REFERENCES hse.normas_stps(id) ON DELETE CASCADE, - numero VARCHAR(20) NOT NULL, - descripcion TEXT NOT NULL, - tipo_evidencia VARCHAR(200), - es_critico BOOLEAN NOT NULL DEFAULT false, - aplica_a VARCHAR(100), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Cumplimiento por obra -CREATE TABLE hse.cumplimiento_obra ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - norma_id UUID NOT NULL REFERENCES hse.normas_stps(id), - requisito_id UUID REFERENCES hse.norma_requisitos(id), - estado hse.estado_cumplimiento NOT NULL DEFAULT 'no_cumple', - evidencia_url VARCHAR(500), - observaciones TEXT, - fecha_evaluacion DATE NOT NULL, - evaluador_id UUID REFERENCES hr.employees(id), - fecha_compromiso DATE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Comisiones de seguridad e higiene -CREATE TABLE hse.comision_seguridad ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - fecha_constitucion DATE NOT NULL, - numero_acta VARCHAR(50), - vigencia_inicio DATE NOT NULL, - vigencia_fin DATE NOT NULL, - estado hse.estado_comision NOT NULL DEFAULT 'activa', - documento_acta_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Integrantes de comision -CREATE TABLE hse.comision_integrantes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - comision_id UUID NOT NULL REFERENCES hse.comision_seguridad(id) ON DELETE CASCADE, - employee_id UUID NOT NULL REFERENCES hr.employees(id), - rol hse.rol_comision NOT NULL, - representacion hse.representacion NOT NULL, - fecha_nombramiento DATE NOT NULL, - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Recorridos de comision -CREATE TABLE hse.comision_recorridos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - comision_id UUID NOT NULL REFERENCES hse.comision_seguridad(id) ON DELETE CASCADE, - fecha_programada DATE NOT NULL, - fecha_realizada DATE, - numero_acta VARCHAR(50), - areas_recorridas TEXT, - hallazgos TEXT, - recomendaciones TEXT, - estado hse.estado_recorrido NOT NULL DEFAULT 'programado', - documento_acta_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Programa de seguridad anual -CREATE TABLE hse.programa_seguridad ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - anio INTEGER NOT NULL, - objetivo_general TEXT, - metas JSONB, - presupuesto DECIMAL(12,2), - estado hse.estado_programa NOT NULL DEFAULT 'borrador', - aprobado_por UUID REFERENCES hr.employees(id), - fecha_aprobacion DATE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Actividades del programa -CREATE TABLE hse.programa_actividades ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - programa_id UUID NOT NULL REFERENCES hse.programa_seguridad(id) ON DELETE CASCADE, - actividad VARCHAR(300) NOT NULL, - tipo hse.tipo_actividad_programa NOT NULL, - fecha_programada DATE NOT NULL, - fecha_realizada DATE, - responsable_id UUID REFERENCES hr.employees(id), - recursos TEXT, - estado hse.estado_actividad NOT NULL DEFAULT 'pendiente', - evidencia_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Documentos STPS emitidos -CREATE TABLE hse.documentos_stps ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - tipo hse.tipo_documento_stps NOT NULL, - folio VARCHAR(30) NOT NULL, - fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), - employee_id UUID REFERENCES hr.employees(id), - fecha_emision DATE NOT NULL, - fecha_vencimiento DATE, - datos_documento JSONB, - documento_url VARCHAR(500), - firmado BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - - CONSTRAINT uq_documentos_stps_folio UNIQUE (tenant_id, tipo, folio) -); - --- Tabla: Auditorias -CREATE TABLE hse.auditorias ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - tipo hse.tipo_auditoria NOT NULL, - fecha_programada DATE NOT NULL, - fecha_realizada DATE, - auditor VARCHAR(200), - resultado hse.resultado_auditoria, - no_conformidades INTEGER DEFAULT 0, - observaciones TEXT, - informe_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- ============================================================================ --- RF-MAA017-006: GESTION AMBIENTAL --- ============================================================================ - --- Tabla: Catalogo de residuos -CREATE TABLE hse.residuos_catalogo ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - codigo VARCHAR(20) NOT NULL UNIQUE, - nombre VARCHAR(200) NOT NULL, - categoria hse.categoria_residuo NOT NULL, - caracteristicas_cretib VARCHAR(6), - norma_referencia VARCHAR(50), - manejo_requerido TEXT, - tiempo_max_almacen_dias INTEGER, - requiere_manifiesto BOOLEAN NOT NULL DEFAULT false, - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Generacion de residuos -CREATE TABLE hse.residuos_generacion ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - residuo_id UUID NOT NULL REFERENCES hse.residuos_catalogo(id), - fecha_generacion DATE NOT NULL, - cantidad DECIMAL(10,2) NOT NULL, - unidad hse.unidad_residuo NOT NULL, - area_generacion VARCHAR(200), - fuente VARCHAR(200), - contenedor_id VARCHAR(50), - foto_url VARCHAR(500), - ubicacion_geo GEOMETRY(Point, 4326), - estado hse.estado_residuo NOT NULL DEFAULT 'almacenado', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- Tabla: Almacenes temporales -CREATE TABLE hse.almacen_temporal ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - nombre VARCHAR(100) NOT NULL, - ubicacion VARCHAR(200), - capacidad_m3 DECIMAL(8,2), - tiene_contencion BOOLEAN NOT NULL DEFAULT true, - tiene_techo BOOLEAN NOT NULL DEFAULT true, - senalizacion_ok BOOLEAN NOT NULL DEFAULT true, - estado hse.estado_almacen NOT NULL DEFAULT 'operativo', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Proveedores ambientales -CREATE TABLE hse.proveedores_ambientales ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - razon_social VARCHAR(300) NOT NULL, - rfc VARCHAR(13), - tipo hse.tipo_proveedor_ambiental NOT NULL, - numero_autorizacion VARCHAR(50), - entidad_autorizadora VARCHAR(100), - fecha_autorizacion DATE, - fecha_vencimiento DATE, - servicios TEXT, - contacto_nombre VARCHAR(200), - contacto_telefono VARCHAR(20), - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Manifiestos de residuos -CREATE TABLE hse.manifiestos_residuos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - folio VARCHAR(30) NOT NULL, - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - transportista_id UUID NOT NULL REFERENCES hse.proveedores_ambientales(id), - destino_id UUID NOT NULL REFERENCES hse.proveedores_ambientales(id), - fecha_recoleccion DATE NOT NULL, - fecha_entrega DATE, - estado hse.estado_manifiesto NOT NULL DEFAULT 'emitido', - observaciones TEXT, - documento_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_manifiestos_residuos_folio UNIQUE (tenant_id, folio) -); - --- Tabla: Detalle de manifiestos -CREATE TABLE hse.manifiesto_detalle ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - manifiesto_id UUID NOT NULL REFERENCES hse.manifiestos_residuos(id) ON DELETE CASCADE, - residuo_id UUID NOT NULL REFERENCES hse.residuos_catalogo(id), - generacion_ids UUID[], - cantidad DECIMAL(10,2) NOT NULL, - unidad hse.unidad_residuo NOT NULL, - descripcion TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Impacto ambiental -CREATE TABLE hse.impacto_ambiental ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - aspecto VARCHAR(200) NOT NULL, - tipo_impacto hse.tipo_impacto NOT NULL, - severidad hse.severidad NOT NULL, - probabilidad hse.probabilidad NOT NULL, - nivel_riesgo hse.nivel_riesgo NOT NULL, - medidas_mitigacion TEXT, - responsable_id UUID REFERENCES hr.employees(id), - estado hse.estado_impacto NOT NULL DEFAULT 'identificado', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Quejas ambientales -CREATE TABLE hse.quejas_ambientales ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - fecha_queja TIMESTAMPTZ NOT NULL, - origen hse.origen_queja NOT NULL, - tipo hse.tipo_queja NOT NULL, - descripcion TEXT NOT NULL, - nombre_quejoso VARCHAR(200), - contacto_quejoso VARCHAR(100), - acciones_tomadas TEXT, - estado hse.estado_queja NOT NULL DEFAULT 'recibida', - fecha_cierre DATE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- ============================================================================ --- RF-MAA017-007: PERMISOS DE TRABAJO --- ============================================================================ - --- Tabla: Tipos de permiso de trabajo -CREATE TABLE hse.tipos_permiso_trabajo ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - codigo VARCHAR(20) NOT NULL, - nombre VARCHAR(200) NOT NULL, - descripcion TEXT, - norma_referencia VARCHAR(50), - vigencia_max_horas INTEGER NOT NULL, - requiere_autorizacion_nivel INTEGER NOT NULL DEFAULT 2, - documentos_requeridos JSONB, - requisitos_personal JSONB, - equipos_requeridos JSONB, - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_tipos_permiso_trabajo_codigo UNIQUE (tenant_id, codigo) -); - --- Tabla: Permisos de trabajo -CREATE TABLE hse.permisos_trabajo ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - folio VARCHAR(30) NOT NULL, - tipo_permiso_id UUID NOT NULL REFERENCES hse.tipos_permiso_trabajo(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - solicitante_id UUID NOT NULL REFERENCES hr.employees(id), - descripcion_trabajo TEXT NOT NULL, - ubicacion VARCHAR(300) NOT NULL, - ubicacion_geo GEOMETRY(Point, 4326), - fecha_inicio_programada TIMESTAMPTZ NOT NULL, - fecha_fin_programada TIMESTAMPTZ NOT NULL, - fecha_inicio_real TIMESTAMPTZ, - fecha_fin_real TIMESTAMPTZ, - estado hse.estado_permiso NOT NULL DEFAULT 'borrador', - motivo_rechazo TEXT, - motivo_suspension TEXT, - observaciones TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_permisos_trabajo_folio UNIQUE (tenant_id, folio) -); - --- Tabla: Personal del permiso -CREATE TABLE hse.permiso_personal ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, - employee_id UUID NOT NULL REFERENCES hr.employees(id), - rol hse.rol_permiso NOT NULL, - verificacion_capacitacion BOOLEAN NOT NULL DEFAULT false, - verificacion_epp BOOLEAN NOT NULL DEFAULT false, - verificacion_aptitud BOOLEAN NOT NULL DEFAULT false, - observaciones TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Autorizaciones de permiso -CREATE TABLE hse.permiso_autorizaciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, - nivel INTEGER NOT NULL, - autorizador_id UUID NOT NULL REFERENCES hr.employees(id), - rol_autorizador VARCHAR(100), - decision hse.decision_autorizacion NOT NULL, - observaciones TEXT, - firma_digital TEXT, - fecha_decision TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Checklist del permiso -CREATE TABLE hse.permiso_checklist ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, - momento hse.momento_checklist NOT NULL, - item_verificacion VARCHAR(300) NOT NULL, - cumple BOOLEAN, - observacion TEXT, - verificador_id UUID REFERENCES hr.employees(id), - fecha_verificacion TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Monitoreos durante permiso -CREATE TABLE hse.permiso_monitoreos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, - fecha_hora TIMESTAMPTZ NOT NULL, - tipo VARCHAR(100) NOT NULL, - valor_medicion VARCHAR(50), - unidad VARCHAR(20), - dentro_rango BOOLEAN NOT NULL DEFAULT true, - observaciones TEXT, - responsable_id UUID NOT NULL REFERENCES hr.employees(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Eventos del permiso -CREATE TABLE hse.permiso_eventos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, - fecha_hora TIMESTAMPTZ NOT NULL DEFAULT NOW(), - tipo_evento hse.tipo_evento_permiso NOT NULL, - descripcion TEXT, - accion_tomada TEXT, - responsable_id UUID NOT NULL REFERENCES hr.employees(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Documentos del permiso -CREATE TABLE hse.permiso_documentos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - permiso_id UUID NOT NULL REFERENCES hse.permisos_trabajo(id) ON DELETE CASCADE, - tipo_documento VARCHAR(100) NOT NULL, - nombre VARCHAR(200) NOT NULL, - archivo_url VARCHAR(500) NOT NULL, - fecha_subida TIMESTAMPTZ NOT NULL DEFAULT NOW(), - subido_por UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- RF-MAA017-008: INDICADORES HSE --- ============================================================================ - --- Tabla: Configuracion de indicadores -CREATE TABLE hse.indicadores_config ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - codigo VARCHAR(20) NOT NULL, - nombre VARCHAR(200) NOT NULL, - descripcion TEXT, - formula TEXT, - unidad VARCHAR(50), - tipo hse.tipo_indicador NOT NULL, - meta_global DECIMAL(10,4), - umbral_verde DECIMAL(10,4), - umbral_amarillo DECIMAL(10,4), - umbral_rojo DECIMAL(10,4), - frecuencia_calculo hse.frecuencia_calculo NOT NULL DEFAULT 'mensual', - activo BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_indicadores_config_codigo UNIQUE (tenant_id, codigo) -); - --- Tabla: Metas por obra -CREATE TABLE hse.indicadores_meta_obra ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - indicador_id UUID NOT NULL REFERENCES hse.indicadores_config(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - anio INTEGER NOT NULL, - meta DECIMAL(10,4) NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Valores calculados de indicadores -CREATE TABLE hse.indicadores_valores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - indicador_id UUID NOT NULL REFERENCES hse.indicadores_config(id), - fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), - periodo_tipo hse.periodo_tipo NOT NULL, - periodo_fecha DATE NOT NULL, - valor DECIMAL(15,4), - numerador DECIMAL(15,4), - denominador DECIMAL(15,4), - estado hse.estado_semaforo, - calculado_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Horas trabajadas -CREATE TABLE hse.horas_trabajadas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - fecha DATE NOT NULL, - horas_totales DECIMAL(12,2) NOT NULL, - trabajadores_promedio INTEGER NOT NULL, - fuente hse.fuente_horas NOT NULL DEFAULT 'manual', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_horas_trabajadas UNIQUE (tenant_id, fraccionamiento_id, fecha) -); - --- Tabla: Dias sin accidente -CREATE TABLE hse.dias_sin_accidente ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - fecha_inicio_conteo DATE NOT NULL, - dias_acumulados INTEGER NOT NULL DEFAULT 0, - record_historico INTEGER NOT NULL DEFAULT 0, - ultimo_incidente_id UUID REFERENCES hse.incidentes(id), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_dias_sin_accidente UNIQUE (tenant_id, fraccionamiento_id) -); - --- Tabla: Reportes programados -CREATE TABLE hse.reportes_programados ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - nombre VARCHAR(200) NOT NULL, - tipo_reporte hse.tipo_reporte_hse NOT NULL, - indicadores UUID[], - fraccionamientos UUID[], - destinatarios VARCHAR[], - dia_envio INTEGER, - hora_envio TIME, - formato hse.formato_reporte NOT NULL DEFAULT 'pdf', - activo BOOLEAN NOT NULL DEFAULT true, - ultimo_envio TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Tabla: Alertas de indicadores -CREATE TABLE hse.alertas_indicadores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id), - indicador_id UUID NOT NULL REFERENCES hse.indicadores_config(id), - fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), - tipo_alerta hse.tipo_alerta_indicador NOT NULL, - mensaje TEXT NOT NULL, - valor_actual DECIMAL(10,4), - valor_meta DECIMAL(10,4), - leida BOOLEAN NOT NULL DEFAULT false, - fecha_alerta TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- ============================================================================ --- INDICES --- ============================================================================ - --- Incidentes -CREATE INDEX idx_incidentes_tenant ON hse.incidentes(tenant_id); -CREATE INDEX idx_incidentes_fraccionamiento ON hse.incidentes(fraccionamiento_id); -CREATE INDEX idx_incidentes_fecha ON hse.incidentes(fecha_hora); -CREATE INDEX idx_incidentes_estado ON hse.incidentes(estado); -CREATE INDEX idx_incidentes_tipo ON hse.incidentes(tipo); -CREATE INDEX idx_incidentes_gravedad ON hse.incidentes(gravedad); -CREATE INDEX idx_incidente_involucrados_employee ON hse.incidente_involucrados(employee_id); - --- Capacitaciones -CREATE INDEX idx_capacitaciones_tenant ON hse.capacitaciones(tenant_id); -CREATE INDEX idx_capacitacion_sesiones_fecha ON hse.capacitacion_sesiones(fecha_programada); -CREATE INDEX idx_capacitacion_asistentes_employee ON hse.capacitacion_asistentes(employee_id); -CREATE INDEX idx_constancias_dc3_employee ON hse.constancias_dc3(employee_id); -CREATE INDEX idx_constancias_dc3_vencimiento ON hse.constancias_dc3(fecha_vencimiento); - --- Inspecciones -CREATE INDEX idx_inspecciones_tenant ON hse.inspecciones(tenant_id); -CREATE INDEX idx_inspecciones_fraccionamiento ON hse.inspecciones(fraccionamiento_id); -CREATE INDEX idx_inspecciones_fecha ON hse.inspecciones(fecha_inicio); -CREATE INDEX idx_programa_inspecciones_fecha ON hse.programa_inspecciones(fecha_programada); -CREATE INDEX idx_hallazgos_estado ON hse.hallazgos(estado); -CREATE INDEX idx_hallazgos_fecha_limite ON hse.hallazgos(fecha_limite); - --- EPP -CREATE INDEX idx_epp_asignaciones_employee ON hse.epp_asignaciones(employee_id); -CREATE INDEX idx_epp_asignaciones_vencimiento ON hse.epp_asignaciones(fecha_vencimiento); -CREATE INDEX idx_epp_asignaciones_estado ON hse.epp_asignaciones(estado); - --- STPS -CREATE INDEX idx_comision_seguridad_vigencia ON hse.comision_seguridad(vigencia_fin); -CREATE INDEX idx_documentos_stps_vencimiento ON hse.documentos_stps(fecha_vencimiento); - --- Ambiental -CREATE INDEX idx_residuos_generacion_fecha ON hse.residuos_generacion(fecha_generacion); -CREATE INDEX idx_manifiestos_estado ON hse.manifiestos_residuos(estado); - --- Permisos -CREATE INDEX idx_permisos_trabajo_estado ON hse.permisos_trabajo(estado); -CREATE INDEX idx_permisos_trabajo_fecha ON hse.permisos_trabajo(fecha_inicio_programada); - --- Indicadores -CREATE INDEX idx_indicadores_valores_fecha ON hse.indicadores_valores(periodo_fecha); -CREATE INDEX idx_horas_trabajadas_fecha ON hse.horas_trabajadas(fecha); - --- ============================================================================ --- ROW LEVEL SECURITY (RLS) --- ============================================================================ - --- Habilitar RLS en todas las tablas con tenant_id -ALTER TABLE hse.incidentes ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.capacitaciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.capacitacion_sesiones ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.instructores ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.constancias_dc3 ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.tipos_inspeccion ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.programa_inspecciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.inspecciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.hallazgos ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.epp_catalogo ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.epp_asignaciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.epp_inventario ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.epp_movimientos ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.cumplimiento_obra ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.comision_seguridad ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.programa_seguridad ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.documentos_stps ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.auditorias ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.residuos_generacion ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.almacen_temporal ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.proveedores_ambientales ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.manifiestos_residuos ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.impacto_ambiental ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.quejas_ambientales ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.tipos_permiso_trabajo ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.permisos_trabajo ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.indicadores_config ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.indicadores_valores ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.horas_trabajadas ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.dias_sin_accidente ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.reportes_programados ENABLE ROW LEVEL SECURITY; -ALTER TABLE hse.alertas_indicadores ENABLE ROW LEVEL SECURITY; - --- Politicas RLS (ejemplo para incidentes, replicar para las demas) -CREATE POLICY tenant_isolation_incidentes ON hse.incidentes - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - -CREATE POLICY tenant_isolation_capacitaciones ON hse.capacitaciones - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - -CREATE POLICY tenant_isolation_inspecciones ON hse.inspecciones - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - -CREATE POLICY tenant_isolation_hallazgos ON hse.hallazgos - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - -CREATE POLICY tenant_isolation_epp_asignaciones ON hse.epp_asignaciones - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - -CREATE POLICY tenant_isolation_permisos_trabajo ON hse.permisos_trabajo - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - -CREATE POLICY tenant_isolation_indicadores_valores ON hse.indicadores_valores - FOR ALL - USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); - --- ============================================================================ --- TRIGGERS PARA UPDATED_AT --- ============================================================================ - -CREATE OR REPLACE FUNCTION hse.update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Aplicar trigger a tablas con updated_at -CREATE TRIGGER trg_incidentes_updated_at - BEFORE UPDATE ON hse.incidentes - FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); - -CREATE TRIGGER trg_capacitaciones_updated_at - BEFORE UPDATE ON hse.capacitaciones - FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); - -CREATE TRIGGER trg_inspecciones_updated_at - BEFORE UPDATE ON hse.inspecciones - FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); - -CREATE TRIGGER trg_hallazgos_updated_at - BEFORE UPDATE ON hse.hallazgos - FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); - -CREATE TRIGGER trg_epp_asignaciones_updated_at - BEFORE UPDATE ON hse.epp_asignaciones - FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); - -CREATE TRIGGER trg_permisos_trabajo_updated_at - BEFORE UPDATE ON hse.permisos_trabajo - FOR EACH ROW EXECUTE FUNCTION hse.update_updated_at(); - --- ============================================================================ --- DATOS INICIALES - CATALOGO DE NORMAS STPS --- ============================================================================ - -INSERT INTO hse.normas_stps (codigo, nombre, descripcion, aplica_construccion) VALUES -('NOM-001-STPS-2008', 'Edificios, locales e instalaciones', 'Condiciones de seguridad', true), -('NOM-002-STPS-2010', 'Prevencion y proteccion contra incendios', 'Prevencion y proteccion contra incendios en centros de trabajo', true), -('NOM-004-STPS-1999', 'Sistemas de proteccion y dispositivos de seguridad en maquinaria', 'Sistemas de proteccion en maquinaria y equipo', true), -('NOM-005-STPS-1998', 'Manejo de sustancias quimicas peligrosas', 'Relativa a condiciones de seguridad e higiene', true), -('NOM-006-STPS-2014', 'Manejo y almacenamiento de materiales', 'Condiciones de seguridad y salud en el trabajo', true), -('NOM-009-STPS-2011', 'Trabajos en altura', 'Condiciones de seguridad para realizar trabajos en altura', true), -('NOM-011-STPS-2001', 'Ruido', 'Condiciones de seguridad e higiene donde se genere ruido', true), -('NOM-017-STPS-2008', 'Equipo de proteccion personal', 'Seleccion, uso y manejo en centros de trabajo', true), -('NOM-019-STPS-2011', 'Comisiones de seguridad e higiene', 'Constitucion, integracion, organizacion y funcionamiento', true), -('NOM-026-STPS-2008', 'Senales de seguridad', 'Colores y senales de seguridad e higiene', true), -('NOM-029-STPS-2011', 'Mantenimiento de instalaciones electricas', 'Condiciones de seguridad para realizar actividades', true), -('NOM-030-STPS-2009', 'Servicios preventivos de seguridad y salud', 'Funciones y actividades', true), -('NOM-031-STPS-2011', 'Construccion', 'Condiciones de seguridad y salud en el trabajo', true); - --- ============================================================================ --- DATOS INICIALES - CATALOGO DE RESIDUOS COMUNES EN CONSTRUCCION --- ============================================================================ - -INSERT INTO hse.residuos_catalogo (codigo, nombre, categoria, caracteristicas_cretib, norma_referencia, tiempo_max_almacen_dias, requiere_manifiesto) VALUES -('RP-001', 'Aceites usados', 'peligroso', 'IT', 'NOM-052-SEMARNAT', 180, true), -('RP-002', 'Solventes usados', 'peligroso', 'IT', 'NOM-052-SEMARNAT', 180, true), -('RP-003', 'Pinturas y barnices', 'peligroso', 'IT', 'NOM-052-SEMARNAT', 180, true), -('RP-004', 'Baterias y acumuladores', 'peligroso', 'CT', 'NOM-052-SEMARNAT', 180, true), -('RP-005', 'Envases contaminados', 'peligroso', 'T', 'NOM-052-SEMARNAT', 180, true), -('RP-006', 'Trapos y estopas impregnados', 'peligroso', 'I', 'NOM-052-SEMARNAT', 180, true), -('RME-001', 'Escombro y cascajo', 'manejo_especial', NULL, 'LGPGIR', 365, false), -('RME-002', 'Tierra excavada', 'manejo_especial', NULL, 'LGPGIR', 365, false), -('RME-003', 'Material de demolicion', 'manejo_especial', NULL, 'LGPGIR', 365, false), -('RSU-001', 'Carton y papel', 'urbano', NULL, NULL, NULL, false), -('RSU-002', 'Plasticos', 'urbano', NULL, NULL, NULL, false), -('RSU-003', 'Residuos organicos', 'urbano', NULL, NULL, NULL, false); - --- ============================================================================ --- COMENTARIOS DE DOCUMENTACION --- ============================================================================ - -COMMENT ON SCHEMA hse IS 'Schema para gestion de Seguridad, Salud Ocupacional y Medio Ambiente (HSE) - MAA-017'; -COMMENT ON TABLE hse.incidentes IS 'Registro de incidentes y accidentes de trabajo'; -COMMENT ON TABLE hse.capacitaciones IS 'Catalogo de capacitaciones de seguridad'; -COMMENT ON TABLE hse.inspecciones IS 'Registro de inspecciones de seguridad ejecutadas'; -COMMENT ON TABLE hse.hallazgos IS 'Hallazgos detectados en inspecciones'; -COMMENT ON TABLE hse.epp_asignaciones IS 'Asignacion de EPP a trabajadores'; -COMMENT ON TABLE hse.permisos_trabajo IS 'Permisos para trabajos de alto riesgo'; -COMMENT ON TABLE hse.indicadores_valores IS 'Valores calculados de indicadores HSE'; - --- ============================================================================ --- FIN DEL DDL --- ============================================================================ diff --git a/projects/erp-construccion/database/schemas/04-estimates-schema-ddl.sql b/projects/erp-construccion/database/schemas/04-estimates-schema-ddl.sql deleted file mode 100644 index c89c11635..000000000 --- a/projects/erp-construccion/database/schemas/04-estimates-schema-ddl.sql +++ /dev/null @@ -1,415 +0,0 @@ --- ============================================================================ --- ESTIMATES Schema DDL - Estimaciones, Anticipos y Retenciones --- Modulos: MAI-008 (Estimaciones y Facturaci贸n) --- Version: 1.0.0 --- Fecha: 2025-12-08 --- ============================================================================ --- PREREQUISITOS: --- 1. ERP-Core instalado (auth.tenants, auth.users) --- 2. Schema construction instalado (fraccionamientos, contratos, conceptos, lotes, departamentos) --- ============================================================================ - --- Verificar prerequisitos -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN - RAISE EXCEPTION 'Schema auth no existe. Ejecutar primero ERP-Core DDL'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN - RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL'; - END IF; -END $$; - --- Crear schema -CREATE SCHEMA IF NOT EXISTS estimates; - --- ============================================================================ --- TYPES (ENUMs) --- ============================================================================ - -DO $$ BEGIN - CREATE TYPE estimates.estimate_status AS ENUM ( - 'draft', 'submitted', 'reviewed', 'approved', 'invoiced', 'paid', 'rejected', 'cancelled' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE estimates.advance_type AS ENUM ( - 'initial', 'progress', 'materials' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE estimates.retention_type AS ENUM ( - 'guarantee', 'tax', 'penalty', 'other' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE estimates.generator_status AS ENUM ( - 'draft', 'in_progress', 'completed', 'approved' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - --- ============================================================================ --- TABLES - ESTIMACIONES --- ============================================================================ - --- Tabla: estimaciones (estimaciones de obra) -CREATE TABLE IF NOT EXISTS estimates.estimaciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - contrato_id UUID NOT NULL REFERENCES construction.contratos(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - estimate_number VARCHAR(30) NOT NULL, - period_start DATE NOT NULL, - period_end DATE NOT NULL, - sequence_number INTEGER NOT NULL, - status estimates.estimate_status NOT NULL DEFAULT 'draft', - subtotal DECIMAL(16,2) DEFAULT 0, - advance_amount DECIMAL(16,2) DEFAULT 0, - retention_amount DECIMAL(16,2) DEFAULT 0, - tax_amount DECIMAL(16,2) DEFAULT 0, - total_amount DECIMAL(16,2) DEFAULT 0, - submitted_at TIMESTAMPTZ, - submitted_by UUID REFERENCES auth.users(id), - reviewed_at TIMESTAMPTZ, - reviewed_by UUID REFERENCES auth.users(id), - approved_at TIMESTAMPTZ, - approved_by UUID REFERENCES auth.users(id), - invoice_id UUID, - invoiced_at TIMESTAMPTZ, - paid_at TIMESTAMPTZ, - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_estimaciones_number_tenant UNIQUE (tenant_id, estimate_number), - CONSTRAINT uq_estimaciones_sequence_contrato UNIQUE (contrato_id, sequence_number), - CONSTRAINT chk_estimaciones_period CHECK (period_end >= period_start) -); - --- Tabla: estimacion_conceptos (l铆neas de estimaci贸n) -CREATE TABLE IF NOT EXISTS estimates.estimacion_conceptos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id) ON DELETE CASCADE, - concepto_id UUID NOT NULL REFERENCES construction.conceptos(id), - contrato_partida_id UUID REFERENCES construction.contrato_partidas(id), - quantity_contract DECIMAL(12,4) DEFAULT 0, - quantity_previous DECIMAL(12,4) DEFAULT 0, - quantity_current DECIMAL(12,4) DEFAULT 0, - quantity_accumulated DECIMAL(12,4) GENERATED ALWAYS AS (quantity_previous + quantity_current) STORED, - unit_price DECIMAL(12,4) NOT NULL DEFAULT 0, - amount_current DECIMAL(14,2) GENERATED ALWAYS AS (quantity_current * unit_price) STORED, - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_est_conceptos_estimacion_concepto UNIQUE (estimacion_id, concepto_id) -); - --- Tabla: generadores (soporte de cantidades) -CREATE TABLE IF NOT EXISTS estimates.generadores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - estimacion_concepto_id UUID NOT NULL REFERENCES estimates.estimacion_conceptos(id) ON DELETE CASCADE, - generator_number VARCHAR(30) NOT NULL, - description TEXT, - status estimates.generator_status NOT NULL DEFAULT 'draft', - lote_id UUID REFERENCES construction.lotes(id), - departamento_id UUID REFERENCES construction.departamentos(id), - location_description VARCHAR(255), - quantity DECIMAL(12,4) NOT NULL DEFAULT 0, - formula TEXT, - photo_url VARCHAR(500), - sketch_url VARCHAR(500), - captured_by UUID NOT NULL REFERENCES auth.users(id), - captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - approved_by UUID REFERENCES auth.users(id), - approved_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- TABLES - ANTICIPOS --- ============================================================================ - --- Tabla: anticipos (anticipos otorgados) -CREATE TABLE IF NOT EXISTS estimates.anticipos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - contrato_id UUID NOT NULL REFERENCES construction.contratos(id), - advance_type estimates.advance_type NOT NULL DEFAULT 'initial', - advance_number VARCHAR(30) NOT NULL, - advance_date DATE NOT NULL, - gross_amount DECIMAL(16,2) NOT NULL, - tax_amount DECIMAL(16,2) DEFAULT 0, - net_amount DECIMAL(16,2) NOT NULL, - amortization_percentage DECIMAL(5,2) DEFAULT 0, - amortized_amount DECIMAL(16,2) DEFAULT 0, - pending_amount DECIMAL(16,2) GENERATED ALWAYS AS (net_amount - amortized_amount) STORED, - is_fully_amortized BOOLEAN DEFAULT FALSE, - approved_at TIMESTAMPTZ, - approved_by UUID REFERENCES auth.users(id), - paid_at TIMESTAMPTZ, - payment_reference VARCHAR(100), - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_anticipos_number_tenant UNIQUE (tenant_id, advance_number) -); - --- Tabla: amortizaciones (amortizaciones de anticipos) -CREATE TABLE IF NOT EXISTS estimates.amortizaciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - anticipo_id UUID NOT NULL REFERENCES estimates.anticipos(id), - estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id), - amount DECIMAL(16,2) NOT NULL, - amortization_date DATE NOT NULL, - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_amortizaciones_anticipo_estimacion UNIQUE (anticipo_id, estimacion_id) -); - --- ============================================================================ --- TABLES - RETENCIONES --- ============================================================================ - --- Tabla: retenciones (retenciones aplicadas) -CREATE TABLE IF NOT EXISTS estimates.retenciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id), - retention_type estimates.retention_type NOT NULL, - description VARCHAR(255) NOT NULL, - percentage DECIMAL(5,2), - amount DECIMAL(16,2) NOT NULL, - release_date DATE, - released_at TIMESTAMPTZ, - released_amount DECIMAL(16,2), - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- Tabla: fondo_garantia (acumulado de fondo de garant铆a) -CREATE TABLE IF NOT EXISTS estimates.fondo_garantia ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - contrato_id UUID NOT NULL REFERENCES construction.contratos(id), - accumulated_amount DECIMAL(16,2) DEFAULT 0, - released_amount DECIMAL(16,2) DEFAULT 0, - pending_amount DECIMAL(16,2) GENERATED ALWAYS AS (accumulated_amount - released_amount) STORED, - release_date DATE, - released_at TIMESTAMPTZ, - released_by UUID REFERENCES auth.users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_fondo_garantia_contrato UNIQUE (contrato_id) -); - --- ============================================================================ --- TABLES - WORKFLOW --- ============================================================================ - --- Tabla: estimacion_workflow (historial de workflow) -CREATE TABLE IF NOT EXISTS estimates.estimacion_workflow ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - estimacion_id UUID NOT NULL REFERENCES estimates.estimaciones(id) ON DELETE CASCADE, - from_status estimates.estimate_status, - to_status estimates.estimate_status NOT NULL, - action VARCHAR(50) NOT NULL, - comments TEXT, - performed_by UUID NOT NULL REFERENCES auth.users(id), - performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- INDICES --- ============================================================================ - -CREATE INDEX IF NOT EXISTS idx_estimaciones_tenant_id ON estimates.estimaciones(tenant_id); -CREATE INDEX IF NOT EXISTS idx_estimaciones_contrato_id ON estimates.estimaciones(contrato_id); -CREATE INDEX IF NOT EXISTS idx_estimaciones_fraccionamiento_id ON estimates.estimaciones(fraccionamiento_id); -CREATE INDEX IF NOT EXISTS idx_estimaciones_status ON estimates.estimaciones(status); -CREATE INDEX IF NOT EXISTS idx_estimaciones_period ON estimates.estimaciones(period_start, period_end); - -CREATE INDEX IF NOT EXISTS idx_est_conceptos_tenant_id ON estimates.estimacion_conceptos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_est_conceptos_estimacion_id ON estimates.estimacion_conceptos(estimacion_id); -CREATE INDEX IF NOT EXISTS idx_est_conceptos_concepto_id ON estimates.estimacion_conceptos(concepto_id); - -CREATE INDEX IF NOT EXISTS idx_generadores_tenant_id ON estimates.generadores(tenant_id); -CREATE INDEX IF NOT EXISTS idx_generadores_est_concepto_id ON estimates.generadores(estimacion_concepto_id); -CREATE INDEX IF NOT EXISTS idx_generadores_status ON estimates.generadores(status); - -CREATE INDEX IF NOT EXISTS idx_anticipos_tenant_id ON estimates.anticipos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_anticipos_contrato_id ON estimates.anticipos(contrato_id); -CREATE INDEX IF NOT EXISTS idx_anticipos_type ON estimates.anticipos(advance_type); - -CREATE INDEX IF NOT EXISTS idx_amortizaciones_tenant_id ON estimates.amortizaciones(tenant_id); -CREATE INDEX IF NOT EXISTS idx_amortizaciones_anticipo_id ON estimates.amortizaciones(anticipo_id); -CREATE INDEX IF NOT EXISTS idx_amortizaciones_estimacion_id ON estimates.amortizaciones(estimacion_id); - -CREATE INDEX IF NOT EXISTS idx_retenciones_tenant_id ON estimates.retenciones(tenant_id); -CREATE INDEX IF NOT EXISTS idx_retenciones_estimacion_id ON estimates.retenciones(estimacion_id); -CREATE INDEX IF NOT EXISTS idx_retenciones_type ON estimates.retenciones(retention_type); - -CREATE INDEX IF NOT EXISTS idx_fondo_garantia_tenant_id ON estimates.fondo_garantia(tenant_id); -CREATE INDEX IF NOT EXISTS idx_fondo_garantia_contrato_id ON estimates.fondo_garantia(contrato_id); - -CREATE INDEX IF NOT EXISTS idx_est_workflow_tenant_id ON estimates.estimacion_workflow(tenant_id); -CREATE INDEX IF NOT EXISTS idx_est_workflow_estimacion_id ON estimates.estimacion_workflow(estimacion_id); - --- ============================================================================ --- ROW LEVEL SECURITY (RLS) --- ============================================================================ - -ALTER TABLE estimates.estimaciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE estimates.estimacion_conceptos ENABLE ROW LEVEL SECURITY; -ALTER TABLE estimates.generadores ENABLE ROW LEVEL SECURITY; -ALTER TABLE estimates.anticipos ENABLE ROW LEVEL SECURITY; -ALTER TABLE estimates.amortizaciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE estimates.retenciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE estimates.fondo_garantia ENABLE ROW LEVEL SECURITY; -ALTER TABLE estimates.estimacion_workflow ENABLE ROW LEVEL SECURITY; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_estimaciones ON estimates.estimaciones; - CREATE POLICY tenant_isolation_estimaciones ON estimates.estimaciones - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_est_conceptos ON estimates.estimacion_conceptos; - CREATE POLICY tenant_isolation_est_conceptos ON estimates.estimacion_conceptos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_generadores ON estimates.generadores; - CREATE POLICY tenant_isolation_generadores ON estimates.generadores - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_anticipos ON estimates.anticipos; - CREATE POLICY tenant_isolation_anticipos ON estimates.anticipos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_amortizaciones ON estimates.amortizaciones; - CREATE POLICY tenant_isolation_amortizaciones ON estimates.amortizaciones - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_retenciones ON estimates.retenciones; - CREATE POLICY tenant_isolation_retenciones ON estimates.retenciones - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_fondo_garantia ON estimates.fondo_garantia; - CREATE POLICY tenant_isolation_fondo_garantia ON estimates.fondo_garantia - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_est_workflow ON estimates.estimacion_workflow; - CREATE POLICY tenant_isolation_est_workflow ON estimates.estimacion_workflow - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - --- ============================================================================ --- FUNCIONES --- ============================================================================ - --- Funci贸n: calcular totales de estimaci贸n -CREATE OR REPLACE FUNCTION estimates.calculate_estimate_totals(p_estimacion_id UUID) -RETURNS VOID AS $$ -DECLARE - v_subtotal DECIMAL(16,2); - v_advance DECIMAL(16,2); - v_retention DECIMAL(16,2); - v_tax_rate DECIMAL(5,2) := 0.16; - v_tax DECIMAL(16,2); - v_total DECIMAL(16,2); -BEGIN - SELECT COALESCE(SUM(amount_current), 0) INTO v_subtotal - FROM estimates.estimacion_conceptos - WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL; - - SELECT COALESCE(SUM(amount), 0) INTO v_advance - FROM estimates.amortizaciones - WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL; - - SELECT COALESCE(SUM(amount), 0) INTO v_retention - FROM estimates.retenciones - WHERE estimacion_id = p_estimacion_id AND deleted_at IS NULL; - - v_tax := v_subtotal * v_tax_rate; - v_total := v_subtotal + v_tax - v_advance - v_retention; - - UPDATE estimates.estimaciones - SET subtotal = v_subtotal, - advance_amount = v_advance, - retention_amount = v_retention, - tax_amount = v_tax, - total_amount = v_total, - updated_at = NOW() - WHERE id = p_estimacion_id; -END; -$$ LANGUAGE plpgsql; - --- ============================================================================ --- COMENTARIOS --- ============================================================================ - -COMMENT ON SCHEMA estimates IS 'Schema de estimaciones, anticipos y retenciones de obra'; -COMMENT ON TABLE estimates.estimaciones IS 'Estimaciones de obra peri贸dicas'; -COMMENT ON TABLE estimates.estimacion_conceptos IS 'L铆neas de concepto por estimaci贸n'; -COMMENT ON TABLE estimates.generadores IS 'Generadores de cantidades para estimaciones'; -COMMENT ON TABLE estimates.anticipos IS 'Anticipos otorgados a subcontratistas'; -COMMENT ON TABLE estimates.amortizaciones IS 'Amortizaciones de anticipos por estimaci贸n'; -COMMENT ON TABLE estimates.retenciones IS 'Retenciones aplicadas a estimaciones'; -COMMENT ON TABLE estimates.fondo_garantia IS 'Fondo de garant铆a acumulado por contrato'; -COMMENT ON TABLE estimates.estimacion_workflow IS 'Historial de workflow de estimaciones'; - --- ============================================================================ --- FIN DEL SCHEMA ESTIMATES --- Total tablas: 8 --- ============================================================================ diff --git a/projects/erp-construccion/database/schemas/05-infonavit-schema-ddl.sql b/projects/erp-construccion/database/schemas/05-infonavit-schema-ddl.sql deleted file mode 100644 index 00676d822..000000000 --- a/projects/erp-construccion/database/schemas/05-infonavit-schema-ddl.sql +++ /dev/null @@ -1,413 +0,0 @@ --- ============================================================================ --- INFONAVIT Schema DDL - Cumplimiento INFONAVIT y Derechohabientes --- Modulos: MAI-010 (CRM Derechohabientes), MAI-011 (Integraci贸n INFONAVIT) --- Version: 1.0.0 --- Fecha: 2025-12-08 --- ============================================================================ --- PREREQUISITOS: --- 1. ERP-Core instalado (auth.tenants, auth.users, auth.companies) --- 2. Schema construction instalado (fraccionamientos, lotes, departamentos) --- ============================================================================ - --- Verificar prerequisitos -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN - RAISE EXCEPTION 'Schema auth no existe. Ejecutar primero ERP-Core DDL'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN - RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL'; - END IF; -END $$; - --- Crear schema -CREATE SCHEMA IF NOT EXISTS infonavit; - --- ============================================================================ --- TYPES (ENUMs) --- ============================================================================ - -DO $$ BEGIN - CREATE TYPE infonavit.derechohabiente_status AS ENUM ( - 'prospect', 'pre_qualified', 'qualified', 'assigned', 'in_process', 'owner', 'cancelled' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE infonavit.credit_type AS ENUM ( - 'infonavit_tradicional', 'infonavit_total', 'cofinavit', 'mejoravit', - 'fovissste', 'fovissste_infonavit', 'bank_credit', 'cash' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE infonavit.acta_type AS ENUM ( - 'inicio_obra', 'verificacion_avance', 'entrega_recepcion', 'conclusion_obra', 'liberacion_vivienda' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE infonavit.acta_status AS ENUM ( - 'draft', 'pending', 'signed', 'submitted', 'approved', 'rejected', 'cancelled' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE infonavit.report_type AS ENUM ( - 'avance_fisico', 'avance_financiero', 'inventario_viviendas', 'asignaciones', 'escrituraciones', 'cartera_vencida' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - --- ============================================================================ --- TABLES - REGISTRO INFONAVIT --- ============================================================================ - --- Tabla: registro_infonavit (registro del constructor ante INFONAVIT) -CREATE TABLE IF NOT EXISTS infonavit.registro_infonavit ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - company_id UUID NOT NULL, - registro_number VARCHAR(50) NOT NULL, - registro_date DATE NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'active', - vigencia_start DATE, - vigencia_end DATE, - responsable_tecnico VARCHAR(255), - cedula_profesional VARCHAR(50), - metadata JSONB DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_registro_infonavit_tenant UNIQUE (tenant_id, registro_number) -); - --- Tabla: oferta_vivienda (oferta de viviendas ante INFONAVIT) -CREATE TABLE IF NOT EXISTS infonavit.oferta_vivienda ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - registro_id UUID NOT NULL REFERENCES infonavit.registro_infonavit(id), - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - oferta_number VARCHAR(50) NOT NULL, - submission_date DATE NOT NULL, - approval_date DATE, - total_units INTEGER NOT NULL DEFAULT 0, - approved_units INTEGER DEFAULT 0, - price_range_min DECIMAL(14,2), - price_range_max DECIMAL(14,2), - status VARCHAR(20) NOT NULL DEFAULT 'pending', - rejection_reason TEXT, - metadata JSONB DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_oferta_vivienda_tenant UNIQUE (tenant_id, oferta_number) -); - --- ============================================================================ --- TABLES - DERECHOHABIENTES --- ============================================================================ - --- Tabla: derechohabientes (compradores con cr茅dito INFONAVIT) -CREATE TABLE IF NOT EXISTS infonavit.derechohabientes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - partner_id UUID, - nss VARCHAR(15) NOT NULL, - curp VARCHAR(18), - rfc VARCHAR(13), - full_name VARCHAR(255) NOT NULL, - first_name VARCHAR(100), - last_name VARCHAR(100), - second_last_name VARCHAR(100), - birth_date DATE, - gender VARCHAR(10), - marital_status VARCHAR(20), - nationality VARCHAR(50) DEFAULT 'Mexicana', - email VARCHAR(255), - phone VARCHAR(20), - mobile VARCHAR(20), - address TEXT, - city VARCHAR(100), - state VARCHAR(100), - zip_code VARCHAR(10), - employer_name VARCHAR(255), - employer_rfc VARCHAR(13), - employment_start_date DATE, - salary DECIMAL(12,2), - cotization_weeks INTEGER, - credit_type infonavit.credit_type, - credit_number VARCHAR(50), - credit_amount DECIMAL(14,2), - puntos_infonavit DECIMAL(10,2), - subcuenta_vivienda DECIMAL(14,2), - precalificacion_date DATE, - precalificacion_amount DECIMAL(14,2), - status infonavit.derechohabiente_status NOT NULL DEFAULT 'prospect', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_derechohabientes_nss_tenant UNIQUE (tenant_id, nss) -); - --- Tabla: asignacion_vivienda (asignaci贸n de vivienda a derechohabiente) -CREATE TABLE IF NOT EXISTS infonavit.asignacion_vivienda ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - derechohabiente_id UUID NOT NULL REFERENCES infonavit.derechohabientes(id), - lote_id UUID REFERENCES construction.lotes(id), - departamento_id UUID REFERENCES construction.departamentos(id), - oferta_id UUID REFERENCES infonavit.oferta_vivienda(id), - assignment_date DATE NOT NULL, - assignment_number VARCHAR(50), - status VARCHAR(20) NOT NULL DEFAULT 'pending', - sale_price DECIMAL(14,2) NOT NULL, - credit_amount DECIMAL(14,2), - down_payment DECIMAL(14,2), - subsidy_amount DECIMAL(14,2), - notary_name VARCHAR(255), - notary_number VARCHAR(50), - deed_date DATE, - deed_number VARCHAR(50), - public_registry_number VARCHAR(50), - public_registry_date DATE, - scheduled_delivery_date DATE, - actual_delivery_date DATE, - delivery_act_id UUID, - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT chk_asignacion_lote_or_depto CHECK ( - (lote_id IS NOT NULL AND departamento_id IS NULL) OR - (lote_id IS NULL AND departamento_id IS NOT NULL) - ) -); - --- ============================================================================ --- TABLES - ACTAS --- ============================================================================ - --- Tabla: actas (actas INFONAVIT) -CREATE TABLE IF NOT EXISTS infonavit.actas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - acta_type infonavit.acta_type NOT NULL, - acta_number VARCHAR(50) NOT NULL, - acta_date DATE NOT NULL, - status infonavit.acta_status NOT NULL DEFAULT 'draft', - infonavit_representative VARCHAR(255), - constructor_representative VARCHAR(255), - perito_name VARCHAR(255), - perito_cedula VARCHAR(50), - description TEXT, - observations TEXT, - agreements TEXT, - physical_advance_percentage DECIMAL(5,2), - financial_advance_percentage DECIMAL(5,2), - signed_at TIMESTAMPTZ, - submitted_to_infonavit_at TIMESTAMPTZ, - infonavit_response_at TIMESTAMPTZ, - infonavit_folio VARCHAR(50), - document_url VARCHAR(500), - signed_document_url VARCHAR(500), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_actas_number_tenant UNIQUE (tenant_id, acta_number) -); - --- Tabla: acta_viviendas (viviendas incluidas en acta) -CREATE TABLE IF NOT EXISTS infonavit.acta_viviendas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - acta_id UUID NOT NULL REFERENCES infonavit.actas(id) ON DELETE CASCADE, - lote_id UUID REFERENCES construction.lotes(id), - departamento_id UUID REFERENCES construction.departamentos(id), - advance_percentage DECIMAL(5,2), - observations TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- TABLES - REPORTES INFONAVIT --- ============================================================================ - --- Tabla: reportes_infonavit (reportes enviados a INFONAVIT) -CREATE TABLE IF NOT EXISTS infonavit.reportes_infonavit ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - report_type infonavit.report_type NOT NULL, - report_number VARCHAR(50) NOT NULL, - period_start DATE NOT NULL, - period_end DATE NOT NULL, - submission_date DATE, - status VARCHAR(20) NOT NULL DEFAULT 'draft', - infonavit_folio VARCHAR(50), - total_units INTEGER, - units_in_progress INTEGER, - units_completed INTEGER, - units_delivered INTEGER, - physical_advance_percentage DECIMAL(5,2), - financial_advance_percentage DECIMAL(5,2), - document_url VARCHAR(500), - acknowledgment_url VARCHAR(500), - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_reportes_number_tenant UNIQUE (tenant_id, report_number) -); - --- Tabla: historico_puntos (hist贸rico de puntos INFONAVIT) -CREATE TABLE IF NOT EXISTS infonavit.historico_puntos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - derechohabiente_id UUID NOT NULL REFERENCES infonavit.derechohabientes(id), - query_date DATE NOT NULL, - puntos DECIMAL(10,2), - subcuenta_vivienda DECIMAL(14,2), - cotization_weeks INTEGER, - credit_capacity DECIMAL(14,2), - source VARCHAR(50), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- INDICES --- ============================================================================ - -CREATE INDEX IF NOT EXISTS idx_registro_infonavit_tenant_id ON infonavit.registro_infonavit(tenant_id); -CREATE INDEX IF NOT EXISTS idx_registro_infonavit_company_id ON infonavit.registro_infonavit(company_id); - -CREATE INDEX IF NOT EXISTS idx_oferta_vivienda_tenant_id ON infonavit.oferta_vivienda(tenant_id); -CREATE INDEX IF NOT EXISTS idx_oferta_vivienda_registro_id ON infonavit.oferta_vivienda(registro_id); -CREATE INDEX IF NOT EXISTS idx_oferta_vivienda_fraccionamiento_id ON infonavit.oferta_vivienda(fraccionamiento_id); -CREATE INDEX IF NOT EXISTS idx_oferta_vivienda_status ON infonavit.oferta_vivienda(status); - -CREATE INDEX IF NOT EXISTS idx_derechohabientes_tenant_id ON infonavit.derechohabientes(tenant_id); -CREATE INDEX IF NOT EXISTS idx_derechohabientes_nss ON infonavit.derechohabientes(nss); -CREATE INDEX IF NOT EXISTS idx_derechohabientes_curp ON infonavit.derechohabientes(curp); -CREATE INDEX IF NOT EXISTS idx_derechohabientes_status ON infonavit.derechohabientes(status); -CREATE INDEX IF NOT EXISTS idx_derechohabientes_credit_type ON infonavit.derechohabientes(credit_type); - -CREATE INDEX IF NOT EXISTS idx_asignacion_tenant_id ON infonavit.asignacion_vivienda(tenant_id); -CREATE INDEX IF NOT EXISTS idx_asignacion_derechohabiente_id ON infonavit.asignacion_vivienda(derechohabiente_id); -CREATE INDEX IF NOT EXISTS idx_asignacion_lote_id ON infonavit.asignacion_vivienda(lote_id); -CREATE INDEX IF NOT EXISTS idx_asignacion_status ON infonavit.asignacion_vivienda(status); - -CREATE INDEX IF NOT EXISTS idx_actas_tenant_id ON infonavit.actas(tenant_id); -CREATE INDEX IF NOT EXISTS idx_actas_fraccionamiento_id ON infonavit.actas(fraccionamiento_id); -CREATE INDEX IF NOT EXISTS idx_actas_type ON infonavit.actas(acta_type); -CREATE INDEX IF NOT EXISTS idx_actas_status ON infonavit.actas(status); - -CREATE INDEX IF NOT EXISTS idx_acta_viviendas_tenant_id ON infonavit.acta_viviendas(tenant_id); -CREATE INDEX IF NOT EXISTS idx_acta_viviendas_acta_id ON infonavit.acta_viviendas(acta_id); - -CREATE INDEX IF NOT EXISTS idx_reportes_tenant_id ON infonavit.reportes_infonavit(tenant_id); -CREATE INDEX IF NOT EXISTS idx_reportes_fraccionamiento_id ON infonavit.reportes_infonavit(fraccionamiento_id); -CREATE INDEX IF NOT EXISTS idx_reportes_type ON infonavit.reportes_infonavit(report_type); - -CREATE INDEX IF NOT EXISTS idx_historico_puntos_tenant_id ON infonavit.historico_puntos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_historico_puntos_derechohabiente_id ON infonavit.historico_puntos(derechohabiente_id); - --- ============================================================================ --- ROW LEVEL SECURITY (RLS) --- ============================================================================ - -ALTER TABLE infonavit.registro_infonavit ENABLE ROW LEVEL SECURITY; -ALTER TABLE infonavit.oferta_vivienda ENABLE ROW LEVEL SECURITY; -ALTER TABLE infonavit.derechohabientes ENABLE ROW LEVEL SECURITY; -ALTER TABLE infonavit.asignacion_vivienda ENABLE ROW LEVEL SECURITY; -ALTER TABLE infonavit.actas ENABLE ROW LEVEL SECURITY; -ALTER TABLE infonavit.acta_viviendas ENABLE ROW LEVEL SECURITY; -ALTER TABLE infonavit.reportes_infonavit ENABLE ROW LEVEL SECURITY; -ALTER TABLE infonavit.historico_puntos ENABLE ROW LEVEL SECURITY; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_registro_infonavit ON infonavit.registro_infonavit; - CREATE POLICY tenant_isolation_registro_infonavit ON infonavit.registro_infonavit - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_oferta_vivienda ON infonavit.oferta_vivienda; - CREATE POLICY tenant_isolation_oferta_vivienda ON infonavit.oferta_vivienda - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_derechohabientes ON infonavit.derechohabientes; - CREATE POLICY tenant_isolation_derechohabientes ON infonavit.derechohabientes - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_asignacion_vivienda ON infonavit.asignacion_vivienda; - CREATE POLICY tenant_isolation_asignacion_vivienda ON infonavit.asignacion_vivienda - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_actas ON infonavit.actas; - CREATE POLICY tenant_isolation_actas ON infonavit.actas - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_acta_viviendas ON infonavit.acta_viviendas; - CREATE POLICY tenant_isolation_acta_viviendas ON infonavit.acta_viviendas - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_reportes_infonavit ON infonavit.reportes_infonavit; - CREATE POLICY tenant_isolation_reportes_infonavit ON infonavit.reportes_infonavit - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_historico_puntos ON infonavit.historico_puntos; - CREATE POLICY tenant_isolation_historico_puntos ON infonavit.historico_puntos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - --- ============================================================================ --- COMENTARIOS --- ============================================================================ - -COMMENT ON SCHEMA infonavit IS 'Schema de cumplimiento INFONAVIT y gesti贸n de derechohabientes'; -COMMENT ON TABLE infonavit.registro_infonavit IS 'Registro del constructor ante INFONAVIT'; -COMMENT ON TABLE infonavit.oferta_vivienda IS 'Oferta de viviendas registrada ante INFONAVIT'; -COMMENT ON TABLE infonavit.derechohabientes IS 'Derechohabientes INFONAVIT/compradores'; -COMMENT ON TABLE infonavit.asignacion_vivienda IS 'Asignaci贸n de vivienda a derechohabiente'; -COMMENT ON TABLE infonavit.actas IS 'Actas oficiales INFONAVIT'; -COMMENT ON TABLE infonavit.acta_viviendas IS 'Viviendas incluidas en cada acta'; -COMMENT ON TABLE infonavit.reportes_infonavit IS 'Reportes peri贸dicos enviados a INFONAVIT'; -COMMENT ON TABLE infonavit.historico_puntos IS 'Hist贸rico de consulta de puntos INFONAVIT'; - --- ============================================================================ --- FIN DEL SCHEMA INFONAVIT --- Total tablas: 8 --- ============================================================================ diff --git a/projects/erp-construccion/database/schemas/06-inventory-ext-schema-ddl.sql b/projects/erp-construccion/database/schemas/06-inventory-ext-schema-ddl.sql deleted file mode 100644 index ce643ab7c..000000000 --- a/projects/erp-construccion/database/schemas/06-inventory-ext-schema-ddl.sql +++ /dev/null @@ -1,213 +0,0 @@ --- ============================================================================ --- INVENTORY EXTENSION Schema DDL - Extensiones de Inventario para Construcci贸n --- Modulos: MAI-004 (Compras e Inventarios) --- Version: 1.0.0 --- Fecha: 2025-12-08 --- ============================================================================ --- TIPO: Extensi贸n del ERP Core (MGN-005 Inventory) --- NOTA: Contiene SOLO extensiones espec铆ficas de construcci贸n. --- Las tablas base est谩n en el ERP Core. --- ============================================================================ --- PREREQUISITOS: --- 1. ERP-Core instalado (auth.tenants, auth.users) --- 2. Schema construction instalado (fraccionamientos, conceptos, lotes, departamentos) --- 3. Schema inventory de ERP-Core instalado (opcional, para FKs) --- ============================================================================ - --- Verificar prerequisitos -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN - RAISE EXCEPTION 'Schema auth no existe. Ejecutar primero ERP-Core DDL'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN - RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL'; - END IF; -END $$; - --- Crear schema si no existe (puede ya existir desde ERP-Core) -CREATE SCHEMA IF NOT EXISTS inventory; - --- ============================================================================ --- TYPES (ENUMs) ADICIONALES --- ============================================================================ - -DO $$ BEGIN - CREATE TYPE inventory.warehouse_type_construction AS ENUM ( - 'central', 'obra', 'temporal', 'transito' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - -DO $$ BEGIN - CREATE TYPE inventory.requisition_status AS ENUM ( - 'draft', 'submitted', 'approved', 'partially_served', 'served', 'cancelled' - ); -EXCEPTION WHEN duplicate_object THEN NULL; END $$; - --- ============================================================================ --- TABLES - EXTENSIONES CONSTRUCCI脫N --- ============================================================================ - --- Tabla: almacenes_proyecto (almac茅n por proyecto/obra) --- Extiende: inventory.warehouses (ERP Core) -CREATE TABLE IF NOT EXISTS inventory.almacenes_proyecto ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - warehouse_id UUID NOT NULL, -- FK a inventory.warehouses (ERP Core) - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - warehouse_type inventory.warehouse_type_construction NOT NULL DEFAULT 'obra', - location_description TEXT, - location GEOMETRY(POINT, 4326), - responsible_id UUID REFERENCES auth.users(id), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_almacenes_proyecto_warehouse UNIQUE (warehouse_id) -); - --- Tabla: requisiciones_obra (requisiciones desde obra) -CREATE TABLE IF NOT EXISTS inventory.requisiciones_obra ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - requisition_number VARCHAR(30) NOT NULL, - requisition_date DATE NOT NULL, - required_date DATE NOT NULL, - status inventory.requisition_status NOT NULL DEFAULT 'draft', - priority VARCHAR(20) DEFAULT 'medium', - requested_by UUID NOT NULL REFERENCES auth.users(id), - destination_warehouse_id UUID, -- FK a inventory.warehouses (ERP Core) - approved_by UUID REFERENCES auth.users(id), - approved_at TIMESTAMPTZ, - rejection_reason TEXT, - purchase_order_id UUID, -- FK a purchase.purchase_orders (ERP Core) - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_requisiciones_obra_number UNIQUE (tenant_id, requisition_number) -); - --- Tabla: requisicion_lineas (l铆neas de requisici贸n) -CREATE TABLE IF NOT EXISTS inventory.requisicion_lineas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - requisicion_id UUID NOT NULL REFERENCES inventory.requisiciones_obra(id) ON DELETE CASCADE, - product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) - concepto_id UUID REFERENCES construction.conceptos(id), - lote_id UUID REFERENCES construction.lotes(id), - quantity_requested DECIMAL(12,4) NOT NULL, - quantity_approved DECIMAL(12,4), - quantity_served DECIMAL(12,4) DEFAULT 0, - unit_id UUID, -- FK a core.uom (ERP Core) - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- Tabla: consumos_obra (consumos de materiales por obra/lote) -CREATE TABLE IF NOT EXISTS inventory.consumos_obra ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - stock_move_id UUID, -- FK a inventory.stock_moves (ERP Core) - fraccionamiento_id UUID NOT NULL REFERENCES construction.fraccionamientos(id), - lote_id UUID REFERENCES construction.lotes(id), - departamento_id UUID REFERENCES construction.departamentos(id), - concepto_id UUID REFERENCES construction.conceptos(id), - product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) - quantity DECIMAL(12,4) NOT NULL, - unit_cost DECIMAL(12,4), - total_cost DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_cost) STORED, - consumption_date DATE NOT NULL, - registered_by UUID NOT NULL REFERENCES auth.users(id), - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- INDICES --- ============================================================================ - -CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_tenant_id ON inventory.almacenes_proyecto(tenant_id); -CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_warehouse_id ON inventory.almacenes_proyecto(warehouse_id); -CREATE INDEX IF NOT EXISTS idx_almacenes_proyecto_fraccionamiento_id ON inventory.almacenes_proyecto(fraccionamiento_id); - -CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_tenant_id ON inventory.requisiciones_obra(tenant_id); -CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_fraccionamiento_id ON inventory.requisiciones_obra(fraccionamiento_id); -CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_status ON inventory.requisiciones_obra(status); -CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_date ON inventory.requisiciones_obra(requisition_date); -CREATE INDEX IF NOT EXISTS idx_requisiciones_obra_required_date ON inventory.requisiciones_obra(required_date); - -CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_tenant_id ON inventory.requisicion_lineas(tenant_id); -CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_requisicion_id ON inventory.requisicion_lineas(requisicion_id); -CREATE INDEX IF NOT EXISTS idx_requisicion_lineas_product_id ON inventory.requisicion_lineas(product_id); - -CREATE INDEX IF NOT EXISTS idx_consumos_obra_tenant_id ON inventory.consumos_obra(tenant_id); -CREATE INDEX IF NOT EXISTS idx_consumos_obra_fraccionamiento_id ON inventory.consumos_obra(fraccionamiento_id); -CREATE INDEX IF NOT EXISTS idx_consumos_obra_lote_id ON inventory.consumos_obra(lote_id); -CREATE INDEX IF NOT EXISTS idx_consumos_obra_concepto_id ON inventory.consumos_obra(concepto_id); -CREATE INDEX IF NOT EXISTS idx_consumos_obra_product_id ON inventory.consumos_obra(product_id); -CREATE INDEX IF NOT EXISTS idx_consumos_obra_date ON inventory.consumos_obra(consumption_date); - --- ============================================================================ --- ROW LEVEL SECURITY (RLS) --- ============================================================================ - -ALTER TABLE inventory.almacenes_proyecto ENABLE ROW LEVEL SECURITY; -ALTER TABLE inventory.requisiciones_obra ENABLE ROW LEVEL SECURITY; -ALTER TABLE inventory.requisicion_lineas ENABLE ROW LEVEL SECURITY; -ALTER TABLE inventory.consumos_obra ENABLE ROW LEVEL SECURITY; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_almacenes_proyecto ON inventory.almacenes_proyecto; - CREATE POLICY tenant_isolation_almacenes_proyecto ON inventory.almacenes_proyecto - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_requisiciones_obra ON inventory.requisiciones_obra; - CREATE POLICY tenant_isolation_requisiciones_obra ON inventory.requisiciones_obra - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_requisicion_lineas ON inventory.requisicion_lineas; - CREATE POLICY tenant_isolation_requisicion_lineas ON inventory.requisicion_lineas - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_consumos_obra ON inventory.consumos_obra; - CREATE POLICY tenant_isolation_consumos_obra ON inventory.consumos_obra - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - --- ============================================================================ --- COMENTARIOS --- ============================================================================ - -COMMENT ON TABLE inventory.almacenes_proyecto IS 'Extensi贸n: almacenes por proyecto de construcci贸n'; -COMMENT ON TABLE inventory.requisiciones_obra IS 'Extensi贸n: requisiciones de material desde obra'; -COMMENT ON TABLE inventory.requisicion_lineas IS 'Extensi贸n: l铆neas de requisici贸n de obra'; -COMMENT ON TABLE inventory.consumos_obra IS 'Extensi贸n: consumos de materiales por obra/lote'; - --- ============================================================================ --- FIN DE EXTENSIONES INVENTORY --- Total tablas: 4 --- ============================================================================ diff --git a/projects/erp-construccion/database/schemas/07-purchase-ext-schema-ddl.sql b/projects/erp-construccion/database/schemas/07-purchase-ext-schema-ddl.sql deleted file mode 100644 index 03d6a43de..000000000 --- a/projects/erp-construccion/database/schemas/07-purchase-ext-schema-ddl.sql +++ /dev/null @@ -1,227 +0,0 @@ --- ============================================================================ --- PURCHASE EXTENSION Schema DDL - Extensiones de Compras para Construcci贸n --- Modulos: MAI-004 (Compras e Inventarios) --- Version: 1.0.0 --- Fecha: 2025-12-08 --- ============================================================================ --- TIPO: Extensi贸n del ERP Core (MGN-006 Purchase) --- NOTA: Contiene SOLO extensiones espec铆ficas de construcci贸n. --- Las tablas base est谩n en el ERP Core. --- ============================================================================ --- PREREQUISITOS: --- 1. ERP-Core instalado (auth.tenants, auth.users) --- 2. Schema construction instalado (fraccionamientos) --- 3. Schema inventory extension instalado (requisiciones_obra) --- 4. Schema purchase de ERP-Core instalado (opcional, para FKs) --- ============================================================================ - --- Verificar prerequisitos -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN - RAISE EXCEPTION 'Schema auth no existe. Ejecutar primero ERP-Core DDL'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'construction') THEN - RAISE EXCEPTION 'Schema construction no existe. Ejecutar primero construction DDL'; - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'inventory') THEN - RAISE EXCEPTION 'Schema inventory no existe. Ejecutar primero inventory extension DDL'; - END IF; -END $$; - --- Crear schema si no existe (puede ya existir desde ERP-Core) -CREATE SCHEMA IF NOT EXISTS purchase; - --- ============================================================================ --- TABLES - EXTENSIONES CONSTRUCCI脫N --- ============================================================================ - --- Tabla: purchase_order_construction (extensi贸n de 贸rdenes de compra) --- Extiende: purchase.purchase_orders (ERP Core) -CREATE TABLE IF NOT EXISTS purchase.purchase_order_construction ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - purchase_order_id UUID NOT NULL, -- FK a purchase.purchase_orders (ERP Core) - fraccionamiento_id UUID REFERENCES construction.fraccionamientos(id), - requisicion_id UUID REFERENCES inventory.requisiciones_obra(id), - delivery_location VARCHAR(255), - delivery_contact VARCHAR(100), - delivery_phone VARCHAR(20), - received_by UUID REFERENCES auth.users(id), - received_at TIMESTAMPTZ, - quality_approved BOOLEAN, - quality_notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_po_construction_po_id UNIQUE (purchase_order_id) -); - --- Tabla: supplier_construction (extensi贸n de proveedores) --- Extiende: purchase.suppliers (ERP Core) -CREATE TABLE IF NOT EXISTS purchase.supplier_construction ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - supplier_id UUID NOT NULL, -- FK a purchase.suppliers (ERP Core) - is_materials_supplier BOOLEAN DEFAULT FALSE, - is_services_supplier BOOLEAN DEFAULT FALSE, - is_equipment_supplier BOOLEAN DEFAULT FALSE, - specialties TEXT[], - quality_rating DECIMAL(3,2), - delivery_rating DECIMAL(3,2), - price_rating DECIMAL(3,2), - overall_rating DECIMAL(3,2) GENERATED ALWAYS AS ( - (COALESCE(quality_rating, 0) + COALESCE(delivery_rating, 0) + COALESCE(price_rating, 0)) / 3 - ) STORED, - last_evaluation_date DATE, - credit_limit DECIMAL(14,2), - payment_days INTEGER DEFAULT 30, - has_valid_documents BOOLEAN DEFAULT FALSE, - documents_expiry_date DATE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_supplier_construction_supplier_id UNIQUE (supplier_id) -); - --- Tabla: comparativo_cotizaciones (cuadro comparativo) -CREATE TABLE IF NOT EXISTS purchase.comparativo_cotizaciones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - requisicion_id UUID REFERENCES inventory.requisiciones_obra(id), - code VARCHAR(30) NOT NULL, - name VARCHAR(255) NOT NULL, - comparison_date DATE NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'draft', - winner_supplier_id UUID, -- FK a purchase.suppliers (ERP Core) - approved_by UUID REFERENCES auth.users(id), - approved_at TIMESTAMPTZ, - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id), - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES auth.users(id), - CONSTRAINT uq_comparativo_code_tenant UNIQUE (tenant_id, code) -); - --- Tabla: comparativo_proveedores (proveedores en comparativo) -CREATE TABLE IF NOT EXISTS purchase.comparativo_proveedores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - comparativo_id UUID NOT NULL REFERENCES purchase.comparativo_cotizaciones(id) ON DELETE CASCADE, - supplier_id UUID NOT NULL, -- FK a purchase.suppliers (ERP Core) - quotation_number VARCHAR(50), - quotation_date DATE, - delivery_days INTEGER, - payment_conditions VARCHAR(100), - total_amount DECIMAL(16,2), - is_selected BOOLEAN DEFAULT FALSE, - evaluation_notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id) -); - --- Tabla: comparativo_productos (productos en comparativo) -CREATE TABLE IF NOT EXISTS purchase.comparativo_productos ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, - comparativo_proveedor_id UUID NOT NULL REFERENCES purchase.comparativo_proveedores(id) ON DELETE CASCADE, - product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) - quantity DECIMAL(12,4) NOT NULL, - unit_price DECIMAL(12,4) NOT NULL, - total_price DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES auth.users(id), - updated_at TIMESTAMPTZ, - updated_by UUID REFERENCES auth.users(id) -); - --- ============================================================================ --- INDICES --- ============================================================================ - -CREATE INDEX IF NOT EXISTS idx_po_construction_tenant_id ON purchase.purchase_order_construction(tenant_id); -CREATE INDEX IF NOT EXISTS idx_po_construction_po_id ON purchase.purchase_order_construction(purchase_order_id); -CREATE INDEX IF NOT EXISTS idx_po_construction_fraccionamiento_id ON purchase.purchase_order_construction(fraccionamiento_id); -CREATE INDEX IF NOT EXISTS idx_po_construction_requisicion_id ON purchase.purchase_order_construction(requisicion_id); - -CREATE INDEX IF NOT EXISTS idx_supplier_construction_tenant_id ON purchase.supplier_construction(tenant_id); -CREATE INDEX IF NOT EXISTS idx_supplier_construction_supplier_id ON purchase.supplier_construction(supplier_id); -CREATE INDEX IF NOT EXISTS idx_supplier_construction_rating ON purchase.supplier_construction(overall_rating); - -CREATE INDEX IF NOT EXISTS idx_comparativo_tenant_id ON purchase.comparativo_cotizaciones(tenant_id); -CREATE INDEX IF NOT EXISTS idx_comparativo_requisicion_id ON purchase.comparativo_cotizaciones(requisicion_id); -CREATE INDEX IF NOT EXISTS idx_comparativo_status ON purchase.comparativo_cotizaciones(status); - -CREATE INDEX IF NOT EXISTS idx_comparativo_prov_tenant_id ON purchase.comparativo_proveedores(tenant_id); -CREATE INDEX IF NOT EXISTS idx_comparativo_prov_comparativo_id ON purchase.comparativo_proveedores(comparativo_id); -CREATE INDEX IF NOT EXISTS idx_comparativo_prov_supplier_id ON purchase.comparativo_proveedores(supplier_id); - -CREATE INDEX IF NOT EXISTS idx_comparativo_prod_tenant_id ON purchase.comparativo_productos(tenant_id); -CREATE INDEX IF NOT EXISTS idx_comparativo_prod_proveedor_id ON purchase.comparativo_productos(comparativo_proveedor_id); - --- ============================================================================ --- ROW LEVEL SECURITY (RLS) --- ============================================================================ - -ALTER TABLE purchase.purchase_order_construction ENABLE ROW LEVEL SECURITY; -ALTER TABLE purchase.supplier_construction ENABLE ROW LEVEL SECURITY; -ALTER TABLE purchase.comparativo_cotizaciones ENABLE ROW LEVEL SECURITY; -ALTER TABLE purchase.comparativo_proveedores ENABLE ROW LEVEL SECURITY; -ALTER TABLE purchase.comparativo_productos ENABLE ROW LEVEL SECURITY; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_po_construction ON purchase.purchase_order_construction; - CREATE POLICY tenant_isolation_po_construction ON purchase.purchase_order_construction - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_supplier_construction ON purchase.supplier_construction; - CREATE POLICY tenant_isolation_supplier_construction ON purchase.supplier_construction - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_comparativo ON purchase.comparativo_cotizaciones; - CREATE POLICY tenant_isolation_comparativo ON purchase.comparativo_cotizaciones - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_comparativo_prov ON purchase.comparativo_proveedores; - CREATE POLICY tenant_isolation_comparativo_prov ON purchase.comparativo_proveedores - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - -DO $$ BEGIN - DROP POLICY IF EXISTS tenant_isolation_comparativo_prod ON purchase.comparativo_productos; - CREATE POLICY tenant_isolation_comparativo_prod ON purchase.comparativo_productos - FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -EXCEPTION WHEN undefined_object THEN NULL; END $$; - --- ============================================================================ --- COMENTARIOS --- ============================================================================ - -COMMENT ON TABLE purchase.purchase_order_construction IS 'Extensi贸n: datos adicionales de OC para construcci贸n'; -COMMENT ON TABLE purchase.supplier_construction IS 'Extensi贸n: datos adicionales de proveedores para construcci贸n'; -COMMENT ON TABLE purchase.comparativo_cotizaciones IS 'Extensi贸n: cuadro comparativo de cotizaciones'; -COMMENT ON TABLE purchase.comparativo_proveedores IS 'Extensi贸n: proveedores participantes en comparativo'; -COMMENT ON TABLE purchase.comparativo_productos IS 'Extensi贸n: productos cotizados por proveedor'; - --- ============================================================================ --- FIN DE EXTENSIONES PURCHASE --- Total tablas: 5 --- ============================================================================ diff --git a/projects/erp-construccion/database/validate-clean-load-policy.sh b/projects/erp-construccion/database/validate-clean-load-policy.sh deleted file mode 100755 index 0a1ad76b8..000000000 --- a/projects/erp-construccion/database/validate-clean-load-policy.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/bin/bash -# ============================================================================= -# VALIDATE CLEAN LOAD POLICY -# ============================================================================= -# Script de validacion de cumplimiento de DIRECTIVA-POLITICA-CARGA-LIMPIA.md -# -# Uso: ./validate-clean-load-policy.sh -# ============================================================================= - -set -e - -# Colores -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Configuracion -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VIOLATIONS=0 - -echo -e "${BLUE}=============================================================================${NC}" -echo -e "${BLUE} VALIDACION DE POLITICA DE CARGA LIMPIA${NC}" -echo -e "${BLUE}=============================================================================${NC}" -echo "" - -# ============================================================================= -# CHECK 1: No debe existir carpeta migrations/ -# ============================================================================= -echo -e "${YELLOW}[1/6] Verificando que NO existe carpeta migrations/...${NC}" - -if [ -d "$SCRIPT_DIR/migrations" ]; then - echo -e "${RED}ERROR: Carpeta migrations/ detectada (PROHIBIDA)${NC}" - echo -e "${RED} Path: $SCRIPT_DIR/migrations${NC}" - echo -e "${YELLOW} Solucion: Eliminar carpeta y mover contenido a schemas/${NC}" - VIOLATIONS=$((VIOLATIONS + 1)) -else - echo -e "${GREEN}OK - No existe carpeta migrations/${NC}" -fi -echo "" - -# ============================================================================= -# CHECK 2: No deben existir archivos fix-*.sql -# ============================================================================= -echo -e "${YELLOW}[2/6] Verificando que NO existen archivos fix-*.sql...${NC}" - -FIX_FILES=$(find "$SCRIPT_DIR" -name "fix-*.sql" -o -name "patch-*.sql" -o -name "hotfix-*.sql" 2>/dev/null) - -if [ -n "$FIX_FILES" ]; then - echo -e "${RED}ERROR: Archivos fix/patch detectados (PROHIBIDOS):${NC}" - echo "$FIX_FILES" | while read -r file; do - echo -e "${RED} - $file${NC}" - done - echo -e "${YELLOW} Solucion: Incorporar cambios en DDL base y eliminar fixes${NC}" - VIOLATIONS=$((VIOLATIONS + 1)) -else - echo -e "${GREEN}OK - No existen archivos fix/patch${NC}" -fi -echo "" - -# ============================================================================= -# CHECK 3: No deben existir archivos migration-*.sql o NNN-*.sql numerados -# ============================================================================= -echo -e "${YELLOW}[3/6] Verificando que NO existen archivos tipo migration...${NC}" - -MIGRATION_FILES=$(find "$SCRIPT_DIR" -regex ".*[0-9][0-9][0-9][-_].*\.sql" ! -path "*/schemas/*" 2>/dev/null) - -if [ -n "$MIGRATION_FILES" ]; then - echo -e "${RED}ERROR: Archivos tipo migration detectados (PROHIBIDOS):${NC}" - echo "$MIGRATION_FILES" | while read -r file; do - echo -e "${RED} - $file${NC}" - done - echo -e "${YELLOW} Solucion: Mover a schemas/ con nomenclatura correcta${NC}" - VIOLATIONS=$((VIOLATIONS + 1)) -else - echo -e "${GREEN}OK - No existen archivos tipo migration fuera de schemas/${NC}" -fi -echo "" - -# ============================================================================= -# CHECK 4: Debe existir script drop-and-recreate-database.sh -# ============================================================================= -echo -e "${YELLOW}[4/6] Verificando que existe drop-and-recreate-database.sh...${NC}" - -if [ -f "$SCRIPT_DIR/drop-and-recreate-database.sh" ]; then - if [ -x "$SCRIPT_DIR/drop-and-recreate-database.sh" ]; then - echo -e "${GREEN}OK - Script existe y es ejecutable${NC}" - else - echo -e "${YELLOW}WARN: Script existe pero no es ejecutable${NC}" - echo -e "${YELLOW} Solucion: chmod +x drop-and-recreate-database.sh${NC}" - fi -else - echo -e "${RED}ERROR: No existe drop-and-recreate-database.sh (REQUERIDO)${NC}" - echo -e "${YELLOW} Solucion: Crear script de recreacion limpia${NC}" - VIOLATIONS=$((VIOLATIONS + 1)) -fi -echo "" - -# ============================================================================= -# CHECK 5: Deben existir archivos DDL en schemas/ -# ============================================================================= -echo -e "${YELLOW}[5/6] Verificando que existen archivos DDL en schemas/...${NC}" - -DDL_COUNT=$(find "$SCRIPT_DIR/schemas" -name "*.sql" -type f 2>/dev/null | wc -l) - -if [ "$DDL_COUNT" -gt 0 ]; then - echo -e "${GREEN}OK - Encontrados $DDL_COUNT archivos DDL en schemas/${NC}" - find "$SCRIPT_DIR/schemas" -name "*.sql" -type f | sort | while read -r file; do - echo -e " - $(basename "$file")" - done -else - echo -e "${YELLOW}WARN: No hay archivos DDL en schemas/${NC}" - echo -e "${YELLOW} La base de datos puede quedar vacia${NC}" -fi -echo "" - -# ============================================================================= -# CHECK 6: Debe existir archivo de inicializacion -# ============================================================================= -echo -e "${YELLOW}[6/6] Verificando archivo de inicializacion...${NC}" - -if [ -f "$SCRIPT_DIR/init-scripts/01-init-database.sql" ]; then - echo -e "${GREEN}OK - Existe init-scripts/01-init-database.sql${NC}" -elif [ -f "$SCRIPT_DIR/ddl/00-init.sql" ]; then - echo -e "${GREEN}OK - Existe ddl/00-init.sql${NC}" -else - echo -e "${RED}ERROR: No existe archivo de inicializacion${NC}" - echo -e "${YELLOW} Solucion: Crear init-scripts/01-init-database.sql${NC}" - VIOLATIONS=$((VIOLATIONS + 1)) -fi -echo "" - -# ============================================================================= -# RESUMEN -# ============================================================================= - -echo -e "${BLUE}=============================================================================${NC}" - -if [ "$VIOLATIONS" -eq 0 ]; then - echo -e "${GREEN} POLITICA DE CARGA LIMPIA: CUMPLIDA${NC}" - echo -e "${GREEN}=============================================================================${NC}" - echo -e "${GREEN} Todas las validaciones pasaron correctamente${NC}" - echo -e "${GREEN}=============================================================================${NC}" - exit 0 -else - echo -e "${RED} POLITICA DE CARGA LIMPIA: VIOLADA${NC}" - echo -e "${RED}=============================================================================${NC}" - echo -e "${RED} Se encontraron $VIOLATIONS violacion(es)${NC}" - echo -e "${RED} Revisar DIRECTIVA-POLITICA-CARGA-LIMPIA.md para corregir${NC}" - echo -e "${RED}=============================================================================${NC}" - exit 1 -fi diff --git a/projects/erp-construccion/devops/scripts/sync-enums.ts b/projects/erp-construccion/devops/scripts/sync-enums.ts deleted file mode 100644 index 42d97a9a6..000000000 --- a/projects/erp-construccion/devops/scripts/sync-enums.ts +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Sync Enums - Backend to Frontend - * - * Este script sincroniza automaticamente las constantes y enums del backend - * al frontend, manteniendo el principio SSOT (Single Source of Truth). - * - * Ejecutar: npm run sync:enums - * - * @author Architecture-Analyst - * @date 2025-12-12 - */ - -import * as fs from 'fs'; -import * as path from 'path'; - -// ============================================================================= -// CONFIGURACION -// ============================================================================= - -const BACKEND_CONSTANTS_DIR = path.resolve(__dirname, '../../backend/src/shared/constants'); -const FRONTEND_CONSTANTS_DIR = path.resolve(__dirname, '../../frontend/web/src/shared/constants'); - -// Archivos a sincronizar -const FILES_TO_SYNC = [ - 'enums.constants.ts', - 'api.constants.ts', -]; - -// Header para archivos generados -const GENERATED_HEADER = `/** - * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY - * - * Este archivo es generado automaticamente desde el backend. - * Cualquier cambio sera sobreescrito en la proxima sincronizacion. - * - * Fuente: backend/src/shared/constants/ - * Generado: ${new Date().toISOString()} - * - * Para modificar, edita el archivo fuente en el backend - * y ejecuta: npm run sync:enums - */ - -`; - -// ============================================================================= -// FUNCIONES -// ============================================================================= - -function ensureDirectoryExists(dir: string): void { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - console.log(`馃搧 Created directory: ${dir}`); - } -} - -function processContent(content: string): string { - // Remover imports que no aplican al frontend - let processed = content - // Remover imports de Node.js - .replace(/import\s+\*\s+as\s+\w+\s+from\s+['"]fs['"];?\n?/g, '') - .replace(/import\s+\*\s+as\s+\w+\s+from\s+['"]path['"];?\n?/g, '') - // Remover comentarios de @module backend - .replace(/@module\s+@shared\/constants\//g, '@module shared/constants/') - // Mantener 'as const' para inferencia de tipos - ; - - return GENERATED_HEADER + processed; -} - -function syncFile(filename: string): void { - const sourcePath = path.join(BACKEND_CONSTANTS_DIR, filename); - const destPath = path.join(FRONTEND_CONSTANTS_DIR, filename); - - if (!fs.existsSync(sourcePath)) { - console.log(`鈿狅笍 Source file not found: ${sourcePath}`); - return; - } - - const content = fs.readFileSync(sourcePath, 'utf-8'); - const processedContent = processContent(content); - - fs.writeFileSync(destPath, processedContent); - console.log(`鉁 Synced: ${filename}`); -} - -function generateIndexFile(): void { - const indexContent = `${GENERATED_HEADER} -// Re-export all constants -export * from './enums.constants'; -export * from './api.constants'; -`; - - const indexPath = path.join(FRONTEND_CONSTANTS_DIR, 'index.ts'); - fs.writeFileSync(indexPath, indexContent); - console.log(`鉁 Generated: index.ts`); -} - -function main(): void { - console.log('馃攧 Syncing constants from Backend to Frontend...\n'); - console.log(`Source: ${BACKEND_CONSTANTS_DIR}`); - console.log(`Target: ${FRONTEND_CONSTANTS_DIR}\n`); - - // Asegurar que el directorio destino existe - ensureDirectoryExists(FRONTEND_CONSTANTS_DIR); - - // Sincronizar cada archivo - for (const file of FILES_TO_SYNC) { - syncFile(file); - } - - // Generar archivo index - generateIndexFile(); - - console.log('\n鉁 Sync completed successfully!'); - console.log('\nRecuerda importar las constantes desde:'); - console.log(' import { ROLES, PROJECT_STATUS, API_ROUTES } from "@/shared/constants";'); -} - -main(); diff --git a/projects/erp-construccion/devops/scripts/validate-constants-usage.ts b/projects/erp-construccion/devops/scripts/validate-constants-usage.ts deleted file mode 100644 index 1103de321..000000000 --- a/projects/erp-construccion/devops/scripts/validate-constants-usage.ts +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env ts-node -/** - * Validate Constants Usage - SSOT Enforcement - * - * Este script detecta hardcoding de schemas, tablas, rutas API y enums - * que deberian estar usando las constantes centralizadas del SSOT. - * - * Ejecutar: npm run validate:constants - * - * @author Architecture-Analyst - * @date 2025-12-12 - */ - -import * as fs from 'fs'; -import * as path from 'path'; - -// ============================================================================= -// CONFIGURACION -// ============================================================================= - -interface ValidationPattern { - pattern: RegExp; - message: string; - severity: 'P0' | 'P1' | 'P2'; - suggestion: string; - exclude?: RegExp[]; -} - -const PATTERNS: ValidationPattern[] = [ - // Database Schemas - { - pattern: /['"`]auth['"`](?!\s*:)/g, - message: 'Hardcoded schema "auth"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.AUTH', - exclude: [/from\s+['"`]\.\/database\.constants['"`]/], - }, - { - pattern: /['"`]construction['"`](?!\s*:)/g, - message: 'Hardcoded schema "construction"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.CONSTRUCTION', - }, - { - pattern: /['"`]hr['"`](?!\s*:)(?!\.entity)/g, - message: 'Hardcoded schema "hr"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.HR', - }, - { - pattern: /['"`]hse['"`](?!\s*:)(?!\/)/g, - message: 'Hardcoded schema "hse"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.HSE', - }, - { - pattern: /['"`]estimates['"`](?!\s*:)/g, - message: 'Hardcoded schema "estimates"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.ESTIMATES', - }, - { - pattern: /['"`]infonavit['"`](?!\s*:)/g, - message: 'Hardcoded schema "infonavit"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.INFONAVIT', - }, - { - pattern: /['"`]inventory['"`](?!\s*:)/g, - message: 'Hardcoded schema "inventory"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.INVENTORY', - }, - { - pattern: /['"`]purchase['"`](?!\s*:)/g, - message: 'Hardcoded schema "purchase"', - severity: 'P0', - suggestion: 'Usa DB_SCHEMAS.PURCHASE', - }, - - // API Routes - { - pattern: /['"`]\/api\/v1\/proyectos['"`]/g, - message: 'Hardcoded API route "/api/v1/proyectos"', - severity: 'P0', - suggestion: 'Usa API_ROUTES.PROYECTOS.BASE', - }, - { - pattern: /['"`]\/api\/v1\/fraccionamientos['"`]/g, - message: 'Hardcoded API route "/api/v1/fraccionamientos"', - severity: 'P0', - suggestion: 'Usa API_ROUTES.FRACCIONAMIENTOS.BASE', - }, - { - pattern: /['"`]\/api\/v1\/employees['"`]/g, - message: 'Hardcoded API route "/api/v1/employees"', - severity: 'P0', - suggestion: 'Usa API_ROUTES.EMPLOYEES.BASE', - }, - { - pattern: /['"`]\/api\/v1\/incidentes['"`]/g, - message: 'Hardcoded API route "/api/v1/incidentes"', - severity: 'P0', - suggestion: 'Usa API_ROUTES.INCIDENTES.BASE', - }, - - // Common Table Names - { - pattern: /FROM\s+proyectos(?!\s+AS|\s+WHERE)/gi, - message: 'Hardcoded table name "proyectos"', - severity: 'P1', - suggestion: 'Usa DB_TABLES.CONSTRUCTION.PROYECTOS', - }, - { - pattern: /FROM\s+fraccionamientos(?!\s+AS|\s+WHERE)/gi, - message: 'Hardcoded table name "fraccionamientos"', - severity: 'P1', - suggestion: 'Usa DB_TABLES.CONSTRUCTION.FRACCIONAMIENTOS', - }, - { - pattern: /FROM\s+employees(?!\s+AS|\s+WHERE)/gi, - message: 'Hardcoded table name "employees"', - severity: 'P1', - suggestion: 'Usa DB_TABLES.HR.EMPLOYEES', - }, - { - pattern: /FROM\s+incidentes(?!\s+AS|\s+WHERE)/gi, - message: 'Hardcoded table name "incidentes"', - severity: 'P1', - suggestion: 'Usa DB_TABLES.HSE.INCIDENTES', - }, - - // Status Values - { - pattern: /status\s*===?\s*['"`]active['"`]/gi, - message: 'Hardcoded status "active"', - severity: 'P1', - suggestion: 'Usa PROJECT_STATUS.ACTIVE o USER_STATUS.ACTIVE', - }, - { - pattern: /status\s*===?\s*['"`]borrador['"`]/gi, - message: 'Hardcoded status "borrador"', - severity: 'P1', - suggestion: 'Usa BUDGET_STATUS.DRAFT o ESTIMATION_STATUS.DRAFT', - }, - { - pattern: /status\s*===?\s*['"`]aprobado['"`]/gi, - message: 'Hardcoded status "aprobado"', - severity: 'P1', - suggestion: 'Usa BUDGET_STATUS.APPROVED o ESTIMATION_STATUS.APPROVED', - }, - - // Role Names - { - pattern: /role\s*===?\s*['"`]admin['"`]/gi, - message: 'Hardcoded role "admin"', - severity: 'P0', - suggestion: 'Usa ROLES.ADMIN', - }, - { - pattern: /role\s*===?\s*['"`]supervisor['"`]/gi, - message: 'Hardcoded role "supervisor"', - severity: 'P1', - suggestion: 'Usa ROLES.SUPERVISOR_OBRA o ROLES.SUPERVISOR_HSE', - }, -]; - -// Archivos a excluir -const EXCLUDED_PATHS = [ - 'node_modules', - 'dist', - '.git', - 'coverage', - 'database.constants.ts', - 'api.constants.ts', - 'enums.constants.ts', - 'index.ts', - '.sql', - '.md', - '.json', - '.yml', - '.yaml', -]; - -// Extensiones a validar -const VALID_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx']; - -// ============================================================================= -// TIPOS -// ============================================================================= - -interface Violation { - file: string; - line: number; - column: number; - pattern: string; - message: string; - severity: 'P0' | 'P1' | 'P2'; - suggestion: string; - context: string; -} - -// ============================================================================= -// FUNCIONES -// ============================================================================= - -function shouldExclude(filePath: string): boolean { - return EXCLUDED_PATHS.some(excluded => filePath.includes(excluded)); -} - -function hasValidExtension(filePath: string): boolean { - return VALID_EXTENSIONS.some(ext => filePath.endsWith(ext)); -} - -function getFiles(dir: string): string[] { - const files: string[] = []; - - if (!fs.existsSync(dir)) { - return files; - } - - const items = fs.readdirSync(dir); - - for (const item of items) { - const fullPath = path.join(dir, item); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - if (!shouldExclude(fullPath)) { - files.push(...getFiles(fullPath)); - } - } else if (stat.isFile() && hasValidExtension(fullPath) && !shouldExclude(fullPath)) { - files.push(fullPath); - } - } - - return files; -} - -function findViolations(filePath: string, content: string, patterns: ValidationPattern[]): Violation[] { - const violations: Violation[] = []; - const lines = content.split('\n'); - - for (const patternConfig of patterns) { - let match: RegExpExecArray | null; - const regex = new RegExp(patternConfig.pattern.source, patternConfig.pattern.flags); - - while ((match = regex.exec(content)) !== null) { - // Check exclusions - if (patternConfig.exclude) { - const shouldSkip = patternConfig.exclude.some(excludePattern => - excludePattern.test(content) - ); - if (shouldSkip) continue; - } - - // Find line number - const beforeMatch = content.substring(0, match.index); - const lineNumber = beforeMatch.split('\n').length; - const lineStart = beforeMatch.lastIndexOf('\n') + 1; - const column = match.index - lineStart + 1; - - violations.push({ - file: filePath, - line: lineNumber, - column, - pattern: match[0], - message: patternConfig.message, - severity: patternConfig.severity, - suggestion: patternConfig.suggestion, - context: lines[lineNumber - 1]?.trim() || '', - }); - } - } - - return violations; -} - -function formatViolation(v: Violation): string { - const severityColor = { - P0: '\x1b[31m', // Red - P1: '\x1b[33m', // Yellow - P2: '\x1b[36m', // Cyan - }; - const reset = '\x1b[0m'; - - return ` -${severityColor[v.severity]}[${v.severity}]${reset} ${v.message} - File: ${v.file}:${v.line}:${v.column} - Found: "${v.pattern}" - Context: ${v.context} - Suggestion: ${v.suggestion} -`; -} - -function generateReport(violations: Violation[]): void { - const p0 = violations.filter(v => v.severity === 'P0'); - const p1 = violations.filter(v => v.severity === 'P1'); - const p2 = violations.filter(v => v.severity === 'P2'); - - console.log('\n========================================'); - console.log('SSOT VALIDATION REPORT'); - console.log('========================================\n'); - - console.log(`Total Violations: ${violations.length}`); - console.log(` P0 (Critical): ${p0.length}`); - console.log(` P1 (High): ${p1.length}`); - console.log(` P2 (Medium): ${p2.length}`); - - if (violations.length > 0) { - console.log('\n----------------------------------------'); - console.log('VIOLATIONS FOUND:'); - console.log('----------------------------------------'); - - // Group by file - const byFile = violations.reduce((acc, v) => { - if (!acc[v.file]) acc[v.file] = []; - acc[v.file].push(v); - return acc; - }, {} as Record); - - for (const [file, fileViolations] of Object.entries(byFile)) { - console.log(`\n馃搧 ${file}`); - for (const v of fileViolations) { - console.log(formatViolation(v)); - } - } - } - - console.log('\n========================================'); - - if (p0.length > 0) { - console.log('\n鉂 FAILED: P0 violations found. Fix before merging.\n'); - process.exit(1); - } else if (violations.length > 0) { - console.log('\n鈿狅笍 WARNING: Non-critical violations found. Consider fixing.\n'); - process.exit(0); - } else { - console.log('\n鉁 PASSED: No SSOT violations found!\n'); - process.exit(0); - } -} - -// ============================================================================= -// MAIN -// ============================================================================= - -function main(): void { - const backendDir = path.resolve(__dirname, '../../backend/src'); - const frontendDir = path.resolve(__dirname, '../../frontend/web/src'); - - console.log('馃攳 Validating SSOT constants usage...\n'); - console.log(`Backend: ${backendDir}`); - console.log(`Frontend: ${frontendDir}`); - - const allViolations: Violation[] = []; - - // Scan backend - if (fs.existsSync(backendDir)) { - const backendFiles = getFiles(backendDir); - console.log(`\nScanning ${backendFiles.length} backend files...`); - - for (const file of backendFiles) { - const content = fs.readFileSync(file, 'utf-8'); - const violations = findViolations(file, content, PATTERNS); - allViolations.push(...violations); - } - } - - // Scan frontend - if (fs.existsSync(frontendDir)) { - const frontendFiles = getFiles(frontendDir); - console.log(`Scanning ${frontendFiles.length} frontend files...`); - - for (const file of frontendFiles) { - const content = fs.readFileSync(file, 'utf-8'); - const violations = findViolations(file, content, PATTERNS); - allViolations.push(...violations); - } - } - - generateReport(allViolations); -} - -main(); diff --git a/projects/erp-construccion/docker-compose.prod.yml b/projects/erp-construccion/docker-compose.prod.yml deleted file mode 100644 index ddfb3e4d9..000000000 --- a/projects/erp-construccion/docker-compose.prod.yml +++ /dev/null @@ -1,129 +0,0 @@ -version: '3.8' - -# ============================================================================= -# ERP-SUITE: CONSTRUCCION - Production Docker Compose -# ============================================================================= -# Vertical: Construccion (35% completado) -# Puerto Frontend: 3020 | Puerto Backend: 3021 -# Schemas BD: construccion (7 sub-schemas, 110 tablas) -# Depende de: auth.*, core.*, inventory.* (erp-core) -# ============================================================================= - -services: - # =========================================================================== - # BACKEND API - # =========================================================================== - backend: - image: ${DOCKER_REGISTRY:-72.60.226.4:5000}/erp-construccion-backend:${VERSION:-latest} - container_name: erp-construccion-backend - restart: unless-stopped - ports: - - "3021:3021" - environment: - - NODE_ENV=production - - PORT=3021 - env_file: - - ./backend/.env.production - volumes: - - construccion-logs:/var/log/construccion - - construccion-uploads:/app/uploads - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3021/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - networks: - - erp-network - - isem-network - depends_on: - - redis - deploy: - resources: - limits: - cpus: '1' - memory: 512M - reservations: - cpus: '0.25' - memory: 256M - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" - - # =========================================================================== - # FRONTEND WEB - # =========================================================================== - frontend: - image: ${DOCKER_REGISTRY:-72.60.226.4:5000}/erp-construccion-frontend:${VERSION:-latest} - container_name: erp-construccion-frontend - restart: unless-stopped - ports: - - "3020:80" - depends_on: - backend: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:80"] - interval: 30s - timeout: 10s - retries: 3 - networks: - - erp-network - deploy: - resources: - limits: - cpus: '0.5' - memory: 128M - logging: - driver: "json-file" - options: - max-size: "5m" - max-file: "2" - - # =========================================================================== - # REDIS (Cache + Sessions + Queue) - # =========================================================================== - redis: - image: redis:7-alpine - container_name: erp-construccion-redis - restart: unless-stopped - ports: - - "6380:6379" - command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru - volumes: - - construccion-redis:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 3 - networks: - - erp-network - deploy: - resources: - limits: - cpus: '0.5' - memory: 256M - -# ============================================================================= -# VOLUMES -# ============================================================================= -volumes: - construccion-logs: - driver: local - construccion-uploads: - driver: local - construccion-redis: - driver: local - -# ============================================================================= -# NETWORKS -# ============================================================================= -networks: - erp-network: - driver: bridge - isem-network: - external: true - name: isem-network diff --git a/projects/erp-construccion/docker-compose.yml b/projects/erp-construccion/docker-compose.yml deleted file mode 100644 index a1038835f..000000000 --- a/projects/erp-construccion/docker-compose.yml +++ /dev/null @@ -1,184 +0,0 @@ -# Docker Compose - ERP Construccion -# Version: 1.0 -# Ambiente: Development - -version: '3.8' - -services: - # ========================================================================== - # DATABASE - PostgreSQL con PostGIS - # ========================================================================== - db: - image: postgis/postgis:15-3.3-alpine - container_name: construccion-db - restart: unless-stopped - environment: - POSTGRES_USER: ${DB_USER:-construccion} - POSTGRES_PASSWORD: ${DB_PASSWORD:-construccion_dev_2024} - POSTGRES_DB: ${DB_NAME:-erp_construccion} - PGDATA: /var/lib/postgresql/data/pgdata - volumes: - - postgres_data:/var/lib/postgresql/data - - ./database/init-scripts:/docker-entrypoint-initdb.d:ro - ports: - - "${DB_PORT:-5433}:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-construccion} -d ${DB_NAME:-erp_construccion}"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - construccion-network - - # ========================================================================== - # REDIS - Cache y Colas - # ========================================================================== - redis: - image: redis:7-alpine - container_name: construccion-redis - restart: unless-stopped - command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis_dev_2024} - volumes: - - redis_data:/data - ports: - - "${REDIS_PORT:-6379}:6379" - healthcheck: - test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis_dev_2024}", "ping"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - construccion-network - - # ========================================================================== - # BACKEND - API Node.js + Express - # ========================================================================== - backend: - build: - context: ./backend - dockerfile: Dockerfile - target: ${BUILD_TARGET:-development} - container_name: construccion-backend - restart: unless-stopped - environment: - NODE_ENV: ${NODE_ENV:-development} - APP_PORT: 3000 - API_VERSION: v1 - # Database - DB_HOST: db - DB_PORT: 5432 - DB_USER: ${DB_USER:-construccion} - DB_PASSWORD: ${DB_PASSWORD:-construccion_dev_2024} - DB_NAME: ${DB_NAME:-erp_construccion} - # Redis - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_dev_2024} - # JWT - JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} - JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-1d} - JWT_REFRESH_EXPIRES_IN: ${JWT_REFRESH_EXPIRES_IN:-7d} - # CORS - CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173,http://localhost:3001} - CORS_CREDENTIALS: "true" - # Logging - LOG_LEVEL: ${LOG_LEVEL:-debug} - LOG_FORMAT: dev - volumes: - - ./backend/src:/app/src:ro - - ./backend/package.json:/app/package.json:ro - - backend_node_modules:/app/node_modules - ports: - - "${BACKEND_PORT:-3000}:3000" - depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - networks: - - construccion-network - - # ========================================================================== - # FRONTEND WEB - React + Vite - # ========================================================================== - frontend: - build: - context: ./frontend/web - dockerfile: Dockerfile - target: ${BUILD_TARGET:-development} - container_name: construccion-frontend - restart: unless-stopped - environment: - VITE_API_URL: ${VITE_API_URL:-http://localhost:3000/api/v1} - VITE_WS_URL: ${VITE_WS_URL:-ws://localhost:3000} - volumes: - - ./frontend/web/src:/app/src:ro - - ./frontend/web/public:/app/public:ro - - ./frontend/web/index.html:/app/index.html:ro - - frontend_node_modules:/app/node_modules - ports: - - "${FRONTEND_PORT:-5173}:5173" - depends_on: - - backend - networks: - - construccion-network - - # ========================================================================== - # ADMINER - Database Management (Development Only) - # ========================================================================== - adminer: - image: adminer:4-standalone - container_name: construccion-adminer - restart: unless-stopped - environment: - ADMINER_DEFAULT_SERVER: db - ADMINER_DESIGN: pepa-linha-dark - ports: - - "${ADMINER_PORT:-8080}:8080" - depends_on: - - db - profiles: - - dev - networks: - - construccion-network - - # ========================================================================== - # MAILHOG - Email Testing (Development Only) - # ========================================================================== - mailhog: - image: mailhog/mailhog:latest - container_name: construccion-mailhog - restart: unless-stopped - ports: - - "${MAILHOG_SMTP_PORT:-1025}:1025" - - "${MAILHOG_WEB_PORT:-8025}:8025" - profiles: - - dev - networks: - - construccion-network - -# ========================================================================== -# VOLUMES -# ========================================================================== -volumes: - postgres_data: - driver: local - redis_data: - driver: local - backend_node_modules: - driver: local - frontend_node_modules: - driver: local - -# ========================================================================== -# NETWORKS -# ========================================================================== -networks: - construccion-network: - driver: bridge diff --git a/projects/erp-construccion/docs/00-vision-general/ARQUITECTURA-SAAS.md b/projects/erp-construccion/docs/00-vision-general/ARQUITECTURA-SAAS.md deleted file mode 100644 index 8d41d4ea7..000000000 --- a/projects/erp-construccion/docs/00-vision-general/ARQUITECTURA-SAAS.md +++ /dev/null @@ -1,1303 +0,0 @@ -# Arquitectura SaaS Multi-tenant - ERP Construcci贸n - -**Versi贸n:** 2.0 SaaS -**Fecha:** 2025-11-17 -**Modelo:** SaaS Multi-tenant B2B - ---- - -## 馃搵 Resumen Ejecutivo - -**De desarrollo a medida 鈫 Plataforma SaaS** - -El sistema evoluciona de un ERP a medida a una **plataforma SaaS multi-tenant** tipo SAP Cloud, donde: - -鉁 **Un solo c贸digo base** sirve a m煤ltiples empresas constructoras -鉁 **M贸dulos activables** por cliente seg煤n su plan de suscripci贸n -鉁 **Portal de administraci贸n** para gestionar tenants, usuarios y configuraciones -鉁 **Marketplace de extensiones** para customizaciones espec铆ficas sin tocar el core -鉁 **Onboarding automatizado** en minutos vs semanas de implementaci贸n -鉁 **Pricing por m贸dulos** con planes B谩sico/Profesional/Enterprise - ---- - -## 馃彈锔 Arquitectura Multi-tenant - -### Modelo de Aislamiento - -**Enfoque: Row-Level Security (RLS) con discriminador `constructora_id`** - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Capa de Aplicaci贸n (Stateless) 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 API 1 鈹 鈹 API 2 鈹 ... 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 PostgreSQL Database 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Schema: constructoras 鈹 鈹 -鈹 鈹 - constructoras (tenant) 鈹 鈹 -鈹 鈹 - user_constructoras 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Schema: projects 鈹 鈹 -鈹 鈹 - projects 鈹 鈹 -鈹 鈹 鈹溾攢 constructora_id (FK) 鈹 鈹 鈫 Discriminador -鈹 鈹 鈹斺攢 RLS Policy 鈹 鈹 -鈹 鈹 - budgets 鈹 鈹 -鈹 鈹 鈹溾攢 constructora_id (FK) 鈹 鈹 -鈹 鈹 鈹斺攢 RLS Policy 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Schema: auth_management 鈹 鈹 -鈹 鈹 - profiles 鈹 鈹 -鈹 鈹 鈹溾攢 constructora_id (FK) 鈹 鈹 -鈹 鈹 鈹斺攢 RLS Policy 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 Contexto por sesi贸n: 鈹 -鈹 app.current_constructora_id = X 鈹 -鈹 app.current_user_role = Y 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Ventajas:** -- 鉁 Escalabilidad ilimitada (millones de constructoras) -- 鉁 Migraciones simples (un solo schema por dominio) -- 鉁 Queries cross-tenant posibles (analytics globales) -- 鉁 Menor overhead de gesti贸n y mantenimiento -- 鉁 **90% de reutilizaci贸n de c贸digo de GAMILIT** -- 鉁 Aislamiento l贸gico robusto mediante RLS policies - -**Mitigaci贸n de riesgos:** -- Seguridad: RLS policies a nivel de BD (no bypasseable desde aplicaci贸n) -- Performance: 脥ndices en `constructora_id` (sin degradaci贸n) -- Compliance: Audit logging detallado por constructora -- Testing: Tests de aislamiento en CI/CD (validar RLS) - ---- - -### Identificaci贸n de Constructora (Tenant) - -**1. Por Subdominio:** - -``` -https://constructora-abc.erp-construccion.com 鈫 constructora: constructora-abc -https://viviendas-xyz.erp-construccion.com 鈫 constructora: viviendas-xyz -``` - -**2. Por JWT Claim (principal):** - -```json -// Token JWT contiene -{ - "userId": "uuid-...", - "constructoraId": "uuid-abc-123", - "role": "engineer", - "email": "usuario@constructora-abc.com" -} -``` - -**3. Por Header HTTP (API externa):** - -```http -GET /api/projects -Host: api.erp-construccion.com -X-Constructora-ID: uuid-abc-123 -Authorization: Bearer -``` - -**Middleware de Constructora Resolver:** - -```typescript -// apps/backend/src/middleware/constructora-resolver.middleware.ts - -export class ConstructoraResolverMiddleware implements NestMiddleware { - constructor( - private constructoraService: ConstructoraService, - private dataSource: DataSource - ) {} - - async use(req: Request, res: Response, next: NextFunction) { - // Extraer constructora desde: - // 1. JWT claim (m谩s com煤n, ya autenticado) - const constructoraId = req.user?.constructoraId; - - // 2. Header (para APIs externas) - const headerConstructoraId = req.headers['x-constructora-id']; - - // 3. Subdomain (UX amigable, requiere lookup) - let subdomain = null; - if (req.hostname.includes('erp-construccion.com')) { - subdomain = req.hostname.split('.')[0]; - } - - const finalConstructoraId = constructoraId || headerConstructoraId; - - if (!finalConstructoraId && !subdomain) { - throw new UnauthorizedException('Constructora not identified'); - } - - // Validar que constructora existe y est谩 activa - let constructora; - if (subdomain) { - constructora = await this.constructoraService.findBySubdomain(subdomain); - } else { - constructora = await this.constructoraService.findById(finalConstructoraId); - } - - if (!constructora || !constructora.active) { - throw new UnauthorizedException('Invalid or inactive constructora'); - } - - // Verificar que usuario tiene acceso a esta constructora - if (req.user?.userId) { - const hasAccess = await this.constructoraService.userHasAccess( - req.user.userId, - constructora.id - ); - if (!hasAccess) { - throw new ForbiddenException('User does not have access to this constructora'); - } - } - - // Inyectar en contexto de request - req.constructora = constructora; - - // 猸 CR脥TICO: Configurar contexto RLS en la sesi贸n de BD - await this.setRLSContext(constructora.id, req.user?.role); - - next(); - } - - private async setRLSContext(constructoraId: string, role?: string) { - // Establecer variables de sesi贸n para RLS policies - await this.dataSource.query(` - SELECT - set_config('app.current_constructora_id', $1, true), - set_config('app.current_user_role', $2, true) - `, [constructoraId, role || 'guest']); - } -} -``` - -**Pol铆ticas RLS en Base de Datos:** - -```sql --- Ejemplo: Tabla projects.projects -CREATE POLICY "projects_select_own_constructora" ON projects.projects - FOR SELECT - TO authenticated - USING ( - constructora_id::text = current_setting('app.current_constructora_id', true) - ); - -CREATE POLICY "projects_insert_own_constructora" ON projects.projects - FOR INSERT - TO authenticated - WITH CHECK ( - constructora_id::text = current_setting('app.current_constructora_id', true) - ); - --- Similar para UPDATE y DELETE -``` - ---- - -### Contexto RLS por Request - -**Interceptor NestJS (aplicado globalmente):** - -```typescript -// apps/backend/src/interceptors/set-rls-context.interceptor.ts - -@Injectable() -export class SetRlsContextInterceptor implements NestInterceptor { - constructor(private dataSource: DataSource) {} - - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest(); - const user = request.user; - const constructora = request.constructora; - - if (!constructora?.id) { - // Contexto p煤blico o sin autenticaci贸n - return next.handle(); - } - - // Establecer contexto RLS al inicio del request - return from( - this.dataSource.query(` - SELECT - set_config('app.current_constructora_id', $1, true), - set_config('app.current_user_id', $2, true), - set_config('app.current_user_role', $3, true) - `, [ - constructora.id, - user?.userId || null, - user?.role || 'guest' - ]) - ).pipe( - switchMap(() => next.handle()), - // El contexto se limpia autom谩ticamente al finalizar la transacci贸n - ); - } -} - -// Registro global en main.ts -app.useGlobalInterceptors(new SetRlsContextInterceptor(dataSource)); -``` - -**Funciones Helper en PostgreSQL:** - -```sql --- apps/database/ddl/schemas/public/functions/ - --- Obtener constructora del contexto actual -CREATE OR REPLACE FUNCTION public.get_current_constructora_id() -RETURNS UUID AS $$ -BEGIN - RETURN current_setting('app.current_constructora_id', true)::UUID; -EXCEPTION - WHEN OTHERS THEN - RETURN NULL; -END; -$$ LANGUAGE plpgsql STABLE; - --- Obtener user_id del contexto actual -CREATE OR REPLACE FUNCTION public.get_current_user_id() -RETURNS UUID AS $$ -BEGIN - RETURN current_setting('app.current_user_id', true)::UUID; -EXCEPTION - WHEN OTHERS THEN - RETURN NULL; -END; -$$ LANGUAGE plpgsql STABLE; - --- Obtener rol del contexto actual -CREATE OR REPLACE FUNCTION public.get_current_user_role() -RETURNS TEXT AS $$ -BEGIN - RETURN current_setting('app.current_user_role', true); -EXCEPTION - WHEN OTHERS THEN - RETURN 'guest'; -END; -$$ LANGUAGE plpgsql STABLE; - --- Verificar si usuario tiene acceso a constructora -CREATE OR REPLACE FUNCTION public.user_has_access_to_constructora( - p_user_id UUID, - p_constructora_id UUID -) -RETURNS BOOLEAN AS $$ -BEGIN - RETURN EXISTS ( - SELECT 1 - FROM auth_management.user_constructoras - WHERE user_id = p_user_id - AND constructora_id = p_constructora_id - AND active = true - ); -END; -$$ LANGUAGE plpgsql STABLE; -``` - -**Ventajas de este enfoque:** -- 鉁 Un solo pool de conexiones (eficiente) -- 鉁 Contexto por request, no por conexi贸n -- 鉁 Compatible con transacciones -- 鉁 F谩cil de testear (mock del contexto) -- 鉁 Reutilizaci贸n directa de c贸digo GAMILIT - ---- - -## 馃帥锔 Portal de Administraci贸n SaaS - -### Roles del Portal - -| Rol | Descripci贸n | Accesos | -|-----|-------------|---------| -| **Super Admin** | Administrador de la plataforma | Todos los tenants, configuraci贸n global | -| **Tenant Admin** | Administrador de empresa cliente | Su tenant, usuarios, m贸dulos, configuraci贸n | -| **Support** | Soporte t茅cnico | Ver datos, ayudar clientes (no modificar) | -| **Billing** | Facturaci贸n y cobranza | Ver uso, generar facturas, suspender por falta de pago | - ---- - -### Funcionalidades del Portal - -#### 1. Gesti贸n de Tenants - -**Dashboard Principal:** - -| Tenant | Plan | Usuarios | M贸dulos Activos | Estado | MRR | Acciones | -|--------|------|----------|-----------------|--------|-----|----------| -| constructora-abc | Enterprise | 45/50 | 15/18 | 馃煝 Activo | $1,500 | Ver 路 Editar 路 Facturar | -| viviendas-xyz | Profesional | 18/25 | 10/18 | 馃煝 Activo | $750 | Ver 路 Editar 路 Facturar | -| obras-norte | B谩sico | 8/10 | 6/18 | 馃煛 Prueba | $0 | Ver 路 Editar 路 Convertir | -| desarrollos-sur | Enterprise | 12/100 | 18/18 | 馃敶 Suspendido | $2,000 | Ver 路 Editar 路 Reactivar | - -**M茅tricas Globales:** -- Total tenants: 234 -- Activos: 198 (84.6%) -- En prueba: 28 (12%) -- Suspendidos: 8 (3.4%) -- MRR Total: $156,780 -- Usuarios totales: 4,523 - ---- - -#### 2. Onboarding de Nuevo Tenant - -**Flujo Automatizado (5 minutos):** - -```yaml -onboarding_steps: - - step: 1 - name: "Registro inicial" - fields: - - company_name: "Constructora ABC SA de CV" - - rfc: "CABC850101AAA" - - industry: "Construcci贸n de vivienda" - - employees_count: "50-100" - - subdomain: "constructora-abc" # Auto-sugerido - - admin_email: "admin@constructora-abc.com" - - admin_name: "Juan P茅rez" - - phone: "+52 442 123 4567" - duration: "2 min" - - - step: 2 - name: "Selecci贸n de plan" - options: - - plan: "B谩sico" - price: "$399/mes" - users: "10" - modules: "6 m贸dulos core" - - plan: "Profesional" # 鈫 Seleccionado - price: "$799/mes" - users: "25" - modules: "12 m贸dulos" - - plan: "Enterprise" - price: "$1,499/mes" - users: "100" - modules: "Todos (18)" - duration: "1 min" - - - step: 3 - name: "Configuraci贸n de m贸dulos" - modules_selected: - - MAI-001: "Fundamentos" (incluido) - - MAI-002: "Proyectos" (incluido) - - MAI-003: "Presupuestos" (incluido) - - MAI-004: "Compras" (incluido) - - MAI-005: "Control de Obra" (incluido) - - MAI-006: "Reportes" (incluido) - - MAI-007: "RRHH" ($100/mes adicional) - - MAI-008: "Estimaciones" (incluido) - - MAI-009: "Calidad" ($50/mes adicional) - - MAI-010: "CRM" (incluido) - duration: "1 min" - - - step: 4 - name: "Provisioning autom谩tico" - actions: - - "Crear registro en tabla constructoras.constructoras" - - "Generar UUID 煤nico para constructora" - - "Crear usuario admin con relaci贸n a constructora" - - "Insertar registro en user_constructoras (rol admin)" - - "Activar m贸dulos seleccionados (feature flags)" - - "Configurar subdomain DNS (CNAME a aplicaci贸n)" - - "Generar datos seed (cat谩logos base)" - - "Generar datos demo (opcional)" - - "Enviar email de bienvenida con credenciales" - duration: "< 1 min (background job)" - - - step: 5 - name: "Primer login" - url: "https://constructora-abc.erp-construccion.com" - credentials: - - email: "admin@constructora-abc.com" - - temp_password: "xxxxxx" (cambiar en primer login) - welcome_tour: true - sample_data: true - duration: "Inmediato" - -total_time: "5 minutos" -status: "鉁 Tenant activo" -``` - ---- - -#### 3. Configuraci贸n de M贸dulos por Tenant - -**Panel de M贸dulos:** - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 M贸dulos Activos: constructora-abc (Plan Profesional)鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 FASE 1: ALCANCE INICIAL 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鉁 MAI-001 Fundamentos Incluido鈹 [鈼廬 鈹 -鈹 鈹 鉁 MAI-002 Proyectos Incluido鈹 [鈼廬 鈹 -鈹 鈹 鉁 MAI-003 Presupuestos Incluido鈹 [鈼廬 鈹 -鈹 鈹 鉁 MAI-004 Compras Incluido鈹 [鈼廬 鈹 -鈹 鈹 鉁 MAI-005 Control de Obra Incluido鈹 [鈼廬 鈹 -鈹 鈹 鉁 MAI-006 Reportes Incluido鈹 [鈼廬 鈹 -鈹 鈹 鉁 MAI-007 RRHH +$100/mes鈹 [鈼廬 鈹 -鈹 鈹 鉁 MAI-008 Estimaciones Incluido鈹 [鈼廬 鈹 -鈹 鈹 鈿 MAI-009 Calidad +$50/mes 鈹 [ ] 鈫 鈹 -鈹 鈹 鉁 MAI-010 CRM Incluido鈹 [鈼廬 鈹 -鈹 鈹 鈿 MAI-011 INFONAVIT +$75/mes 鈹 [ ] 鈹 -鈹 鈹 鈿 MAI-012 Contratos +$75/mes 鈹 [ ] 鈹 -鈹 鈹 鈿 MAI-013 Administraci贸n Incluido 鈹 [ ] 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 FASE 2: ENTERPRISE 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鈿 MAE-014 Finanzas +$200/mes鈹 [ ] 鈹 -鈹 鈹 鈿 MAE-015 Activos +$150/mes鈹 [ ] 鈹 -鈹 鈹 鈿 MAE-016 DMS +$100/mes鈹 [ ] 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 FASE 3: AVANZADA 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鈿 MAA-017 HSE + IA +$300/mes鈹 [ ] 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 Subtotal Plan Profesional: $799/mes 鈹 -鈹 Add-ons activados: +$100/mes 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Total mensual: $899/mes 鈹 -鈹 鈹 -鈹 [Guardar Cambios] [Vista Previa] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Al activar/desactivar m贸dulos:** -- Se ejecuta migration espec铆fica del m贸dulo -- Se actualizan permisos de usuarios -- Se calcula nuevo pricing -- Se notifica a tenant admin -- Cambios efectivos en <5 minutos - ---- - -#### 4. Gesti贸n de Usuarios por Tenant - -**Vista de Tenant Admin:** - -| Usuario | Email | Rol | M贸dulos | 脷ltimo acceso | Estado | Acciones | -|---------|-------|-----|---------|---------------|--------|----------| -| Juan P茅rez | admin@const-abc.com | Admin | Todos | Hace 2 hrs | 馃煝 Activo | Editar 路 Desactivar | -| Mar铆a L贸pez | maria@const-abc.com | Engineer | 8 m贸dulos | Hace 1 d铆a | 馃煝 Activo | Editar 路 Desactivar | -| Pedro Mart铆nez | pedro@const-abc.com | Resident | 6 m贸dulos | Hace 3 hrs | 馃煝 Activo | Editar 路 Desactivar | -| ... | ... | ... | ... | ... | ... | ... | - -**Usuarios: 18 / 25 (72% de capacidad)** - -[+ Invitar Usuario] [Importar CSV] [Exportar] - ---- - -#### 5. Configuraci贸n de Tenant - -**Categor铆as de configuraci贸n:** - -**General:** -- Nombre de empresa -- Logo (usado en reportes y emails) -- Zona horaria -- Idioma (ES/EN) -- Moneda (MXN/USD) - -**Personalizaci贸n:** -- Colores de marca (primary, secondary) -- Email remitente personalizado -- Dominio custom (opcional): `erp.constructora-abc.com` - -**Seguridad:** -- 2FA obligatorio (s铆/no) -- Pol铆tica de contrase帽as -- Sesiones concurrentes -- IP whitelisting - -**Integraciones:** -- SAP/CONTPAQi (credenciales) -- WhatsApp Business API -- SMS provider -- Storage (AWS S3 / Azure Blob) - -**Facturaci贸n:** -- M茅todo de pago -- Datos fiscales -- Historial de facturas -- Uso mensual - ---- - -## 馃挸 Modelo de Pricing - -### Planes Base - -| Plan | Precio/mes | Usuarios | M贸dulos Incluidos | Almacenamiento | Soporte | -|------|------------|----------|-------------------|----------------|---------| -| **B谩sico** | $399 USD | 10 | 6 core | 10 GB | Email (48h) | -| **Profesional** | $799 USD | 25 | 12 m贸dulos | 50 GB | Email + Chat (24h) | -| **Enterprise** | $1,499 USD | 100 | Todos (18) | 200 GB | Dedicado (4h) | -| **Enterprise Plus** | Custom | Ilimitado | Todos + Custom | Ilimitado | Dedicado (1h) | - ---- - -### M贸dulos Add-on (por m贸dulo/mes) - -| M贸dulo | Precio/mes | Disponible en | -|--------|------------|---------------| -| MAI-007 RRHH Avanzado | $100 | Todos los planes | -| MAI-009 Calidad y Postventa | $50 | Profesional+ | -| MAI-011 INFONAVIT | $75 | Profesional+ | -| MAI-012 Contratos | $75 | Profesional+ | -| MAE-014 Finanzas | $200 | Enterprise | -| MAE-015 Activos | $150 | Enterprise | -| MAE-016 DMS | $100 | Profesional+ | -| MAA-017 HSE + IA | $300 | Enterprise | - ---- - -### Usuarios Adicionales - -| Plan | Precio/usuario/mes | -|------|--------------------| -| B谩sico | $20 USD | -| Profesional | $15 USD | -| Enterprise | $10 USD | - ---- - -### C谩lculo de Ejemplo - -**Constructora ABC (Plan Profesional):** - -``` -Plan Profesional base: $799/mes - - 25 usuarios incluidos - - 12 m贸dulos - -Add-ons activados: - + MAI-007 RRHH $100/mes - + MAI-011 INFONAVIT $75/mes - -Usuarios adicionales: - + 5 usuarios 脳 $15 $75/mes - -Almacenamiento adicional: - + 20 GB 脳 $2/GB $40/mes - -鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -Total mensual: $1,089/mes -Anual (15% descuento): $11,107/a帽o ($925/mes) -``` - ---- - -### Costos de Contrataci贸n Inicial (One-Time) - -Adem谩s de la suscripci贸n mensual, existe un **costo 煤nico de implementaci贸n inicial** que cubre: - -鉁 **Migraci贸n de datos** desde sistemas legacy (Excel, ERP anterior, etc.) -鉁 **Capacitaci贸n** a usuarios (sesiones remotas + material) -鉁 **Adaptaci贸n al negocio** (configuraci贸n de workflows, cat谩logos, permisos) -鉁 **Implementaciones dentro de configuraciones** (reportes personalizados, dashboards, etc.) - ---- - -#### Paquetes de Onboarding - -| Paquete | Precio | Usuarios | Registros a Migrar | Horas Capacitaci贸n | Horas Configuraci贸n | Ideal para | -|---------|--------|----------|-------------------|-------------------|---------------------|------------| -| **Starter** | $2,500 USD | <10 | <5,000 | 4 horas | 8 horas | Empresas peque帽as con datos simples | -| **Profesional** | $7,500 USD | 10-50 | <50,000 | 12 horas | 20 horas | Empresas medianas con procesos establecidos | -| **Enterprise** | $15,000 USD | 50-100 | <200,000 | 24 horas | 40 horas | Constructoras grandes con ERP previo | -| **Enterprise Plus** | Custom | 100+ | Ilimitado | Custom | Custom | Corporativos multinacionales | - ---- - -#### Desglose del Servicio de Onboarding - -**1. Migraci贸n de Datos (30% del tiempo)** - -Incluye: -- An谩lisis de datos fuente (Excel, CSVs, base de datos legacy) -- Limpieza y normalizaci贸n de datos -- Mapping de campos a esquema del ERP -- Importaci贸n automatizada con validaciones -- Verificaci贸n de integridad post-migraci贸n - -**Entregables:** -- Plan de migraci贸n documentado -- Scripts de importaci贸n -- Reporte de validaci贸n con discrepancias -- Backup de datos originales - -**Datos migrados t铆picos:** -- Cat谩logo de clientes/proveedores -- Proyectos hist贸ricos (煤ltimos 2 a帽os) -- Presupuestos y estimaciones -- Personal y n贸minas -- Inventarios de almac茅n -- Documentos y planos (opcional) - ---- - -**2. Capacitaci贸n (25% del tiempo)** - -**Metodolog铆a:** -- Sesiones remotas por Zoom/Teams -- Grabaciones disponibles 1 a帽o -- Material did谩ctico en PDF -- Certificado de participaci贸n - -**Programa:** - -| Sesi贸n | Audiencia | Duraci贸n | Contenido | -|--------|-----------|----------|-----------| -| **Sesi贸n 1: Administradores** | Admins de sistema | 3 hrs | Portal admin, usuarios, m贸dulos, configuraci贸n | -| **Sesi贸n 2: Operaciones** | Residentes de obra, almac茅n | 3 hrs | Proyectos, control de obra, compras, inventarios | -| **Sesi贸n 3: Finanzas** | Contadores, finanzas | 2 hrs | Presupuestos, estimaciones, reportes financieros | -| **Sesi贸n 4: RRHH** | Recursos humanos | 2 hrs | N贸minas, asistencias, incidencias | -| **Sesi贸n 5: Ejecutivos** | Directores, gerentes | 2 hrs | Dashboards, analytics, reportes ejecutivos | - -**Material incluido:** -- Manual de usuario por m贸dulo (PDF) -- Videos tutoriales (10-15 min c/u) -- FAQs y troubleshooting -- Acceso a knowledge base - ---- - -**3. Adaptaci贸n al Negocio (25% del tiempo)** - -Configuraciones personalizadas sin c贸digo: - -**Cat谩logos maestros:** -- Tipos de proyecto espec铆ficos (residencial, industrial, etc.) -- Cat谩logo de conceptos de obra (partidas est谩ndar) -- Plantillas de presupuesto por tipo de obra -- Roles y permisos personalizados -- Centros de costo / 谩reas organizacionales - -**Workflows de aprobaci贸n:** -- Flujo de aprobaci贸n de compras (niveles, montos) -- Flujo de estimaciones (revisi贸n, autorizaci贸n) -- Flujo de requisiciones de almac茅n -- Flujo de incidencias de calidad - -**Branding:** -- Logo de empresa en sistema -- Colores corporativos -- Plantillas de reportes con membrete -- Emails transaccionales personalizados - ---- - -**4. Implementaciones de Configuraci贸n (20% del tiempo)** - -Desarrollo de reportes y dashboards personalizados: - -**Reportes custom:** -- Reporte ejecutivo mensual (formato espec铆fico del cliente) -- Reporte de avance de obra para clientes finales -- Formatos oficiales (INFONAVIT, CFE, etc.) -- Reporte de rentabilidad por proyecto - -**Dashboards:** -- Dashboard ejecutivo C-level -- Dashboard de obra para residentes -- Dashboard financiero para contadores - -**Integraciones:** -- Configuraci贸n de integraci贸n SAP/CONTPAQi -- Configuraci贸n de WhatsApp Business API -- Configuraci贸n de storage (S3/Azure) - ---- - -#### Calendario de Implementaci贸n - -**Paquete Starter (2-3 semanas):** - -``` -Semana 1: - - D铆a 1-2: Kickoff + an谩lisis de datos - - D铆a 3-4: Migraci贸n de datos - - D铆a 5: Validaci贸n de migraci贸n - -Semana 2: - - D铆a 1-2: Configuraci贸n de cat谩logos y workflows - - D铆a 3-4: Capacitaci贸n usuarios (2 sesiones) - - D铆a 5: Ajustes finales - -Semana 3: - - D铆a 1-2: Configuraci贸n de reportes - - D铆a 3: Sesi贸n final y go-live - - D铆a 4-5: Soporte post go-live -``` - -**Paquete Profesional (4-6 semanas):** - -``` -Semana 1-2: Migraci贸n de datos + validaci贸n -Semana 3: Configuraci贸n avanzada -Semana 4-5: Capacitaci贸n (4-5 sesiones) -Semana 6: Reportes custom + go-live -``` - -**Paquete Enterprise (8-12 semanas):** - -``` -Semana 1-3: An谩lisis y migraci贸n de datos complejos -Semana 4-6: Configuraci贸n enterprise + integraciones -Semana 7-9: Capacitaci贸n intensiva (6+ sesiones) -Semana 10-11: Desarrollo de reportes y dashboards -Semana 12: UAT, ajustes y go-live -``` - ---- - -#### Soporte Post-Onboarding - -**Incluido en el onboarding (primeros 30 d铆as):** -- Soporte prioritario v铆a email/chat -- Webinars de Q&A semanales -- Ajustes menores de configuraci贸n -- Resoluci贸n de dudas operativas - -**Posterior (seg煤n plan de suscripci贸n):** -- Plan B谩sico: Email (48h) -- Plan Profesional: Email + Chat (24h) -- Plan Enterprise: Soporte dedicado (4h) - ---- - -#### Servicios Adicionales (Opcionales) - -| Servicio | Precio | Descripci贸n | -|----------|--------|-------------| -| **Capacitaci贸n on-site** | $3,000 USD/d铆a + vi谩ticos | Sesiones presenciales en oficinas del cliente | -| **Migraci贸n de documentos** | $0.10 USD/documento | Digitalizaci贸n y clasificaci贸n de planos/contratos | -| **Desarrollo de extensi贸n custom** | $150 USD/hora | Funcionalidad no disponible en configuraci贸n est谩ndar | -| **Consultor铆a de procesos** | $200 USD/hora | Optimizaci贸n de workflows y mejores pr谩cticas | -| **Integraci贸n legacy custom** | Desde $5,000 USD | Integraci贸n con sistemas propietarios complejos | -| **Auditor铆a de datos** | $2,000 USD | Validaci贸n exhaustiva de integridad de datos migrados | - ---- - -#### Garant铆a de Onboarding - -**Compromiso:** -- Sistema funcional al 100% al t茅rmino del onboarding -- Usuarios capacitados y productivos -- Datos migrados con >98% de precisi贸n - -**Si no se cumple:** -- Extensi贸n de soporte sin costo hasta lograrlo -- Reembolso parcial si no se alcanza funcionalidad m铆nima acordada -- Consultor铆a adicional sin cargo - ---- - -#### Ejemplo de Presupuesto Completo - -**Constructora ABC (50 empleados, 15,000 registros, ERP previo):** - -``` -INVERSI脫N INICIAL (One-time): - Paquete Profesional Onboarding: $7,500 USD - 鉁 Migraci贸n 15,000 registros - 鉁 12 horas capacitaci贸n (4 sesiones) - 鉁 20 horas configuraci贸n - 鉁 Soporte 30 d铆as post go-live - - Servicios adicionales: - + Migraci贸n 2,000 planos PDF $200 USD - + Integraci贸n CONTPAQi $5,000 USD - 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - Subtotal inicial: $12,700 USD - - -SUSCRIPCI脫N MENSUAL: - Plan Profesional base: $799/mes - 鉁 25 usuarios incluidos - 鉁 12 m贸dulos - 鉁 50 GB almacenamiento - 鉁 Soporte 24h - - Add-ons activados: - + MAI-007 RRHH Avanzado $100/mes - + MAI-011 INFONAVIT $75/mes - + MAI-012 Contratos $75/mes - - Usuarios adicionales: - + 5 usuarios 脳 $15 $75/mes - 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - Total mensual: $1,124/mes - - -COSTO TOTAL A脩O 1: - Inversi贸n inicial: $12,700 USD - Suscripci贸n 12 meses: $13,488 USD (1,124 脳 12) - 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - Total a帽o 1: $26,188 USD - -COSTO TOTAL A脩OS SUBSECUENTES: - Suscripci贸n 12 meses: $13,488 USD/a帽o - (sin costo de onboarding) - - -ROI vs ERP Tradicional: - SAP/Oracle implementaci贸n: $150K-$500K inicial - SAP/Oracle suscripci贸n: $50K-$150K/a帽o - - Ahorro a帽o 1: $123K-$473K (vs SAP m铆nimo) - Payback: Inmediato -``` - ---- - -## 馃攲 Marketplace de Extensiones - -### Tipos de Extensiones - -| Tipo | Descripci贸n | Ejemplo | -|------|-------------|---------| -| **Integraciones** | Conectores a sistemas externos | Integraci贸n con WhatsApp Business, Slack, Zoom | -| **Reportes Custom** | Plantillas de reportes espec铆ficas | Reporte para licitaciones CFE, reporte INFONAVIT especial | -| **M贸dulos Verticales** | Funcionalidad espec铆fica de industria | M贸dulo de Obra Civil Pesada, M贸dulo de Edificaci贸n Alta | -| **Workflows Custom** | Flujos de aprobaci贸n personalizados | Workflow de estimaciones 5 niveles | -| **Dashboards** | Dashboards tem谩ticos | Dashboard Ejecutivo C-Level | -| **Templates** | Plantillas de documentos | Contratos tipo, formatos oficiales | - ---- - -### Cat谩logo de Marketplace - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 馃洅 Marketplace de Extensiones 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 INTEGRACIONES 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 馃摫 WhatsApp Business API 鈹 鈹 -鈹 鈹 Notificaciones autom谩ticas 鈹 鈹 -鈹 鈹 猸愨瓙猸愨瓙猸 (45 reviews) 鈹 鈹 -鈹 鈹 Gratis | [Instalar] 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 馃捈 SAP S/4HANA Connector 鈹 鈹 -鈹 鈹 Export de p贸lizas contables 鈹 鈹 -鈹 鈹 猸愨瓙猸愨瓙 (23 reviews) 鈹 鈹 -鈹 鈹 $99/mes | [Instalar] 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 REPORTES CUSTOM 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 馃搳 Reporte INFONAVIT EVC 鈹 鈹 -鈹 鈹 Formato oficial actualizado 2025 鈹 鈹 -鈹 鈹 猸愨瓙猸愨瓙猸 (89 reviews) 鈹 鈹 -鈹 鈹 $49 煤nico | [Comprar] 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 M脫DULOS VERTICALES 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 馃彈锔 Obra Civil Pesada 鈹 鈹 -鈹 鈹 Puentes, carreteras, presas 鈹 鈹 -鈹 鈹 猸愨瓙猸愨瓙 (12 reviews) 鈹 鈹 -鈹 鈹 $299/mes | [Ver Demo] 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 [Explorar M谩s] [Mis Extensiones] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -### Desarrollo de Extensiones - -**SDK de Extensiones:** - -```typescript -// apps/extensions/ejemplo-extension/index.ts - -import { Extension, Hook, MenuItem } from '@erp-construccion/sdk'; - -@Extension({ - id: 'whatsapp-notifier', - name: 'WhatsApp Notifier', - version: '1.0.0', - author: 'ERP Construcci贸n', - description: 'Env铆a notificaciones por WhatsApp', - permissions: ['notifications.send', 'users.read'], - pricing: { - type: 'free', - }, -}) -export class WhatsAppNotifierExtension { - - // Hook: Se ejecuta cuando se crea una estimaci贸n - @Hook('estimations.created') - async onEstimationCreated(estimation: Estimation) { - const tenant = this.context.tenant; - const users = await this.api.users.findByRole('finance'); - - for (const user of users) { - if (user.phone && user.notificationsEnabled) { - await this.sendWhatsApp(user.phone, { - template: 'estimation_created', - params: { - estimationNumber: estimation.number, - amount: estimation.amount, - project: estimation.project.name, - }, - }); - } - } - } - - // Agregar 铆tem al men煤 lateral - @MenuItem({ - section: 'settings', - label: 'Configurar WhatsApp', - icon: 'whatsapp', - route: '/settings/whatsapp', - }) - menuItem() { - return { - component: WhatsAppSettingsPage, - }; - } - - private async sendWhatsApp(phone: string, message: any) { - // Implementaci贸n... - } -} -``` - ---- - -## 馃攧 Ciclo de Vida del Tenant - -### Estados de Tenant - -```mermaid -stateDiagram-v2 - [*] --> Registering - Registering --> Trial: Onboarding completado - Trial --> Active: Pago confirmado - Trial --> Expired: 14 d铆as sin pago - Active --> Suspended: Falta de pago - Suspended --> Active: Pago recibido - Suspended --> Canceled: 30 d铆as suspendido - Active --> Canceled: Solicitud de cliente - Expired --> Active: Pago recibido - Canceled --> [*] -``` - -| Estado | Descripci贸n | Acceso | Duraci贸n | -|--------|-------------|--------|----------| -| **Registering** | Alta en proceso | No | <5 min | -| **Trial** | Per铆odo de prueba | Completo | 14 d铆as | -| **Active** | Suscripci贸n activa | Completo | Indefinido | -| **Suspended** | Falta de pago | Solo lectura | Hasta 30 d铆as | -| **Expired** | Trial vencido | Login deshabilitado | Hasta reactivaci贸n | -| **Canceled** | Cancelado por cliente o sistema | No | Soft-delete 90 d铆as | - ---- - -### Pol铆ticas de Cancelaci贸n - -**Cancelaci贸n por Cliente:** -1. Cliente solicita cancelaci贸n desde portal -2. Confirmaci贸n con raz贸n (opcional: encuesta) -3. Export de datos ofrecido (formato SQL/Excel) -4. Tenant pasa a estado `Canceled` -5. Datos retenidos 90 d铆as (compliance) -6. Eliminaci贸n permanente tras 90 d铆as - -**Suspensi贸n por Falta de Pago:** -1. Intento de cargo fallido (d铆a 1) -2. Reintento autom谩tico (d铆a 3) -3. Email de recordatorio (d铆a 5) -4. 脷ltimo reintento (d铆a 7) -5. **Suspensi贸n** (d铆a 8): Solo lectura -6. Email de suspensi贸n con link de pago -7. Cancelaci贸n autom谩tica si no paga en 30 d铆as - ---- - -## 馃洜锔 Gesti贸n de Configuraciones - -### Niveles de Configuraci贸n - -``` -1. Global (Platform-level) - 鈹溾攢鈹 Configuraci贸n de infraestructura - 鈹溾攢鈹 L铆mites globales - 鈹斺攢鈹 Features flags - -2. Tenant-level - 鈹溾攢鈹 M贸dulos activados - 鈹溾攢鈹 Usuarios y permisos - 鈹溾攢鈹 Branding - 鈹溾攢鈹 Integraciones - 鈹斺攢鈹 Datos maestros - -3. User-level - 鈹溾攢鈹 Preferencias personales - 鈹溾攢鈹 Dashboard layout - 鈹斺攢鈹 Notificaciones -``` - ---- - -### Feature Flags - -Permiten activar/desactivar funcionalidades sin deploy: - -```typescript -// apps/backend/src/config/feature-flags.service.ts - -export class FeatureFlagsService { - async isFeatureEnabled( - featureName: string, - tenantId?: string - ): Promise { - // 1. Verificar a nivel global - const globalFlag = await this.getGlobalFlag(featureName); - if (globalFlag === false) return false; - - // 2. Verificar a nivel tenant (si aplica) - if (tenantId) { - const tenantFlag = await this.getTenantFlag(featureName, tenantId); - if (tenantFlag !== null) return tenantFlag; - } - - // 3. Default - return globalFlag; - } -} - -// Uso en controlador -@Get('ai-insights') -@UseGuards(FeatureGuard('ai_risk_prediction')) -async getAIInsights() { - // Solo accesible si feature est谩 habilitada para el tenant - // ... -} -``` - -**Casos de uso:** -- Gradual rollout de nuevas features -- A/B testing -- Deprecaci贸n controlada de features -- Habilitaci贸n por plan (Enterprise features) - ---- - -## 馃搳 M茅tricas SaaS - -### KPIs del Negocio - -| M茅trica | Descripci贸n | Target | -|---------|-------------|--------| -| **MRR** | Monthly Recurring Revenue | Crecimiento 15% M/M | -| **ARR** | Annual Recurring Revenue | $2M a帽o 1 | -| **Churn Rate** | % de clientes que cancelan | <5% mensual | -| **CAC** | Customer Acquisition Cost | <$1,500 | -| **LTV** | Lifetime Value | >$18,000 (12脳 CAC) | -| **Activation Rate** | % que activan m贸dulos en 7 d铆as | >80% | -| **NPS** | Net Promoter Score | >50 | - ---- - -### M茅tricas T茅cnicas - -| M茅trica | Target | Monitoreo | -|---------|--------|-----------| -| **Uptime** | 99.9% | StatusPage.io | -| **API Response Time** | p95 <200ms | DataDog | -| **Database Query Time** | p95 <100ms | pg_stat_statements | -| **Onboarding Time** | <5 min | Analytics | -| **Time to First Value** | <1 hr | Mixpanel | - ---- - -## 馃殌 Roadmap SaaS - -### Fase 1: MVP SaaS (Semanas 1-8) -- 鉁 Arquitectura multi-tenant -- 鉁 Portal de admin b谩sico -- 鉁 Onboarding automatizado -- 鉁 6 m贸dulos core -- 鉁 Pricing y billing - -### Fase 2: Enterprise Features (Semanas 9-16) -- 鉁 12 m贸dulos adicionales -- 鉁 M贸dulos activables din谩micamente -- 鉁 Marketplace MVP -- 鉁 Extensiones SDK -- 鉁 Custom domains - -### Fase 3: Scale & Growth (Semanas 17-24) -- 鈴 IA predictiva -- 鈴 Analytics avanzado -- 鈴 Integraciones nativas (SAP, WhatsApp) -- 鈴 Mobile app completa -- 鈴 API p煤blica para partners - -### Fase 4: Expansi贸n (Semanas 25+) -- 馃搵 Marketplace p煤blico -- 馃搵 White-label para partners -- 馃搵 Internacionalizaci贸n (US, LATAM) -- 馃搵 Cumplimiento (SOC2, ISO 27001) - ---- - -## 馃攼 Seguridad Multi-tenant - -### Aislamiento de Datos - -1. **Row-level security (RLS)**: Aislamiento l贸gico mediante pol铆ticas PostgreSQL -2. **Columna discriminadora**: `constructora_id` en todas las tablas multi-tenant -3. **Contexto por sesi贸n**: `app.current_constructora_id` configurado por request -4. **API-level validation**: Validaci贸n de acceso a constructora en middleware -5. **Audit logging**: Registro de accesos con constructora_id en cada operaci贸n -6. **Testing de aislamiento**: Tests autom谩ticos que validan RLS policies - -### Prevenci贸n de Data Leakage - -```typescript -// Guard que previene acceso cross-constructora -@Injectable() -export class ConstructoraGuard implements CanActivate { - constructor(private constructoraService: ConstructoraService) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user = request.user; - const constructora = request.constructora; - - if (!user || !constructora) { - throw new UnauthorizedException('User or constructora not identified'); - } - - // Validar que el usuario tiene acceso a esta constructora - const hasAccess = await this.constructoraService.userHasAccess( - user.userId, - constructora.id - ); - - if (!hasAccess) { - // Intent贸 acceder a datos de otra constructora - await this.auditService.logSecurityViolation({ - userId: user.userId, - attemptedConstructoraId: constructora.id, - event: 'cross_constructora_access_denied', - ip: request.ip, - }); - - throw new ForbiddenException('Access denied to this constructora'); - } - - return true; - } -} - -// Uso en controlador -@Controller('projects') -@UseGuards(JwtAuthGuard, ConstructoraGuard) -export class ProjectsController { - // Todos los endpoints requieren acceso v谩lido a constructora -} -``` - -**Tests de Aislamiento:** - -```typescript -// apps/backend/test/security/rls-isolation.spec.ts - -describe('RLS Isolation Tests', () => { - it('should prevent cross-constructora data access', async () => { - // Setup: Crear 2 constructoras y usuarios - const constructoraA = await createConstructora('Constructora A'); - const constructoraB = await createConstructora('Constructora B'); - - const userA = await createUser({ constructoraId: constructoraA.id }); - const userB = await createUser({ constructoraId: constructoraB.id }); - - // Crear proyecto para constructora A - const projectA = await createProject({ - name: 'Proyecto A', - constructoraId: constructoraA.id - }); - - // Login como usuario B - const tokenB = await loginAs(userB); - - // Intentar acceder a proyecto de constructora A (debe fallar) - const response = await request(app) - .get(`/api/projects/${projectA.id}`) - .set('Authorization', `Bearer ${tokenB}`) - .expect(403); - - expect(response.body.message).toContain('Access denied'); - }); - - it('should enforce RLS at database level', async () => { - // Setup similar... - - // Intentar query directo con constructora incorrecta en contexto - await dataSource.query(` - SELECT set_config('app.current_constructora_id', $1, true) - `, [constructoraB.id]); - - // Query debe retornar 0 resultados (RLS bloquea) - const projects = await dataSource.query(` - SELECT * FROM projects.projects WHERE id = $1 - `, [projectA.id]); - - expect(projects).toHaveLength(0); // RLS bloque贸 el acceso - }); -}); -``` - ---- - -## 馃摑 Conclusi贸n - -Esta arquitectura SaaS multi-tenant permite: - -鉁 **Escalabilidad**: De 10 a 10,000 tenants sin cambios arquitect贸nicos -鉁 **Time-to-market**: Onboarding de clientes en minutos -鉁 **Flexibilidad**: M贸dulos activables, extensiones, customizaci贸n -鉁 **Econom铆a**: Costo operativo distribuido, mejor margen -鉁 **Innovaci贸n**: Feature flags, A/B testing, rollout gradual - -**Pr贸ximo paso:** Implementar la transformaci贸n en el MVP-APP.md principal. - ---- - -**Generado:** 2025-11-17 -**Versi贸n:** 2.0 SaaS -**Modelo:** Multi-tenant B2B SaaS diff --git a/projects/erp-construccion/docs/00-vision-general/CAMBIOS-SAAS-MVP.md b/projects/erp-construccion/docs/00-vision-general/CAMBIOS-SAAS-MVP.md deleted file mode 100644 index b5c73fa41..000000000 --- a/projects/erp-construccion/docs/00-vision-general/CAMBIOS-SAAS-MVP.md +++ /dev/null @@ -1,656 +0,0 @@ -# Cambios para Transformar MVP a Modelo SaaS - -**Documento:** MVP-APP.md -**Versi贸n actual:** 1.1 (Desarrollo a medida) -**Versi贸n objetivo:** 2.0 (SaaS Multi-tenant) -**Fecha:** 2025-11-17 - ---- - -## 馃搵 Resumen de Cambios - -Este documento detalla los cambios espec铆ficos que deben aplicarse al MVP-APP.md para transformarlo de un sistema de desarrollo a medida a una plataforma SaaS multi-tenant. - ---- - -## 1. Resumen Ejecutivo (Secci贸n 0) - -### REEMPLAZAR: - -```markdown -## 0) Resumen ejecutivo - -Sistema **ERP de construcci贸n enterprise-ready** para constructoras de vivienda en serie... -``` - -### POR: - -```markdown -## 0) Resumen ejecutivo - -**Plataforma SaaS ERP de construcci贸n enterprise-ready** para constructoras de vivienda en serie que desarrollan conjuntos habitacionales (fraccionamientos, privadas, edificios) y participan en licitaciones y programas con INFONAVIT. - -### Modelo de Negocio: SaaS Multi-tenant B2B - -El sistema opera como **plataforma SaaS** donde: - -鉁 **M煤ltiples empresas constructoras** comparten la misma infraestructura (multi-tenant) -鉁 **M贸dulos activables** seg煤n plan de suscripci贸n (B谩sico/Profesional/Enterprise) -鉁 **Portal de administraci贸n** para gestionar configuraciones, usuarios y m贸dulos -鉁 **Onboarding automatizado** en 5 minutos vs semanas de implementaci贸n tradicional -鉁 **Marketplace de extensiones** para customizaciones sin modificar el core -鉁 **Pricing por uso** con facturaci贸n autom谩tica mensual/anual - -**Diferenciador clave:** Similar a SAP Cloud o Salesforce, pero especializado en construcci贸n. - -El sistema integra **18 m贸dulos funcionales** activables por cliente, que cubren el ciclo completo desde preconstrucci贸n hasta postventa, con capacidades comparables a ERPs l铆deres (SAP S/4HANA Construction, Procore, Autodesk Construction Cloud) pero con arquitectura moderna SaaS, onboarding r谩pido y menor TCO. -``` - ---- - -## 2. Nueva Secci贸n: Arquitectura SaaS (Despu茅s de Secci贸n 0) - -### AGREGAR NUEVA SECCI脫N: - -```markdown -## 0.1) Arquitectura SaaS Multi-tenant - -> Ver documentaci贸n completa en [ARQUITECTURA-SAAS.md](./ARQUITECTURA-SAAS.md) - -### Modelo Multi-tenant - -**Un solo c贸digo base, m煤ltiples clientes aislados:** - -``` -PostgreSQL Database -鈹溾攢鈹 tenant_constructora_abc (Schema) -鈹 鈹溾攢鈹 projects -鈹 鈹溾攢鈹 budgets -鈹 鈹溾攢鈹 purchases -鈹 鈹斺攢鈹 ... -鈹溾攢鈹 tenant_viviendas_xyz (Schema) -鈹 鈹溾攢鈹 projects -鈹 鈹溾攢鈹 budgets -鈹 鈹斺攢鈹 ... -鈹斺攢鈹 tenant_obras_norte (Schema) - 鈹斺攢鈹 ... -``` - -**Beneficios:** -- **Aislamiento fuerte**: Datos f铆sicamente separados por schema -- **Escalabilidad**: De 10 a 10,000 tenants sin cambios arquitect贸nicos -- **Seguridad**: Imposible acceso cross-tenant -- **Performance**: No hay degradaci贸n con m谩s tenants - -### Portal de Administraci贸n SaaS - -Dashboard central para gestionar: -- **Tenants**: Alta, configuraci贸n, suspensi贸n, cancelaci贸n -- **M贸dulos**: Activaci贸n/desactivaci贸n din谩mica por tenant -- **Usuarios**: Gesti贸n de usuarios y roles por empresa -- **Facturaci贸n**: Generaci贸n autom谩tica de facturas, seguimiento de pagos -- **M茅tricas**: MRR, churn, activaci贸n, uso por m贸dulo - -### Onboarding Automatizado (5 minutos) - -1. **Registro** (2 min): Datos de empresa, admin, subdominio -2. **Selecci贸n de plan** (1 min): B谩sico / Profesional / Enterprise -3. **Configuraci贸n de m贸dulos** (1 min): Activar m贸dulos deseados -4. **Provisioning autom谩tico** (<1 min): Schema creado, migraciones ejecutadas -5. **Primer login** (Inmediato): Sistema listo para usar - -### Planes y Pricing - -| Plan | Precio/mes | Usuarios | M贸dulos | Soporte | -|------|------------|----------|---------|---------| -| **B谩sico** | $399 USD | 10 | 6 core | Email 48h | -| **Profesional** | $799 USD | 25 | 12 m贸dulos | Chat 24h | -| **Enterprise** | $1,499 USD | 100 | Todos (18) | Dedicado 4h | - -**Add-ons disponibles:** -- M贸dulos adicionales: $50-$300/mes seg煤n complejidad -- Usuarios extra: $10-$20/usuario/mes seg煤n plan -- Almacenamiento: $2/GB/mes adicional - -### Marketplace de Extensiones - -Ecosistema de extensiones desarrolladas por: -- **Equipo interno**: Integraciones oficiales (SAP, WhatsApp, etc.) -- **Partners certificados**: M贸dulos verticales especializados -- **Clientes**: Custom workflows y reportes propios - -**Tipos de extensiones:** -- Integraciones (APIs externas) -- Reportes custom -- M贸dulos verticales (ej: Obra Civil Pesada) -- Workflows personalizados -- Dashboards tem谩ticos - -### Personalizaci贸n sin Modificar Core - -**Configuraci贸n (90% de casos):** -- Cat谩logos personalizados -- Workflows de aprobaci贸n -- Plantillas de documentos -- Campos custom (metadata JSON) -- Reglas de negocio (rule engine) - -**Extensiones (10% de casos):** -- SDK de desarrollo disponible -- Hooks en puntos clave del sistema -- API completa para integraciones -- Deploy aislado por tenant -``` - ---- - -## 3. M贸dulos (Secci贸n 2) - -### AGREGAR AL INICIO DE SECCI脫N 2: - -```markdown -## 2) M贸dulos y funciones (detalle inicial) - -### Activaci贸n de M贸dulos por Plan - -Los m贸dulos se activan/desactivan din谩micamente seg煤n el plan de suscripci贸n del cliente: - -**Plan B谩sico (6 m贸dulos core):** -- MAI-001 Fundamentos 鉁 -- MAI-002 Proyectos 鉁 -- MAI-003 Presupuestos 鉁 -- MAI-004 Compras 鉁 -- MAI-005 Control de Obra 鉁 -- MAI-006 Reportes 鉁 - -**Plan Profesional (12 m贸dulos):** -- Incluye los 6 del plan B谩sico -- + MAI-007 RRHH (add-on $100/mes) -- + MAI-008 Estimaciones 鉁 -- + MAI-009 Calidad (add-on $50/mes) -- + MAI-010 CRM 鉁 -- + MAI-011 INFONAVIT (add-on $75/mes) -- + MAI-012 Contratos (add-on $75/mes) - -**Plan Enterprise (18 m贸dulos):** -- Incluye los 12 del plan Profesional -- + MAI-013 Administraci贸n 鉁 -- + MAI-018 Preconstrucci贸n 鉁 -- + MAE-014 Finanzas (add-on $200/mes) -- + MAE-015 Activos (add-on $150/mes) -- + MAE-016 DMS (add-on $100/mes) -- + MAA-017 HSE + IA (add-on $300/mes) - -**Cambio de plan:** El cliente puede upgradearse en cualquier momento desde su portal. Los m贸dulos se activan instant谩neamente. - -> Aqu铆 se baja a un nivel m谩s operativo lo definido en 1A. -``` - ---- - -## 4. Arquitectura T茅cnica (Nueva Secci贸n despu茅s de M贸dulos) - -### AGREGAR NUEVA SECCI脫N: - -```markdown -## 4) Arquitectura T茅cnica SaaS - -### 4.1 Stack Tecnol贸gico - -**Backend:** -- Node.js 20+ + Express + TypeScript -- Multi-tenant architecture (schema-level isolation) -- Feature flags para rollout gradual -- Background jobs con Bull/BullMQ - -**Frontend:** -- React 18 + Vite + TypeScript -- Tenant-aware routing -- Dynamic module loading (lazy loading por m贸dulo) -- Branding personalizable por tenant - -**Base de Datos:** -- PostgreSQL 15+ con schemas por tenant -- Connection pooling optimizado -- Automated migrations per tenant -- Point-in-time recovery per schema - -**Infraestructura:** -- AWS/Azure/GCP (agn贸stico) -- Kubernetes para orchestration -- Redis para caching y sessions -- S3/Blob Storage para archivos -- CloudFront/CDN para assets est谩ticos - -### 4.2 Escalabilidad - -**Horizontal Scaling:** -- API servers: Stateless, escalan horizontalmente -- Database: Read replicas, sharding por tenant (futuro) -- Cache: Redis cluster -- Storage: Object storage escalable - -**Vertical Scaling:** -- Database: Upgrade de instancia seg煤n carga -- Tenants grandes: Schema dedicado en instancia separada (opcional) - -**Performance Targets:** -- API response time: p95 <200ms -- Page load time: <2s -- Database query time: p95 <100ms -- Uptime: 99.9% (8.76 horas downtime/a帽o) - -### 4.3 Seguridad Multi-tenant - -**Aislamiento de Datos:** -1. Schema-level isolation (primera capa) -2. Row-level security (segunda capa) -3. API-level validation (tercera capa) -4. Audit logging de accesos - -**Prevenci贸n de Leaks:** -- Tenant resolver middleware obligatorio -- Guards en todos los endpoints -- Validaci贸n de tenant_id en queries -- Monitoreo de accesos an贸malos - -**Cumplimiento:** -- GDPR (Europa) -- LFPDPPP (M茅xico) -- SOC 2 Type II (roadmap) -- ISO 27001 (roadmap) - -### 4.4 Despliegue y CI/CD - -**Environments:** -- Development (local) -- Staging (pre-producci贸n) -- Production (multi-region) - -**Pipeline:** -``` -Git Push 鈫 GitHub Actions - 鈫 -Unit Tests 鈫 Integration Tests 鈫 E2E Tests - 鈫 -Build Docker Images - 鈫 -Deploy to Staging 鈫 Smoke Tests - 鈫 -Manual Approval (para Production) - 鈫 -Blue-Green Deployment 鈫 Health Checks - 鈫 -Production Live -``` - -**Migrations:** -- Ejecutadas autom谩ticamente en cada tenant -- Rollback autom谩tico si falla -- Dry-run en staging primero -- Notification a tenants afectados - -### 4.5 Monitoreo y Observabilidad - -**M茅tricas:** -- Application metrics (DataDog/New Relic) -- Business metrics (MRR, churn, activaci贸n) -- Infrastructure metrics (CPU, RAM, disk) -- Custom metrics por m贸dulo - -**Logs:** -- Centralized logging (ELK/Splunk) -- Structured logs (JSON) -- Log levels por environment -- Retention: 90 d铆as production, 30 d铆as staging - -**Alerting:** -- PagerDuty para incidents cr铆ticos -- Slack para warnings -- Email para info -- Escalation policies definidas - -**Dashboards:** -- StatusPage.io p煤blico (uptime) -- Grafana interno (m茅tricas t茅cnicas) -- Admin portal (m茅tricas de negocio) -``` - ---- - -## 5. Roadmap (Actualizar Secci贸n Existente) - -### REEMPLAZAR: - -```markdown -### Roadmap y tiempos - -* **MVP Core (Fase 1)**: 6 semanas - Sistema operativo completo. -* **MVP + Enterprise (Fases 1-2)**: 12 semanas - Competitivo vs. ERPs medianos. -* **Sistema Completo (Fases 1-3)**: 18 semanas - Paridad con ERPs enterprise. -* **Optimizaci贸n (Fase 4 opcional)**: 22 semanas - Features avanzadas (IoT, AR, ML). -``` - -### POR: - -```markdown -### Roadmap y tiempos - -**Fase 1: MVP SaaS (Semanas 1-8)** -- Arquitectura multi-tenant implementada -- Portal de administraci贸n b谩sico -- Onboarding automatizado -- 6 m贸dulos core activables -- Pricing y billing automatizado -- **Entregable**: Primeros 10 clientes piloto - -**Fase 2: Enterprise Features (Semanas 9-16)** -- 12 m贸dulos adicionales (total 18) -- M贸dulos activables din谩micamente -- Marketplace MVP (5 extensiones) -- SDK para extensiones -- Custom domains -- **Entregable**: 50 clientes activos, $40K MRR - -**Fase 3: Scale & Growth (Semanas 17-24)** -- IA predictiva (HSE) -- Analytics avanzado por tenant -- Integraciones nativas (SAP, WhatsApp, CONTPAQi) -- Mobile app completa (React Native) -- API p煤blica para partners -- **Entregable**: 200 clientes, $150K MRR - -**Fase 4: Expansi贸n (Semanas 25-36)** -- Marketplace p煤blico (50+ extensiones) -- White-label para partners -- Internacionalizaci贸n (US, Colombia, Chile) -- Cumplimiento (SOC2, ISO 27001) -- Capacidades multi-region -- **Entregable**: 500 clientes, $400K MRR - -**vs. Desarrollo desde cero**: 30-35 semanas (ahorro ~40% gracias a reutilizaci贸n GAMILIT). -**vs. Desarrollo a medida tradicional**: Imposible escalar, cada cliente requiere deployment separado. -``` - ---- - -## 6. Ventajas Competitivas (Actualizar Secci贸n 0) - -### AGREGAR AL FINAL DE SECCI脫N 0: - -```markdown -### Ventajas competitivas del modelo SaaS - -1. **Time-to-value**: Cliente productivo en 5 minutos vs semanas de implementaci贸n. -2. **Pricing flexible**: Paga solo por lo que usa, puede upgradear/downgrear mensualmente. -3. **Actualizaciones autom谩ticas**: Nuevas features sin costo adicional ni downtime. -4. **Escalabilidad instant谩nea**: Agregar usuarios/m贸dulos en segundos. -5. **TCO menor**: No hay costos de infraestructura, mantenimiento, actualizaciones. -6. **Innovaci贸n continua**: Releases semanales con nuevas features y mejoras. -7. **Ecosystem**: Marketplace con extensiones de partners y comunidad. -8. **Multi-device**: Acceso desde web, m贸vil, tablet sin instalaciones. -9. **Seguridad enterprise**: Backups autom谩ticos, disaster recovery, uptime 99.9%. -10. **Soporte incluido**: Seg煤n plan, desde email 48h hasta dedicado 1h. - -### Comparaci贸n: SaaS vs On-Premise vs Desarrollo a Medida - -| Aspecto | SaaS (Este Sistema) | On-Premise | Desarrollo a Medida | -|---------|---------------------|------------|---------------------| -| **Time-to-market** | 5 minutos | 3-6 meses | 12-18 meses | -| **Costo inicial** | $0 (solo suscripci贸n) | $50K-$200K | $200K-$500K | -| **Costo mensual** | $399-$1,499/mes | $5K-$10K/mes (infra + soporte) | $10K-$20K/mes | -| **Actualizaciones** | Autom谩ticas, gratis | Manual, costosas | Manual, muy costosas | -| **Escalabilidad** | Instant谩nea | Limitada (hardware) | Muy limitada | -| **Personalizaci贸n** | Extensiones + config | Modificar core | Total | -| **Mantenimiento** | Incluido | Cliente responsable | Cliente responsable | -| **Uptime** | 99.9% (SLA) | Variable | Variable | -| **Soporte** | Incluido 24/7 | No incluido | No incluido | -| **ROI** | 3-6 meses | 18-24 meses | 24-36 meses | -``` - ---- - -## 7. Casos de Uso (Agregar Nueva Secci贸n) - -### AGREGAR NUEVA SECCI脫N: - -```markdown -## 8) Casos de Uso SaaS - -### Caso 1: Constructora Mediana (25 empleados) - -**Perfil:** -- 3 proyectos simult谩neos (150 viviendas/a帽o) -- Facturaci贸n: $80M MXN/a帽o -- Personal: 25 oficina + 150 campo - -**Plan:** Profesional ($799/mes) -- 25 usuarios incluidos -- 12 m贸dulos activados -- Add-ons: RRHH ($100/mes), INFONAVIT ($75/mes) -- **Total: $974/mes ($11,688/a帽o)** - -**ROI:** -- Antes (Excel + WhatsApp): P茅rdida 5% por descontrol = $4M/a帽o -- Despu茅s (ERP): P茅rdida reducida a 1% = $800K/a帽o -- **Ahorro: $3.2M/a帽o** -- **ROI: 27,000% (recuperaci贸n en 1.3 meses)** - ---- - -### Caso 2: Constructora Grande (100 empleados) - -**Perfil:** -- 10 proyectos simult谩neos (500 viviendas/a帽o) -- Facturaci贸n: $300M MXN/a帽o -- Personal: 100 oficina + 800 campo - -**Plan:** Enterprise ($1,499/mes) -- 100 usuarios incluidos -- 18 m贸dulos (todos) -- Add-ons: HSE IA ($300/mes), Finanzas ($200/mes) -- **Total: $1,999/mes ($23,988/a帽o)** - -**ROI:** -- Antes (ERP legacy costoso): $150K/a帽o licencias + $50K/a帽o mantenimiento = $200K/a帽o -- Despu茅s (Este SaaS): $24K/a帽o -- **Ahorro: $176K/a帽o** -- **Adem谩s**: Mayor productividad, menos errores, decisiones data-driven - ---- - -### Caso 3: Startup Constructora (5 empleados) - -**Perfil:** -- 1 proyecto piloto (30 viviendas) -- Facturaci贸n: $15M MXN/a帽o -- Personal: 5 oficina + 30 campo - -**Plan:** B谩sico ($399/mes) + Trial 14 d铆as gratis -- 10 usuarios incluidos -- 6 m贸dulos core -- Sin add-ons inicialmente -- **Total: $399/mes ($4,788/a帽o)** - -**Ventaja:** -- Herramientas enterprise desde d铆a 1 -- Puede crecer agregando m贸dulos -- Sin inversi贸n inicial en tecnolog铆a -- Competir con constructoras grandes - ---- - -### Caso 4: Extensi贸n para Obra Civil Pesada - -**Perfil:** -- Constructora especializada en puentes y carreteras -- Requiere funcionalidad espec铆fica no incluida en core - -**Soluci贸n:** Extensi贸n del Marketplace -- Plan Enterprise ($1,499/mes) -- + Extensi贸n "Obra Civil Pesada" ($299/mes) -- **Total: $1,798/mes** - -**Funcionalidad de extensi贸n:** -- Gesti贸n de tramos de carretera -- Control de acarreos y vol煤menes -- Reportes para SCT -- Integraci贸n con laboratorio - -**Desarrollo:** Partner certificado lo desarroll贸 usando el SDK, no modific贸 el core. -``` - ---- - -## 8. Migraci贸n de Clientes Existentes (Nueva Secci贸n) - -### AGREGAR NUEVA SECCI脫N: - -```markdown -## 9) Migraci贸n de Clientes Existentes - -### Si ya tienen un sistema legacy - -**Proceso de migraci贸n:** - -1. **An谩lisis de datos** (1 semana) - - Auditor铆a de datos actuales - - Identificaci贸n de datos cr铆ticos - - Mapeo de campos - - Limpieza de datos - -2. **Setup de tenant** (1 d铆a) - - Onboarding est谩ndar - - Configuraci贸n inicial - - Usuarios y permisos - -3. **Migraci贸n de datos** (1-2 semanas) - - Import de proyectos hist贸ricos - - Import de cat谩logos - - Import de transacciones - - Validaci贸n de integridad - -4. **Capacitaci贸n** (1 semana) - - Training del equipo administrativo - - Training del equipo de campo - - Documentaci贸n personalizada - - Soporte 1-1 - -5. **Go-live** (1 d铆a) - - Cutover del sistema legacy - - Monitoreo intensivo - - Soporte en sitio (opcional) - -6. **Estabilizaci贸n** (2 semanas) - - Soporte prioritario - - Ajustes de configuraci贸n - - Resoluci贸n de incidencias - -**Total tiempo de migraci贸n: 4-6 semanas** - -**Costo de migraci贸n:** -- Plan B谩sico/Profesional: Incluido (datos <10K registros) -- Plan Enterprise: Incluido (datos <50K registros) -- Datos masivos: $0.10 USD/registro adicional -- Soporte on-site: $1,500 USD/d铆a - -### Si no tienen sistema - -**Inicio desde cero:** -- Onboarding: 5 minutos -- Configuraci贸n inicial: 2 horas (cat谩logos, usuarios) -- Primer proyecto: 1 d铆a -- **Productivos: < 1 semana** -``` - ---- - -## 9. Actualizar Secci贸n de Comparaci贸n con Competidores - -### AGREGAR COLUMNA "MODELO" EN TABLA: - -```markdown -### Comparaci贸n con ERPs de mercado - -| Caracter铆stica | MVP-APP | SAP S/4HANA | Procore | Autodesk | -|----------------|---------|-------------|---------|----------| -| **Modelo** | **SaaS Multi-tenant** | On-Premise / Cloud | SaaS | SaaS | -| **Onboarding** | **5 minutos** | 6-12 meses | 2-4 semanas | 2-4 semanas | -| **M贸dulos activables** | 鉁 **Din谩mico** | 鉂 Todo o nada | 鈿狅笍 Limitado | 鈿狅笍 Limitado | -| **Marketplace** | 鉁 **S铆** | 鈿狅笍 Limitado | 鉂 No | 鈿狅笍 Limitado | -| **Pricing** | **$399-$1,499/mes** | $50K-$200K inicial + $5K/mes | $500-$2K/mes | $600-$1.5K/mes | -| **Personalizaci贸n** | 鉁 Extensiones SDK | 鈿狅笍 Consultores | 鉂 Limitado | 鉂 Limitado | -| Finanzas integradas | 鉁 Fase 2 | 鉁 Core | 鉂 Limitado | 鉂 No | -| HSE + IA predictiva | 鉁 **Diferenciador** | 鉂 No IA | 鈿狅笍 Sin IA | 鉂 No | -| Time & Attendance GPS+Bio | 鉁 Integrado | 鈿狅笍 M贸dulo aparte | 鈿狅笍 3rd party | 鉂 No | -| WhatsApp + IA agent | 鉁 **脷nico** | 鉂 No | 鉂 No | 鉂 No | -``` - ---- - -## 10. Documentos Relacionados - -Al final del MVP, agregar referencias a los nuevos documentos: - -```markdown ---- - -## Documentaci贸n Relacionada - -- **[ARQUITECTURA-SAAS.md](./ARQUITECTURA-SAAS.md)**: Arquitectura multi-tenant detallada -- **[PORTAL-ADMIN-SAAS.md](./PORTAL-ADMIN-SAAS.md)**: Especificaci贸n del portal de administraci贸n (por crear) -- **[MARKETPLACE-EXTENSIONES.md](./MARKETPLACE-EXTENSIONES.md)**: Gu铆a de desarrollo de extensiones (por crear) -- **[PRICING-ESTRATEGIA.md](./PRICING-ESTRATEGIA.md)**: Estrategia de pricing y planes (por crear) -- **[ONBOARDING-PLAYBOOK.md](./ONBOARDING-PLAYBOOK.md)**: Gu铆a de onboarding de clientes (por crear) - ---- - -**Versi贸n:** 2.0 SaaS Multi-tenant -**脷ltima actualizaci贸n:** 2025-11-17 -**Modelo de negocio:** B2B SaaS, Subscription-based -``` - ---- - -## 鉁 Checklist de Aplicaci贸n - -Para aplicar estos cambios al MVP-APP.md: - -- [ ] 1. Actualizar Resumen Ejecutivo (Secci贸n 0) -- [ ] 2. Agregar Secci贸n 0.1 (Arquitectura SaaS) -- [ ] 3. Actualizar Secci贸n 2 (M贸dulos activables) -- [ ] 4. Agregar Secci贸n 4 (Arquitectura T茅cnica SaaS) -- [ ] 5. Actualizar Roadmap -- [ ] 6. Agregar Ventajas Competitivas SaaS -- [ ] 7. Agregar Casos de Uso SaaS -- [ ] 8. Agregar Migraci贸n de Clientes -- [ ] 9. Actualizar Comparaci贸n con Competidores -- [ ] 10. Agregar Referencias a Documentaci贸n - ---- - -## 馃攧 Pr贸ximos Pasos - -Despu茅s de actualizar el MVP-APP.md: - -1. **Actualizar documentaci贸n en cascada:** - - README.md de cada fase - - _MAP.md de cada m贸dulo - - Agregar secciones de "Configuraci贸n SaaS" en cada m贸dulo - -2. **Crear documentos adicionales:** - - PORTAL-ADMIN-SAAS.md (especificaci贸n detallada) - - MARKETPLACE-EXTENSIONES.md (gu铆a de desarrollo) - - PRICING-ESTRATEGIA.md (an谩lisis de pricing) - - ONBOARDING-PLAYBOOK.md (gu铆a operativa) - -3. **Actualizar arquitectura t茅cnica:** - - Diagramas de arquitectura multi-tenant - - Flujos de onboarding - - Security model - - Deployment strategy - ---- - -**Generado:** 2025-11-17 -**Prop贸sito:** Gu铆a de transformaci贸n a modelo SaaS diff --git a/projects/erp-construccion/docs/00-vision-general/GLOSARIO.md b/projects/erp-construccion/docs/00-vision-general/GLOSARIO.md deleted file mode 100644 index a854ae717..000000000 --- a/projects/erp-construccion/docs/00-vision-general/GLOSARIO.md +++ /dev/null @@ -1,589 +0,0 @@ -# Glosario de T茅rminos - ERP Construcci贸n SaaS - -**Versi贸n:** 2.0 SaaS -**Fecha:** 2025-11-17 -**Prop贸sito:** Unificar terminolog铆a entre documentaci贸n t茅cnica, negocio y c贸digo - ---- - -## 馃幆 T茅rminos Clave - -### Multi-tenancy Concepts - -#### Tenant (T茅rmino T茅cnico SaaS) - -**Definici贸n:** -Entidad de aislamiento l贸gico en una arquitectura SaaS multi-tenant. En nuestro sistema, **tenant = constructora**. - -**Uso:** -- **Documentaci贸n t茅cnica:** Usar cuando se habla de arquitectura SaaS gen茅rica -- **C贸digo:** Usar en nombres de variables/funciones cuando el contexto es claro -- **Comunicaci贸n con clientes:** 鉂 Evitar, usar "constructora" en su lugar - -**Sin贸nimos:** -- Constructora (t茅rmino de negocio) -- Cliente (en contexto de suscripci贸n SaaS) -- Empresa (en contexto general) - -**Ejemplos:** -```typescript -// 鉁 Aceptable en c贸digo t茅cnico -interface TenantConfig { - id: string; - plan: SubscriptionPlan; -} - -// 鉁 Mejor para dominio de negocio -interface ConstructoraConfig { - id: string; - plan: SubscriptionPlan; -} -``` - ---- - -#### Constructora (T茅rmino de Negocio) - -**Definici贸n:** -Empresa de construcci贸n que es cliente del sistema SaaS. Es el equivalente de "tenant" en el dominio de negocio. - -**Uso:** -- **Documentaci贸n de negocio:** Usar siempre -- **Comunicaci贸n con usuarios:** Usar siempre -- **Base de datos:** Tabla `constructoras.constructoras` -- **API:** Endpoint `/api/constructoras` - -**Atributos principales:** -- `id` (UUID): Identificador 煤nico -- `nombre`: Nombre comercial (ej: "Constructora ABC") -- `razon_social`: Raz贸n social legal -- `rfc`: Registro Federal de Contribuyentes (M茅xico) -- `subdomain`: Subdominio asignado (ej: "constructora-abc") -- `plan`: Plan de suscripci贸n (B谩sico, Profesional, Enterprise) - -**Ejemplos:** -```sql --- 鉁 Tabla en BD -CREATE TABLE constructoras.constructoras ( - id UUID PRIMARY KEY, - nombre VARCHAR(255) NOT NULL, - subdomain VARCHAR(100) UNIQUE NOT NULL, - active BOOLEAN DEFAULT TRUE -); -``` - -```typescript -// 鉁 En servicios de negocio -export class ConstructoraService { - async findBySubdomain(subdomain: string): Promise { - // ... - } -} -``` - -**Relaci贸n con usuarios:** -- Un usuario puede pertenecer a m煤ltiples constructoras -- La relaci贸n se gestiona en `auth_management.user_constructoras` -- Cada relaci贸n tiene un rol espec铆fico (director, engineer, resident, etc.) - ---- - -### Modelo de Aislamiento - -#### Row-Level Security (RLS) - -**Definici贸n:** -Mecanismo de PostgreSQL que aplica pol铆ticas de seguridad a nivel de fila para aislar datos entre constructoras. - -**Funcionamiento:** -1. Cada tabla multi-tenant tiene columna `constructora_id` -2. Se configuran RLS policies que filtran por `constructora_id` -3. El contexto se establece por sesi贸n: `app.current_constructora_id` -4. PostgreSQL aplica autom谩ticamente las pol铆ticas - -**Ventajas:** -- 鉁 Aislamiento l贸gico robusto -- 鉁 Escalabilidad ilimitada -- 鉁 Migraciones simples -- 鉁 Reutilizaci贸n de c贸digo GAMILIT (90%) - -**Ejemplo:** -```sql --- Pol铆tica RLS en tabla projects -CREATE POLICY "projects_select_own_constructora" -ON projects.projects -FOR SELECT -TO authenticated -USING ( - constructora_id::text = current_setting('app.current_constructora_id', true) -); -``` - -```typescript -// Establecer contexto al inicio del request -await dataSource.query(` - SELECT set_config('app.current_constructora_id', $1, true) -`, [constructoraId]); -``` - ---- - -#### Columna Discriminadora - -**Definici贸n:** -Columna `constructora_id` (tipo UUID) presente en todas las tablas que contienen datos espec铆ficos de una constructora. - -**Uso:** -- **Foreign Key:** Apunta a `constructoras.constructoras(id)` -- **Indexada:** Para performance en queries filtrados -- **NOT NULL:** En tablas multi-tenant -- **Nullable:** En tablas compartidas (cat谩logos globales) - -**Tablas con discriminador:** -```sql --- Ejemplos -projects.projects.constructora_id -budgets.budgets.constructora_id -purchases.purchase_orders.constructora_id -hr.employees.constructora_id -auth_management.profiles.constructora_id -``` - -**Tablas SIN discriminador (compartidas):** -```sql --- Cat谩logos globales -public.countries -public.currencies -public.units_of_measure -``` - ---- - -### Roles y Permisos - -#### Roles de Construcci贸n - -**Definici贸n:** -7 roles espec铆ficos del dominio de construcci贸n, definidos en ENUM `construction_role`. - -**Lista completa:** -1. **`director`**: Director general/proyectos -2. **`engineer`**: Ingeniero de planeaci贸n/control -3. **`resident`**: Residente de obra/supervisor -4. **`purchases`**: Compras/almac茅n -5. **`finance`**: Administraci贸n/finanzas -6. **`hr`**: Recursos humanos -7. **`post_sales`**: Postventa/garant铆as - -**Uso en RLS:** -```sql -CREATE FUNCTION get_current_user_role() -RETURNS TEXT AS $$ -BEGIN - RETURN current_setting('app.current_user_role', true); -END; -$$ LANGUAGE plpgsql STABLE; - --- Pol铆tica que considera el rol -CREATE POLICY "budgets_directors_see_margins" -ON budgets.budgets -FOR SELECT -TO authenticated -USING ( - constructora_id::text = current_setting('app.current_constructora_id', true) - AND ( - get_current_user_role() IN ('director', 'finance') - OR hide_margins = false - ) -); -``` - ---- - -#### Relaci贸n Usuario-Constructora - -**Definici贸n:** -Asociaci贸n many-to-many entre usuarios y constructoras, gestionada en `auth_management.user_constructoras`. - -**Atributos:** -- `user_id`: FK a `auth_management.profiles` -- `constructora_id`: FK a `constructoras.constructoras` -- `role`: Rol del usuario en esta constructora -- `is_primary`: Si es la constructora principal del usuario -- `active`: Si la relaci贸n est谩 activa - -**Casos de uso:** -1. Usuario trabaja en m煤ltiples constructoras -2. Cambio de rol sin perder historial -3. Desactivaci贸n temporal (sin borrado) -4. Usuario externo (consultor, auditor) - -**Ejemplo:** -```typescript -// Usuario puede tener m煤ltiples constructoras -const userConstructoras = await db.query(` - SELECT c.*, uc.role, uc.is_primary - FROM constructoras.constructoras c - INNER JOIN auth_management.user_constructoras uc ON uc.constructora_id = c.id - WHERE uc.user_id = $1 AND uc.active = true -`, [userId]); - -// Usuario selecciona constructora activa al login -const switchConstructora = async (userId, constructoraId) => { - // Validar acceso - const hasAccess = await userHasAccessToConstructora(userId, constructoraId); - if (!hasAccess) throw new ForbiddenException(); - - // Actualizar JWT con nuevo constructoraId - return generateJWT({ userId, constructoraId, role }); -}; -``` - ---- - -### Arquitectura SaaS - -#### Subdominio - -**Definici贸n:** -Identificador 煤nico en formato de subdominio DNS usado para acceder a la instancia de una constructora. - -**Formato:** -``` -https://{subdomain}.erp-construccion.com -``` - -**Ejemplos:** -- `constructora-abc.erp-construccion.com` -- `viviendas-xyz.erp-construccion.com` -- `desarrollos-norte.erp-construccion.com` - -**Validaciones:** -- Solo lowercase, n煤meros y guiones -- Sin espacios ni caracteres especiales -- 脷nico en toda la plataforma -- M铆nimo 3, m谩ximo 50 caracteres - -**Lookup:** -```typescript -// Resolver constructora desde subdomain -const subdomain = req.hostname.split('.')[0]; // "constructora-abc" -const constructora = await db.query(` - SELECT * FROM constructoras.constructoras WHERE subdomain = $1 -`, [subdomain]); -``` - ---- - -#### Plan de Suscripci贸n - -**Definici贸n:** -Nivel de servicio contratado por la constructora. - -**Opciones:** -1. **B谩sico** ($399/mes): 10 usuarios, 6 m贸dulos core -2. **Profesional** ($799/mes): 25 usuarios, 12 m贸dulos -3. **Enterprise** ($1,499/mes): 100 usuarios, todos los m贸dulos (18) - -**Atributos por plan:** -- L铆mite de usuarios -- M贸dulos incluidos -- M贸dulos disponibles como add-on -- Almacenamiento -- Nivel de soporte -- SLA de uptime - -**Activaci贸n de m贸dulos:** -```typescript -// Feature flag basado en plan -export class FeatureFlagsService { - async isModuleEnabled( - moduleCode: string, - constructoraId: string - ): Promise { - const constructora = await this.getConstructora(constructoraId); - - // Verificar si m贸dulo est谩 incluido en el plan - const planModules = this.getModulesForPlan(constructora.plan); - if (planModules.includes(moduleCode)) return true; - - // Verificar si est谩 habilitado como add-on - const addOns = await this.getActiveAddOns(constructoraId); - return addOns.includes(moduleCode); - } -} -``` - ---- - -### Contexto de Ejecuci贸n - -#### Contexto RLS - -**Definici贸n:** -Variables de sesi贸n PostgreSQL que almacenan el contexto del request actual para aplicar RLS policies. - -**Variables principales:** -- `app.current_constructora_id`: UUID de la constructora activa -- `app.current_user_id`: UUID del usuario autenticado -- `app.current_user_role`: Rol del usuario - -**Ciclo de vida:** -1. **Request inicia:** Middleware extrae `constructoraId` del JWT -2. **Contexto se establece:** `set_config()` antes de queries -3. **RLS se aplica:** PostgreSQL usa variables en policies -4. **Request termina:** Contexto se limpia autom谩ticamente - -**Implementaci贸n:** -```typescript -// Interceptor global NestJS -@Injectable() -export class SetRlsContextInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest(); - const { constructoraId, userId, role } = request.user; - - return from( - this.dataSource.query(` - SELECT - set_config('app.current_constructora_id', $1, true), - set_config('app.current_user_id', $2, true), - set_config('app.current_user_role', $3, true) - `, [constructoraId, userId, role]) - ).pipe( - switchMap(() => next.handle()) - ); - } -} -``` - -**Funciones helper:** -```sql --- Obtener constructora del contexto -CREATE FUNCTION get_current_constructora_id() RETURNS UUID AS $$ -BEGIN - RETURN current_setting('app.current_constructora_id', true)::UUID; -EXCEPTION WHEN OTHERS THEN - RETURN NULL; -END; -$$ LANGUAGE plpgsql STABLE; -``` - ---- - -### M贸dulos y Features - -#### M贸dulo - -**Definici贸n:** -Unidad funcional del sistema que puede activarse/desactivarse por constructora seg煤n su plan. - -**Estructura:** -``` -MAI-XXX: M贸dulos de Fase 1 (Alcance Inicial) -MAE-XXX: M贸dulos de Fase 2 (Enterprise B谩sico) -MAA-XXX: M贸dulos de Fase 3 (Avanzada con IA) -``` - -**Estados por constructora:** -- **Incluido:** M贸dulo incluido en el plan base -- **Add-on:** Disponible por pago adicional -- **No disponible:** No compatible con el plan -- **Activo:** M贸dulo contratado y funcional -- **Inactivo:** M贸dulo no habilitado - -**Ejemplo:** -```yaml -# Configuraci贸n de m贸dulo MAI-007 (RRHH) -modules: - MAI-007: - name: "RRHH, Asistencias y N贸mina" - plans: - basic: addon # Add-on $100/mes - professional: included - enterprise: included - dependencies: - - MAI-001 # Requiere Fundamentos -``` - ---- - -#### Feature Flag - -**Definici贸n:** -Bandera de configuraci贸n que habilita/deshabilita funcionalidades espec铆ficas sin necesidad de deploy. - -**Niveles:** -1. **Global:** Afecta a toda la plataforma -2. **Por constructora:** Espec铆fico de un cliente -3. **Por m贸dulo:** Asociado a un m贸dulo -4. **Por usuario:** Experimental para usuarios espec铆ficos - -**Casos de uso:** -- Gradual rollout de nuevas features -- A/B testing -- Habilitaci贸n de features enterprise -- Deprecaci贸n controlada - -**Implementaci贸n:** -```typescript -// Decorador para proteger endpoint con feature flag -@Get('ai-insights') -@RequiresFeature('ai_risk_prediction') -@RequiresPlan(['enterprise']) -async getAIInsights() { - // Solo accesible si: - // 1. Constructora tiene plan Enterprise - // 2. Feature flag 'ai_risk_prediction' est谩 habilitado -} -``` - ---- - -## 馃摉 Conversiones Terminol贸gicas - -### De T茅rminos T茅cnicos a Negocio - -| T茅rmino T茅cnico (SaaS) | T茅rmino de Negocio (ERP) | Contexto de uso | -|------------------------|--------------------------|-----------------| -| Tenant | Constructora | Siempre en docs de negocio | -| Tenant ID | ID de Constructora | Base de datos, API | -| Multi-tenancy | Multi-empresa | Comunicaci贸n con clientes | -| Tenant isolation | Aislamiento de datos por constructora | Seguridad | -| Tenant admin | Administrador de constructora | Roles de usuario | -| Tenant provisioning | Alta de constructora | Onboarding | -| Cross-tenant access | Acceso entre constructoras | Auditor铆a de seguridad | -| Schema-level isolation | 鉂 Ya no se usa | Arquitectura legacy | -| Row-level security (RLS) | Aislamiento por filas | Arquitectura actual | - ---- - -### De Jerga de Construcci贸n a Sistema - -| T茅rmino de Construcci贸n | T茅rmino en Sistema | Tabla/M贸dulo | -|------------------------|-------------------|--------------| -| Obra | Proyecto | `projects.projects` | -| Partida | Concepto de presupuesto | `budgets.budget_items` | -| Estimaci贸n | Estimaci贸n de obra | `estimations.estimations` | -| Cuadrilla | Cuadrilla/Equipo | `hr.crews` | -| Frente de trabajo | Frente | `construction.work_fronts` | -| Residente | Usuario con rol `resident` | Rol de sistema | -| Avance | Avance f铆sico/financiero | `construction.progress` | -| OC (Orden de Compra) | Purchase Order | `purchases.purchase_orders` | -| Requisici贸n | Requisici贸n de materiales | `purchases.requisitions` | -| Tarjeta (asistencia) | Registro de asistencia | `hr.attendances` | - ---- - -## 馃攧 Migraci贸n de T茅rminos Legacy - -### Si encuentras estos t茅rminos, reemplaza: - -| 鉂 T茅rmino Antiguo | 鉁 T茅rmino Correcto | Raz贸n | -|-------------------|-------------------|-------| -| `tenant_schema` | `constructora_id` | Ya no usamos schemas separados | -| `tenant_001` | UUID 煤nico | IDs descriptivos cambiaron a UUIDs | -| `search_path` | Contexto RLS (`set_config`) | Cambio de arquitectura | -| `setTenantSchema()` | `setRLSContext()` | Nueva implementaci贸n | -| `TenantConnectionService` | 鉂 Eliminar | Ya no se necesita | -| `schema: tenant_${id}` | 鉂 Eliminar | Configuraci贸n legacy | - ---- - -## 馃摑 Gu铆as de Estilo - -### En C贸digo (TypeScript/SQL) - -**鉁 Preferir:** -```typescript -// Variables de dominio -const constructora = await getConstructora(id); -const constructoraId = user.constructoraId; - -// Servicios -export class ConstructoraService { } - -// DTOs -export class CreateConstructoraDto { } -``` - -**鈿狅笍 Aceptable en contexto t茅cnico:** -```typescript -// Cuando el contexto SaaS es claro -interface TenantConfig { } -class TenantGuard implements CanActivate { } -``` - -**鉂 Evitar:** -```typescript -// Ambiguo o confuso -const company = ...; // 驴Empresa cliente? 驴Empresa del sistema? -const client = ...; // 驴Cliente del tenant? 驴Tenant mismo? -``` - ---- - -### En Documentaci贸n - -**Documentaci贸n t茅cnica (arquitectura, c贸digo):** -- 鉁 Usar "tenant" cuando se habla de patrones SaaS gen茅ricos -- 鉁 Siempre aclarar: "tenant (constructora)" -- 鉁 Definir en primera menci贸n - -**Documentaci贸n de negocio (requerimientos, casos de uso):** -- 鉁 Usar "constructora" exclusivamente -- 鉂 Evitar "tenant" -- 鉁 Usar t茅rminos del dominio de construcci贸n - -**Documentaci贸n de usuario (manuales, FAQs):** -- 鉁 Usar "su empresa", "su constructora" -- 鉂 Nunca usar "tenant" -- 鉁 Lenguaje natural y cercano - ---- - -## 馃И Testing y Validaci贸n - -### Nomenclatura de Tests - -**鉁 Tests de RLS:** -```typescript -describe('RLS Isolation - Constructora', () => { - it('should prevent cross-constructora data access', () => { - // ... - }); - - it('should allow multi-constructora user to switch context', () => { - // ... - }); -}); -``` - -**鉁 Tests de autorizaci贸n:** -```typescript -describe('ConstructoraGuard', () => { - it('should deny access if user does not belong to constructora', () => { - // ... - }); -}); -``` - ---- - -## 馃摎 Referencias - -**Documentos relacionados:** -- [ARQUITECTURA-SAAS.md](./ARQUITECTURA-SAAS.md) - Arquitectura SaaS completa -- [RF-AUTH-003-multi-tenancy.md](../01-fase-alcance-inicial/MAI-001-fundamentos/requerimientos/RF-AUTH-003-multi-tenancy.md) - Especificaci贸n de multi-tenancy -- [TRACEABILITY.yml](../01-fase-alcance-inicial/MAI-001-fundamentos/implementacion/TRACEABILITY.yml) - Trazabilidad de implementaci贸n - -**Decisiones arquitect贸nicas:** -- **2025-11-17:** Adopci贸n de Row-Level Security (RLS) en lugar de schema-level isolation - - Raz贸n: Escalabilidad ilimitada, migraciones simples, 90% de reutilizaci贸n de GAMILIT - - Impacto: Cambio de `tenant_XXX` schemas a `constructora_id` discriminador - ---- - -**Generado:** 2025-11-17 -**Versi贸n:** 2.0 SaaS -**Mantenedores:** @tech-lead @documentation-team diff --git a/projects/erp-construccion/docs/00-vision-general/MARKETPLACE-EXTENSIONES.md b/projects/erp-construccion/docs/00-vision-general/MARKETPLACE-EXTENSIONES.md deleted file mode 100644 index 002f47f00..000000000 --- a/projects/erp-construccion/docs/00-vision-general/MARKETPLACE-EXTENSIONES.md +++ /dev/null @@ -1,1081 +0,0 @@ -# Marketplace de Extensiones - Gu铆a de Desarrollo - -**Versi贸n:** 1.0 -**Fecha:** 2025-11-17 -**SDK Version:** 1.0.0 -**Modelo:** SaaS Multi-tenant B2B - ---- - -## 馃搵 Resumen Ejecutivo - -El Marketplace de Extensiones permite a constructoras (tenants), partners y desarrolladores externos crear funcionalidad custom sin modificar el core del sistema. Las extensiones se desarrollan usando el SDK oficial y se distribuyen a trav茅s del marketplace centralizado. - -**Tipos de desarrolladores:** -- **Equipo interno**: Extensiones oficiales (integraciones, reportes) -- **Partners certificados**: M贸dulos verticales especializados -- **Clientes enterprise**: Extensiones privadas para uso interno -- **Comunidad**: Extensiones p煤blicas open-source - ---- - -## 馃幆 Tipos de Extensiones - -### 1. Integraciones (Connectors) - -Conectores a sistemas externos: - -**Ejemplos:** -- SAP S/4HANA Connector -- CONTPAQi Connector -- WhatsApp Business API -- QuickBooks Online -- Slack Notifications -- Microsoft Teams -- Google Workspace -- Zoom Meetings - -**Caracter铆sticas:** -- Autenticaci贸n OAuth 2.0 -- Webhooks bidireccionales -- Retry logic y error handling -- Rate limiting inteligente -- Logs de sincronizaci贸n - -**Pricing t铆pico:** $0-$199/mes - ---- - -### 2. Reportes Custom - -Plantillas de reportes especializados: - -**Ejemplos:** -- Reporte INFONAVIT EVC (formato oficial 2025) -- Reporte para licitaciones CFE -- Reporte de cumplimiento NOM-031-STPS -- Dashboard ejecutivo C-level -- Reporte de rentabilidad por proyecto -- An谩lisis de variaciones de presupuesto - -**Caracter铆sticas:** -- Exportaci贸n a PDF, Excel, CSV -- Scheduling autom谩tico (diario, semanal, mensual) -- Email delivery -- Plantillas con branding de la constructora -- C谩lculos complejos pre-configurados - -**Pricing t铆pico:** $29-$99 (one-time) o $10-$50/mes - ---- - -### 3. M贸dulos Verticales - -Funcionalidad espec铆fica por tipo de constructora: - -**Ejemplos:** -- M贸dulo de Obra Civil Pesada (puentes, carreteras) -- M贸dulo de Edificaci贸n Alta (rascacielos, oficinas) -- M贸dulo de Obra Industrial (plantas, f谩bricas) -- M贸dulo de Infraestructura (aeropuertos, estaciones) -- M贸dulo de Restauraci贸n (edificios hist贸ricos) - -**Caracter铆sticas:** -- Cat谩logos especializados (actividades, riesgos, EPP) -- Workflows espec铆ficos -- Reportes regulatorios verticales -- Integraciones con herramientas especializadas - -**Pricing t铆pico:** $199-$599/mes - ---- - -### 4. Workflows Custom - -Flujos de aprobaci贸n personalizados: - -**Ejemplos:** -- Workflow de estimaciones 5 niveles (Resident 鈫 Super 鈫 Director 鈫 Finance 鈫 Cliente) -- Workflow de compras con 3 cotizaciones obligatorias -- Workflow de cambios de alcance con firma digital -- Workflow de liberaci贸n de pagos a subcontratistas - -**Caracter铆sticas:** -- Aprobaciones paralelas o secuenciales -- Escalamiento autom谩tico por tiempo -- Notificaciones por m煤ltiples canales -- Firma digital integrada -- Audit trail completo - -**Pricing t铆pico:** $49-$199/mes - ---- - -### 5. Dashboards Tem谩ticos - -Dashboards especializados: - -**Ejemplos:** -- Dashboard Financiero (CFO) -- Dashboard de Producci贸n (Gerente de Obra) -- Dashboard de Calidad (QA Manager) -- Dashboard de HSE (Safety Manager) -- Dashboard de Compras (Procurement) - -**Caracter铆sticas:** -- Widgets personalizables -- Drill-down interactivo -- Alertas en tiempo real -- Exportaci贸n programada -- Mobile responsive - -**Pricing t铆pico:** $29-$99/mes - ---- - -### 6. Templates - -Plantillas de documentos y procesos: - -**Ejemplos:** -- Contratos tipo (obra, subcontrato, arrendamiento) -- Formatos oficiales (permisos, licencias) -- Checklists de calidad por actividad -- Procedimientos de seguridad -- Minutas de junta - -**Caracter铆sticas:** -- Merge de datos del sistema -- Variables din谩micas -- Versionado de templates -- Firma digital -- Multi-idioma - -**Pricing t铆pico:** $9-$49 (one-time) - ---- - -## 馃洜锔 SDK de Desarrollo - -### Instalaci贸n - -```bash -npm install @erp-construccion/extension-sdk -``` - -### Estructura de una Extensi贸n - -```typescript -// mi-extension/ -// 鈹溾攢鈹 package.json -// 鈹溾攢鈹 extension.config.ts -// 鈹溾攢鈹 src/ -// 鈹 鈹溾攢鈹 index.ts // Entry point -// 鈹 鈹溾攢鈹 hooks/ // Event hooks -// 鈹 鈹溾攢鈹 components/ // UI components (React) -// 鈹 鈹溾攢鈹 services/ // Business logic -// 鈹 鈹斺攢鈹 utils/ // Utilities -// 鈹斺攢鈹 tests/ - -// package.json -{ - "name": "@mi-empresa/extension-whatsapp", - "version": "1.0.0", - "description": "Notificaciones por WhatsApp", - "main": "dist/index.js", - "dependencies": { - "@erp-construccion/extension-sdk": "^1.0.0" - }, - "extensionMetadata": { - "displayName": "WhatsApp Notifier", - "category": "integrations", - "pricing": { - "type": "free" - }, - "permissions": [ - "notifications.send", - "users.read" - ] - } -} -``` - -### Configuraci贸n de Extensi贸n - -```typescript -// extension.config.ts -import { ExtensionConfig } from '@erp-construccion/extension-sdk'; - -export default { - id: 'whatsapp-notifier', - name: 'WhatsApp Notifier', - version: '1.0.0', - author: { - name: 'Mi Empresa', - email: 'soporte@mi-empresa.com', - website: 'https://mi-empresa.com' - }, - description: 'Env铆a notificaciones autom谩ticas por WhatsApp', - icon: 'https://cdn.mi-empresa.com/whatsapp-icon.png', - - // Compatibilidad - minPlatformVersion: '2.0.0', - maxPlatformVersion: '3.0.0', - - // Pricing - pricing: { - type: 'free', // 'free' | 'one_time' | 'subscription' - amount: 0, - currency: 'USD', - trial: false - }, - - // Permisos requeridos - permissions: [ - 'notifications.send', - 'users.read', - 'projects.read' - ], - - // Configuraci贸n - settings: [ - { - key: 'whatsapp_api_key', - type: 'string', - required: true, - secure: true, - label: 'WhatsApp API Key', - description: 'API key de WhatsApp Business' - }, - { - key: 'default_template', - type: 'select', - options: ['template1', 'template2'], - default: 'template1', - label: 'Plantilla por defecto' - } - ] -} as ExtensionConfig; -``` - ---- - -## 馃摎 API del SDK - -### 1. Hooks (Eventos) - -```typescript -import { Extension, Hook } from '@erp-construccion/extension-sdk'; - -@Extension({ - id: 'whatsapp-notifier', - name: 'WhatsApp Notifier' -}) -export class WhatsAppNotifierExtension { - - // Hook: Cuando se crea una estimaci贸n - @Hook('estimations.created') - async onEstimationCreated(estimation: Estimation) { - const constructora = this.context.constructora; - const users = await this.api.users.findByRole('finance'); - - for (const user of users) { - if (user.phone && user.notificationsEnabled) { - await this.sendWhatsApp(user.phone, { - template: 'estimation_created', - params: { - estimationNumber: estimation.number, - amount: estimation.amount, - project: estimation.project.name - } - }); - } - } - } - - // Hook: Cuando se aprueba un presupuesto - @Hook('budgets.approved') - async onBudgetApproved(budget: Budget) { - // L贸gica custom - } - - // Hook: Cuando un proyecto excede presupuesto - @Hook('projects.budget_exceeded') - async onBudgetExceeded(project: Project, overrun: number) { - // Alertar al director de proyecto - const director = await this.api.users.findById(project.directorId); - - await this.sendWhatsApp(director.phone, { - template: 'budget_alert', - params: { - projectName: project.name, - overrunPercentage: (overrun * 100).toFixed(2) - } - }); - } -} -``` - -**Hooks disponibles:** - -```typescript -// Proyectos -'projects.created' -'projects.updated' -'projects.deleted' -'projects.budget_exceeded' -'projects.milestone_reached' - -// Presupuestos -'budgets.created' -'budgets.approved' -'budgets.rejected' - -// Compras -'purchases.order_created' -'purchases.order_approved' -'purchases.goods_received' - -// Estimaciones -'estimations.created' -'estimations.approved' -'estimations.paid' - -// Control de Obra -'progress.updated' -'progress.milestone_completed' - -// RRHH -'employees.hired' -'employees.terminated' -'attendance.checked_in' -'attendance.anomaly_detected' - -// HSE -'incidents.registered' -'incidents.investigated' -'risks.predicted' // IA -'patterns.detected' // IA - -// Calidad -'defects.reported' -'defects.resolved' - -// Finanzas -'accounting.entry_created' -'payments.made' -'invoices.received' - -// General -'notifications.sent' -'reports.generated' -``` - ---- - -### 2. API de Datos - -```typescript -// Acceso a datos de la constructora -import { API } from '@erp-construccion/extension-sdk'; - -export class MiExtension { - - async getMiData() { - const api = this.context.api; - - // Proyectos - const projects = await api.projects.findAll({ - status: 'active', - limit: 10 - }); - - // Presupuestos - const budget = await api.budgets.findById('budget-123'); - - // Usuarios - const users = await api.users.findByRole('engineer'); - - // Custom queries - const result = await api.query(` - SELECT p.name, SUM(b.amount) as total - FROM projects p - JOIN budgets b ON b.project_id = p.id - WHERE p.status = 'active' - GROUP BY p.id - `); - - return result; - } - - // Crear datos - async createProject() { - const project = await this.context.api.projects.create({ - name: 'Nuevo Proyecto', - code: 'PRJ-2025-001', - startDate: '2025-01-01', - endDate: '2025-12-31' - }); - - return project; - } - - // Actualizar datos - async updateProject(id: string) { - const project = await this.context.api.projects.update(id, { - status: 'completed' - }); - - return project; - } -} -``` - ---- - -### 3. UI Components (React) - -```typescript -import { Component, MenuItem } from '@erp-construccion/extension-sdk'; -import { useState } from 'react'; - -// Agregar 铆tem al men煤 lateral -@MenuItem({ - section: 'settings', - label: 'Configurar WhatsApp', - icon: 'whatsapp', - route: '/settings/whatsapp' -}) -export function WhatsAppSettingsPage() { - const [apiKey, setApiKey] = useState(''); - - const handleSave = async () => { - await context.settings.save({ whatsapp_api_key: apiKey }); - }; - - return ( -
-

Configuraci贸n de WhatsApp

- setApiKey(e.target.value)} - placeholder="API Key" - /> - -
- ); -} - -// Agregar widget al dashboard -@Component({ - type: 'dashboard-widget', - title: 'Notificaciones WhatsApp Enviadas', - defaultSize: { w: 2, h: 1 } -}) -export function WhatsAppStatsWidget() { - const [stats, setStats] = useState({ sent: 0, delivered: 0, failed: 0 }); - - useEffect(() => { - // Cargar estad铆sticas - const loadStats = async () => { - const data = await context.api.custom('/whatsapp/stats'); - setStats(data); - }; - loadStats(); - }, []); - - return ( -
-

WhatsApp Stats (脷ltimos 7 d铆as)

-
Enviados: {stats.sent}
-
Entregados: {stats.delivered}
-
Fallidos: {stats.failed}
-
- ); -} - -// Agregar bot贸n en p谩gina de estimaciones -@Component({ - type: 'action-button', - page: 'estimations.detail', - label: 'Enviar por WhatsApp', - icon: 'send' -}) -export function SendEstimationButton({ estimation }: { estimation: Estimation }) { - const handleSend = async () => { - // Enviar estimaci贸n por WhatsApp - await sendEstimationViaWhatsApp(estimation); - }; - - return ; -} -``` - ---- - -### 4. Servicios y Utilidades - -```typescript -import { Service, injectable } from '@erp-construccion/extension-sdk'; - -@Service() -export class WhatsAppService { - - async sendMessage(phone: string, message: string) { - const apiKey = await this.context.settings.get('whatsapp_api_key'); - - const response = await fetch('https://api.whatsapp.com/send', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - to: phone, - message: message - }) - }); - - if (!response.ok) { - throw new Error('Failed to send WhatsApp message'); - } - - // Registrar en audit log - await this.context.audit.log({ - action: 'whatsapp.message_sent', - details: { phone, message } - }); - - return response.json(); - } - - async sendTemplate(phone: string, templateId: string, params: any) { - // Implementaci贸n de template - } -} -``` - ---- - -### 5. Storage (Persistencia) - -```typescript -// Cada extensi贸n tiene su propio almacenamiento aislado por constructora -import { Storage } from '@erp-construccion/extension-sdk'; - -export class MiExtension { - - async guardarConfig() { - // Guardar en storage de la extensi贸n - await this.context.storage.set('mi_config', { - apiKey: 'xxx', - enabled: true, - lastSync: new Date() - }); - } - - async cargarConfig() { - const config = await this.context.storage.get('mi_config'); - return config; - } - - async eliminarConfig() { - await this.context.storage.delete('mi_config'); - } - - // Storage de archivos - async subirArchivo(file: File) { - const url = await this.context.storage.uploadFile(file, { - folder: 'whatsapp-attachments', - maxSize: 10 * 1024 * 1024, // 10 MB - allowedTypes: ['image/png', 'image/jpeg', 'application/pdf'] - }); - - return url; - } -} -``` - ---- - -### 6. Notificaciones - -```typescript -import { Notifications } from '@erp-construccion/extension-sdk'; - -export class MiExtension { - - async enviarNotificacion() { - // Notificaci贸n in-app - await this.context.notifications.send({ - userId: 'user-123', - title: 'Estimaci贸n Aprobada', - message: 'La estimaci贸n #EST-2025-001 ha sido aprobada', - type: 'success', - link: '/estimations/EST-2025-001' - }); - - // Email - await this.context.notifications.sendEmail({ - to: 'user@example.com', - subject: 'Estimaci贸n Aprobada', - template: 'estimation_approved', - data: { - estimationNumber: 'EST-2025-001', - amount: '$250,000 USD' - } - }); - - // SMS - await this.context.notifications.sendSMS({ - phone: '+52 442 123 4567', - message: 'Estimaci贸n EST-2025-001 aprobada por $250,000 USD' - }); - } -} -``` - ---- - -### 7. Scheduled Jobs (Tareas Programadas) - -```typescript -import { Job, Schedule } from '@erp-construccion/extension-sdk'; - -@Job() -export class SyncJob { - - // Ejecutar cada hora - @Schedule('0 * * * *') - async syncData() { - console.log('Sincronizando datos con sistema externo...'); - - // Obtener datos pendientes - const pending = await this.context.storage.get('pending_sync'); - - // Sincronizar - for (const item of pending) { - await this.syncItem(item); - } - - // Limpiar - await this.context.storage.delete('pending_sync'); - } - - // Ejecutar diariamente a las 2 AM - @Schedule('0 2 * * *') - async dailyReport() { - // Generar reporte diario - const report = await this.generateReport(); - - // Enviar por email - await this.context.notifications.sendEmail({ - to: 'admin@company.com', - subject: 'Reporte Diario WhatsApp', - body: report - }); - } - - private async syncItem(item: any) { - // Implementaci贸n - } - - private async generateReport() { - // Implementaci贸n - return 'Reporte...'; - } -} -``` - ---- - -## 馃摝 Publicaci贸n en el Marketplace - -### 1. Desarrollo Local - -```bash -# Clonar template -git clone https://github.com/erp-construccion/extension-template -cd extension-template - -# Instalar dependencias -npm install - -# Desarrollo en modo watch -npm run dev - -# Testing -npm test - -# Build para producci贸n -npm run build -``` - -### 2. Testing en Constructora de Prueba - -```bash -# Subir extensi贸n a constructora de prueba -npm run deploy --constructora=my-test-constructora - -# Ver logs en tiempo real -npm run logs --constructora=my-test-constructora -``` - -### 3. Validaci贸n y Certificaci贸n - -**Checklist de validaci贸n:** - -- [ ] 鉁 Todos los tests pasan (coverage 鈮80%) -- [ ] 鉁 Sin vulnerabilidades de seguridad (npm audit) -- [ ] 鉁 Documentaci贸n completa (README.md) -- [ ] 鉁 Screenshots/demo video -- [ ] 鉁 Pricing definido -- [ ] 鉁 Soporte definido (email, docs, SLA) -- [ ] 鉁 Compatible con versiones de plataforma -- [ ] 鉁 No usa APIs privadas/no documentadas -- [ ] 鉁 Maneja errores gracefully -- [ ] 鉁 Logs apropiados (no spam) -- [ ] 鉁 Performance aceptable (no bloquea UI) - -**Revisi贸n del equipo:** -- Seguridad: Audit de c贸digo (SAST/DAST) -- Compliance: Verificaci贸n de licencias -- UX: Revisi贸n de interfaz -- Performance: Load testing - -### 4. Publicar al Marketplace - -```bash -# Login con credenciales de desarrollador -npm run marketplace:login - -# Publicar (primera vez) -npm run marketplace:publish - -# Actualizar versi贸n existente -npm run marketplace:publish --version=1.1.0 --changelog="Bug fixes" -``` - -**Formulario de publicaci贸n:** -```yaml -name: "WhatsApp Notifier" -description: "Env铆a notificaciones autom谩ticas por WhatsApp Business API" -category: "integrations" -version: "1.0.0" -author: "Mi Empresa" -support_email: "soporte@mi-empresa.com" -support_url: "https://docs.mi-empresa.com/whatsapp" -documentation_url: "https://docs.mi-empresa.com/whatsapp" -privacy_policy_url: "https://mi-empresa.com/privacy" -terms_url: "https://mi-empresa.com/terms" - -pricing: - type: "free" - -screenshots: - - url: "https://cdn.mi-empresa.com/screenshot1.png" - caption: "Dashboard principal" - - url: "https://cdn.mi-empresa.com/screenshot2.png" - caption: "Configuraci贸n" - -demo_video: "https://youtube.com/watch?v=xxx" - -keywords: - - whatsapp - - notifications - - messaging - -compatible_plans: - - professional - - enterprise - -permissions: - - notifications.send - - users.read - -changelog: | - ## v1.0.0 (2025-11-17) - - Initial release - - WhatsApp Business API integration - - Templates for estimations and budgets -``` - ---- - -## 馃挵 Modelos de Monetizaci贸n - -### 1. Gratis - -```typescript -pricing: { - type: 'free' -} -``` - -**Casos de uso:** -- Extensiones open-source -- Marketing (lead generation) -- Complemento de servicio principal - ---- - -### 2. Pago 脷nico - -```typescript -pricing: { - type: 'one_time', - amount: 49, - currency: 'USD' -} -``` - -**Casos de uso:** -- Templates -- Reportes est谩ticos -- Herramientas simples - ---- - -### 3. Suscripci贸n Mensual - -```typescript -pricing: { - type: 'subscription', - amount: 99, - currency: 'USD', - interval: 'month', - trial: { - enabled: true, - days: 14 - } -} -``` - -**Casos de uso:** -- Integraciones -- M贸dulos con mantenimiento -- Servicios externos (APIs) - ---- - -### 4. Basado en Uso - -```typescript -pricing: { - type: 'usage_based', - base: 29, // Base mensual - tiers: [ - { upTo: 1000, pricePerUnit: 0.05 }, // $0.05 por notificaci贸n - { upTo: 10000, pricePerUnit: 0.03 }, - { upTo: null, pricePerUnit: 0.01 } // Ilimitado a $0.01 - ], - unit: 'notification' -} -``` - -**Casos de uso:** -- SMS/WhatsApp -- APIs externas con costo -- Servicios de IA/ML - ---- - -### 5. Freemium - -```typescript -pricing: { - type: 'freemium', - free: { - limits: { - notifications: 100, // 100 notificaciones/mes gratis - users: 5 - } - }, - pro: { - amount: 49, - currency: 'USD', - interval: 'month', - limits: { - notifications: 10000, - users: null // Ilimitado - } - } -} -``` - ---- - -## 馃敀 Seguridad de Extensiones - -### Sandboxing - -Las extensiones corren en un entorno aislado: - -```typescript -// Restricciones autom谩ticas: -- No pueden acceder directamente a la base de datos -- Solo pueden usar APIs expuestas por el SDK -- No pueden ejecutar c贸digo nativo -- Timeouts autom谩ticos (30s por request) -- Rate limiting por constructora -- Memory limits (512 MB por extensi贸n) -``` - -### Permisos Granulares - -```typescript -permissions: [ - 'projects.read', // Leer proyectos - 'projects.write', // Crear/editar proyectos - 'budgets.read', - 'users.read', - 'notifications.send', - 'files.upload', - 'webhooks.create' -] -``` - -**La constructora debe aprobar los permisos al instalar la extensi贸n.** - -### Audit Logging - -Todas las acciones de extensiones se registran: - -```typescript -await this.context.audit.log({ - action: 'extension.data_accessed', - extension: 'whatsapp-notifier', - resource: 'projects', - resourceId: 'proj-123', - details: { action: 'read' } -}); -``` - ---- - -## 馃搳 Analytics para Desarrolladores - -Dashboard de m茅tricas de tu extensi贸n: - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 WhatsApp Notifier - Analytics 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃搳 Instalaciones 鈹 -鈹 Total: 234 tenants 鈹 -鈹 Activos: 198 (84.6%) 鈹 -鈹 Trial: 28 (12%) 鈹 -鈹 Cancelados: 8 (3.4%) 鈹 -鈹 鈹 -鈹 馃挵 Revenue (MRR) 鈹 -鈹 $0 (extensi贸n gratuita) 鈹 -鈹 鈹 -鈹 猸 Ratings 鈹 -鈹 Promedio: 4.7/5.0 (89 reviews) 鈹 -鈹 鈹 -鈹 馃搱 Uso 鈹 -鈹 Notificaciones enviadas (30 d铆as): 45,678 鈹 -鈹 Promedio por tenant: 230/mes 鈹 -鈹 鈹 -鈹 馃悰 Errores (煤ltimos 7 d铆as) 鈹 -鈹 Total: 12 (0.026% error rate) 鈹 -鈹 Por tipo: 鈹 -鈹 - API timeout: 8 鈹 -鈹 - Invalid phone: 3 鈹 -鈹 - Rate limit: 1 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 馃幆 Casos de 脡xito - -### Ejemplo 1: SAP Connector (Partner Oficial) - -**Desarrollador:** SAP Integration Partners -**Tipo:** Integraci贸n -**Pricing:** $99/mes -**Instalaciones:** 45 tenants enterprise -**MRR:** $4,455 - -**Descripci贸n:** -Conecta el ERP de construcci贸n con SAP S/4HANA para sincronizar p贸lizas contables, cuentas por pagar/cobrar y datos maestros. - -**Features:** -- Sincronizaci贸n bidireccional autom谩tica -- Mapping personalizable de cuentas -- Logs de sincronizaci贸n -- Soporte 24/7 - ---- - -### Ejemplo 2: Reporte INFONAVIT (Comunidad) - -**Desarrollador:** Juan P茅rez (independiente) -**Tipo:** Reporte custom -**Pricing:** $49 (one-time) -**Ventas:** 127 instalaciones -**Revenue total:** $6,223 - -**Descripci贸n:** -Genera el reporte oficial de INFONAVIT en formato EVC actualizado 2025, con todos los campos requeridos pre-llenados desde los datos del sistema. - ---- - -### Ejemplo 3: M贸dulo Obra Civil (Partner Certificado) - -**Desarrollador:** CivilTech Solutions -**Tipo:** M贸dulo vertical -**Pricing:** $299/mes -**Instalaciones:** 12 tenants -**MRR:** $3,588 - -**Descripci贸n:** -M贸dulo especializado para constructoras de obra civil pesada (puentes, carreteras, presas) con funcionalidad espec铆fica de control de acarreos, laboratorio de suelos, y reportes para SCT. - ---- - -## 馃殌 Roadmap del SDK - -**Q1 2026:** -- 鉁 SDK v2.0 con TypeScript full support -- 鉁 Webhooks mejorados (retry exponential backoff) -- 鉁 GraphQL API (adem谩s de REST) - -**Q2 2026:** -- 馃搵 Mobile SDK (React Native components) -- 馃搵 Extension templates (quickstart) -- 馃搵 CLI tool mejorado - -**Q3 2026:** -- 馃搵 Serverless functions (AWS Lambda integration) -- 馃搵 Real-time data subscriptions (WebSockets) -- 馃搵 AI/ML integration (TensorFlow.js) - ---- - -## 馃摎 Recursos - -**Documentaci贸n:** -- [SDK Reference](https://docs.erp-construccion.com/sdk) -- [API Reference](https://docs.erp-construccion.com/api) -- [Extension Examples](https://github.com/erp-construccion/extension-examples) -- [Best Practices](https://docs.erp-construccion.com/best-practices) - -**Comunidad:** -- [Discord](https://discord.gg/erp-construccion) -- [Stack Overflow](https://stackoverflow.com/questions/tagged/erp-construccion) -- [GitHub Discussions](https://github.com/erp-construccion/sdk/discussions) - -**Soporte:** -- Email: developers@erp-construccion.com -- Office Hours: Viernes 10am-12pm PST (Zoom) - ---- - -**Generado:** 2025-11-17 -**SDK Versi贸n:** 1.0.0 -**Modelo:** SaaS Multi-tenant Marketplace diff --git a/projects/erp-construccion/docs/00-vision-general/MVP-APP.md b/projects/erp-construccion/docs/00-vision-general/MVP-APP.md deleted file mode 100644 index 3593278f3..000000000 --- a/projects/erp-construccion/docs/00-vision-general/MVP-APP.md +++ /dev/null @@ -1,1592 +0,0 @@ -# MVP Sistema de Administraci贸n de Obra e INFONAVIT 鈥 Definici贸n de M贸dulos, Planes y Arquitectura (Backend: Node.js + Express, Frontend: React + Vite Web + React Native App) - -> Versi贸n: 2.0 SaaS Multi-tenant 路 Fecha: 2025-11-17 路 Autor: Adri谩n / Strategos AI -> Stack: Node.js + Express + TypeScript 路 React + Vite 路 PostgreSQL -> Modelo: SaaS Multi-tenant B2B 路 Compatible con ecosistema GAMILIT - -## 0) Resumen ejecutivo - -**Plataforma SaaS ERP de construcci贸n enterprise-ready** para constructoras de vivienda en serie que desarrollan conjuntos habitacionales (fraccionamientos, privadas, edificios) y participan en licitaciones y programas con INFONAVIT. - -### Modelo de Negocio: SaaS Multi-tenant B2B - -El sistema opera como **plataforma SaaS** donde: - -鉁 **M煤ltiples empresas constructoras** comparten la misma infraestructura (multi-tenant) -鉁 **M贸dulos activables** seg煤n plan de suscripci贸n (B谩sico/Profesional/Enterprise) -鉁 **Portal de administraci贸n** para gestionar configuraciones, usuarios y m贸dulos -鉁 **Onboarding automatizado** en 5 minutos vs semanas de implementaci贸n tradicional -鉁 **Marketplace de extensiones** para customizaciones sin modificar el core -鉁 **Pricing por uso** con facturaci贸n autom谩tica mensual/anual - -**Diferenciador clave:** Similar a SAP Cloud o Salesforce, pero especializado en construcci贸n. - -El sistema integra **18 m贸dulos funcionales** activables por cliente, que cubren el ciclo completo desde preconstrucci贸n hasta postventa, con capacidades comparables a ERPs l铆deres (SAP S/4HANA Construction, Procore, Autodesk Construction Cloud) pero con arquitectura moderna SaaS, onboarding r谩pido y menor TCO. - -### Alcance funcional - -**M贸dulos core (MVP Fase 1 - 6 semanas):** -* Preconstrucci贸n y licitaciones (pipeline de oportunidades). -* Proyectos, obras y estructura de fraccionamientos. -* Presupuestos, costos y control de desviaciones. -* Compras, inventarios y almacenes de obra. -* Contratos, subcontratos y estimaciones. -* Control de avances con evidencia fotogr谩fica y curva S. -* CRM de derechohabientes e INFONAVIT b谩sico. -* Reportes ejecutivos y BI. - -**M贸dulos enterprise (Fases 2-3 - 12 semanas adicionales):** -* **Finanzas y Controlling**: Libro mayor, cuentas por pagar/cobrar, flujo de efectivo, integraci贸n contable. -* **Activos y Maquinaria**: Gesti贸n de maquinaria pesada, mantenimiento preventivo/correctivo, costeo TCO. -* **Gesti贸n Documental (DMS)**: Repositorio centralizado, versionado de planos, flujos de aprobaci贸n. -* **RRHH Avanzado**: Time & Attendance con GPS + biom茅trico, control anti-fraude, multi-sitio. -* **Seguridad y HSE**: Matriz de riesgos, incidentes, cumplimiento normativo, **IA predictiva de riesgos**. -* **Postventa y garant铆as**: Gesti贸n completa de post-entrega. - -### Stack tecnol贸gico (compatible con ecosistema GAMILIT) - -* **Backend**: Node.js 20+ + Express + TypeScript. -* **Frontend oficina**: React 18 + Vite + TypeScript. -* **Frontend campo**: App m贸vil/tablet (React Native + TypeScript) para supervisores y residentes de obra. -* **Base de datos**: PostgreSQL 15+ con schemas modulares (arquitectura inspirada en GAMILIT). -* **IA y automatizaci贸n**: - * Alertas de desviaciones costo/tiempo. - * Modelos predictivos de riesgo de retraso/sobrecosto. - * **IA para detecci贸n de patrones de riesgo de seguridad** (diferenciador clave). - * WhatsApp Business + agente IA (MCP) para captura de avances, incidencias y consultas. - -### Ventajas competitivas - -1. **Reutilizaci贸n de ecosistema GAMILIT**: Reducci贸n ~40% en tiempo de desarrollo, c贸digo probado en producci贸n. -2. **Arquitectura moderna**: Node.js + React vs. tecnolog铆as legacy de ERPs tradicionales. -3. **IA nativa**: No es add-on, est谩 integrada en el core (HSE predictivo, alertas inteligentes). -4. **Time & Attendance de clase mundial**: GPS + biom茅trico integrado nativamente (competidores cobran esto separado). -5. **Mobile-first para campo**: App React Native completa, no solo "vista m贸vil" del web. -6. **TCO menor**: Stack open-source, sin licencias per-seat costosas, deployment flexible (cloud/on-prem). - -### Comparaci贸n con ERPs de mercado - -| Caracter铆stica | MVP-APP | SAP S/4HANA | Procore | Autodesk | -|----------------|---------|-------------|---------|----------| -| **Modelo** | **SaaS Multi-tenant** | On-Premise / Cloud | SaaS | SaaS | -| **Onboarding** | **5 minutos** | 6-12 meses | 2-4 semanas | 2-4 semanas | -| **M贸dulos activables** | 鉁 **Din谩mico** | 鉂 Todo o nada | 鈿狅笍 Limitado | 鈿狅笍 Limitado | -| **Marketplace** | 鉁 **S铆** | 鈿狅笍 Limitado | 鉂 No | 鈿狅笍 Limitado | -| **Pricing** | **$399-$1,499/mes** | $50K-$200K inicial + $5K/mes | $500-$2K/mes | $600-$1.5K/mes | -| **Personalizaci贸n** | 鉁 Extensiones SDK | 鈿狅笍 Consultores | 鉂 Limitado | 鉂 Limitado | -| Finanzas integradas | 鉁 Fase 2 | 鉁 Core | 鉂 Limitado | 鉂 No | -| Gesti贸n de activos | 鉁 Fase 2 | 鉁 Core | 鈿狅笍 B谩sico | 鈿狅笍 B谩sico | -| DMS con versionado | 鉁 Fase 2 | 鉁 Core | 鉁 Core | 鉁 Core | -| HSE + IA predictiva | 鉁 **Diferenciador** | 鉂 No IA | 鈿狅笍 Sin IA | 鉂 No | -| Time & Attendance GPS+Bio | 鉁 Integrado | 鈿狅笍 M贸dulo aparte | 鈿狅笍 Integraci贸n 3rd | 鉂 No | -| WhatsApp + IA agent | 鉁 **脷nico** | 鉂 No | 鉂 No | 鉂 No | -| Stack tecnol贸gico | Moderno (Node+React) | Legacy | Mixto | Mixto | - -### Roadmap y tiempos - -**Fase 1: MVP SaaS (Semanas 1-8)** -- Arquitectura multi-tenant implementada -- Portal de administraci贸n b谩sico -- Onboarding automatizado -- 6 m贸dulos core activables -- Pricing y billing automatizado -- **Entregable**: Primeros 10 clientes piloto - -**Fase 2: Enterprise Features (Semanas 9-16)** -- 12 m贸dulos adicionales (total 18) -- M贸dulos activables din谩micamente -- Marketplace MVP (5 extensiones) -- SDK para extensiones -- Custom domains -- **Entregable**: 50 clientes activos, $40K MRR - -**Fase 3: Scale & Growth (Semanas 17-24)** -- IA predictiva (HSE) -- Analytics avanzado por tenant -- Integraciones nativas (SAP, WhatsApp, CONTPAQi) -- Mobile app completa (React Native) -- API p煤blica para partners -- **Entregable**: 200 clientes, $150K MRR - -**Fase 4: Expansi贸n (Semanas 25-36)** -- Marketplace p煤blico (50+ extensiones) -- White-label para partners -- Internacionalizaci贸n (US, Colombia, Chile) -- Cumplimiento (SOC2, ISO 27001) -- Capacidades multi-region -- **Entregable**: 500 clientes, $400K MRR - -**vs. Desarrollo desde cero**: 30-35 semanas (ahorro ~40% gracias a reutilizaci贸n GAMILIT). -**vs. Desarrollo a medida tradicional**: Imposible escalar, cada cliente requiere deployment separado. - -### Ventajas competitivas del modelo SaaS - -1. **Time-to-value**: Cliente productivo en 5 minutos vs semanas de implementaci贸n. -2. **Pricing flexible**: Paga solo por lo que usa, puede upgradear/downgrear mensualmente. -3. **Actualizaciones autom谩ticas**: Nuevas features sin costo adicional ni downtime. -4. **Escalabilidad instant谩nea**: Agregar usuarios/m贸dulos en segundos. -5. **TCO menor**: No hay costos de infraestructura, mantenimiento, actualizaciones. -6. **Innovaci贸n continua**: Releases semanales con nuevas features y mejoras. -7. **Ecosystem**: Marketplace con extensiones de partners y comunidad. -8. **Multi-device**: Acceso desde web, m贸vil, tablet sin instalaciones. -9. **Seguridad enterprise**: Backups autom谩ticos, disaster recovery, uptime 99.9%. -10. **Soporte incluido**: Seg煤n plan, desde email 48h hasta dedicado 1h. - -### Comparaci贸n: SaaS vs On-Premise vs Desarrollo a Medida - -| Aspecto | SaaS (Este Sistema) | On-Premise | Desarrollo a Medida | -|---------|---------------------|------------|---------------------| -| **Time-to-market** | 5 minutos | 3-6 meses | 12-18 meses | -| **Costo inicial** | $0 (solo suscripci贸n) | $50K-$200K | $200K-$500K | -| **Costo mensual** | $399-$1,499/mes | $5K-$10K/mes (infra + soporte) | $10K-$20K/mes | -| **Actualizaciones** | Autom谩ticas, gratis | Manual, costosas | Manual, muy costosas | -| **Escalabilidad** | Instant谩nea | Limitada (hardware) | Muy limitada | -| **Personalizaci贸n** | Extensiones + config | Modificar core | Total | -| **Mantenimiento** | Incluido | Cliente responsable | Cliente responsable | -| **Uptime** | 99.9% (SLA) | Variable | Variable | -| **Soporte** | Incluido 24/7 | No incluido | No incluido | -| **ROI** | 3-6 meses | 18-24 meses | 24-36 meses | - ---- - -## 0.1) Arquitectura SaaS Multi-tenant - -> Ver documentaci贸n completa en [ARQUITECTURA-SAAS.md](./ARQUITECTURA-SAAS.md) - -### Modelo Multi-tenant - -**Un solo c贸digo base, m煤ltiples clientes aislados:** - -``` -PostgreSQL Database -鈹溾攢鈹 tenant_constructora_abc (Schema) -鈹 鈹溾攢鈹 projects -鈹 鈹溾攢鈹 budgets -鈹 鈹溾攢鈹 purchases -鈹 鈹斺攢鈹 ... -鈹溾攢鈹 tenant_viviendas_xyz (Schema) -鈹 鈹溾攢鈹 projects -鈹 鈹溾攢鈹 budgets -鈹 鈹斺攢鈹 ... -鈹斺攢鈹 tenant_obras_norte (Schema) - 鈹斺攢鈹 ... -``` - -**Beneficios:** -- **Aislamiento fuerte**: Datos f铆sicamente separados por schema -- **Escalabilidad**: De 10 a 10,000 tenants sin cambios arquitect贸nicos -- **Seguridad**: Imposible acceso cross-tenant -- **Performance**: No hay degradaci贸n con m谩s tenants - -### Portal de Administraci贸n SaaS - -Dashboard central para gestionar: -- **Tenants**: Alta, configuraci贸n, suspensi贸n, cancelaci贸n -- **M贸dulos**: Activaci贸n/desactivaci贸n din谩mica por tenant -- **Usuarios**: Gesti贸n de usuarios y roles por empresa -- **Facturaci贸n**: Generaci贸n autom谩tica de facturas, seguimiento de pagos -- **M茅tricas**: MRR, churn, activaci贸n, uso por m贸dulo - -### Onboarding Automatizado (5 minutos) - -1. **Registro** (2 min): Datos de empresa, admin, subdominio -2. **Selecci贸n de plan** (1 min): B谩sico / Profesional / Enterprise -3. **Configuraci贸n de m贸dulos** (1 min): Activar m贸dulos deseados -4. **Provisioning autom谩tico** (<1 min): Schema creado, migraciones ejecutadas -5. **Primer login** (Inmediato): Sistema listo para usar - -### Planes y Pricing - -| Plan | Precio/mes | Usuarios | M贸dulos | Soporte | -|------|------------|----------|---------|---------| -| **B谩sico** | $399 USD | 10 | 6 core | Email 48h | -| **Profesional** | $799 USD | 25 | 12 m贸dulos | Chat 24h | -| **Enterprise** | $1,499 USD | 100 | Todos (18) | Dedicado 4h | - -**Add-ons disponibles:** -- M贸dulos adicionales: $50-$300/mes seg煤n complejidad -- Usuarios extra: $10-$20/usuario/mes seg煤n plan -- Almacenamiento: $2/GB/mes adicional - -**Costo de Contrataci贸n Inicial (One-Time):** - -Adem谩s de la suscripci贸n mensual, existe un costo 煤nico de implementaci贸n que cubre migraci贸n de datos, capacitaci贸n, adaptaci贸n al negocio e implementaciones de configuraci贸n: - -| Paquete | Precio | Usuarios | Registros | Ideal para | -|---------|--------|----------|-----------|------------| -| **Starter** | $2,500 USD | <10 | <5,000 | Empresas peque帽as | -| **Profesional** | $7,500 USD | 10-50 | <50,000 | Empresas medianas | -| **Enterprise** | $15,000 USD | 50-100 | <200,000 | Constructoras grandes | -| **Enterprise Plus** | Custom | 100+ | Ilimitado | Corporativos | - -> Ver detalles completos en [ARQUITECTURA-SAAS.md - Costos de Contrataci贸n Inicial](./ARQUITECTURA-SAAS.md#costos-de-contrataci贸n-inicial-one-time) - -### Marketplace de Extensiones - -Ecosistema de extensiones desarrolladas por: -- **Equipo interno**: Integraciones oficiales (SAP, WhatsApp, etc.) -- **Partners certificados**: M贸dulos verticales especializados -- **Clientes**: Custom workflows y reportes propios - -**Tipos de extensiones:** -- Integraciones (APIs externas) -- Reportes custom -- M贸dulos verticales (ej: Obra Civil Pesada) -- Workflows personalizados -- Dashboards tem谩ticos - -### Personalizaci贸n sin Modificar Core - -**Configuraci贸n (90% de casos):** -- Cat谩logos personalizados -- Workflows de aprobaci贸n -- Plantillas de documentos -- Campos custom (metadata JSON) -- Reglas de negocio (rule engine) - -**Extensiones (10% de casos):** -- SDK de desarrollo disponible -- Hooks en puntos clave del sistema -- API completa para integraciones -- Deploy aislado por tenant - ---- - -## 1) Alcance del MVP - -**Objetivo:** -Tener control trazable y en tiempo casi real de: - -* Obras y etapas. -* Presupuestos y costos. -* Compras, almacenes e inventarios de obra. -* Contratos/subcontratos y estimaciones. -* Avances f铆sicos (con evidencia). -* RRHH de obra (asistencias, cuadrillas, n贸mina). -* Postventa y garant铆as. -* Cumplimiento b谩sico con INFONAVIT (evidencias, checklists, avances). - -**Plataformas:** - -* Web para administraci贸n/finanzas/ingenier铆a. -* App m贸vil/tablet para residentes de obra, supervisores y cuadrillas. - -**IA (fase inicial):** - -* Alertas de desviaciones costo/tiempo. -* Recordatorios de hitos (estimaciones, visitas, licencias por vencer). -* B煤squeda inteligente de documentos y planos. - -**WhatsApp Business:** - -* Canal para que residentes y supervisores puedan reportar avances, incidencias y fotos. -* Consultas r谩pidas: "estatus lote 23", "avance etapa 1", "qu茅 falta para estimaci贸n 3", etc. - ---- - -## 1A) Cat谩logo de m贸dulos y funciones (versi贸n ejecutiva) - -> Esta secci贸n define **qu茅 debe existir**. Servir谩 para detallar especificaciones, APIs y datos en documentos posteriores. - -### M贸dulo 1 鈥 **Proyectos, Obras y Viviendas** - -Funciones clave: - -* Cat谩logo de proyectos (fraccionamientos, conjuntos). -* Definici贸n de etapas, manzanas, lotes, viviendas/prototipos. -* Asignaci贸n de responsables (director de obra, residente, supervisor). -* Calendario general de obra (hitos clave). - -### M贸dulo 2 鈥 **Presupuestos y Costos de Obra** - -Funciones clave: - -* Presupuesto maestro por obra y por prototipo de vivienda. -* Cat谩logo de conceptos de obra y precios unitarios. -* Matriz de insumos (material, mano de obra, herramienta, maquinaria). -* Presupuesto vs costo real (desviaciones por partida, frente, vivienda). - -### M贸dulo 3 鈥 **Compras y Abastecimiento** - -Funciones clave: - -* Requisiciones desde obra (material, herramienta, servicios). -* 脫rdenes de compra ligadas a conceptos/presupuesto. -* Comparativo de cotizaciones de proveedores. -* Seguimiento de entregas (parcial/completa) y condiciones de pago. - -### M贸dulo 4 鈥 **Inventarios y Almacenes de Obra** - -Funciones clave: - -* Almac茅n general + almacenes por obra. -* Entradas/salidas/traspasos entre obras. -* K谩rdex por material, obra y frente. -* Alertas de m铆nimos y sobreconsumo vs presupuesto. - -### M贸dulo 5 鈥 **Contratos y Subcontratos** - -Funciones clave: - -* Registro de contratos de obra (proveedores, subcontratistas, servicios). -* Control de vol煤menes contratados, precios y alcances. -* 脫rdenes de cambio (obra adicional/modificada). -* Retenciones, garant铆as, penalizaciones. - -### M贸dulo 6 鈥 **Control de Obra y Avances** - -Funciones clave: - -* Captura de avance f铆sico por concepto, frente y/o vivienda. -* Curva S (programado vs ejecutado). -* Evidencias fotogr谩ficas y geolocalizadas. -* Checklists de actividades por etapa (cimentaci贸n, estructura, instalaciones, acabados). - -### M贸dulo 7 鈥 **Estimaciones y Facturaci贸n** - -Funciones clave: - -* Estimaciones hacia el cliente (INFONAVIT/fideicomiso/ente financiero). -* Estimaciones hacia subcontratistas y proveedores. -* Anticipos, amortizaciones, retenciones y garant铆as. -* Generaci贸n de reportes y exportables para revisi贸n y firma. - -### M贸dulo 8 鈥 **Recursos Humanos, Asistencias y N贸mina de Obra** - -Funciones clave: - -* **Cat谩logo de personal**: - * Empleados directos y cuadrillas subcontratadas. - * Clasificaci贸n por oficio (alba帽il, fierrero, plomero, electricista, etc.). - * Datos personales, contacto, IMSS, INFONAVIT. - * Certificaciones y capacitaciones. - * Historial laboral en la empresa. -* **Time & Attendance con GPS + Biom茅trico** (subm贸dulo reforzado): - * **Reloj checador m贸vil**: - * Check-in/Check-out desde app m贸vil con validaci贸n GPS. - * Geofencing: alertas si empleado no est谩 en radio de obra. - * Validaci贸n biom茅trica en dispositivo (huella digital, reconocimiento facial). - * Soporte para check-in offline con sincronizaci贸n posterior. - * **Control multi-sitio**: - * Asistencia simult谩nea en varias obras. - * Transferencia de personal entre frentes/obras en mismo d铆a. - * Dashboard de ubicaci贸n en tiempo real de cuadrillas. - * **Validaciones anti-fraude**: - * Detecci贸n de check-in duplicado o fuera de horario. - * Verificaci贸n de identidad con biometr铆a (evita "checadas por otro"). - * Alertas de patrones an贸malos (ausencias recurrentes, retardos). - * **Jornada y tiempo extra**: - * C谩lculo autom谩tico de horas trabajadas vs horario base. - * Detecci贸n de tiempo extra, jornadas nocturnas, dominicales. - * Reglas configurables por obra, puesto, sindicato. - * Aprobaci贸n de tiempo extra por supervisor. - * **Integraci贸n con costos**: - * Imputaci贸n de horas-hombre a partidas/frentes de obra. - * C谩lculo de costo de mano de obra real vs presupuestado. - * An谩lisis de productividad (avance vs horas invertidas). -* **N贸mina de obra**: - * Costeo de mano de obra por obra/partida. - * Exportaci贸n de datos hacia sistema de n贸mina/IMSS/INFONAVIT (como patr贸n). - * Reporte de incidencias (faltas, retardos, permisos, incapacidades). - * C谩lculo de destajo y bonos por productividad (opcional). - -**Beneficio diferenciador**: Control robusto de asistencias con GPS + biometr铆a similar a soluciones especializadas (Jibble, Infobric), integrado nativamente con costos de obra y productividad. - -### M贸dulo 9 鈥 **Calidad, Postventa y Garant铆as** - -Funciones clave: - -* Control de no conformidades durante la obra (checklists de calidad). -* Registro de incidencias postventa por vivienda/lote. -* Seguimiento de garant铆as (tiempos de respuesta, estatus). -* Historial por vivienda para auditor铆as y reclamaciones. - -### M贸dulo 10 鈥 **CRM de Derechohabientes y Comercializaci贸n** - -Funciones clave: - -* Registro de derechohabientes/prospectos. -* Estatus de cada vivienda: disponible, apartada, vendida, escriturada, entregada. -* Seguimiento de expediente del cr茅dito (documentos, avances, citas). -* Comunicaci贸n por WhatsApp/email con compradores. - -### M贸dulo 11 鈥 **INFONAVIT & Cumplimiento** - -Funciones clave: - -* Registro del proyecto bajo programa espec铆fico de INFONAVIT. -* Checklists de requisitos t茅cnicos, urbanos y de servicios. -* Evidencias (documentos, fotos, actas, visitas). -* Reportes para verificadores/auditores. - -### M贸dulo 12 鈥 **Reportes & BI** - -Funciones clave: - -* Estado de obra por avance f铆sico/financiero. -* Desviaciones costo/tiempo por obra y por etapa. -* Margen por proyecto. -* Reportes de estimaciones, pagos, cartera y flujo de efectivo de obra. - -### M贸dulo 13 鈥 **Administraci贸n & Seguridad** - -Funciones clave: - -* Usuarios/roles/permisos (direcci贸n, ingenier铆a, residente, compras, financiero). -* Centros de costo por obra. -* Bit谩cora de actividades y logs de cambios. -* Backups y restauraci贸n. - -### M贸dulo 14 鈥 **Finanzas y Controlling de Obra** (Fase 2/3) - -> M贸dulo enterprise para competir con ERPs tipo SAP. Permite gesti贸n financiera integrada a nivel proyecto. - -Funciones clave: - -* **Libro mayor y centros de costo**: - * Imputaci贸n por proyecto, obra, etapa y centro de costo. - * Cat谩logo de cuentas contables ligado a conceptos de obra. - * Conciliaci贸n bancaria por proyecto. -* **Cuentas por pagar/cobrar**: - * AP (Accounts Payable) ligadas a 贸rdenes de compra y contratos. - * AR (Accounts Receivable) ligadas a estimaciones y facturaci贸n. - * Aging de cuentas por cobrar/pagar. - * Anticipos y amortizaciones. -* **Flujo de efectivo**: - * Cash flow proyectado vs real por obra. - * An谩lisis de liquidez por periodo. - * Proyecci贸n de necesidades de financiamiento. -* **Integraci贸n contable**: - * Exportaci贸n a sistemas contables externos (SAP, CONTPAQi, etc.). - * P贸lizas contables autom谩ticas desde transacciones de obra. - * Modo "contabilidad ligera" standalone o integraci贸n v铆a API. - -**Beneficio**: Visibilidad financiera completa integrada con operaci贸n de obra, similar a m贸dulo financiero de SAP S/4HANA para construcci贸n. - -### M贸dulo 15 鈥 **Activos, Maquinaria y Mantenimiento** (Fase 2/3) - -> Gesti贸n de activos fijos, maquinaria pesada, equipo y veh铆culos de obra con control de mantenimiento. - -Funciones clave: - -* **Cat谩logo de activos**: - * Maquinaria pesada (excavadoras, gr煤as, revolvedoras, compactadoras). - * Equipo ligero (andamios, vibradores, herramienta especializada). - * Veh铆culos de obra (camiones, camionetas, pick-ups). - * Clasificaci贸n por tipo, marca, modelo, a帽o. - * Datos de adquisici贸n, depreciaci贸n y valor en libros. -* **Control de ubicaci贸n y asignaci贸n**: - * Asignaci贸n de activo a obra/frente espec铆fico. - * Transferencias entre obras. - * Localizaci贸n GPS en tiempo real (opcional con IoT). - * Disponibilidad por periodo (calendario de uso). -* **Mantenimiento preventivo y correctivo**: - * Programaci贸n de mantenimiento por horas de uso, km o fechas. - * 脫rdenes de trabajo de mantenimiento. - * Checklist de revisi贸n por tipo de activo. - * Historial completo de mantenimientos. - * Alertas autom谩ticas de mantenimientos vencidos. -* **Costeo**: - * Costo por hora/d铆a de uso del activo. - * Imputaci贸n a proyectos y partidas. - * Costos de mantenimiento (refacciones + mano de obra). - * An谩lisis de costo total de propiedad (TCO). -* **Integraci贸n**: - * Almac茅n (refacciones y consumibles). - * Compras (贸rdenes de servicio). - * Presupuestos (costos de maquinaria). - -**Beneficio**: Control total de activos costosos, optimizaci贸n de uso, reducci贸n de tiempos muertos, similar a Asset Management en ERPs de construcci贸n modernos. - -### M贸dulo 16 鈥 **Gesti贸n Documental y Planos (DMS)** (Fase 2/3) - -> Sistema de gesti贸n documental con versionado, control de acceso y flujos de aprobaci贸n. - -Funciones clave: - -* **Repositorio centralizado**: - * Planos arquitect贸nicos, estructurales, instalaciones. - * Contratos y convenios modificatorios. - * RFIs (Request for Information). - * Submittals (entregas t茅cnicas). - * Minutas de reuni贸n y actas. - * Manuales de operaci贸n y mantenimiento. - * Certificados, permisos y licencias. -* **Versionado y control de cambios**: - * Control de versiones de planos (rev. A, B, C). - * Comparaci贸n visual entre versiones. - * Trazabilidad de cambios (qui茅n, cu谩ndo, por qu茅). - * Planos vigentes vs obsoletos. -* **Organizaci贸n y clasificaci贸n**: - * Taxonom铆a por proyecto/etapa/disciplina. - * Metadata y tags personalizables. - * B煤squeda avanzada (texto completo, OCR). - * Carpetas virtuales por rol. -* **Control de acceso y permisos**: - * Permisos por documento, carpeta o proyecto. - * Roles: solo lectura, comentarios, aprobaci贸n, edici贸n. - * Watermarks y restricciones de descarga. -* **Flujos de aprobaci贸n**: - * Workflow de revisi贸n y aprobaci贸n de planos. - * Ciclo de vida de documentos: borrador 鈫 revisi贸n 鈫 aprobado 鈫 vigente. - * Notificaciones autom谩ticas a responsables. -* **Integraci贸n con campo**: - * Acceso desde app m贸vil (offline/online). - * Anotaciones y marcas sobre planos en sitio. - * Vinculaci贸n de evidencias fotogr谩ficas a planos. - -**Beneficio**: Elimina caos documental, asegura uso de versiones correctas, cumple auditor铆as y certificaciones, como DMS de Procore o Autodesk Docs. - -### M贸dulo 17 鈥 **Seguridad, Riesgos y HSE** (Fase 3) - -> Health, Safety & Environment. Gesti贸n de seguridad en obra, prevenci贸n de riesgos y cumplimiento normativo. - -Funciones clave: - -* **Registro de incidentes y accidentes**: - * Reporte de incidentes in-situ desde app m贸vil. - * Clasificaci贸n por severidad (leve, grave, fatal). - * Investigaci贸n de causas ra铆z. - * Acciones correctivas y preventivas (CAPA). - * Integraci贸n con RRHH (empleados involucrados). -* **Matriz de riesgos**: - * Identificaci贸n de peligros por actividad/frente. - * Evaluaci贸n de probabilidad e impacto. - * Matriz de riesgos (bajo, medio, alto, cr铆tico). - * Plan de mitigaci贸n por riesgo. - * Re-evaluaci贸n peri贸dica. -* **Checklists de seguridad**: - * Inspecciones de seguridad por 谩rea/actividad. - * Verificaci贸n de EPP (Equipo de Protecci贸n Personal). - * Permisos de trabajo en altura, espacios confinados, etc. - * Checklists de maquinaria y equipo. - * Charlas de seguridad (toolbox talks). -* **Cumplimiento normativo**: - * Normativas NOM-031-STPS (M茅xico), OSHA (EUA). - * Auditor铆as de seguridad programadas. - * Certificaciones y capacitaciones obligatorias. - * Registro de simulacros. -* **Analytics e IA**: - * **Detecci贸n de patrones de riesgo** con IA: - * Por horarios (fatiga, turnos nocturnos). - * Por cuadrillas (historial de incidentes). - * Por frentes de obra (actividades de alto riesgo). - * Por condiciones clim谩ticas. - * Predicci贸n de probabilidad de incidentes. - * Recomendaciones proactivas de reforzamiento. - * Dashboard de KPIs: frecuencia, severidad, d铆as sin accidentes. - -**Beneficio**: Reducci贸n de accidentes, cumplimiento legal, cultura de seguridad, diferenciador competitivo con IA predictiva. - -### M贸dulo 18 鈥 **Preconstrucci贸n y Licitaciones** (Fase 1/2) - -> Elevado desde secci贸n 1C. Gesti贸n del ciclo completo de oportunidades, licitaciones y conversi贸n a proyectos adjudicados. - -Funciones clave: - -* **Pipeline de oportunidades**: - * Registro de licitaciones p煤blicas y privadas. - * Fuentes: portales gubernamentales, clientes directos. - * Clasificaci贸n por tipo, monto, regi贸n. - * Calendario de fechas clave (visita a sitio, junta de aclaraciones, entrega). - * Estatus: en evaluaci贸n, go/no-go, en preparaci贸n, entregada, adjudicada, perdida. -* **Gesti贸n de propuesta**: - * Carga de bases de licitaci贸n y anexos t茅cnicos. - * Cat谩logo de informaci贸n base: terreno, n煤mero de viviendas, prototipos, alcances. - * Presupuesto base para ofertar (versi贸n "ofertado"). - * Colaboraci贸n en equipo para armado de propuesta. - * Generaci贸n de documentos licitatorios. -* **Conversi贸n a proyecto**: - * Una vez adjudicado: convertir propuesta 鈫 proyecto contratado. - * Ajustes entre presupuesto ofertado y contratado. - * Creaci贸n autom谩tica de estructura de proyecto (etapas, manzanas, lotes). - * Transferencia de informaci贸n t茅cnica a m贸dulos operativos. -* **An谩lisis de licitaciones**: - * Tasa de 茅xito (ganadas/perdidas). - * An谩lisis de m谩rgenes ofertados vs reales. - * Competidores recurrentes y estrategias. - * Dashboard de pipeline (valor potencial, probabilidad ponderada). - -**Beneficio**: Visibilidad de cartera de oportunidades, mejora en tasa de ganado, transici贸n suave de preventa a ejecuci贸n, propio de ERPs de construcci贸n enterprise. - ---- - -## 1B) Agente de Obra y Oficina (MCP de alcance total) - -> El agente debe **poder ejecutar las acciones cr铆ticas** de consulta y captura para minimizar fricci贸n, especialmente desde campo. - -Capacidades m铆nimas: - -* **Entrada de texto, voz (audios) e imagen (fotos)**: - - * Transcripci贸n de audios para interpretar instrucciones de residentes/supervisores. - * Lectura de fotos (planos, avances, incidencias) y v铆nculo a obra/etapa/lote. -* **Gesti贸n por chat para usuarios internos**: - - * Registrar avances ("avance de loza en lote 23 al 80%"). - * Reportar incidencias ("filtraci贸n en ba帽o rec谩mara principal lote 12"). - * Consultar estatus ("驴qu茅 falta para cerrar estimaci贸n 4 de obra A?"). - * Crear tareas o checklists ("revisi贸n instalaciones hidrosanitarias etapa 2"). -* **Atenci贸n a derechohabientes (opcional)**: - - * Responder dudas b谩sicas de estatus de vivienda, citas, documentaci贸n faltante. -* **Confirmaciones inteligentes**: - - * Antes de aprobar estimaciones, cerrar etapas o liberar pagos. -* **Seguridad y trazabilidad**: - - * Tel茅fonos autorizados, roles, bit谩cora de acciones, l铆mites de uso. - ---- - -## 1C) Operativa t铆pica en construcci贸n de vivienda (licitaci贸n, obra, INFONAVIT) - -> Esta subsecci贸n traduce los **procesos t铆picos de una constructora de vivienda** a requerimientos funcionales. - -### A. Desde licitaci贸n hasta contrato - -* Registro de oportunidades de licitaci贸n y proyectos. -* Carga de informaci贸n base (terreno, n煤mero de viviendas, prototipos, alcances). -* Presupuesto base para ofertar. -* Una vez adjudicado: convertir propuesta a proyecto contratado con ajustes. - -**Requerimientos del sistema** - -* Cat谩logo de proyectos en etapas (propuesta, en licitaci贸n, adjudicado, en ejecuci贸n, cerrado). -* Versionado de presupuestos (ofertado vs contratado). -* Documentos clave vinculados (bases de licitaci贸n, actas, contratos). - -### B. Arranque de obra y planeaci贸n - -* Definici贸n de calendario y curva S. -* Carga de contrato de obra, subcontratos y proveedores clave. -* Planeaci贸n de suministros cr铆ticos (cemento, acero, prefabricados). - -**Requerimientos** - -* M贸dulo de planeaci贸n con hitos y duraciones. -* Ligado a compras y almac茅n para prever necesidades. - -### C. Ejecuci贸n y control de avances - -* Registro peri贸dico (diario/semanal) de avances f铆sicos por concepto. -* Evidencias fotogr谩ficas y checklists de calidad. -* Seguimiento de incidencias y correcciones. - -**Requerimientos** - -* App de campo para captura de avances y fotos. -* Consola de control de obra en oficina (dashboard por obra/etapa). - -### D. Estimaciones y pagos - -* Preparaci贸n de estimaciones hacia el cliente (INFONAVIT/fideicomiso). -* Estimaciones hacia subcontratistas y proveedores de servicios. -* Aplicaci贸n de retenciones, amortizaci贸n de anticipos y fondos de garant铆a. - -**Requerimientos** - -* Flujo de trabajo (workflow) de estimaciones con estatus: borrador 鈫 revisada 鈫 autorizada 鈫 pagada. -* Reportes de estimaciones por obra y proveedor. - -### E. Entrega de viviendas y postventa - -* Programaci贸n de entregas por lote/vivienda. -* Actas de entrega con checklists. -* Registro y atenci贸n de quejas/incidencias postventa. - -**Requerimientos** - -* Ficha de cada vivienda con historial completo (obra + postventa). -* M贸dulo de tickets de postventa y seguimiento. - ---- - -## 2) M贸dulos y funciones (detalle inicial) - -### Activaci贸n de M贸dulos por Plan - -Los m贸dulos se activan/desactivan din谩micamente seg煤n el plan de suscripci贸n del cliente: - -**Plan B谩sico (6 m贸dulos core):** -- MAI-001 Fundamentos 鉁 -- MAI-002 Proyectos 鉁 -- MAI-003 Presupuestos 鉁 -- MAI-004 Compras 鉁 -- MAI-005 Control de Obra 鉁 -- MAI-006 Reportes 鉁 - -**Plan Profesional (12 m贸dulos):** -- Incluye los 6 del plan B谩sico -- + MAI-007 RRHH (add-on $100/mes) -- + MAI-008 Estimaciones 鉁 -- + MAI-009 Calidad (add-on $50/mes) -- + MAI-010 CRM 鉁 -- + MAI-011 INFONAVIT (add-on $75/mes) -- + MAI-012 Contratos (add-on $75/mes) - -**Plan Enterprise (18 m贸dulos):** -- Incluye los 12 del plan Profesional -- + MAI-013 Administraci贸n 鉁 -- + MAI-018 Preconstrucci贸n 鉁 -- + MAE-014 Finanzas (add-on $200/mes) -- + MAE-015 Activos (add-on $150/mes) -- + MAE-016 DMS (add-on $100/mes) -- + MAA-017 HSE + IA (add-on $300/mes) - -**Cambio de plan:** El cliente puede upgradearse en cualquier momento desde su portal. Los m贸dulos se activan instant谩neamente. - -> Aqu铆 se baja a un nivel m谩s operativo lo definido en 1A. - -### 2.1 Proyectos, Obras y Viviendas 鈥 N煤cleo - -* Alta/edici贸n de proyectos. -* Estructura: etapas 鈫 manzanas 鈫 lotes/viviendas. -* Prototipos de vivienda (tipos A/B/C, departamentos, etc.). -* Asignaci贸n de responsables y equipo. - -### 2.2 Presupuestos y Costos - -* Cat谩logo de conceptos (obra civil, instalaciones, urbanizaci贸n, etc.). -* Precios unitarios (insumos, rendimientos, indirectos). -* Presupuesto por obra, por etapa y por prototipo. -* Comparaci贸n presupuesto vs costo real por partida. - -### 2.3 Compras y Proveedores - -* Solicitudes de compra desde obra. -* 脫rdenes de compra centralizadas por obra/proyecto. -* Recepci贸n parcial/completa de materiales. -* Cat谩logo de proveedores (materiales, servicios, arrendamientos). - -### 2.4 Inventarios y Almacenes - -* Almacenes por obra. -* Movimientos: entradas, salidas, traspasos, devoluciones. -* K谩rdex y hojas de consumo por concepto. -* Alertas de stock cr铆tico y sobreconsumo. - -### 2.5 Contratos y Subcontratos - -* Contratos principales con montos y alcances. -* Subcontratos por partida/rubro (alba帽iler铆a, herrer铆a, instalaciones, etc.). -* 脫rdenes de cambio/vol煤menes adicionales. -* Control de fianzas, p贸lizas y vigencias. - -### 2.6 Control de Obra y Avances - -* Captura de avances por concepto (porcentaje, cantidades). -* Plan vs real por semana. -* Curva S por obra. -* Evidencias (fotos, notas, checklists). - -### 2.7 Estimaciones y Facturaci贸n - -* Estimaciones de obra ejecutada (vol煤menes, importes). -* Estimaciones a subcontratistas vinculadas a avances. -* Exportaciones PDF/Excel para revisi贸n/firma. -* Registro de pagos y estatus. - -### 2.8 RRHH, Asistencias y Mano de Obra - -* Lista de empleados y cuadrillas. -* Registro de asistencia (web/app). -* Costeo de mano de obra por obra/partida. -* Exportaci贸n hacia n贸mina externa. - -### 2.9 Calidad, Postventa y Garant铆as - -* Listas de verificaci贸n por etapa constructiva. -* No conformidades en obra (qu茅, d贸nde, qui茅n). -* Tickets de postventa por vivienda. -* Status: recibido, en revisi贸n, en reparaci贸n, cerrado. - -### 2.10 CRM de Derechohabientes - -* Datos del comprador/derechohabiente. -* Estatus del expediente (documentos faltantes). -* Historial de contacto y citas. -* Relaci贸n vivienda鈥揷liente鈥揷r茅dito. - -### 2.11 INFONAVIT & Cumplimiento - -* Registro del proyecto en programa INFONAVIT aplicable. -* Checklists de requisitos urban铆sticos, de servicios, sustentabilidad, etc. -* Carga de documentos obligatorios. -* Registro de visitas de verificaci贸n y hallazgos. - -### 2.12 Reportes y Dashboard - -* KPIs por obra: avance f铆sico %, avance financiero %, margen, desviaciones. -* Estimaciones vs pagos. -* Costos por m虏/vivienda. -* Reportes exportables (PDF/Excel). - -### 2.13 Administraci贸n - -* Usuarios, roles y permisos. -* Bit谩cora de acciones cr铆ticas. -* Configuraci贸n de obras, centros de costo, plantillas de reportes. -* Gesti贸n de backups. - ---- - -## 3) Perfiles de usuario y l铆mites (borrador) - -> Esta secci贸n sustituye al modelo de planes POS. Aqu铆 se plantea una posible segmentaci贸n de licencias para eventual modelo SaaS multi-cliente. - -| Perfil | Funci贸n principal | Ejemplos | -| ------------------------ | ------------------------------------------------ | -------------------------------------------- | -| Direcci贸n | Visi贸n global de proyectos, m谩rgenes, riesgos | Director general, director de proyectos | -| Administraci贸n/Finanzas | Presupuestos, compras, pagos, flujo de efectivo | Gerente admvo, contabilidad | -| Ingenier铆a/Planeaci贸n | Presupuestos, programaci贸n, control de obra | Ing. residente, planeador, control de obra | -| Compras/Almac茅n | 脫rdenes de compra, inventarios, recepci贸n | Encargado de compras, almacenista | -| RRHH/N贸mina | Asistencias, costeo de mano de obra | Departamento de RH | -| Postventa | Incidencias, garant铆as, seguimiento a clientes | Coordinador de postventa | -| Residente/Supervisor | Avances de obra, incidencias, checklists desde app | Residente, supervisores de frente | - -> (Los l铆mites de usuarios/obras se definir谩n comercialmente seg煤n el modelo que se acuerde con el cliente.) - ---- - -## 4) Arquitectura T茅cnica SaaS - -> **Nota de compatibilidad**: Este stack tecnol贸gico est谩 alineado con el proyecto GAMILIT existente, permitiendo reutilizar componentes, utilidades y patrones arquitect贸nicos ya desarrollados. -> -> **Arquitectura SaaS Multi-tenant**: Ver documentaci贸n completa en [ARQUITECTURA-SAAS.md](./ARQUITECTURA-SAAS.md) - -### 4.1 Stack Tecnol贸gico - -**Backend:** -* Node.js 20+ + Express + TypeScript -* **Multi-tenant architecture** (schema-level isolation) -* Feature flags para rollout gradual -* Background jobs con Bull/BullMQ - -**Frontend:** -* React 18 + Vite + TypeScript -* Tenant-aware routing -* Dynamic module loading (lazy loading por m贸dulo) -* Branding personalizable por tenant - -**Base de Datos:** -* PostgreSQL 15+ con schemas por tenant -* Connection pooling optimizado -* Automated migrations per tenant -* Point-in-time recovery per schema - -**Infraestructura:** -* AWS/Azure/GCP (agn贸stico) -* Kubernetes para orchestration -* Redis para caching y sessions -* S3/Blob Storage para archivos -* CloudFront/CDN para assets est谩ticos - -### 4.2 Backend 鈥 Node.js 20+ 路 Express + TypeScript (Multi-tenant) - -* **Framework**: Express.js con TypeScript para type safety. -* **Arquitectura Multi-tenant**: - * Tenant Resolver Middleware (identifica tenant por subdomain o header) - * Schema-level isolation (tenant_xxx schemas) - * Tenant-aware database connections - * Feature flags por tenant -* **Estructura modular**: Organizaci贸n por dominios/m贸dulos (similar a GAMILIT): - * `/src/modules/projects` - * `/src/modules/budgets` - * `/src/modules/purchases` - * `/src/modules/progress` - * `/src/modules/hr` - * `/src/modules/infonavit` - * `/src/shared` (utilidades compartidas) - * `/src/config` (configuraci贸n centralizada) -* **Autenticaci贸n**: JWT con refresh tokens; roles y permisos por m贸dulo. -* **Persistencia**: PostgreSQL con modelos normalizados: - - * `projects`, `stages`, `blocks`, `lots`, `units` - * `budgets`, `budget_items`, `boq_items` (cat谩logo de conceptos) - * `suppliers`, `contracts`, `subcontracts` - * `purchase_orders`, `po_items`, `goods_receipts` - * `warehouses`, `stock_items`, `stock_movements` - * `estimations`, `estimation_items`, `payments` - * `employees`, `attendances`, `labor_costs` - * `issues`, `checklists`, `post_sales_tickets` - * `beneficiaries`, `financing_files` (INFONAVIT) -* **ORM/Query Builder**: Prisma o Sequelize para gesti贸n de modelos y migraciones. -* **Servicios de dominio**: - - * Control de obra, presupuestos, compras, estimaciones, RRHH. - * Arquitectura modular con routers y controllers por m贸dulo. -* **Integraciones iniciales**: - - * WhatsApp Business (webhook + API). - * SMTP/email para notificaciones (Nodemailer). -* **API REST**: versionada `/api/v1` con Express Router. -* **MCP**: capa de acciones at贸micas para el agente IA (consultas y operaciones controladas). -* **Seguridad**: - * CORS con configuraci贸n restrictiva. - * Helmet para headers de seguridad. - * Rate limiting (express-rate-limit). - * Validaci贸n de entrada con express-validator o Joi. - * Logs de auditor铆a con Winston o Pino. - -### 4.2 Frontend 鈥 React Web + React Native (App Obra) - -**Web (React + Vite + TypeScript):** - -* **Build Tool**: Vite para desarrollo r谩pido y builds optimizados. -* **State Management**: Zustand o Redux Toolkit para gesti贸n de estado global. -* **UI Framework**: Componentes reutilizables con TailwindCSS o Material-UI. -* **Routing**: React Router v6. -* **Funcionalidades**: - * Panel administrativo y operativo (proyectos, presupuestos, compras, estimaciones, reportes). - * Dashboards por obra y globales con gr谩ficos (Chart.js o Recharts). - * Formularios con validaci贸n (React Hook Form + Zod). - -**App m贸vil/tablet (React Native + TypeScript):** - -* **Framework**: React Native con Expo (recomendado) o CLI nativo. -* **Navegaci贸n**: React Navigation. -* **State Management**: Zustand (compartido con web). -* **Funcionalidades**: - * Captura de avances (porcentaje, fotos, notas). - * Registro de incidencias/defectos con geolocalizaci贸n (react-native-maps). - * Checklists de calidad por etapa. - * Consulta r谩pida de estatus de lote/vivienda. - * C谩mara para evidencias fotogr谩ficas (Expo Camera). - -**Modo offline (m铆nimo para app):** - -* **Storage local**: AsyncStorage o SQLite (expo-sqlite). -* Cola local de registros (avances, incidencias) 鈫 sincronizaci贸n al reconectar. -* Detecci贸n autom谩tica de conectividad (NetInfo). - -### 4.3 WhatsApp + Agente IA - -* Webhook para mensajes entrantes. -* Integraci贸n con MCP para: - - * Avances r谩pidos ("avance cimentaci贸n etapa 2 30%"). - * Consultas ("estatus estimaci贸n 3 obra X"). - * Registro de incidencias con foto. -* Control de tel茅fonos autorizados y roles. - -### 4.4 Despliegue y operaci贸n - -* **Contenedores Docker**: - * `api` (Node.js + Express) - * `db` (PostgreSQL 15+) - * `worker` (procesamiento as铆ncrono con Bull/BullMQ) - * `whatsapp-gateway` (servicio de integraci贸n WA) - * `redis` (cach茅 y colas) -* **Process Manager**: PM2 para gesti贸n de procesos Node.js. -* **Observabilidad**: - * Logs estructurados con Winston/Pino 鈫 agregaci贸n en ELK/Loki. - * M茅tricas con Prometheus + Grafana. - * APM con New Relic o Datadog (opcional). -* **Backups**: - * Base de datos: dumps autom谩ticos diarios con retenci贸n seg煤n acuerdo. - * Almacenamiento en S3 o compatible. -* **CI/CD**: GitHub Actions o GitLab CI con tests automatizados pre-deploy. - -### 4.5 Escalabilidad - -**Horizontal Scaling:** -* API servers: Stateless, escalan horizontalmente -* Database: Read replicas, sharding por tenant (futuro) -* Cache: Redis cluster -* Storage: Object storage escalable - -**Vertical Scaling:** -* Database: Upgrade de instancia seg煤n carga -* Tenants grandes: Schema dedicado en instancia separada (opcional) - -**Performance Targets:** -* API response time: p95 <200ms -* Page load time: <2s -* Database query time: p95 <100ms -* Uptime: 99.9% (8.76 horas downtime/a帽o) - -### 4.6 Seguridad Multi-tenant - -**Aislamiento de Datos:** -1. Schema-level isolation (primera capa) -2. Row-level security (segunda capa) -3. API-level validation (tercera capa) -4. Audit logging de accesos - -**Prevenci贸n de Leaks:** -* Tenant resolver middleware obligatorio -* Guards en todos los endpoints -* Validaci贸n de tenant_id en queries -* Monitoreo de accesos an贸malos - -**Cumplimiento:** -* GDPR (Europa) -* LFPDPPP (M茅xico) -* SOC 2 Type II (roadmap) -* ISO 27001 (roadmap) - -### 4.7 Despliegue y CI/CD - -**Environments:** -* Development (local) -* Staging (pre-producci贸n) -* Production (multi-region) - -**Pipeline:** -``` -Git Push 鈫 GitHub Actions - 鈫 -Unit Tests 鈫 Integration Tests 鈫 E2E Tests - 鈫 -Build Docker Images - 鈫 -Deploy to Staging 鈫 Smoke Tests - 鈫 -Manual Approval (para Production) - 鈫 -Blue-Green Deployment 鈫 Health Checks - 鈫 -Production Live -``` - -**Migrations:** -* Ejecutadas autom谩ticamente en cada tenant -* Rollback autom谩tico si falla -* Dry-run en staging primero -* Notification a tenants afectados - -### 4.8 Monitoreo y Observabilidad - -**M茅tricas:** -* Application metrics (DataDog/New Relic) -* Business metrics (MRR, churn, activaci贸n) -* Infrastructure metrics (CPU, RAM, disk) -* Custom metrics por m贸dulo - -**Logs:** -* Centralized logging (ELK/Splunk) -* Structured logs (JSON) -* Log levels por environment -* Retention: 90 d铆as production, 30 d铆as staging - -**Alerting:** -* PagerDuty para incidents cr铆ticos -* Slack para warnings -* Email para info -* Escalation policies definidas - -**Dashboards:** -* StatusPage.io p煤blico (uptime) -* Grafana interno (m茅tricas t茅cnicas) -* Admin portal (m茅tricas de negocio) - -### 4.9 Componentes reutilizables del ecosistema GAMILIT - -El proyecto GAMILIT ya cuenta con componentes y patrones que pueden adaptarse: - -**Backend (reutilizables):** -* Sistema de autenticaci贸n JWT con roles y permisos. -* Middleware de validaci贸n y manejo de errores. -* Sistema de logging estructurado. -* Patrones de repository/service para acceso a datos. -* Sistema de notificaciones multi-canal. -* Gesti贸n de archivos y uploads. -* Sistema de auditor铆a (audit logging). - -**Frontend Web (reutilizables):** -* Componentes UI base (botones, inputs, modales, tablas). -* Sistema de formularios con validaci贸n. -* Componentes de dashboards y gr谩ficos. -* Manejo de autenticaci贸n y rutas protegidas. -* Hooks personalizados para API calls. -* Sistema de notificaciones/toasts. -* Layouts responsivos. - -**Base de Datos (patrones):** -* Schemas modulares por dominio. -* Pol铆ticas RLS (Row Level Security) para PostgreSQL. -* Triggers y funciones comunes. -* Sistema de migraciones con control de versiones. - -**Beneficios de reutilizaci贸n:** -* Reducci贸n de tiempo de desarrollo: ~30-40% -* C贸digo probado en producci贸n (GAMILIT). -* Patrones arquitect贸nicos consistentes. -* Facilita mantenimiento futuro del ecosistema. - ---- - -## 5) API REST m铆nima (borrador) - -Ejemplos de endpoints: - -* **Auth**: `POST /auth/login`, `POST /auth/refresh` -* **Proyectos**: `GET/POST /projects`, `GET /projects/:id` -* **Estructura de obra**: `POST /projects/:id/structure` (etapas/manzanas/lotes) -* **Presupuestos**: `GET/POST /budgets`, `GET /budgets/:id` -* **Compras**: `POST /purchase-orders`, `POST /purchase-orders/:id/receive` -* **Almac茅n**: `POST /stock-movements`, `GET /stock/:warehouseId` -* **Avances**: `POST /progress`, `GET /projects/:id/progress` -* **Estimaciones**: `POST /estimations`, `GET /estimations/:id` -* **RRHH**: `POST /attendances`, `GET /attendances?project_id=...` -* **Postventa**: `POST /post-sales`, `GET /post-sales/:id` -* **INFONAVIT**: `GET/POST /infonavit/projects/:id/checklists` -* **WhatsApp**: `POST /webhooks/whatsapp` - ---- - -## 6) MCP 鈥 Acciones (borrador) - -Ejemplos de acciones para el agente IA: - -* `mcp.project.status {project_id}` -* `mcp.progress.register {project_id, stage_id, concept_id, qty|percent, notes}` -* `mcp.progress.get {project_id, stage_id}` -* `mcp.issue.create {project_id, location, description, photo_ref}` -* `mcp.estimation.status {project_id, estimation_no}` -* `mcp.post_sale.create {unit_id, description, channel}` -* `mcp.infonavit.checklist.get {project_id}` -* `mcp.infonavit.checklist.update {project_id, item_id, status}` - -Con validaci贸n de roles, scopes y confirmaciones para acciones sensibles. - ---- - -## 7) No funcionales - -* **Disponibilidad**: meta 99.0% (on-prem o nube seg煤n se acuerde). -* **Rendimiento**: operaciones clave (<500 ms en consultas para dashboards principales). -* **Seguridad**: - - * TLS en tr谩nsito. - * Hash de contrase帽as con Argon2/bcrypt. - * Roles y permisos granulares. -* **Privacidad**: - - * Control de acceso a archivos sensibles (contratos, datos personales). - * Logs auditables por usuario y fecha. - ---- - -## 8) Roadmap - -> **Aceleraci贸n por reutilizaci贸n**: Los tiempos estimados consideran la reutilizaci贸n de componentes del ecosistema GAMILIT, reduciendo el desarrollo aproximadamente 30-40% vs. desarrollo desde cero. - -### Sprint 0 鈥 Setup y migraci贸n de base (1 semana) - -* Configuraci贸n de repositorio y estructura modular. -* Migraci贸n de componentes base de GAMILIT: - * Sistema de autenticaci贸n y autorizaci贸n. - * Middleware com煤n y manejo de errores. - * Componentes UI base y layouts. - * Sistema de logging y auditor铆a. -* Setup de base de datos con schemas modulares. -* Configuraci贸n de CI/CD b谩sico. - -### MVP 鈥 Fase 1 (Semanas 1鈥6) - -**Core del sistema (M贸dulos 1-7, 10-13, 18):** -* **M贸dulo 18**: Preconstrucci贸n y licitaciones (pipeline de oportunidades, conversi贸n a proyectos). -* **M贸dulo 1**: Proyectos/obras, estructura de fraccionamiento. -* **M贸dulo 2**: Presupuesto b谩sico y cat谩logo de conceptos. -* **M贸dulo 3**: Compras e inventarios de obra. -* **M贸dulo 4**: Inventarios y almacenes. -* **M贸dulo 5**: Contratos y subcontratos b谩sicos. -* **M贸dulo 6**: Control de avances simple (porcentaje + fotos). -* **M贸dulo 7**: Estimaciones b谩sicas (hacia cliente y subcontratos). -* **M贸dulo 10**: CRM b谩sico de derechohabientes. -* **M贸dulo 11**: INFONAVIT checklists b谩sico. -* **M贸dulo 12**: Reportes esenciales de avance f铆sico y financiero. -* **M贸dulo 13**: Administraci贸n y seguridad. - -**Componentes adaptados de GAMILIT:** -* Dashboard base y navegaci贸n. -* Sistema de formularios con validaci贸n. -* Tablas y paginaci贸n. -* Sistema de notificaciones. - -### Fase 2 鈥 Gesti贸n de Personal, Calidad y M贸dulos Enterprise (Semanas 7鈥12) - -**M贸dulos operativos (8, 9):** -* **M贸dulo 8**: RRHH y asistencias completo con **Time & Attendance GPS + Biom茅trico**. - * Reloj checador m贸vil con geofencing. - * Validaci贸n biom茅trica (huella/rostro). - * Control anti-fraude y multi-sitio. - * Integraci贸n con costeo de mano de obra. -* **M贸dulo 9**: Postventa y garant铆as completo. - -**M贸dulos enterprise iniciales (14, 15, 16):** -* **M贸dulo 14**: Finanzas y Controlling de Obra (versi贸n inicial): - * Cuentas por pagar/cobrar ligadas a compras y estimaciones. - * Flujo de efectivo proyectado vs real. - * Integraci贸n b谩sica con sistemas contables externos. -* **M贸dulo 15**: Activos, Maquinaria y Mantenimiento (versi贸n inicial): - * Cat谩logo de activos y maquinaria pesada. - * Asignaci贸n a obras y control de ubicaci贸n. - * Mantenimiento preventivo b谩sico. -* **M贸dulo 16**: Gesti贸n Documental y Planos (versi贸n inicial): - * Repositorio centralizado de documentos. - * Versionado de planos. - * Control de acceso b谩sico. - -**Dashboards y analytics:** -* Dashboards ejecutivos con gr谩ficos avanzados. -* Analytics de productividad y costos. - -### Fase 3 鈥 IA, HSE y Extensiones Enterprise (Semanas 13鈥18) - -**IA y automatizaci贸n:** -* IA (alertas de desviaciones costo/tiempo y recordatorios inteligentes). -* Modelos predictivos de riesgo de retraso/costo. -* WhatsApp Business + Agente IA (MCP) con capacidades completas. - -**M贸dulo 17 鈥 Seguridad, Riesgos y HSE:** -* Registro de incidentes y accidentes desde app m贸vil. -* Matriz de riesgos y checklists de seguridad. -* Cumplimiento normativo (NOM-031-STPS, OSHA). -* **IA para detecci贸n de patrones de riesgo** (diferenciador competitivo). - -**App m贸vil completa:** -* React Native para campo con todas las funcionalidades. -* Modo offline robusto. -* Sincronizaci贸n optimizada. - -**Extensiones de m贸dulos enterprise:** -* **M贸dulo 14** (extendido): Libro mayor, controlling completo, conciliaci贸n bancaria. -* **M贸dulo 15** (extendido): Mantenimiento correctivo, IoT para localizaci贸n GPS de activos. -* **M贸dulo 16** (extendido): Flujos de aprobaci贸n complejos, OCR, comparaci贸n visual de planos. - -**Integraciones adicionales:** -* Contabilidad (SAP, CONTPAQi, etc.). -* N贸mina externa. -* Portales gubernamentales de licitaciones. - -### Fase 4 (Opcional) 鈥 Optimizaci贸n y Features Avanzadas (Semanas 19鈥22) - -* Performance optimization y escalamiento. -* Features avanzadas de IA predictiva. -* Integraciones con IoT (sensores, drones). -* Mobile app features avanzadas (AR para visualizaci贸n de planos en sitio). -* Business Intelligence avanzado con ML. - -**Tiempo total estimado**: -* **MVP Core (Fase 1)**: 6 semanas -* **MVP + Enterprise B谩sico (Fases 1-2)**: 12 semanas -* **Sistema Completo (Fases 1-3)**: 18 semanas -* **Sistema Optimizado (Fases 1-4)**: 22 semanas - -> **vs. Desarrollo desde cero**: 30-35 semanas para alcance equivalente (ahorro ~40%) - ---- - -## 9) T茅rminos comerciales (borrador) - -> (A ajustar con n煤meros concretos cuando definas tu estrategia de precio para este cliente.) - -**Modalidad sugerida**: licencia por proyecto + mantenimiento anual / SaaS mensual. - -**Incluye:** - -* Implementaci贸n del MVP. -* Capacitaci贸n inicial a usuarios clave. -* Soporte correctivo en horario laboral. - -**Exclusiones:** - -* Adecuaciones contables espec铆ficas del despacho del cliente. -* Integraciones especiales no descritas (ERP contable, n贸mina propia, etc.). - ---- - -## 10) Criterios de aceptaci贸n (MVP) - -1. Registrar al menos 1 proyecto completo con etapas, manzanas, lotes y prototipos. -2. Cargar un presupuesto maestro y visualizar comparaci贸n presupuesto vs costo real por partida. -3. Generar y recibir al menos 3 贸rdenes de compra y reflejar el movimiento en inventarios. -4. Registrar avances f铆sicos por concepto y visualizar la curva S programado vs real. -5. Elaborar al menos 1 estimaci贸n hacia el cliente y 1 a un subcontratista, con exportaci贸n PDF/Excel. -6. Registrar asistencias de cuadrillas por obra y obtener un reporte de costo de mano de obra. -7. Crear m铆nimo 5 tickets de postventa y cerrar al menos 2, con evidencia. -8. Subir evidencias y checklists para al menos 1 checklist INFONAVIT. -9. Consultar v铆a WhatsApp el avance de una obra y registrar al menos 1 incidencia usando el agente. - ---- - -## 11) Estrategia de migraci贸n y adaptaci贸n de componentes GAMILIT - -Esta secci贸n detalla c贸mo aprovechar el c贸digo existente del proyecto GAMILIT para acelerar el desarrollo. - -### 11.1 Componentes de infraestructura (Alta prioridad) - -**Autenticaci贸n y autorizaci贸n:** -* Migrar sistema JWT de GAMILIT. -* Adaptar middleware de autenticaci贸n. -* Reutilizar sistema de roles y permisos (ajustar roles espec铆ficos de construcci贸n). - -**Middleware y utilidades:** -* Error handlers y validadores. -* Logging estructurado (Winston/Pino). -* Rate limiting y CORS. -* Helpers de respuesta HTTP estandarizados. - -**Base de datos:** -* Estructura de schemas modulares de GAMILIT como referencia. -* Reutilizar pol铆ticas RLS como template. -* Adaptar sistema de migraciones. -* Sistema de seeds para datos de prueba. - -### 11.2 Componentes de frontend (Media prioridad) - -**UI Base:** -* Botones, inputs, selects con variantes. -* Modales y di谩logos. -* Sistema de notificaciones/toasts. -* Loaders y skeletons. - -**Layouts:** -* Sidebar y navbar. -* Estructura de p谩ginas admin. -* Grids y containers responsivos. - -**Formularios:** -* Componentes de formulario con validaci贸n. -* Hooks personalizados (useForm, useApi). -* Manejo de estados de carga/error. - -**Dashboards:** -* Componentes de gr谩ficos (Recharts). -* Cards de m茅tricas. -* Tablas con paginaci贸n, filtros y ordenamiento. - -### 11.3 Patrones y arquitectura (Baja prioridad - gu铆as) - -**Backend:** -* Patr贸n Repository/Service. -* Estructura de routers y controllers. -* Organizaci贸n de m贸dulos. -* Manejo de transacciones. - -**Frontend:** -* Arquitectura de state management. -* Estructura de carpetas. -* Patrones de composici贸n de componentes. -* Testing patterns. - -### 11.4 Plan de migraci贸n por sprint - -**Sprint 0:** -1. Crear repositorio con estructura base de GAMILIT. -2. Migrar sistema de autenticaci贸n completo. -3. Setup de base de datos con schemas modulares. -4. Migrar componentes UI base (botones, inputs, modales). -5. Configurar sistema de logging y error handling. - -**Sprint 1:** -6. Adaptar layout principal y navegaci贸n. -7. Migrar sistema de formularios. -8. Implementar tablas reutilizables. -9. Setup de sistema de notificaciones. - -**Sprint 2+:** -10. Migrar componentes espec铆ficos seg煤n necesidad. -11. Adaptar hooks y utilidades. -12. Ajustar componentes de dashboards. - -### 11.5 Consideraciones de adaptaci贸n - -**Diferencias de dominio:** -* GAMILIT: Plataforma educativa (estudiantes, cursos, actividades). -* MVP-APP: Construcci贸n (proyectos, obras, presupuestos). -* **Acci贸n**: Renombrar entidades pero mantener estructura de relaciones. - -**T茅rminos a adaptar:** - -| GAMILIT | MVP-APP | -|---------|---------| -| `students` | `employees` / `beneficiaries` | -| `courses` | `projects` | -| `activities` | `tasks` / `checklists` | -| `progress` | `progress` (compatible) | -| `achievements` | `milestones` | - -**Mantenimiento del c贸digo compartido:** -* Documentar componentes reutilizados con referencia a GAMILIT. -* Considerar extraer a librer铆a compartida en futuro. -* Sincronizar mejoras cr铆ticas entre proyectos. - -### 11.6 Estimaci贸n de ahorro - -| Componente | Desarrollo desde cero | Con reutilizaci贸n | Ahorro | -|------------|----------------------|-------------------|---------| -| Autenticaci贸n | 2 semanas | 3 d铆as | 65% | -| UI Base | 3 semanas | 1 semana | 67% | -| Dashboards | 2 semanas | 1 semana | 50% | -| Formularios | 1.5 semanas | 3 d铆as | 60% | -| BD Setup | 1 semana | 3 d铆as | 60% | -| **TOTAL** | **9.5 semanas** | **3.4 semanas** | **~64%** | - -**Beneficio adicional**: C贸digo ya probado en producci贸n reduce bugs y tiempo de QA. - ---- - -## 8) Casos de Uso SaaS - -### Caso 1: Constructora Mediana (25 empleados) - -**Perfil:** -- 3 proyectos simult谩neos (150 viviendas/a帽o) -- Facturaci贸n: $80M MXN/a帽o -- Personal: 25 oficina + 150 campo - -**Plan:** Profesional ($799/mes) -- 25 usuarios incluidos -- 12 m贸dulos activados -- Add-ons: RRHH ($100/mes), INFONAVIT ($75/mes) -- **Total: $974/mes ($11,688/a帽o)** - -**ROI:** -- Antes (Excel + WhatsApp): P茅rdida 5% por descontrol = $4M/a帽o -- Despu茅s (ERP): P茅rdida reducida a 1% = $800K/a帽o -- **Ahorro: $3.2M/a帽o** -- **ROI: 27,000% (recuperaci贸n en 1.3 meses)** - ---- - -### Caso 2: Constructora Grande (100 empleados) - -**Perfil:** -- 10 proyectos simult谩neos (500 viviendas/a帽o) -- Facturaci贸n: $300M MXN/a帽o -- Personal: 100 oficina + 800 campo - -**Plan:** Enterprise ($1,499/mes) -- 100 usuarios incluidos -- 18 m贸dulos (todos) -- Add-ons: HSE IA ($300/mes), Finanzas ($200/mes) -- **Total: $1,999/mes ($23,988/a帽o)** - -**ROI:** -- Antes (ERP legacy costoso): $150K/a帽o licencias + $50K/a帽o mantenimiento = $200K/a帽o -- Despu茅s (Este SaaS): $24K/a帽o -- **Ahorro: $176K/a帽o** -- **Adem谩s**: Mayor productividad, menos errores, decisiones data-driven - ---- - -### Caso 3: Startup Constructora (5 empleados) - -**Perfil:** -- 1 proyecto piloto (30 viviendas) -- Facturaci贸n: $15M MXN/a帽o -- Personal: 5 oficina + 30 campo - -**Plan:** B谩sico ($399/mes) + Trial 14 d铆as gratis -- 10 usuarios incluidos -- 6 m贸dulos core -- Sin add-ons inicialmente -- **Total: $399/mes ($4,788/a帽o)** - -**Ventaja:** -- Herramientas enterprise desde d铆a 1 -- Puede crecer agregando m贸dulos -- Sin inversi贸n inicial en tecnolog铆a -- Competir con constructoras grandes - ---- - -### Caso 4: Extensi贸n para Obra Civil Pesada - -**Perfil:** -- Constructora especializada en puentes y carreteras -- Requiere funcionalidad espec铆fica no incluida en core - -**Soluci贸n:** Extensi贸n del Marketplace -- Plan Enterprise ($1,499/mes) -- + Extensi贸n "Obra Civil Pesada" ($299/mes) -- **Total: $1,798/mes** - -**Funcionalidad de extensi贸n:** -- Gesti贸n de tramos de carretera -- Control de acarreos y vol煤menes -- Reportes para SCT -- Integraci贸n con laboratorio - -**Desarrollo:** Partner certificado lo desarroll贸 usando el SDK, no modific贸 el core. - ---- - -## 9) Migraci贸n de Clientes Existentes - -### Si ya tienen un sistema legacy - -**Proceso de migraci贸n:** - -1. **An谩lisis de datos** (1 semana) - - Auditor铆a de datos actuales - - Identificaci贸n de datos cr铆ticos - - Mapeo de campos - - Limpieza de datos - -2. **Setup de tenant** (1 d铆a) - - Onboarding est谩ndar - - Configuraci贸n inicial - - Usuarios y permisos - -3. **Migraci贸n de datos** (1-2 semanas) - - Import de proyectos hist贸ricos - - Import de cat谩logos - - Import de transacciones - - Validaci贸n de integridad - -4. **Capacitaci贸n** (1 semana) - - Training del equipo administrativo - - Training del equipo de campo - - Documentaci贸n personalizada - - Soporte 1-1 - -5. **Go-live** (1 d铆a) - - Cutover del sistema legacy - - Monitoreo intensivo - - Soporte en sitio (opcional) - -6. **Estabilizaci贸n** (2 semanas) - - Soporte prioritario - - Ajustes de configuraci贸n - - Resoluci贸n de incidencias - -**Total tiempo de migraci贸n: 4-6 semanas** - -**Costo de migraci贸n:** -- Paquete Starter: $2,500 USD (datos <5K registros) -- Paquete Profesional: $7,500 USD (datos <50K registros) -- Paquete Enterprise: $15,000 USD (datos <200K registros) -- Servicios adicionales: Ver [ARQUITECTURA-SAAS.md](./ARQUITECTURA-SAAS.md#servicios-adicionales-opcionales) - -### Si no tienen sistema - -**Inicio desde cero:** -- Onboarding: 5 minutos -- Configuraci贸n inicial: 2 horas (cat谩logos, usuarios) -- Primer proyecto: 1 d铆a -- **Productivos: < 1 semana** - ---- - -## Documentaci贸n Relacionada - -- **[ARQUITECTURA-SAAS.md](./ARQUITECTURA-SAAS.md)**: Arquitectura multi-tenant detallada -- **[CAMBIOS-SAAS-MVP.md](./CAMBIOS-SAAS-MVP.md)**: Gu铆a de transformaci贸n a modelo SaaS -- **[01-fase-alcance-inicial/](../01-fase-alcance-inicial/)**: Documentaci贸n Fase 1 (M贸dulos MAI-001 a MAI-013 + MAI-018) -- **[02-fase-enterprise/](../02-fase-enterprise/)**: Documentaci贸n Fase 2 (M贸dulos MAE-014 a MAE-016) -- **[03-fase-avanzada/](../03-fase-avanzada/)**: Documentaci贸n Fase 3 (M贸dulo MAA-017) -- **[ESTRUCTURA-COMPLETA.md](../ESTRUCTURA-COMPLETA.md)**: Mapa completo del sistema - ---- - -**Versi贸n:** 2.0 SaaS Multi-tenant -**脷ltima actualizaci贸n:** 2025-11-17 -**Modelo de negocio:** B2B SaaS, Subscription-based -**Stack:** Node.js + Express + TypeScript 路 React + Vite 路 PostgreSQL Multi-tenant - ---- \ No newline at end of file diff --git a/projects/erp-construccion/docs/00-vision-general/PORTAL-ADMIN-SAAS.md b/projects/erp-construccion/docs/00-vision-general/PORTAL-ADMIN-SAAS.md deleted file mode 100644 index 3e79f0134..000000000 --- a/projects/erp-construccion/docs/00-vision-general/PORTAL-ADMIN-SAAS.md +++ /dev/null @@ -1,749 +0,0 @@ -# Portal de Administraci贸n SaaS - Especificaci贸n Completa - -**Versi贸n:** 1.0 -**Fecha:** 2025-11-17 -**Modelo:** SaaS Multi-tenant B2B - ---- - -## 馃搵 Resumen Ejecutivo - -El Portal de Administraci贸n SaaS es el centro de control de la plataforma multi-tenant, permitiendo gestionar tenants, usuarios, m贸dulos, facturaci贸n y m茅tricas del negocio desde una interfaz centralizada. - -**Usuarios objetivo:** -- **Super Admin**: Administrador de la plataforma (equipo interno) -- **Tenant Admin**: Administrador de empresa cliente -- **Support**: Equipo de soporte t茅cnico -- **Billing**: Equipo de facturaci贸n y cobranza - ---- - -## 馃幆 Roles y Permisos - -### Super Admin (Plataforma) - -**Acceso completo a:** -- 鉁 Gesti贸n de todos los tenants (crear, editar, suspender, cancelar) -- 鉁 Configuraci贸n global de la plataforma -- 鉁 Feature flags y experimentos A/B -- 鉁 M茅tricas de negocio (MRR, ARR, churn) -- 鉁 Logs y auditor铆a de todos los tenants -- 鉁 Configuraci贸n de pricing y planes -- 鉁 Gesti贸n de marketplace (aprobar extensiones) - -**Dashboard principal:** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Super Admin Dashboard 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃搳 M茅tricas Clave (脷ltimos 30 d铆as) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Tenants 鈹 MRR 鈹 Churn 鈹 Nuevos 鈹 鈹 -鈹 鈹 234 鈹 $156,780 鈹 2.3% 鈹 28 鈹 鈹 -鈹 鈹 +12% 鈹 +8.5% 鈹 -0.5% 鈹 +4 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 馃搱 Crecimiento MRR 鈹 -鈹 [Gr谩fica de l铆nea - 煤ltimos 12 meses] 鈹 -鈹 鈹 -鈹 馃敟 Alertas Cr铆ticas 鈹 -鈹 鈥 3 tenants en riesgo de churn (NPS < 30) 鈹 -鈹 鈥 5 tenants excediendo l铆mites de uso 鈹 -鈹 鈥 2 incidentes de seguridad en las 煤ltimas 24h 鈹 -鈹 鈹 -鈹 馃搵 脷ltimas Acciones 鈹 -鈹 鈥 Tenant "constructora-abc" actualizado a Enterprise 鈹 -鈹 鈥 Feature "ai_risk_prediction" activado para 10 tenants 鈹 -鈹 鈥 Extension "SAP Connector v2.0" aprobada en marketplace 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -### Tenant Admin (Cliente) - -**Acceso limitado a su tenant:** -- 鉁 Gesti贸n de usuarios de su empresa -- 鉁 Configuraci贸n de m贸dulos activados (seg煤n plan) -- 鉁 Personalizaci贸n (branding, workflows) -- 鉁 Reportes de uso de su tenant -- 鉁 Facturaci贸n y m茅todos de pago -- 鉁 Extensiones instaladas -- 鉂 NO puede ver otros tenants -- 鉂 NO puede modificar pricing - -**Dashboard del tenant:** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Constructora ABC - Admin Dashboard 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃搳 Uso del Sistema (脷ltimos 30 d铆as) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Usuarios 鈹 Proyectos 鈹 M贸dulos 鈹 Uso 鈹 鈹 -鈹 鈹 18/25 鈹 12 鈹 10/12 鈹 78% 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 馃挸 Suscripci贸n Actual 鈹 -鈹 Plan: Profesional ($799/mes) 鈹 -鈹 Add-ons: RRHH ($100/mes), INFONAVIT ($75/mes) 鈹 -鈹 Total: $974/mes 鈹 -鈹 Pr贸xima factura: 2025-12-01 ($974 USD) 鈹 -鈹 [Actualizar Plan] [Agregar M贸dulos] 鈹 -鈹 鈹 -鈹 馃懃 Usuarios (18/25) 鈹 -鈹 [+ Invitar Usuario] [Gestionar Roles] 鈹 -鈹 鈹 -鈹 鈿欙笍 M贸dulos Activos (10/12) 鈹 -鈹 鉁 Proyectos 鉁 Presupuestos 鉁 Compras 鉁 Control 鈹 -鈹 鉁 RRHH 鉁 Estimaciones 鉁 CRM 鉁 INFONAVIT 鈹 -鈹 鈿 Calidad ($50/mes) [Activar] 鈹 -鈹 鈿 Contratos ($75/mes) [Activar] 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -### Support (Equipo de Soporte) - -**Acceso de solo lectura:** -- 鉁 Ver datos de cualquier tenant (para troubleshooting) -- 鉁 Ver logs y errores -- 鉁 Ejecutar queries de diagn贸stico -- 鉂 NO puede modificar datos de tenants -- 鉂 NO puede suspender/cancelar tenants -- 鉁 Puede crear tickets internos -- 鉁 Puede ver historial de soporte del tenant - ---- - -### Billing (Facturaci贸n) - -**Acceso espec铆fico:** -- 鉁 Ver uso y facturaci贸n de todos los tenants -- 鉁 Generar facturas manualmente -- 鉁 Suspender tenants por falta de pago -- 鉁 Aplicar descuentos y cr茅ditos -- 鉂 NO puede modificar funcionalidad -- 鉁 Exportar reportes contables - ---- - -## 馃彈锔 Funcionalidades del Portal - -### 1. Gesti贸n de Tenants - -#### 1.1 Listado de Tenants - -**Vista principal:** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Tenants (234 total) [+ Nuevo Tenant] 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 馃攳 Buscar: [________________] Filtros: [Plan 鈻糫 [Estado 鈻糫 [Regi贸n 鈻糫 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 Tenant Plan Usuarios M贸dulos Estado MRR Acci贸n鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹 -鈹 馃彚 const-abc Enterprise 45/50 15/18 馃煝 Activo $1,500 鈿欙笍馃搳馃摟鈹 -鈹 馃彚 viviendas Profesional 18/25 10/18 馃煝 Activo $750 鈿欙笍馃搳馃摟鈹 -鈹 馃彚 obras-norte B谩sico 8/10 6/18 馃煛 Prueba $0 鈿欙笍馃搳馃摟鈹 -鈹 馃彚 desarr-sur Enterprise 12/100 18/18 馃敶 Susp. $2,000 鈿欙笍馃搳馃摟鈹 -鈹 馃彚 edificios Profesional 22/25 12/18 馃煝 Activo $899 鈿欙笍馃搳馃摟鈹 -鈹 ... 鈹 -鈹 鈹 -鈹 Mostrando 1-5 de 234 [< 1 2 3 ... 47 >] 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -鈿欙笍 = Configurar 馃搳 = Analytics 馃摟 = Contactar -``` - -**Filtros avanzados:** -- Por plan (B谩sico, Profesional, Enterprise) -- Por estado (Activo, Trial, Suspendido, Cancelado) -- Por regi贸n/pa铆s -- Por fecha de creaci贸n -- Por MRR (rango) -- Por uso (% de capacidad) -- Por riesgo de churn (NPS score) - -#### 1.2 Onboarding de Nuevo Tenant - -**Wizard de 5 pasos:** - -**Paso 1: Informaci贸n de Empresa** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Nuevo Tenant - Paso 1/5: Informaci贸n Empresa 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 Nombre Empresa: [_____________________________]鈹 -鈹 RFC/Tax ID: [_____________________________]鈹 -鈹 Industria: [Construcci贸n Residencial 鈻糫 鈹 -鈹 Tama帽o: [ ] <10 [x] 10-50 [ ] 50-100鈹 -鈹 Pa铆s: [M茅xico 鈻糫 鈹 -鈹 Zona Horaria: [America/Mexico_City 鈻糫 鈹 -鈹 鈹 -鈹 [Cancelar] [Siguiente >] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Paso 2: Subdominio y Admin** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Nuevo Tenant - Paso 2/5: Acceso 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 Subdominio: 鈹 -鈹 [constructora-abc].erp-construccion.com 鈹 -鈹 鉁 Disponible 鈹 -鈹 鈹 -鈹 Administrador Principal: 鈹 -鈹 Nombre: [Juan P茅rez________________________] 鈹 -鈹 Email: [admin@constructora-abc.com_______] 鈹 -鈹 Tel茅fono: [+52 442 123 4567________________] 鈹 -鈹 鈹 -鈹 [< Anterior] [Siguiente >] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Paso 3: Selecci贸n de Plan** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Nuevo Tenant - Paso 3/5: Plan de Suscripci贸n 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 B脕SICO 鈹 鈹侾ROFESIONAL鈹 鈹 ENTERPRISE鈹 鈹 -鈹 鈹 $399/mes 鈹 鈹 $799/mes 鈹 鈹$1,499/mes 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹10 usuarios鈹 鈹25 usuarios鈹 鈹100 usuario鈹 鈫 Seleccionado鈹 -鈹 鈹 6 m贸dulos 鈹 鈹12 m贸dulos 鈹 鈹18 m贸dulos 鈹 鈹 -鈹 鈹 10 GB 鈹 鈹 50 GB 鈹 鈹 200 GB 鈹 鈹 -鈹 鈹侲mail 48h 鈹 鈹侰hat 24h 鈹 鈹侱edic. 4h 鈹 鈹 -鈹 鈹 鈹 鈹 鈹 鈹 鈹 鈹 -鈹 鈹俒Elegir] 鈹 鈹俒Elegir] 鈹 鈹俒鉁揈legido] 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈽戯笍 Trial de 14 d铆as (sin cargo) 鈹 -鈹 鈽 Facturaci贸n anual (15% descuento) 鈹 -鈹 鈹 -鈹 [< Anterior] [Siguiente >] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Paso 4: M贸dulos Add-on** (si aplica) -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Nuevo Tenant - Paso 4/5: M贸dulos Adicionales 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 M贸dulos incluidos en Plan Enterprise: (18) 鈹 -鈹 鉁 Todos los m贸dulos core 鈹 -鈹 鉁 Todos los m贸dulos enterprise 鈹 -鈹 鉁 HSE + IA predictiva 鈹 -鈹 鈹 -鈹 驴Desea agregar servicios adicionales? 鈹 -鈹 鈹 -鈹 鈽 Modelo ML custom (+$100/mes) 鈹 -鈹 Modelo de IA personalizado con datos propios鈹 -鈹 鈹 -鈹 鈽 Usuarios adicionales (+$10 c/u) 鈹 -鈹 Agregar m谩s de 100 usuarios: [____] users 鈹 -鈹 鈹 -鈹 鈽 Almacenamiento adicional (+$2/GB) 鈹 -鈹 Agregar m谩s de 200 GB: [____] GB 鈹 -鈹 鈹 -鈹 [< Anterior] [Siguiente >] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Paso 5: Confirmaci贸n y Provisioning** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Nuevo Tenant - Paso 5/5: Confirmaci贸n 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 Resumen: 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Empresa: Constructora ABC SA de CV 鈹 -鈹 Subdominio: constructora-abc 鈹 -鈹 Plan: Enterprise 鈹 -鈹 Admin: admin@constructora-abc.com 鈹 -鈹 鈹 -鈹 Costo mensual: 鈹 -鈹 - Plan Enterprise: $1,499/mes 鈹 -鈹 - Usuarios extra (0): $0/mes 鈹 -鈹 - Almacenamiento (0): $0/mes 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Total: $1,499/mes 鈹 -鈹 鈹 -鈹 Trial: 14 d铆as gratis 鈹 -鈹 Primera factura: 2025-12-01 鈹 -鈹 鈹 -鈹 鈽戯笍 He le铆do y acepto los T茅rminos de Servicio 鈹 -鈹 鈹 -鈹 [< Anterior] [Crear Tenant] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Al hacer clic en "Crear Tenant":** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈴 Provisionando Tenant... 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 鉁 Creando registro en tabla constructoras 鈹 -鈹 鉁 Generando UUID 煤nico 鈹 -鈹 鈴 Creando usuario admin... (2s) 鈹 -鈹 鈴 Vinculando usuario a constructora... 鈹 -鈹 鈴 Activando m贸dulos (18)... 鈹 -鈹 鈴 Configurando subdomain DNS... 鈹 -鈹 鈴 Generando datos seed (cat谩logos)... 鈹 -鈹 鈴 Generando datos demo... 鈹 -鈹 鈴 Enviando email de bienvenida... 鈹 -鈹 鈹 -鈹 Progreso: 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻戔枒鈻戔枒鈻戔枒鈻 65% 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Confirmaci贸n final:** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鉁 Tenant Creado Exitosamente! 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 El tenant "Constructora ABC" ha sido 鈹 -鈹 provisionado correctamente. 鈹 -鈹 鈹 -鈹 URL de acceso: 鈹 -鈹 https://constructora-abc.erp-construccion.com 鈹 -鈹 鈹 -鈹 Credenciales enviadas a: 鈹 -鈹 admin@constructora-abc.com 鈹 -鈹 鈹 -鈹 Estado: Trial (14 d铆as) 鈹 -鈹 Expira: 2025-12-01 鈹 -鈹 鈹 -鈹 [Ver Tenant] [Volver al Listado] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -### 2. Configuraci贸n de Tenant - -#### 2.1 Informaci贸n General - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Tenant: Constructora ABC - Configuraci贸n 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 [General] [M贸dulos] [Usuarios] [Facturaci贸n] [Seguridad] 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃摑 Informaci贸n de Empresa 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Nombre: [Constructora ABC SA de CV___________] 鈹 -鈹 RFC: [CABC850101AAA_______________________] 鈹 -鈹 Direcci贸n: [Av. Constituyentes 123_____________] 鈹 -鈹 Ciudad: [Quer茅taro________________________] 鈹 -鈹 Pa铆s: [M茅xico 鈻糫 鈹 -鈹 Zona Horaria: [America/Mexico_City 鈻糫 鈹 -鈹 Idioma: [Espa帽ol (M茅xico) 鈻糫 鈹 -鈹 Moneda: [MXN - Peso Mexicano 鈻糫 鈹 -鈹 鈹 -鈹 馃帹 Branding 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Logo: [馃搧 Subir Logo] (actual: logo.png) 鈹 -鈹 Color Primario: [#0066CC] 馃帹 鈹 -鈹 Color Secundario: [#FF6600] 馃帹 鈹 -鈹 鈹 -鈹 馃摟 Contactos 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Email Admin: [admin@constructora-abc.com__________] 鈹 -鈹 Email Soporte: [soporte@constructora-abc.com________] 鈹 -鈹 Tel茅fono: [+52 442 123 4567___________________] 鈹 -鈹 鈹 -鈹 馃寪 Dominio Custom (opcional) 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈽戯笍 Usar dominio personalizado 鈹 -鈹 Dominio: [erp.constructora-abc.com____________] 鈹 -鈹 Estado: 馃煝 Verificado (DNS configurado) 鈹 -鈹 鈹 -鈹 [Guardar Cambios] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -#### 2.2 Gesti贸n de M贸dulos - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Tenant: Constructora ABC - M贸dulos 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 [General] [M贸dulos] [Usuarios] [Facturaci贸n] [Seguridad] 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃摝 Plan Actual: Enterprise ($1,499/mes) 鈹 -鈹 M贸dulos incluidos: 18 de 18 鉁 Todos activos 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 FASE 1: ALCANCE INICIAL 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鉁 MAI-001 Fundamentos Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-002 Proyectos Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-003 Presupuestos Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-004 Compras Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-005 Control de Obra Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-006 Reportes Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-007 RRHH Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-008 Estimaciones Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-009 Calidad Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-010 CRM Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-011 INFONAVIT Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-012 Contratos Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-013 Administraci贸n Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAI-018 Preconstrucci贸n Incluido [鈼廬 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 FASE 2: ENTERPRISE 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鉁 MAE-014 Finanzas Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAE-015 Activos Incluido [鈼廬 鈹 鈹 -鈹 鈹 鉁 MAE-016 DMS Incluido [鈼廬 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 FASE 3: AVANZADA 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鉁 MAA-017 HSE + IA Incluido [鈼廬 鈹 鈹 -鈹 鈹 鈿欙笍 Configuraci贸n IA: [Configurar Modelo ML] 鈹 鈹 -鈹 鈹 馃搳 Status: Modelo entrenado (78% accuracy) 鈹 鈹 -鈹 鈹 馃攧 Pr贸ximo re-entrenamiento: 2025-12-01 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 馃攲 Extensiones del Marketplace (3 instaladas) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 鉁 SAP S/4HANA Connector $99/mes [鈼廬 鈹 鈹 -鈹 鈹 鉁 WhatsApp Business API Gratis [鈼廬 鈹 鈹 -鈹 鈹 鉁 Reporte INFONAVIT EVC $49 煤nico [鈼廬 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 [Explorar Marketplace] 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -### 3. Gesti贸n de Usuarios - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Tenant: Constructora ABC - Usuarios (18/25) 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 [General] [M贸dulos] [Usuarios] [Facturaci贸n] [Seguridad] 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 [+ Invitar Usuario] [Importar CSV] [Exportar] 鈹 -鈹 鈹 -鈹 馃攳 Buscar: [________________] Filtro: [Rol 鈻糫 [Estado 鈻糫鈹 -鈹 鈹 -鈹 Usuario Email Rol 脷ltimo Acc. 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 馃懁 Juan P茅rez admin@const.com Admin Hace 2 hrs 鈹 -鈹 馃懁 Mar铆a L贸pez maria@const.com Engineer Hace 1 d铆a 鈹 -鈹 馃懁 Pedro Mtez pedro@const.com Resident Hace 3 hrs 鈹 -鈹 馃懁 Ana Garc铆a ana@const.com Finance Hace 5 hrs 鈹 -鈹 馃懁 Carlos Ruiz carlos@const.com Warehouse Nunca 鈹 -鈹 ... 鈹 -鈹 鈹 -鈹 Mostrando 1-5 de 18 鈹 -鈹 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 L铆mite de usuarios: 18/25 (72% utilizado) 鈹 -鈹 驴Necesitas m谩s usuarios? [Agregar 10 usuarios (+$100/mes)]鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -### 4. Facturaci贸n y Pagos - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Tenant: Constructora ABC - Facturaci贸n 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 [General] [M贸dulos] [Usuarios] [Facturaci贸n] [Seguridad] 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃挸 Suscripci贸n Actual 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Plan: Enterprise 鈹 -鈹 Precio base: $1,499/mes 鈹 -鈹 Inicio: 2024-06-01 鈹 -鈹 Pr贸xima factura: 2025-12-01 鈹 -鈹 Estado: 鉁 Activo 鈹 -鈹 鈹 -鈹 馃搳 Desglose de Costos 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Plan Enterprise (100 usuarios): $1,499/mes 鈹 -鈹 Extensi贸n SAP Connector: $99/mes 鈹 -鈹 Usuarios adicionales (0): $0/mes 鈹 -鈹 Almacenamiento adicional (0 GB): $0/mes 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Subtotal: $1,598/mes 鈹 -鈹 Descuento anual (15%): -$240/mes 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Total mensual: $1,358/mes 鈹 -鈹 Total anual: $16,296/a帽o 鈹 -鈹 鈹 -鈹 [Cambiar Plan] [Actualizar M贸dulos] 鈹 -鈹 鈹 -鈹 馃挵 M茅todo de Pago 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 馃挸 Visa 鈥⑩⑩⑩ 1234 Exp: 12/26 鉁 V谩lida 鈹 -鈹 [Actualizar Tarjeta] [Agregar M茅todo de Pago] 鈹 -鈹 鈹 -鈹 馃搫 Historial de Facturas 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Fecha Concepto Monto Estado PDF 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹鈹鈹 鈹鈹鈹鈹 鈹 -鈹 2025-11-01 Suscripci贸n Nov $1,358 USD 鉁 Pagado 馃摜 鈹 -鈹 2025-10-01 Suscripci贸n Oct $1,358 USD 鉁 Pagado 馃摜 鈹 -鈹 2025-09-01 Suscripci贸n Sep $1,358 USD 鉁 Pagado 馃摜 鈹 -鈹 2025-08-01 Suscripci贸n Ago $1,358 USD 鉁 Pagado 馃摜 鈹 -鈹 ... 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 馃搳 Analytics y M茅tricas - -### Dashboard de M茅tricas de Negocio (Super Admin) - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Analytics - M茅tricas de Negocio 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃搱 KPIs Principales (脷ltimos 30 d铆as) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 MRR ARR Churn CAC LTV 鈹 鈹 -鈹 鈹 $156,780 $1,881,360 2.3% $1,250 $18,000 鈹 鈹 -鈹 鈹 +8.5% 鈫 +8.5% 鈫 -0.5% 鈫 -5% 鈫 +12% 鈫 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 馃搳 Crecimiento MRR (脷ltimos 12 meses) 鈹 -鈹 [Gr谩fica de barras apiladas - por plan] 鈹 -鈹 $180K 鈹 鈻堚枅鈻堚枅鈻堚枅 鈹 -鈹 $160K 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 鈹 -鈹 $140K 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 鈹 -鈹 $120K 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 鈹 -鈹 $100K 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 鈹 -鈹 $80K 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 鈹 -鈹 $60K 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 鈹 -鈹 $40K 鈹 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 鈹 -鈹 鈹斺敩鈹鈹鈹鈹鈹攢鈹鈹鈹鈹攢鈹鈹鈹鈹攢鈹鈹鈹鈹攢鈹鈹鈹鈹攢鈹鈹鈹鈹攢鈹鈹鈹鈹攢鈹鈹鈹鈹攢鈹鈹 鈹 -鈹 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec 鈹 -鈹 鈹 -鈹 Legend: 鈻堚枅 B谩sico 鈻堚枅 Profesional 鈻堚枅 Enterprise 鈹 -鈹 鈹 -鈹 馃搲 Churn Rate (por cohorte) 鈹 -鈹 Mes 1: 5% Mes 3: 3% Mes 6: 2% Mes 12: 1% 鈹 -鈹 鈹 -鈹 馃幆 Activaci贸n Rate (primeros 7 d铆as) 鈹 -鈹 85% de nuevos tenants activan 鈮3 m贸dulos 鈹 -鈹 92% suben 鈮1 proyecto 鈹 -鈹 78% invitan 鈮3 usuarios 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 馃敀 Seguridad y Compliance - -### Configuraci贸n de Seguridad (Tenant Admin) - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Tenant: Constructora ABC - Seguridad 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 [General] [M贸dulos] [Usuarios] [Facturaci贸n] [Seguridad] 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃攼 Autenticaci贸n 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈽戯笍 Requerir 2FA para todos los usuarios 鈹 -鈹 鈽戯笍 Forzar cambio de contrase帽a cada 90 d铆as 鈹 -鈹 鈽戯笍 Pol铆tica de contrase帽as fuertes 鈹 -鈹 鈥 M铆nimo 12 caracteres 鈹 -鈹 鈥 Incluir may煤sculas, min煤sculas, n煤meros y s铆mbolos 鈹 -鈹 鈽 Permitir login con Google Workspace 鈹 -鈹 鈽 Permitir login con Microsoft 365 鈹 -鈹 鈹 -鈹 馃寪 Control de Acceso 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈽戯笍 Restringir acceso por IP 鈹 -鈹 IPs permitidas: 鈹 -鈹 鈥 201.123.45.0/24 (Oficina central) 鈹 -鈹 鈥 189.234.56.0/24 (Oficina regional) 鈹 -鈹 [+ Agregar IP] 鈹 -鈹 鈹 -鈹 鈽戯笍 Limitar sesiones concurrentes 鈹 -鈹 M谩ximo de sesiones por usuario: [3___] 鈹 -鈹 鈹 -鈹 馃摑 Audit Log 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈽戯笍 Registrar todos los cambios en datos sensibles 鈹 -鈹 鈽戯笍 Registrar intentos fallidos de login 鈹 -鈹 Retenci贸n de logs: [90 d铆as 鈻糫 鈹 -鈹 鈹 -鈹 [Ver Audit Log Completo] 鈹 -鈹 鈹 -鈹 馃敀 Encriptaci贸n 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鉁 Datos en reposo: AES-256 鈹 -鈹 鉁 Datos en tr谩nsito: TLS 1.3 鈹 -鈹 鉁 Backups encriptados 鈹 -鈹 鈹 -鈹 馃搳 Compliance 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鉁 GDPR (Europa) 鈹 -鈹 鉁 LFPDPPP (M茅xico) 鈹 -鈹 鈴 SOC 2 Type II (en proceso) 鈹 -鈹 鈹 -鈹 [Guardar Cambios] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 馃洜锔 Soporte y Troubleshooting - -### Panel de Soporte (Support Role) - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Soporte - Constructora ABC 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃攳 Informaci贸n del Tenant 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 ID: f47ac10b-58cc-4372-a567-0e02b2c3d479 鈹 -鈹 Subdomain: constructora-abc 鈹 -鈹 Plan: Enterprise 鈹 -鈹 Creado: 2024-06-01 鈹 -鈹 脷ltimo acceso: Hace 2 horas 鈹 -鈹 Usuarios activos: 18/25 鈹 -鈹 Proyectos: 12 鈹 -鈹 鈹 -鈹 馃搳 Salud del Sistema 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鉁 API Response Time: 145ms (p95) 鈹 -鈹 鉁 Database Query Time: 78ms (p95) 鈹 -鈹 鉁 Uptime (30 d铆as): 99.97% 鈹 -鈹 鈿狅笍 Storage Usage: 187 GB / 200 GB (93%) 鈹 -鈹 鈹 -鈹 馃悰 Errores Recientes (煤ltimas 24h) 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈥 3 errores 500 (Internal Server Error) 鈹 -鈹 鈥 12 errores 404 (Not Found) 鈹 -鈹 鈥 0 errores cr铆ticos 鈹 -鈹 [Ver Logs Completos] 鈹 -鈹 鈹 -鈹 馃帿 Tickets de Soporte 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 #12345 - No puedo exportar reportes (Abierto) 鈹 -鈹 #12344 - Lentitud en m贸dulo de presupuestos (Resuelto) 鈹 -鈹 #12340 - Error al subir planos (Cerrado) 鈹 -鈹 [Ver Todos los Tickets] 鈹 -鈹 鈹 -鈹 馃敡 Acciones de Soporte 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 [Impersonar Usuario] [Ejecutar Query] [Ver Logs] 鈹 -鈹 [Limpiar Cache] [Reiniciar Jobs] [Generar Reporte] 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 馃攧 Ciclo de Vida del Tenant - -### Estados y Transiciones - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Estado del Tenant: Constructora ABC 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 Estado actual: 馃煝 Activo 鈹 -鈹 鈹 -鈹 Transiciones permitidas: 鈹 -鈹 鈥 鈫 Suspendido (por falta de pago) 鈹 -鈹 鈥 鈫 Cancelado (a solicitud del cliente) 鈹 -鈹 鈹 -鈹 Historial de estados: 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 2025-11-17 馃煝 Activo (actual) 鈹 -鈹 2024-06-15 馃煝 Activo (conversi贸n de trial) 鈹 -鈹 2024-06-01 馃煛 Trial (14 d铆as) 鈹 -鈹 2024-06-01 鈿 Registering (onboarding) 鈹 -鈹 鈹 -鈹 M茅tricas de retention: 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 D铆as activo: 534 d铆as 鈹 -鈹 LTV actual: $23,256 USD 鈹 -鈹 Churn risk: 馃煝 Bajo (NPS: 78) 鈹 -鈹 鈹 -鈹 [Suspender Tenant] [Cancelar Tenant] [Reactivar] 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 馃摫 Versi贸n M贸vil del Portal - -El Portal de Administraci贸n SaaS tiene una versi贸n m贸vil responsive para: -- Consultar m茅tricas clave (MRR, tenants activos) -- Ver alertas cr铆ticas -- Aprobar/rechazar solicitudes de activaci贸n de m贸dulos -- Responder tickets de soporte urgentes -- Ver estado de facturaci贸n - ---- - -## 馃殌 Pr贸ximas Funcionalidades (Roadmap) - -**Q1 2026:** -- 鉁 Gesti贸n de tenants multi-regi贸n (US, LATAM, EU) -- 鉁 A/B testing de features por cohorte -- 鉁 Automated churn prediction (ML) - -**Q2 2026:** -- 馃搵 White-label para partners -- 馃搵 Reseller management -- 馃搵 API p煤blica para integraciones - -**Q3 2026:** -- 馃搵 Self-service marketplace (partners suben extensiones) -- 馃搵 Automated compliance reporting (SOC2, ISO 27001) - ---- - -**Generado:** 2025-11-17 -**Sistema:** Portal de Administraci贸n SaaS Multi-tenant -**Versi贸n:** 1.0 -**Modelo:** B2B SaaS diff --git a/projects/erp-construccion/docs/01-analisis-referencias/MAPEO-MAI-TO-MGN.md b/projects/erp-construccion/docs/01-analisis-referencias/MAPEO-MAI-TO-MGN.md deleted file mode 100644 index 2711c3820..000000000 --- a/projects/erp-construccion/docs/01-analisis-referencias/MAPEO-MAI-TO-MGN.md +++ /dev/null @@ -1,477 +0,0 @@ -# Mapeo de Modulos: MAI (Construccion) -> MGN (Generico) - -**Documento:** Mapeo de Modulos entre ERP Construccion y ERP Generico -**Fecha:** 2025-11-24 -**Responsable:** Architecture-Analyst -**Version:** 1.0.0 - ---- - -## Proposito - -Este documento define el mapeo entre los modulos del ERP Construccion (prefijo MAI/MAE/MAA) y los modulos del ERP Generico (prefijo MGN), identificando que componentes son reutilizables y cuales son especificos de construccion. - -## Resumen de Reutilizacion - -| Tipo | Cantidad | % | -|------|----------|---| -| **Modulos 100% Genericos** | 5 | 29% | -| **Modulos Parcialmente Genericos** | 6 | 35% | -| **Modulos 100% Especificos** | 6 | 35% | -| **TOTAL** | **17** | 100% | - -**Porcentaje Global de Reutilizacion:** 61% - ---- - -## Mapeo Detallado - -### FASE 1: Alcance Inicial (MAI-XXX) - -#### MAI-001: Fundamentos -> MGN-001: Fundamentos - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 100% GENERICO | -| **Modulo Generico** | MGN-001 Fundamentos | -| **Componentes Genericos** | auth, users, roles, permissions, sessions | -| **Componentes Especificos** | Ninguno | - -**Detalle:** -```yaml -tablas_genericas: - - auth.users - - auth.roles - - auth.permissions - - auth.user_roles - - auth.sessions - - auth.refresh_tokens - -backend_generico: - - AuthModule - - UsersModule - - RolesModule - - PermissionsModule - -frontend_generico: - - LoginPage - - UserManagement - - RoleManagement -``` - ---- - -#### MAI-002: Proyectos y Estructura -> MGN-009: Proyectos + ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 40% GENERICO / 60% ESPECIFICO | -| **Modulo Generico** | MGN-009 Proyectos (base) | -| **Componentes Genericos** | projects base, project_status | -| **Componentes Especificos** | fraccionamientos, etapas, manzanas, lotes, torres, prototipos | - -**Detalle:** -```yaml -tablas_genericas: - - projects.projects (base) - - projects.project_members - -tablas_especificas: - - construction.fraccionamientos - - construction.etapas - - construction.manzanas - - construction.lotes - - construction.torres - - construction.niveles - - construction.departamentos - - construction.prototipos - - construction.lote_prototipo - -backend_especifico: - - FraccionamientosModule - - LotesModule - - PrototiposModule - - EstructuraObraModule - -frontend_especifico: - - FraccionamientoForm - - LotesGrid - - PrototipoSelector - - EstructuraTree -``` - ---- - -#### MAI-003: Presupuestos y Control de Costos -> ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 0% - 100% ESPECIFICO | -| **Modulo Generico** | Ninguno | -| **Componentes Especificos** | presupuestos, APUs, conceptos, explosiones, control costos | - -**Detalle:** -```yaml -tablas_especificas: - - construction.presupuestos - - construction.presupuesto_partidas - - construction.apus # Analisis de Precios Unitarios - - construction.apu_insumos - - construction.conceptos - - construction.explosiones_insumos - - construction.control_costos - - construction.comparativo_presupuestal - -backend_especifico: - - PresupuestosModule - - APUsModule - - ControlCostosModule - -frontend_especifico: - - PresupuestoEditor - - APUForm - - ComparativoPresupuestal - - ExplosionInsumos -``` - ---- - -#### MAI-004: Compras e Inventarios -> MGN-005 + MGN-006 - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 80% GENERICO / 20% ESPECIFICO | -| **Modulos Genericos** | MGN-005 Inventario, MGN-006 Compras | -| **Componentes Genericos** | productos, almacenes, movimientos, ordenes compra, proveedores | -| **Componentes Especificos** | requisiciones obra, almacen por proyecto | - -**Detalle:** -```yaml -tablas_genericas: - - inventory.products - - inventory.warehouses - - inventory.stock_moves - - inventory.stock_quants - - purchase.suppliers - - purchase.purchase_orders - - purchase.purchase_order_lines - -tablas_especificas: - - construction.requisiciones_obra - - construction.requisicion_lineas - - construction.almacen_proyecto # Almacen por obra - -backend_generico: - - ProductsModule - - WarehousesModule - - StockModule - - PurchaseModule - - SuppliersModule - -backend_especifico: - - RequisicionesObraModule - -frontend_especifico: - - RequisicionObraForm - - AlmacenProyectoView -``` - ---- - -#### MAI-005: Control de Obra y Avances -> ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 0% - 100% ESPECIFICO | -| **Modulo Generico** | Ninguno | -| **Componentes Especificos** | avances fisicos, % completado, bitacora, checkpoints | - -**Detalle:** -```yaml -tablas_especificas: - - construction.avances_obra - - construction.avance_conceptos - - construction.bitacora_obra - - construction.checkpoints - - construction.fotos_avance - - construction.programa_obra # Gantt - -backend_especifico: - - AvancesObraModule - - BitacoraModule - - ProgramaObraModule - -frontend_especifico: - - AvanceCapture - - BitacoraView - - GanttChart - - FotosAvance -``` - ---- - -#### MAI-006: Reportes y Analytics -> MGN-014 + MGN-008 - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 60% GENERICO / 40% ESPECIFICO | -| **Modulos Genericos** | MGN-014 Reportes, MGN-008 Analitica | -| **Componentes Genericos** | dashboards base, reportes financieros | -| **Componentes Especificos** | reportes obra, comparativos, avance grafico | - -**Detalle:** -```yaml -generico: - - Dashboard base - - Reportes financieros (P&L, Balance) - - Contabilidad analitica por proyecto - - Graficas base (Chart.js) - -especifico: - - Dashboard Director de Obra - - Dashboard Residente - - Reporte Avance Fisico vs Financiero - - Comparativo Presupuestal - - Curva S - - Reporte INFONAVIT -``` - ---- - -#### MAI-007: RRHH y Asistencias -> MGN-010: RRHH + ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 50% GENERICO / 50% ESPECIFICO | -| **Modulo Generico** | MGN-010 RRHH | -| **Componentes Genericos** | employees, contracts, departments | -| **Componentes Especificos** | asistencias GPS, biometrico, destajo | - -**Detalle:** -```yaml -tablas_genericas: - - hr.employees - - hr.contracts - - hr.departments - - hr.job_positions - -tablas_especificas: - - construction.asistencias - - construction.asistencia_gps - - construction.asistencia_biometrico - - construction.destajo - - construction.destajo_empleado - -backend_especifico: - - AsistenciasModule (GPS) - - BiometricoModule - - DestajoModule - -frontend_especifico: - - AsistenciaCapture (Mobile) - - GPSTracker - - DestajoCalculator -``` - ---- - -#### MAI-008: Estimaciones y Facturacion -> ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 20% GENERICO / 80% ESPECIFICO | -| **Modulo Generico** | MGN-004 Financiero (parcial) | -| **Componentes Especificos** | estimaciones, anticipos, retenciones, generadores | - -**Detalle:** -```yaml -tablas_genericas: - - financial.invoices (base) - -tablas_especificas: - - estimates.estimaciones - - estimates.estimacion_conceptos - - estimates.generadores - - estimates.anticipos - - estimates.retenciones - - estimates.amortizaciones - - estimates.workflow_estimacion - -backend_especifico: - - EstimacionesModule - - GeneradoresModule - - AnticiposModule - - RetencionesModule - -frontend_especifico: - - EstimacionForm - - GeneradorEditor - - AnticiposRetenciones - - WorkflowEstimacion -``` - ---- - -#### MAI-009: Calidad, Postventa y Garantias -> ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 10% GENERICO / 90% ESPECIFICO | -| **Componentes Especificos** | check-lists, punchlists, garantias, tickets postventa | - ---- - -#### MAI-010: CRM Derechohabientes -> MGN-011 + MGN-013 + ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 40% GENERICO / 60% ESPECIFICO | -| **Modulos Genericos** | MGN-011 CRM (parcial), MGN-013 Portal | -| **Componentes Especificos** | derechohabientes INFONAVIT, asignacion vivienda | - ---- - -#### MAI-011: INFONAVIT y Cumplimiento -> ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 0% - 100% ESPECIFICO | -| **Componentes Especificos** | reportes INFONAVIT, actas, cumplimiento regulatorio | - ---- - -#### MAI-012: Contratos y Subcontratos -> ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 20% GENERICO / 80% ESPECIFICO | -| **Componentes Especificos** | contratos subcontratistas, destajo, penalizaciones | - ---- - -#### MAI-013: Administracion y Seguridad -> MGN-001 + MGN-012 - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 80% GENERICO / 20% ESPECIFICO | -| **Modulos Genericos** | MGN-001 Auth, MGN-012 Notificaciones | - ---- - -### FASE 2: Enterprise (MAE-XXX) - -#### MAE-014: Finanzas y Controlling -> MGN-004 + MGN-008 - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 70% GENERICO / 30% ESPECIFICO | - ---- - -#### MAE-015: Activos y Maquinaria -> ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 30% GENERICO / 70% ESPECIFICO | - ---- - -#### MAE-016: Gestion Documental -> MGN-014 (parcial) - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 50% GENERICO / 50% ESPECIFICO | - ---- - -### FASE 3: Avanzada (MAA-XXX) - -#### MAA-017: Seguridad, Riesgos y HSE -> ESPECIFICO - -| Aspecto | Valor | -|---------|-------| -| **Reutilizacion** | 0% - 100% ESPECIFICO | - ---- - -## Matriz de Dependencias - -``` -MGN-001 (Fundamentos) <-- MAI-001 (100%) - <-- MAI-013 (80%) - -MGN-004 (Financiero) <-- MAI-008 (20%) - <-- MAE-014 (70%) - -MGN-005 (Inventario) <-- MAI-004 (80%) - -MGN-006 (Compras) <-- MAI-004 (80%) - -MGN-008 (Analitica) <-- MAI-006 (60%) - <-- MAE-014 (70%) - -MGN-009 (Proyectos) <-- MAI-002 (40%) - -MGN-010 (RRHH) <-- MAI-007 (50%) - -MGN-011 (CRM) <-- MAI-010 (40%) - -MGN-013 (Portal) <-- MAI-010 (40%) - -MGN-014 (Reportes) <-- MAI-006 (60%) - <-- MAE-016 (50%) -``` - ---- - -## Estrategia de Implementacion - -### Paso 1: Dependencias del ERP Generico -```json -{ - "dependencies": { - "@erp-generic/auth": "^1.0.0", - "@erp-generic/core": "^1.0.0", - "@erp-generic/financial": "^1.0.0", - "@erp-generic/inventory": "^1.0.0", - "@erp-generic/purchasing": "^1.0.0", - "@erp-generic/analytics": "^1.0.0", - "@erp-generic/hr": "^1.0.0", - "@erp-generic/ui-components": "^1.0.0" - } -} -``` - -### Paso 2: Extension de Modulos Genericos -```typescript -// Ejemplo: Extender proyecto generico para construccion -import { Project } from '@erp-generic/projects'; - -export class Fraccionamiento extends Project { - etapas: Etapa[]; - manzanas: Manzana[]; - // Especifico de construccion -} -``` - -### Paso 3: Modulos Especificos de Construccion -``` -@construccion/presupuestos -@construccion/estimaciones -@construccion/avances-obra -@construccion/infonavit -@construccion/calidad -``` - ---- - -## Referencias - -- [ERP Generico - Lista de Modulos](/projects/erp-generic/docs/01-definicion-modulos/LISTA-MODULOS-ERP-GENERICO.md) -- [ERP Generico - Alcance por Modulo](/projects/erp-generic/docs/01-definicion-modulos/ALCANCE-POR-MODULO.md) -- [Retroalimentacion ERP Construccion](/projects/erp-generic/docs/01-definicion-modulos/RETROALIMENTACION-ERP-CONSTRUCCION.md) - ---- - -**Ultima actualizacion:** 2025-11-24 -**Version:** 1.0.0 diff --git a/projects/erp-construccion/docs/01-analisis-referencias/README.md b/projects/erp-construccion/docs/01-analisis-referencias/README.md deleted file mode 100644 index ee91ecfea..000000000 --- a/projects/erp-construccion/docs/01-analisis-referencias/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Analisis de Referencias - ERP Construccion - -**Proyecto:** ERP Construccion -**Fecha:** 2025-11-24 -**Responsable:** Architecture-Analyst - ---- - -## Proposito - -Esta carpeta contiene el analisis de referencias externas y retroalimentacion recibida del ERP Generico para mejorar la documentacion y arquitectura del ERP de Construccion. - -## Estructura - -``` -00-analisis-referencias/ -+-- README.md # Este archivo -+-- MAPEO-MAI-TO-MGN.md # Mapeo de modulos MAI -> MGN -+-- GAP-ANALYSIS-CONSOLIDADO.md # Gaps identificados (futuro) -+-- odoo/ # Analisis de modulos Odoo relevantes -| +-- README.md -+-- gamilit/ # Patrones adoptados de Gamilit -| +-- README.md -+-- erp-generico/ # Retroalimentacion del ERP Generico - +-- README.md -``` - -## Contenido - -### 1. Analisis de Odoo (`odoo/`) -Analisis de modulos de Odoo relevantes para la industria de construccion: -- `base`: Fundamentos y multi-tenancy -- `account`: Contabilidad y facturacion -- `stock`: Inventarios y almacenes -- `purchase`: Compras y proveedores -- `project`: Gestion de proyectos -- `hr`: Recursos humanos - -### 2. Patrones de Gamilit (`gamilit/`) -Patrones arquitectonicos adoptados de Gamilit: -- Database multi-schema -- SSOT (Single Source of Truth) -- Feature-Sliced Design -- Backend patterns -- DevOps automation - -### 3. Retroalimentacion ERP Generico (`erp-generico/`) -Documentacion de retroalimentacion recibida: -- Componentes genericos identificados (61%) -- Componentes especificos de construccion (39%) -- Mejoras arquitectonicas recomendadas -- Gap analysis - -## Mapeo de Modulos - -Ver [MAPEO-MAI-TO-MGN.md](./MAPEO-MAI-TO-MGN.md) para el mapeo completo entre: -- **MAI-XXX**: Modulos de Alcance Inicial (ERP Construccion) -- **MGN-XXX**: Modulos Genericos (ERP Generico) - -## Referencias Cruzadas - -| Fuente | Ubicacion | Contenido | -|--------|-----------|-----------| -| ERP Generico - Retroalimentacion | `/projects/erp-generic/docs/00-analisis-referencias/construccion/` | Analisis completo | -| ERP Generico - Gap Analysis | `/projects/erp-generic/docs/01-definicion-modulos/RETROALIMENTACION-ERP-CONSTRUCCION.md` | Gaps y mejoras | -| Odoo Analisis | `/shared/reference/ODOO-MODULES-ANALYSIS.md` | Analisis de Odoo | -| Gamilit Reference | `/shared/reference/gamilit/` | Patrones Gamilit | - ---- - -**Ultima actualizacion:** 2025-11-24 diff --git a/projects/erp-construccion/docs/01-analisis-referencias/erp-generico/README.md b/projects/erp-construccion/docs/01-analisis-referencias/erp-generico/README.md deleted file mode 100644 index 25b81b801..000000000 --- a/projects/erp-construccion/docs/01-analisis-referencias/erp-generico/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# Retroalimentacion del ERP Generico - ERP Construccion - -**Fecha:** 2025-11-24 -**Responsable:** Architecture-Analyst - ---- - -## Proposito - -Documentar la retroalimentacion recibida del ERP Generico para mejorar la documentacion y arquitectura del ERP de Construccion. - -## Resumen de Retroalimentacion - -### Componentes Identificados - -| Categoria | Total | Genericos | Especificos | % Reutilizacion | -|-----------|-------|-----------|-------------|-----------------| -| Schemas DB | 7 | 5 | 2 | 71% | -| Tablas DB | 67 | 44 | 23 | 66% | -| Modulos Backend | 12 | 8 | 4 | 67% | -| Componentes Frontend | 52 | 31 | 21 | 60% | -| **TOTAL** | **138** | **88** | **50** | **64%** | - -### Gaps Funcionales Identificados - -| Prioridad | Cantidad | Ejemplos | -|-----------|----------|----------| -| P0 (Criticos) | 12 | Contabilidad analitica, SSOT, Docker | -| P1 (Altos) | 16 | Portal, Tracking, 2FA | -| P2 (Medios) | 8 | Ubicaciones jerarquicas, OAuth | -| **TOTAL** | **36** | - | - -### Mejoras Arquitectonicas Recomendadas - -1. **[P0] Arquitectura Multi-Schema** - Reorganizar BD -2. **[P0] Sistema SSOT** - Eliminar duplicacion de constantes -3. **[P0] Contabilidad Analitica** - P&L por proyecto -4. **[P1] Feature-Sliced Design** - Arquitectura frontend -5. **[P1] mail.thread Pattern** - Tracking automatico -6. **[P1] Portal de Usuarios** - Para derechohabientes -7. **[P1] Test Coverage 70%+** - Evitar deuda tecnica -8. **[P2] Docker + CI/CD** - Deployment automatizado -9. **[P2] ORM (TypeORM/Prisma)** - Type safety en queries - -## Documentos de Referencia - -### En ERP Generico - -| Documento | Ubicacion | Contenido | -|-----------|-----------|-----------| -| RETROALIMENTACION.md | `/projects/erp-generic/docs/00-analisis-referencias/construccion/` | Retroalimentacion consolidada | -| GAP-ANALYSIS.md | `/projects/erp-generic/docs/00-analisis-referencias/construccion/` | 42 gaps identificados | -| COMPONENTES-GENERICOS.md | `/projects/erp-generic/docs/00-analisis-referencias/construccion/` | 143 componentes genericos | -| COMPONENTES-ESPECIFICOS.md | `/projects/erp-generic/docs/00-analisis-referencias/construccion/` | 67 componentes especificos | -| MEJORAS-ARQUITECTONICAS.md | `/projects/erp-generic/docs/00-analisis-referencias/construccion/` | 15 mejoras recomendadas | -| RETROALIMENTACION-ERP-CONSTRUCCION.md | `/projects/erp-generic/docs/01-definicion-modulos/` | Retroalimentacion detallada | - -### En ERP Construccion - -| Documento | Ubicacion | Contenido | -|-----------|-----------|-----------| -| PLAN-RETROALIMENTACION.md | `/projects/erp-construccion/docs/` | Plan de implementacion | -| MAPEO-MAI-TO-MGN.md | `/projects/erp-construccion/docs/00-analisis-referencias/` | Mapeo de modulos | -| ADRs | `/projects/erp-construccion/docs/adr/` | 12 decisiones arquitectonicas | - -## Acciones Tomadas - -### Sprint 1 (2025-11-24) -- [x] Crear 12 ADRs adaptados de ERP Generico -- [x] Crear estructura 00-analisis-referencias/ -- [x] Crear MAPEO-MAI-TO-MGN.md -- [ ] Actualizar README principal - -### Pendientes (Sprints 2-5) -- [ ] Crear estructura 02-modelado/ -- [ ] Crear trazabilidad centralizada -- [ ] Crear database-design/ -- [ ] Mejorar documentacion existente - -## ROI Esperado - -- **Reutilizacion:** 64% de componentes del ERP Generico -- **Reduccion desarrollo:** -36% en tiempo -- **Reduccion bugs:** -70% con test coverage 70%+ -- **ROI Total:** 3.5x en 18 meses - ---- - -**Estado:** Retroalimentacion documentada y en proceso de implementacion -**Proxima revision:** Al completar Sprint 1 diff --git a/projects/erp-construccion/docs/01-analisis-referencias/gamilit/README.md b/projects/erp-construccion/docs/01-analisis-referencias/gamilit/README.md deleted file mode 100644 index 1b971abad..000000000 --- a/projects/erp-construccion/docs/01-analisis-referencias/gamilit/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Patrones de Gamilit - ERP Construccion - -**Fecha:** 2025-11-24 -**Responsable:** Architecture-Analyst - ---- - -## Proposito - -Documentar los patrones arquitectonicos de Gamilit adoptados para el ERP de Construccion. - -## Patrones Adoptados - -### 1. Database Multi-Schema -**Referencia:** ADR-007 - -``` -Schemas propuestos para Construccion: -+-- auth_management # Autenticacion (GENERICO) -+-- core # Catalogos (GENERICO) -+-- financial_management # Financiero (GENERICO) -+-- inventory_management # Inventarios (GENERICO) -+-- purchasing_management # Compras (GENERICO) -+-- construction_mgmt # ESPECIFICO: Obras, lotes, prototipos -+-- infonavit_compliance # ESPECIFICO: INFONAVIT -+-- estimates_mgmt # ESPECIFICO: Estimaciones -``` - -**Estado:** Adoptado - Pendiente implementacion - -### 2. Sistema SSOT (Single Source of Truth) -**Referencia:** ADR-004 - -- Backend como fuente de verdad -- Script `sync-enums.ts` para sincronizar -- Validacion pre-commit - -**Estado:** Adoptado - Pendiente implementacion - -### 3. Feature-Sliced Design (Frontend) -**Referencia:** ADR-009 - -``` -frontend/src/ -+-- shared/ # Componentes reutilizables -+-- features/ -| +-- director/ # Dashboard director -| +-- residente/ # Vistas residente -| +-- almacenista/ # Inventarios -| +-- portal/ # Derechohabientes -+-- pages/ -+-- app/ -``` - -**Estado:** Adoptado - Pendiente migracion - -### 4. Path Aliases -**Referencia:** ADR-005 - -- `@shared` - Componentes compartidos -- `@modules` - Modulos de negocio -- `@construccion` - Especificos de construccion - -**Estado:** Adoptado - Pendiente configuracion - -### 5. RLS Policies -**Referencia:** ADR-006 - -- Row-Level Security en PostgreSQL -- Policies por tenant y rol -- 159+ policies planeadas - -**Estado:** Parcialmente implementado (~20 policies) - -## Patrones NO Adoptados (Gaps de Gamilit) - -| Patron | Razon | Alternativa | -|--------|-------|-------------| -| Sin Docker | Gamilit no tiene Docker | Implementar Docker (ADR recomendado) | -| Sin CI/CD | Gamilit deployment manual | Implementar GitHub Actions | -| 14% Test Coverage | Inaceptable | Objetivo 70%+ (ADR-010) | - -## Referencias - -- [Gamilit Database Architecture](/shared/reference/gamilit/database-architecture.md) -- [Gamilit Backend Patterns](/shared/reference/gamilit/backend-patterns.md) -- [Gamilit Frontend Patterns](/shared/reference/gamilit/frontend-patterns.md) -- [Gamilit SSOT System](/shared/reference/gamilit/ssot-system.md) -- [ERP Generico - Analisis Gamilit](/projects/erp-generic/docs/00-analisis-referencias/gamilit/) - ---- - -**Estado:** Patrones documentados y adoptados via ADRs diff --git a/projects/erp-construccion/docs/01-analisis-referencias/odoo/ODOO-CONSTRUCCION-MAPPING.md b/projects/erp-construccion/docs/01-analisis-referencias/odoo/ODOO-CONSTRUCCION-MAPPING.md deleted file mode 100644 index 38adfeb97..000000000 --- a/projects/erp-construccion/docs/01-analisis-referencias/odoo/ODOO-CONSTRUCCION-MAPPING.md +++ /dev/null @@ -1,373 +0,0 @@ -# Mapeo Odoo - ERP Construccion - -**Version:** 1.0.0 -**Fecha:** 2025-12-05 -**Responsable:** Requirements-Analyst - ---- - -## Resumen - -Este documento detalla el mapeo entre los modulos de Odoo Community y los modulos del ERP Construccion, incluyendo que funcionalidades se adoptan, adaptan o crean desde cero. - ---- - -## Matriz de Mapeo por Modulo - -### MAI-001: Autenticacion y Multi-tenancy - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Login/Logout | `auth_signup`, `base` | Adaptar | JWT en lugar de sesiones | -| Multi-company | `base.res_company` | Adaptar | Schema-level RLS vs company_id | -| Roles y permisos | `base.res_groups` | Crear nuevo | RBAC con claims personalizados | -| 2FA | `auth_totp` | Adoptar patron | Implementar TOTP compatible | -| SSO | No nativo | Crear nuevo | SAML/OIDC para enterprise | - -**Patrones Adoptados:** -- Concepto de `company_id` como filtro global -- Grupos de permisos jerarquicos -- Modelo de usuario con partner asociado - ---- - -### MAI-002: Gestion de Proyectos - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Proyectos | `project.project` | Adoptar | Extender con campos construccion | -| Tareas | `project.task` | Adaptar | Convertir en actividades de obra | -| Stages | `project.task.type` | Adoptar | Personalizar para construccion | -| Milestones | `project.milestone` | Adoptar | Hitos de obra | -| Team | `project.tags` | Crear nuevo | Equipo de proyecto dedicado | - -**Mapeo de Estados:** -``` -Odoo: draft -> open -> pending -> done -> cancel -Ours: PLANNING -> IN_PROGRESS -> PAUSED -> COMPLETED -> CANCELLED -``` - -**Campos Adicionales (no en Odoo):** -- `progress_percentage` - Avance calculado -- `development_id` - Vinculo con desarrollo inmobiliario -- `infonavit_registration` - Registro INFONAVIT -- `contractor_id` - Contratista principal - ---- - -### MAI-003: Presupuestos y APU - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Catalogo conceptos | No existe | Crear nuevo | Catalogo CMIC base | -| APU (Analisis Precios) | No existe | Crear nuevo | Formato mexicano | -| Presupuesto base | `account.budget` | Referencia | Concepto similar, diferente impl | -| Partidas | No existe | Crear nuevo | Jerarquia de costos | -| Versiones | No existe | Crear nuevo | Control de versiones | - -**Estructura APU (Creacion propia):** -``` -BudgetItem (Partida) -鈹溾攢鈹 concept_code -鈹溾攢鈹 description -鈹溾攢鈹 unit -鈹溾攢鈹 quantity -鈹溾攢鈹 unit_price (compuesto de): -鈹 鈹溾攢鈹 materials[] -鈹 鈹溾攢鈹 labor[] -鈹 鈹溾攢鈹 equipment[] -鈹 鈹斺攢鈹 indirect_costs -鈹斺攢鈹 total_amount -``` - ---- - -### MAI-004: Compras e Inventario - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Ordenes de compra | `purchase.order` | Adoptar | Estados identicos | -| Lineas de compra | `purchase.order.line` | Adoptar | Con proyecto vinculado | -| Proveedores | `res.partner` (supplier) | Adoptar | Con campos adicionales | -| Almacenes | `stock.warehouse` | Adoptar | Multi-almacen por proyecto | -| Movimientos | `stock.move` | Adoptar | Trazabilidad completa | -| Inventario | `stock.quant` | Adoptar | Stock actual calculado | -| Requisiciones | `purchase.requisition` | Adoptar | Flujo de requisicion | - -**Estados de Orden de Compra:** -``` -Odoo: draft -> sent -> to_approve -> purchase -> done -> cancel -Ours: DRAFT -> SENT -> TO_APPROVE -> APPROVED -> RECEIVED -> CANCELLED -``` - -**Campos Adicionales:** -- `project_id` - Vinculo con proyecto -- `budget_item_id` - Partida de presupuesto -- `cost_center` - Centro de costos - ---- - -### MAI-005: Avances de Obra - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Registro avances | No existe | Crear nuevo | % completado por actividad | -| Curva S | No existe | Crear nuevo | Grafico planeado vs real | -| Cronograma | `project.task` (dates) | Adaptar | Extender con Gantt | -| Evidencia fotografica | No existe | Crear nuevo | Fotos con geolocalizacion | - -**Modelo de Avance (Creacion propia):** -``` -ProgressEntry -鈹溾攢鈹 activity_id -鈹溾攢鈹 report_date -鈹溾攢鈹 planned_percentage -鈹溾攢鈹 actual_percentage -鈹溾攢鈹 quantity_completed -鈹溾攢鈹 photos[] (with GPS) -鈹斺攢鈹 notes -``` - ---- - -### MAI-006: Finanzas y Contabilidad - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Plan de cuentas | `account.account` | Adaptar | Cuenta contable mexicana | -| Polizas | `account.move` | Adoptar | Journal entries | -| Lineas contables | `account.move.line` | Adoptar | Con cuenta analitica | -| Cuenta analitica | `account.analytic` | Adoptar | Por proyecto | -| Facturas | `account.move` (invoice) | Adaptar | CFDI requerido | -| Pagos | `account.payment` | Adoptar | Multiples metodos | -| Conciliacion | `account.bank.statement` | Adoptar | Conciliacion bancaria | - -**Integracion CFDI (Creacion propia):** -- PAC integration -- Timbrado automatico -- Descarga masiva SAT -- Cancelacion CFDI - ---- - -### MAI-007: Nomina - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Empleados | `hr.employee` | Adoptar | Con CURP, RFC, IMSS | -| Contratos | `hr.contract` | Adoptar | Tipos mexicanos | -| Nomina | `hr.payslip` | Referencia | Muy diferente, crear nuevo | -| Percepciones/Ded | No compatible | Crear nuevo | Ley mexicana | -| IMSS | No existe | Crear nuevo | SUA compatible | -| ISN | No existe | Crear nuevo | Impuesto estatal | - -**Nota:** La nomina mexicana difiere significativamente de Odoo. Se toma como referencia la arquitectura pero se implementa logica propia para: -- Tabla ISR -- Calculo IMSS patron/trabajador -- PTU -- Aguinaldo -- Prima vacacional - ---- - -### MAI-008: Estimaciones INFONAVIT - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Todo el modulo | No existe | Crear nuevo | 100% propietario | - -**Funcionalidades propias:** -- Generacion de estimaciones por vivienda -- Calculo automatico de avances -- Formato INFONAVIT oficial -- Validacion de requisitos -- Envio electronico - ---- - -### MAI-009: Calidad - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Puntos de control | `quality.point` | Adoptar | Checklist de calidad | -| Inspecciones | `quality.check` | Adoptar | Con evidencia | -| No conformidades | `quality.alert` | Adoptar | Workflow de correccion | -| Acciones correctivas | No nativo completo | Crear nuevo | CAPA process | - ---- - -### MAI-010: Portal Derechohabientes - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Portal base | `portal` | Referencia | UI completamente diferente | -| Autenticacion | `auth_signup` | Referencia | Con CURP validation | -| Documentos | `documents` | Referencia | Firma electronica NOM-151 | - -**Funcionalidades propias:** -- Vinculacion por NSS INFONAVIT -- Estado de vivienda -- Documentos de entrega -- Citas de entrega -- Garantias post-venta - ---- - -### MAI-011: Cumplimiento INFONAVIT - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Todo el modulo | No existe | Crear nuevo | 100% propietario | - -**Funcionalidades propias:** -- Catalogo de requisitos por programa -- Checklist de cumplimiento -- Evidencia documental -- Auditorias de verificacion -- Alertas de vencimiento - ---- - -### MAE-014: Modulo Financiero Avanzado - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| CxP avanzado | `account` | Extender | Flujos de aprobacion | -| CxC avanzado | `account` | Extender | Cobranza automatizada | -| Flujo de caja | No nativo | Crear nuevo | Proyeccion y control | -| Presupuesto financiero | `account.budget` | Adaptar | Por proyecto | -| Reportes SAT | No existe | Crear nuevo | DIOT, Balanza, etc | - ---- - -### MAE-015: Gestion de Activos - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Catalogo activos | `account.asset` | Adoptar | Registro de activos | -| Depreciacion | `account.asset` | Adoptar | Metodos mexicanos | -| Mantenimiento | `maintenance.request` | Adoptar | Preventivo/correctivo | -| Ordenes trabajo | `maintenance.request` | Extender | Workflow completo | -| GPS/Geofence | No existe | Crear nuevo | Tracking en tiempo real | - ---- - -### MAE-016: Gestion Documental - -| Funcionalidad | Modulo Odoo | Estrategia | Notas | -|---------------|-------------|------------|-------| -| Carpetas | `documents.folder` | Adoptar | Estructura jerarquica | -| Documentos | `documents.document` | Adoptar | Con versionado | -| Tags | `documents.tag` | Adoptar | Categorizacion | -| Workflow | `documents.workflow` | Referencia | Aprobaciones custom | -| Planos | No existe | Crear nuevo | Gestion de revisiones | - ---- - -## Patrones Arquitectonicos de Odoo Adoptados - -### 1. State Machine Pattern - -Todos los documentos principales usan estados: -```python -# Odoo pattern -state = fields.Selection([ - ('draft', 'Draft'), - ('confirm', 'Confirmed'), - ('done', 'Done'), - ('cancel', 'Cancelled') -]) -``` - -```typescript -// Our adaptation -enum DocumentStatus { - DRAFT = 'draft', - CONFIRMED = 'confirmed', - DONE = 'done', - CANCELLED = 'cancelled' -} -``` - -### 2. Analytic Distribution - -Costos distribuidos entre proyectos/centros: -```python -# Odoo -analytic_distribution = fields.Json() -# {"project_1_id": 60, "project_2_id": 40} -``` - -```typescript -// Our adaptation -@Column('jsonb') -analyticDistribution: Record; -``` - -### 3. Mail Thread (Audit Trail) - -Tracking de cambios en entidades: -```python -# Odoo -_inherit = ['mail.thread'] -name = fields.Char(tracking=True) -``` - -```typescript -// Our adaptation -@EventSubscriber() -class AuditTrailSubscriber { - @AfterUpdate() - logChanges(event: UpdateEvent) { } -} -``` - -### 4. Computed Fields with Store - -Campos calculados que se almacenan: -```python -# Odoo -@api.depends('line_ids.amount') -def _compute_total(self): - self.total = sum(self.line_ids.mapped('amount')) - -total = fields.Float(compute='_compute_total', store=True) -``` - -```typescript -// Our adaptation -@BeforeInsert() -@BeforeUpdate() -computeTotal() { - this.total = this.lines.reduce((sum, l) => sum + l.amount, 0); -} -``` - ---- - -## Modulos Odoo No Utilizados - -Los siguientes modulos de Odoo no aplican directamente: - -| Modulo | Razon | -|--------|-------| -| `website`, `website_sale` | Frontend custom en React | -| `crm` | No requerido para construccion | -| `mrp` | Manufactura no aplica | -| `fleet` | Activos propios implementados | -| `event` | No requerido | -| `pos` | No requerido | -| `lunch` | No requerido | -| `l10n_*` | Localizacion propia para Mexico | - ---- - -## Referencias - -- [GUIA-USO-REFERENCIAS-ODOO.md](../../GUIA-USO-REFERENCIAS-ODOO.md) -- [Odoo Developer Documentation](https://www.odoo.com/documentation) -- [Backend Specifications](../../05-backend-specs/) - ---- - -*Ultima actualizacion: 2025-12-05* diff --git a/projects/erp-construccion/docs/01-analisis-referencias/odoo/README.md b/projects/erp-construccion/docs/01-analisis-referencias/odoo/README.md deleted file mode 100644 index bf7083743..000000000 --- a/projects/erp-construccion/docs/01-analisis-referencias/odoo/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Analisis de Odoo - ERP Construccion - -**Fecha:** 2025-12-05 -**Responsable:** Requirements-Analyst - ---- - -## Contenido - -| Documento | Descripcion | -|-----------|-------------| -| [README.md](README.md) | Este archivo - Indice y resumen | -| [ODOO-CONSTRUCCION-MAPPING.md](ODOO-CONSTRUCCION-MAPPING.md) | Mapeo detallado Odoo vs ERP Construccion | - ---- - -## Proposito - -Documentar el analisis de modulos de Odoo relevantes para la industria de construccion y su aplicabilidad al ERP Construccion. - -## Resumen de Mapeo - -### Modulos Adoptados de Odoo - -| Modulo Odoo | Modulo ERP | Nivel Adopcion | -|-------------|------------|----------------| -| `project` | MAI-002 | Alto (70%) | -| `purchase` | MAI-004 | Alto (80%) | -| `stock` | MAI-004 | Alto (80%) | -| `account` | MAI-006 | Medio (60%) | -| `hr` | MAI-007 | Bajo (40%) | -| `quality` | MAI-009 | Medio (50%) | -| `documents` | MAE-016 | Medio (50%) | -| `maintenance` | MAE-015 | Medio (60%) | - -### Modulos Creados Desde Cero - -| Modulo ERP | Razon | -|------------|-------| -| MAI-003 (Presupuestos APU) | No existe en Odoo, formato mexicano | -| MAI-005 (Avances de Obra) | Funcionalidad muy especifica | -| MAI-008 (Estimaciones INFONAVIT) | 100% propietario | -| MAI-010 (Portal Derechohabientes) | Requiere firma NOM-151 | -| MAI-011 (Cumplimiento INFONAVIT) | 100% propietario | -| MAE-014 (Finanzas MX) | Requiere CFDI/SAT | - ---- - -## Patrones Adoptados de Odoo - -### 1. Multi-Company (res.company) -- Adoptado en ADR-003 (Multi-tenancy) -- Implementacion: Schema-level RLS (mas robusto que company_id) - -### 2. State Machine Pattern -- Todos los documentos usan estados similares -- draft -> confirmed -> done -> cancelled - -### 3. Contabilidad Analitica (account.analytic) -- Critico para reportes por proyecto -- Campo `analytic_account_id` universal - -### 4. mail.thread (Audit Trail) -- Tracking de cambios en entidades criticas -- Implementado via EventSubscriber - -### 5. Computed Fields with Store -- Campos calculados que se persisten -- Implementado con @BeforeInsert/@BeforeUpdate - ---- - -## Referencias - -- [GUIA-USO-REFERENCIAS-ODOO.md](../../GUIA-USO-REFERENCIAS-ODOO.md) -- [ODOO-CONSTRUCCION-MAPPING.md](ODOO-CONSTRUCCION-MAPPING.md) -- [Backend Specifications](../../05-backend-specs/) - ---- - -**Estado:** Documentacion completa -**Ultima actualizacion:** 2025-12-05 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/ANALISIS-REUTILIZACION-GAMILIT.md b/projects/erp-construccion/docs/02-definicion-modulos/ANALISIS-REUTILIZACION-GAMILIT.md deleted file mode 100644 index 0d1c677e4..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/ANALISIS-REUTILIZACION-GAMILIT.md +++ /dev/null @@ -1,477 +0,0 @@ -# An谩lisis de Reutilizaci贸n de Componentes GAMILIT para MVP Inmobiliario - -**Proyecto Origen:** GAMILIT (Plataforma Educativa) -**Proyecto Destino:** Sistema de Administraci贸n de Obra e INFONAVIT -**Fecha An谩lisis:** 2025-11-17 -**Stack Tecnol贸gico:** Node.js + Express + TypeScript | React + Vite | PostgreSQL - ---- - -## Resumen Ejecutivo - -Este documento analiza qu茅 componentes del proyecto GAMILIT pueden ser reutilizados en el desarrollo del MVP inmobiliario, estimando el ahorro de tiempo y esfuerzo. - -**Resultado estimado:** -- **Reducci贸n de tiempo de desarrollo:** 30-40% (similar a lo estimado en el MVP-APP.md) -- **C贸digo ya probado en producci贸n:** ~60% de la infraestructura base -- **Ahorro estimado:** ~6-8 semanas de desarrollo - ---- - -## 1. Componentes Reutilizables de GAMILIT - -### 1.1 Infraestructura Base (Reutilizaci贸n: 90%) - -#### Backend (Node.js + Express + TypeScript) - -| Componente | Archivo Origen GAMILIT | Aplicaci贸n Inmobiliaria | Adaptaci贸n Requerida | -|------------|------------------------|-------------------------|---------------------| -| **Sistema de Autenticaci贸n JWT** | `apps/backend/src/modules/auth/` | Autenticaci贸n de usuarios (Direcci贸n, Ingenier铆a, Residentes, etc.) | M铆nima - Ajustar roles | -| **RBAC (Roles y Permisos)** | `apps/backend/src/shared/guards/roles.guard.ts` | Sistema de permisos por perfil (7 roles del MVP) | Media - Adaptar roles espec铆ficos | -| **Multi-tenancy** | `apps/database/ddl/schemas/auth_management/` | Soporte de m煤ltiples constructoras | M铆nima - Ya implementado | -| **Row Level Security (RLS)** | Pol铆ticas RLS en PostgreSQL | Seguridad a nivel de datos por proyecto/obra | Media - Adaptar pol铆ticas | -| **Sistema de Auditor铆a** | `apps/database/ddl/schemas/audit_logging/` | Bit谩cora de actividades y cambios cr铆ticos | M铆nima - Reutilizaci贸n directa | -| **Middleware de Validaci贸n** | `apps/backend/src/shared/middleware/` | Validaci贸n de entrada en endpoints | M铆nima - Reutilizaci贸n directa | -| **Manejo de Errores** | `apps/backend/src/shared/filters/` | Gesti贸n centralizada de errores | Ninguna - Reutilizaci贸n directa | -| **Logging Estructurado** | Winston/Pino config | Logs estructurados con niveles | Ninguna - Reutilizaci贸n directa | -| **Gesti贸n de Archivos** | Sistema de uploads | Gesti贸n de documentos, planos, fotos | Baja - Adaptaci贸n de categor铆as | - -**Ahorro estimado Backend:** 3-4 semanas - ---- - -#### Frontend Web (React + Vite + TypeScript) - -| Componente | Archivo Origen GAMILIT | Aplicaci贸n Inmobiliaria | Adaptaci贸n Requerida | -|------------|------------------------|-------------------------|---------------------| -| **Componentes UI Base** | `apps/frontend/src/components/ui/` | Botones, Inputs, Modales, Alerts | Ninguna - Reutilizaci贸n directa | -| **Sistema de Formularios** | React Hook Form + Zod | Formularios de captura (avances, compras, etc.) | Baja - Adaptaci贸n de esquemas | -| **Tablas con Paginaci贸n** | `apps/frontend/src/components/tables/` | Listados de obras, presupuestos, compras | Baja - Adaptaci贸n de columnas | -| **Dashboards y Gr谩ficos** | Chart.js / Recharts | Dashboards de obra, desviaciones, KPIs | Media - Nuevas m茅tricas | -| **Layouts Responsivos** | `apps/frontend/src/layouts/` | Layout principal admin + m贸vil | Baja - Adaptaci贸n de men煤s | -| **Autenticaci贸n y Rutas Protegidas** | `apps/frontend/src/guards/` | Protecci贸n de rutas por rol | M铆nima - Ajustar roles | -| **Hooks Personalizados** | `apps/frontend/src/hooks/` | useApi, useAuth, useForm | Ninguna - Reutilizaci贸n directa | -| **Sistema de Notificaciones** | Toast/Alerts | Notificaciones de sistema | Ninguna - Reutilizaci贸n directa | -| **State Management (Zustand)** | Stores de autenticaci贸n, perfiles | Stores para obras, presupuestos, etc. | Media - Nuevos stores | - -**Ahorro estimado Frontend:** 2-3 semanas - ---- - -#### Base de Datos (PostgreSQL 15+) - -| Componente | Archivo Origen GAMILIT | Aplicaci贸n Inmobiliaria | Adaptaci贸n Requerida | -|------------|------------------------|-------------------------|---------------------| -| **Schemas Modulares** | Organizaci贸n por dominios | Schemas por m贸dulo (projects, budgets, purchases, etc.) | Media - Nueva organizaci贸n | -| **Pol铆ticas RLS** | Row Level Security | Pol铆ticas por proyecto/obra | Media - Adaptaci贸n de pol铆ticas | -| **Triggers de Auditor铆a** | Triggers autom谩ticos | Auditor铆a de cambios cr铆ticos | Baja - Reutilizaci贸n con ajustes | -| **Funciones Comunes** | `gamilit.now_mexico()`, `gamilit.get_current_user_id()` | Funciones de utilidad | Ninguna - Reutilizaci贸n directa | -| **Sistema de Migraciones** | Control de versiones DDL | Gesti贸n de cambios en BD | Ninguna - Reutilizaci贸n directa | -| **ENUMs para Estados** | Estados de cuenta, tipos | Estados de obra, estimaciones, etc. | Alta - Nuevos ENUMs espec铆ficos | - -**Ahorro estimado Database:** 1-2 semanas - ---- - -### 1.2 Patrones Arquitect贸nicos (Reutilizaci贸n: 100%) - -Estos patrones se copian directamente sin modificaci贸n: - -| Patr贸n | Descripci贸n | Beneficio | -|--------|-------------|-----------| -| **Repository/Service** | Separaci贸n de l贸gica de negocio y acceso a datos | C贸digo limpio y testeable | -| **Modularizaci贸n por Dominio** | Organizaci贸n `/modules/{domain}` | Escalabilidad y mantenibilidad | -| **DTOs con Validaci贸n** | class-validator + class-transformer | Validaci贸n robusta de entrada | -| **Guards y Decorators** | @Roles(), @Public(), @CurrentUser() | Seguridad declarativa | -| **Error Handling Centralizado** | Exception filters personalizados | Respuestas consistentes | -| **Testing Patterns** | Unit + E2E tests con Jest | Cobertura de tests | - ---- - -## 2. Mapeo de Funcionalidades GAMILIT 鈫 Inmobiliario - -### 2.1 Equivalencias Directas - -| GAMILIT | Inmobiliario | Reutilizaci贸n | -|---------|--------------|---------------| -| `students` tabla | `employees` / `beneficiaries` | 70% - Cambio de nombres | -| `courses` tabla | `projects` | 60% - Similar estructura jer谩rquica | -| `modules` tabla | `stages` (etapas de obra) | 50% - Concepto similar | -| `activities` tabla | `tasks` / `checklists` | 70% - Similar tracking | -| `progress_tracking` schema | `progress_tracking` (avances de obra) | 80% - Muy similar | -| `user_stats` tabla | `project_stats` | 70% - M茅tricas similares | -| `achievements` tabla | `milestones` (hitos de proyecto) | 50% - Concepto adaptable | -| Sistema de notificaciones | Alertas de desviaciones/hitos | 90% - Reutilizaci贸n directa | - -### 2.2 Adaptaciones Conceptuales - -**GAMILIT:** Sistema de "Aulas" con profesores y estudiantes -**Inmobiliario:** Sistema de "Proyectos/Obras" con equipo asignado - -**GAMILIT:** Tracking de "Progreso en Ejercicios" -**Inmobiliario:** Tracking de "Avances F铆sicos de Obra" - -**GAMILIT:** Sistema de "ML Coins" (monedas lectoras) -**Inmobiliario:** Sistema de "Presupuesto vs Costo Real" - -**GAMILIT:** "Rangos Maya" de progreso -**Inmobiliario:** "Estados de Obra" (Licitaci贸n, Ejecuci贸n, Entrega, etc.) - ---- - -## 3. Mapeo de M贸dulos a 脡picas - -### Fase 1: Alcance Inicial (14 semanas) - 6 脡picas - -**Presupuesto Estimado:** $150,000 MXN -**Story Points:** ~280 SP - -| 脡pica | Nombre | M贸dulos MVP Relacionados | Reutilizaci贸n GAMILIT | Presupuesto | SP | -|-------|--------|--------------------------|------------------------|-------------|-----| -| **MAI-001** | Fundamentos | M贸dulo 13 (Admin & Seguridad) | EAI-001 (90%) | $25,000 | 50 | -| **MAI-002** | Proyectos y Estructura de Obra | M贸dulo 1 (Proyectos, Obras, Viviendas) | EAI-002 (40%) | $25,000 | 45 | -| **MAI-003** | Presupuestos y Control de Costos | M贸dulo 2 (Presupuestos y Costos) | Nuevo (10%) | $25,000 | 50 | -| **MAI-004** | Compras e Inventarios | M贸dulo 3, 4 (Compras, Inventarios) | Nuevo (15%) | $25,000 | 50 | -| **MAI-005** | Control de Obra y Avances | M贸dulo 6 (Control de Obra y Avances) | EAI-002 (60%) | $25,000 | 45 | -| **MAI-006** | Reportes y Analytics Base | M贸dulo 12 (Reportes & BI) | EAI-004 (70%) | $25,000 | 40 | - -**Total Fase 1:** $150,000 MXN | 280 SP | ~14 semanas - ---- - -### Fase 2: Gesti贸n Avanzada (10 semanas) - 5 脡picas - -**Presupuesto Estimado:** $125,000 MXN -**Story Points:** ~220 SP - -| 脡pica | Nombre | M贸dulos MVP Relacionados | Reutilizaci贸n GAMILIT | Presupuesto | SP | -|-------|--------|--------------------------|------------------------|-------------|-----| -| **MAI-007** | Contratos y Estimaciones | M贸dulo 5, 7 (Contratos, Estimaciones) | Nuevo (20%) | $25,000 | 45 | -| **MAI-008** | RRHH y N贸mina de Obra | M贸dulo 8 (RRHH, Asistencias) | EAI-005 (30%) | $25,000 | 45 | -| **MAI-009** | Calidad y Postventa | M贸dulo 9 (Calidad, Garant铆as) | Nuevo (25%) | $25,000 | 45 | -| **MAI-010** | CRM Derechohabientes | M贸dulo 10 (CRM) | Nuevo (30%) | $25,000 | 40 | -| **MAI-011** | INFONAVIT & Cumplimiento | M贸dulo 11 (INFONAVIT) | Nuevo (10%) | $25,000 | 45 | - -**Total Fase 2:** $125,000 MXN | 220 SP | ~10 semanas - ---- - -### Fase 3: IA y Extensiones (6 semanas) - 2 脡picas - -**Presupuesto Estimado:** $75,000 MXN -**Story Points:** ~140 SP - -| 脡pica | Nombre | M贸dulos MVP Relacionados | Reutilizaci贸n GAMILIT | Presupuesto | SP | -|-------|--------|--------------------------|------------------------|-------------|-----| -| **MAI-012** | Admin y Configuraci贸n Avanzada | M贸dulo 13 (Admin completo) | EAI-005, EAI-006 (60%) | $25,000 | 50 | -| **MAI-013** | IA, WhatsApp Business y App M贸vil | Agente IA, WhatsApp, App React Native | Nuevo (20%) | $50,000 | 90 | - -**Total Fase 3:** $75,000 MXN | 140 SP | ~6 semanas - ---- - -## 4. Desglose de Reutilizaci贸n por Componente - -### 4.1 Infraestructura y Base (Sprint 0) - -| Componente | Origen GAMILIT | Tiempo sin Reutilizaci贸n | Tiempo con Reutilizaci贸n | Ahorro | -|------------|----------------|-------------------------|--------------------------|--------| -| Sistema de Autenticaci贸n | 2 semanas | 3 d铆as | **65%** | -| RBAC | 1.5 semanas | 2 d铆as | **70%** | -| Configuraci贸n Base de Datos | 1 semana | 3 d铆as | **60%** | -| UI Base y Layouts | 3 semanas | 1 semana | **67%** | -| Dashboards Base | 2 semanas | 1 semana | **50%** | -| Sistema de Formularios | 1.5 semanas | 3 d铆as | **60%** | -| **TOTAL Sprint 0** | **11 semanas** | **3.8 semanas** | **~65%** | - ---- - -### 4.2 M贸dulos de Negocio - -| M贸dulo MVP | Complejidad | Reutilizaci贸n GAMILIT | Tiempo Estimado (sin/con) | -|------------|-------------|----------------------|---------------------------| -| 1. Proyectos y Obras | Media | 40% | 3 sem / 2 sem | -| 2. Presupuestos | Alta | 10% | 4 sem / 3.5 sem | -| 3. Compras | Media | 15% | 3 sem / 2.5 sem | -| 4. Inventarios | Media | 20% | 3 sem / 2.5 sem | -| 5. Contratos | Alta | 20% | 4 sem / 3 sem | -| 6. Control de Obra | Alta | 60% | 4 sem / 2 sem | -| 7. Estimaciones | Alta | 25% | 4 sem / 3 sem | -| 8. RRHH | Media | 30% | 3 sem / 2 sem | -| 9. Calidad/Postventa | Media | 25% | 3 sem / 2.5 sem | -| 10. CRM | Media | 30% | 3 sem / 2 sem | -| 11. INFONAVIT | Alta | 10% | 4 sem / 3.5 sem | -| 12. Reportes/BI | Media | 70% | 3 sem / 1.5 sem | -| 13. Admin | Baja | 80% | 2 sem / 0.5 sem | - ---- - -## 5. Plan de Migraci贸n de Componentes - -### Sprint 0: Migraci贸n de Base (1 semana) - -**Componentes a migrar:** -1. Sistema de autenticaci贸n JWT -2. Middleware de autenticaci贸n y autorizaci贸n -3. Sistema de logging estructurado -4. Componentes UI base (Buttons, Inputs, Modales) -5. Layouts principales -6. Setup de base de datos con schemas modulares - -**Actividades:** -- [x] Crear repositorio con estructura base de GAMILIT -- [x] Migrar sistema de autenticaci贸n completo -- [x] Setup de base de datos con schemas modulares -- [x] Migrar componentes UI base -- [x] Configurar sistema de logging y error handling - ---- - -### Sprint 1-2: Fundamentos (2 semanas) - -**Componentes a adaptar:** -1. Sistema de roles espec铆ficos de construcci贸n: - - `student` 鈫 `resident` (Residente de obra) - - `admin_teacher` 鈫 `engineer` (Ingeniero) - - `super_admin` 鈫 `director` (Director de obra) - - Nuevos: `purchases`, `finance`, `hr`, `post_sales` - -2. Layouts y navegaci贸n: - - Adaptar men煤 principal - - Crear dashboards por rol - -3. Sistema de permisos: - - Adaptar RLS policies para obras/proyectos - - Definir permisos por m贸dulo - ---- - -### Sprint 3+: M贸dulos de Negocio - -**Estrategia:** -1. Reutilizar patrones de tracking de GAMILIT para "Control de Obra" -2. Adaptar sistema de "Aulas" a "Proyectos/Obras" -3. Crear nuevos m贸dulos espec铆ficos (Presupuestos, Compras, Contratos) -4. Reutilizar componentes de dashboards y gr谩ficos - ---- - -## 6. Consideraciones T茅cnicas - -### 6.1 Diferencias de Dominio - -| Aspecto | GAMILIT | MVP Inmobiliario | Impacto | -|---------|---------|------------------|---------| -| **Modelo de Datos** | Educativo (cursos, m贸dulos, ejercicios) | Constructivo (obras, etapas, conceptos) | Alto - Nuevas entidades | -| **Flujos de Trabajo** | Lineal (progreso en curso) | Complejo (m煤ltiples frentes paralelos) | Alto - Nueva l贸gica | -| **Roles de Usuario** | Estudiante, Profesor, Admin | 7 roles espec铆ficos de construcci贸n | Medio - Adaptaci贸n de RBAC | -| **M茅tricas** | Educativas (XP, progreso, logros) | Financieras (presupuesto, costo, avance) | Alto - Nuevas m茅tricas | -| **Integraciones** | OAuth social | WhatsApp Business, INFONAVIT | Alto - Nuevas integraciones | - ---- - -### 6.2 T茅rminos a Adaptar - -| GAMILIT | MVP Inmobiliario | -|---------|------------------| -| `students` | `employees` / `beneficiaries` | -| `teachers` | `engineers` / `residents` | -| `courses` | `projects` | -| `modules` | `stages` (etapas) | -| `activities` | `tasks` / `work_items` | -| `progress` | `physical_progress` / `financial_progress` | -| `achievements` | `milestones` | -| `classrooms` | `construction_sites` / `work_fronts` | -| `xp_points` | `progress_percentage` | -| `ml_coins` | `budget_balance` | - ---- - -## 7. Estrategia de Mantenimiento del C贸digo Compartido - -### 7.1 Componentes Compartidos - -**Opci贸n 1: Copiar y Divergir** -- Copiar componentes de GAMILIT al proyecto inmobiliario -- Evolucionar independientemente -- **Ventaja:** Independencia total -- **Desventaja:** Duplicaci贸n de c贸digo - -**Opci贸n 2: Librer铆a Compartida (Futuro)** -- Extraer componentes comunes a librer铆a npm privada -- Compartir entre GAMILIT e Inmobiliario -- **Ventaja:** Reutilizaci贸n real, fixes compartidos -- **Desventaja:** Complejidad adicional - -**Recomendaci贸n:** Opci贸n 1 para MVP, Opci贸n 2 en Fase 2 - ---- - -### 7.2 Documentaci贸n de Componentes Reutilizados - -Todos los componentes reutilizados deben documentarse con: - -```typescript -/** - * @reused-from GAMILIT - * @original-file apps/backend/src/shared/guards/roles.guard.ts - * @adaptations - * - Roles espec铆ficos de construcci贸n - * - Permisos por proyecto/obra - * @last-sync 2025-11-17 - */ -``` - ---- - -## 8. Estimaci贸n de Ahorro - -### 8.1 Resumen por Fase - -| Fase | Sin Reutilizaci贸n | Con Reutilizaci贸n | Ahorro | -|------|------------------|-------------------|--------| -| **Fase 1** | 20 semanas | 14 semanas | **30%** | -| **Fase 2** | 14 semanas | 10 semanas | **29%** | -| **Fase 3** | 8 semanas | 6 semanas | **25%** | -| **TOTAL** | **42 semanas** | **30 semanas** | **~29%** | - -**Ahorro total estimado:** 12 semanas (~3 meses) - ---- - -### 8.2 Resumen por Componente - -| Componente | Ahorro Estimado | -|------------|----------------| -| **Autenticaci贸n** | 65% | -| **UI Base** | 67% | -| **Dashboards** | 50% | -| **Formularios** | 60% | -| **BD Setup** | 60% | -| **Logging/Auditor铆a** | 90% | -| **Middleware** | 85% | -| **PROMEDIO** | **~65%** | - ---- - -## 9. Riesgos y Mitigaciones - -### 9.1 Riesgos Identificados - -| Riesgo | Probabilidad | Impacto | Mitigaci贸n | -|--------|-------------|---------|------------| -| **Incompatibilidad de versiones** | Media | Alto | Documentar versiones exactas de dependencias | -| **Over-engineering** | Media | Medio | Simplificar componentes no necesarios | -| **Divergencia de arquitectura** | Baja | Alto | Mantener patrones consistentes | -| **Deuda t茅cnica** | Media | Medio | Code reviews rigurosos | - ---- - -### 9.2 Plan de Mitigaci贸n - -1. **Sprint 0 obligatorio:** No saltar directo a m贸dulos de negocio -2. **Code reviews cruzados:** Team GAMILIT revisa c贸digo inmobiliario -3. **Documentaci贸n exhaustiva:** Cada adaptaci贸n debe documentarse -4. **Tests rigurosos:** Mantener >80% coverage como en GAMILIT - ---- - -## 10. Conclusiones y Recomendaciones - -### 10.1 Componentes Prioritarios a Reutilizar - -**Alta Prioridad (Reutilizar tal cual):** -- Sistema de autenticaci贸n JWT -- RBAC y guards -- Logging y auditor铆a -- Componentes UI base -- Middleware de validaci贸n -- Error handling - -**Media Prioridad (Adaptar):** -- Sistema de tracking de progreso 鈫 Avances de obra -- Dashboards y gr谩ficos 鈫 Dashboards de obra -- Sistema de notificaciones -- Gesti贸n de archivos - -**Baja Prioridad (Inspiraci贸n):** -- Sistema de gamificaci贸n 鈫 No aplica -- Achievements 鈫 Hitos de proyecto (concepto adaptado) - ---- - -### 10.2 Roadmap Recomendado - -**Semanas 1-2: Sprint 0 (Migraci贸n de Base)** -- Configurar repositorio -- Migrar infraestructura base -- Adaptar sistema de autenticaci贸n - -**Semanas 3-6: Fase 1A (Fundamentos + Proyectos)** -- MAI-001: Fundamentos -- MAI-002: Proyectos y Estructura - -**Semanas 7-14: Fase 1B (Core de Obra)** -- MAI-003: Presupuestos -- MAI-004: Compras e Inventarios -- MAI-005: Control de Obra -- MAI-006: Reportes Base - -**Semanas 15-24: Fase 2 (Gesti贸n Avanzada)** -- MAI-007 a MAI-011 - -**Semanas 25-30: Fase 3 (IA y Extensiones)** -- MAI-012 y MAI-013 - ---- - -### 10.3 KPIs de 脡xito - -| KPI | Target | -|-----|--------| -| **Tiempo de desarrollo** | 鈮 30 semanas | -| **Reducci贸n vs desarrollo desde cero** | 鈮 25% | -| **C贸digo reutilizado** | 鈮 50% de infraestructura | -| **Coverage de tests** | 鈮 80% | -| **Bugs cr铆ticos en producci贸n** | 0 | - ---- - -## Ap茅ndice A: Checklist de Migraci贸n - -### Infraestructura Base - -- [ ] Sistema de autenticaci贸n JWT -- [ ] Guards y decorators -- [ ] Middleware de validaci贸n -- [ ] Error handlers -- [ ] Logging estructurado -- [ ] Sistema de auditor铆a -- [ ] Gesti贸n de archivos -- [ ] RLS policies base - -### Frontend - -- [ ] Componentes UI base -- [ ] Layouts principales -- [ ] Sistema de formularios -- [ ] Tablas con paginaci贸n -- [ ] Dashboards base -- [ ] Hooks personalizados -- [ ] Guards de routing -- [ ] State management - -### Database - -- [ ] Schemas modulares -- [ ] Funciones comunes -- [ ] Triggers de auditor铆a -- [ ] Sistema de migraciones -- [ ] Seeds de datos de prueba - ---- - -**Documento generado:** 2025-11-17 -**Autor:** An谩lisis T茅cnico -**Versi贸n:** 1.0 -**Pr贸xima revisi贸n:** Post Sprint 0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/CAMBIOS-Y-ACTUALIZACIONES.md b/projects/erp-construccion/docs/02-definicion-modulos/CAMBIOS-Y-ACTUALIZACIONES.md deleted file mode 100644 index 8542128b6..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/CAMBIOS-Y-ACTUALIZACIONES.md +++ /dev/null @@ -1,429 +0,0 @@ -# Cambios y Actualizaciones - Fase 1 Ajustada - -**Fecha:** 2025-11-17 -**Versi贸n:** 2.0.0 -**Estado:** 鉁 Completo - Listo para revisi贸n - ---- - -## 馃摙 Resumen de Cambios - -Basado en el feedback del usuario sobre la importancia de **RRHH y asistencias** para obra, se realizaron los siguientes ajustes al roadmap inicial: - -### Cambio Principal: RRHH Movido a Fase 1 - -**Antes:** -- Fase 1: 6 茅picas | 14 semanas | $150,000 MXN | 280 SP -- RRHH estaba en Fase 2 - -**Despu茅s:** -- Fase 1: **7 茅picas** | **16 semanas** | **$175,000 MXN** | **330 SP** -- RRHH incluido en Fase 1 como **茅pica MAI-006** (Prioridad P0) - ---- - -## 馃幆 Justificaci贸n del Cambio - -### Por qu茅 RRHH debe estar en Fase 1: - -1. **Costeo de Mano de Obra es Cr铆tico** - - Necesario para calcular costos reales vs presupuesto - - Sin RRHH, los reportes de costos est谩n incompletos - -2. **Cumplimiento Legal desde D铆a 1** - - IMSS: Afiliaci贸n obligatoria desde el primer d铆a de trabajo - - INFONAVIT: Aportaciones patronales del 5% mensuales - - Sanciones por incumplimiento son severas - -3. **Control de Personal en Obra** - - Asistencia biom茅trica evita "aviadores" (trabajadores fantasma) - - GPS valida que el personal est茅 realmente en obra - - Fundamental para productividad y seguridad - -4. **Sinergia con App M贸vil** - - La app ya se usa para captura de avances (MAI-005) - - Agregar asistencia biom茅trica aprovecha el mismo dispositivo - - Residentes de obra usan 1 sola app para todo - ---- - -## 馃搧 Documentaci贸n Generada (Nueva y Actualizada) - -### Documentos Nuevos 猸 - -| Archivo | Descripci贸n | Tama帽o | -|---------|-------------|--------| -| **[ROADMAP-DETALLADO.md](./ROADMAP-DETALLADO.md)** | Roadmap completo con 11 sprints detallados | ~30 KB | -| **[MAI-007-rrhh-asistencias/_MAP.md](./MAI-007-rrhh-asistencias/_MAP.md)** | 脡pica completa de RRHH | ~15 KB | -| **[CAMBIOS-Y-ACTUALIZACIONES.md](./CAMBIOS-Y-ACTUALIZACIONES.md)** | Este documento | ~8 KB | - -### Documentos Actualizados 馃攧 - -| Archivo | Cambios | -|---------|---------| -| **[README.md](./README.md)** | Actualizado a 7 茅picas, presupuesto $175K, 16 semanas | -| **[_MAP.md](./_MAP.md)** | Tabla de 茅picas actualizada, nuevo total de SP | -| **[RESUMEN-EJECUTIVO.md](./RESUMEN-EJECUTIVO.md)** | Pendiente de actualizaci贸n | - ---- - -## 馃棑锔 Roadmap Ajustado - Fase 1 - -### Sprints Overview - -| Sprint | Semanas | 脡pica | Entregable Principal | -|--------|---------|-------|----------------------| -| **Sprint 0** | 1 | Migraci贸n | Infraestructura base desde GAMILIT | -| **Sprint 1** | 2-3 | MAI-001 | Autenticaci贸n + Multi-tenancy | -| **Sprint 2** | 4-5 | MAI-002 | Gesti贸n de proyectos | -| **Sprint 3-4** | 6-8 | MAI-003 | Presupuestos y costos | -| **Sprint 5-6** | 9-10.5 | MAI-004 | Compras e inventarios | -| **Sprint 7-8** | 11-13 | MAI-005 | Control de obra + App m贸vil v1 | -| **Sprint 9-10** | 13.5-16 | **MAI-006 猸** | **RRHH + Asistencia biom茅trica + IMSS/INFONAVIT** | -| **Sprint 11** | 16-17 | MAI-007 | Reportes y analytics | - -**Total:** 16 semanas | 11 sprints | $175,000 MXN - ---- - -## 馃摫 Especificaciones de App M贸vil (MAI-006) - -### Funcionalidades Principales - -#### 1. Asistencia con Biom茅trico 猸愨瓙 -**M茅todos de registro:** -- **Huella dactilar** (react-native-biometrics) -- **Reconocimiento facial** (react-native-camera) -- **QR Code** (react-native-qrcode-scanner) -- **Lista manual** (fallback) - -**Validaciones autom谩ticas:** -- 鉁 GPS: Empleado dentro del radio de la obra (100m) -- 鉁 Horario: Dentro de jornada laboral (6am-8pm) -- 鉁 Estado: Empleado activo y asignado a la obra -- 鉁 Duplicados: No permitir doble check-in - -#### 2. Modo Offline -- Base de datos local (expo-sqlite) -- Cola de hasta 500 registros -- Sincronizaci贸n autom谩tica al reconectar -- Cache de empleados y templates biom茅tricos - -#### 3. Flujo de Usuario -``` -1. Residente abre app -2. Selecciona obra activa -3. Modo "Check-in" o "Check-out" -4. Opciones: - a) Escanear QR del empleado - b) Buscar en lista - c) Captura biom茅trica -5. Sistema valida (GPS, horario, estado) -6. Captura foto opcional -7. Registra asistencia (online o cola) -8. Confirmaci贸n con vibraci贸n/sonido -``` - ---- - -## 馃攲 Integraciones Externas - -### 1. IMSS (Instituto Mexicano del Seguro Social) - -**API:** SOAP/REST -**Autenticaci贸n:** Certificado digital (.cer + .key) - -**Funcionalidades:** -- Alta/baja/modificaci贸n de trabajadores -- Generaci贸n de archivos SUA (Sistema 脷nico de Autodeterminaci贸n) -- Consulta de vigencia de derechos -- C谩lculo de cuotas obrero-patronales - -**Endpoints implementados:** -``` -POST /api/imss/afiliacion/alta -POST /api/imss/afiliacion/baja -POST /api/imss/afiliacion/modificacion -POST /api/imss/sua/generar -GET /api/imss/vigencia/:nss -``` - ---- - -### 2. INFONAVIT - -**API:** REST -**Autenticaci贸n:** OAuth 2.0 + API Key - -**Funcionalidades:** -- Registro patronal -- C谩lculo de aportaciones (5% del salario base) -- Generaci贸n de archivo de pago -- Consulta de trabajadores acreditados (con cr茅dito INFONAVIT) -- Descuentos de cr茅dito - -**Endpoints implementados:** -``` -POST /api/infonavit/patron/registro -POST /api/infonavit/aportaciones/calcular -POST /api/infonavit/aportaciones/generar-archivo -GET /api/infonavit/trabajadores/acreditados/:rfc -POST /api/infonavit/descuentos/aplicar -``` - -**C谩lculo de aportaci贸n:** -```javascript -const aportacionMensual = salarioBaseCotizacion * diasTrabajados * 0.05; -``` - ---- - -## 馃搳 Comparaci贸n Antes vs Despu茅s - -| M茅trica | Versi贸n 1.0 (Antes) | Versi贸n 2.0 (Despu茅s) | Diferencia | -|---------|---------------------|----------------------|------------| -| **脡picas** | 6 | 7 | +1 (RRHH) | -| **Presupuesto** | $150,000 | $175,000 | +$25,000 | -| **Story Points** | 280 SP | 330 SP | +50 SP | -| **Duraci贸n** | 14 semanas | 16 semanas | +2 semanas | -| **Integraciones** | 0 | 2 (IMSS, INFONAVIT) | +2 | -| **App m贸vil features** | Avances + Incidencias | + Asistencia biom茅trica | +1 m贸dulo | - ---- - -## 馃幆 脡picas de Fase 1 (Final) - -| # | 脡pica | Nombre | SP | Presupuesto | Prioridad | -|---|-------|--------|----|-----------:|-----------| -| 1 | MAI-001 | Fundamentos | 50 | $25,000 | P0 - Cr铆tico | -| 2 | MAI-002 | Proyectos y Estructura | 45 | $25,000 | P0 - Cr铆tico | -| 3 | MAI-003 | Presupuestos y Costos | 50 | $25,000 | P1 - Alto | -| 4 | MAI-004 | Compras e Inventarios | 50 | $25,000 | P1 - Alto | -| 5 | MAI-005 | Control de Obra y Avances | 45 | $25,000 | P0 - Cr铆tico | -| 6 | **MAI-006 猸** | **RRHH, Asistencias y N贸mina** | **50** | **$25,000** | **P0 - Cr铆tico** | -| 7 | MAI-007 | Reportes y Analytics | 40 | $25,000 | P1 - Alto | - -**Total:** 330 SP | $175,000 MXN | 16 semanas - ---- - -## 馃殌 Hitos Cr铆ticos (Actualizados) - -| Semana | Hito | Entregable | -|--------|------|------------| -| **Semana 1** | Sprint 0 completado | Infraestructura base migrada | -| **Semana 3** | MAI-001 completado | Auth + Multi-tenancy | -| **Semana 5** | MAI-002 completado | Gesti贸n de proyectos | -| **Semana 8** | MAI-003 completado | Presupuestos y costos | -| **Semana 10.5** | MAI-004 completado | Compras e inventarios | -| **Semana 13** | MAI-005 completado | Control de obra + App m贸vil v1 | -| **Semana 16** | **MAI-006 completado 猸** | **RRHH + Asistencia biom茅trica + IMSS/INFONAVIT** | -| **Semana 17** | MAI-007 completado | Reportes y analytics | -| **Semana 17** | **馃帀 Fase 1 completa** | **Deploy a staging** | - ---- - -## 馃搵 Stack Tecnol贸gico de App M贸vil - -### Dependencias Principales - -```json -{ - "dependencies": { - "react": "18.2.0", - "react-native": "0.73.0", - "expo": "~50.0.0", - "expo-sqlite": "~13.0.0", - "expo-camera": "~14.0.0", - "@react-native-community/geolocation": "^3.1.0", - "react-native-biometrics": "^3.0.0", - "react-native-qrcode-scanner": "^1.5.5", - "react-native-maps": "^1.10.0", - "zustand": "^4.4.0", - "axios": "^1.6.0", - "@tanstack/react-query": "^5.0.0" - } -} -``` - -### Features de la App - -| Feature | Biblioteca | Prop贸sito | -|---------|-----------|-----------| -| **Biom茅trico (huella)** | react-native-biometrics | Autenticaci贸n y asistencia | -| **Biom茅trico (facial)** | expo-camera + ML | Reconocimiento facial | -| **QR Scanner** | react-native-qrcode-scanner | Escanear QR de empleados | -| **GPS** | @react-native-community/geolocation | Validar ubicaci贸n en obra | -| **Maps** | react-native-maps | Visualizar radio de obra | -| **Database Local** | expo-sqlite | Modo offline | -| **State Management** | Zustand | Estado global | -| **API Calls** | axios + react-query | Comunicaci贸n con backend | - ---- - -## 馃帗 Capacitaci贸n Adicional Requerida - -### Para Equipo de Desarrollo - -| Tema | Duraci贸n | Cu谩ndo | Qui茅n | -|------|----------|--------|-------| -| **React Native Fundamentals** | 8 horas | Sprint 7 | Mobile developer | -| **Biometrics en React Native** | 4 horas | Sprint 9 | Mobile developer | -| **Integraci贸n IMSS/INFONAVIT** | 4 horas | Sprint 9 | Backend lead | -| **Generaci贸n de archivos SUA** | 2 horas | Sprint 9 | Backend team | -| **Testing de app m贸vil** | 4 horas | Sprint 9 | QA engineer | - ---- - -## 馃毃 Riesgos Espec铆ficos de MAI-006 - -| Riesgo | Probabilidad | Impacto | Mitigaci贸n | -|--------|-------------|---------|------------| -| **Integraci贸n IMSS/INFONAVIT compleja** | Alta | Alto | Iniciar pruebas con sandbox desde Sprint 5 | -| **Certificados IMSS dif铆ciles de obtener** | Media | Alto | Solicitar certificados al inicio del proyecto | -| **APIs gubernamentales inestables** | Media | Medio | Implementar retry logic y fallbacks | -| **Biom茅trico no funciona en todos devices** | Media | Medio | Fallback a QR + foto | -| **GPS impreciso en algunas obras** | Alta | Bajo | Radio amplio (100m), permitir override | -| **Sincronizaci贸n offline falla** | Media | Alto | Cola persistente con retry autom谩tico | - ---- - -## 鉁 Checklist de Implementaci贸n MAI-006 - -### Backend -- [ ] CRUD de empleados, cuadrillas, oficios -- [ ] API de registro de asistencia -- [ ] Validaciones (GPS, horario, estado) -- [ ] Servicio de c谩lculo de costeo de mano de obra -- [ ] Integraci贸n IMSS (sandbox 鈫 producci贸n) -- [ ] Integraci贸n INFONAVIT (sandbox 鈫 producci贸n) -- [ ] Generaci贸n de archivos SUA -- [ ] Exportaci贸n de reportes - -### App M贸vil -- [ ] Login y autenticaci贸n -- [ ] Selector de obra -- [ ] Scanner QR -- [ ] Captura biom茅trica (huella) -- [ ] Captura biom茅trica (facial) - opcional -- [ ] GPS validation -- [ ] Base de datos local (SQLite) -- [ ] Cola de sincronizaci贸n offline -- [ ] UI/UX optimizada para campo - -### Frontend Web -- [ ] Gesti贸n de empleados -- [ ] Dashboard de asistencias -- [ ] Reportes de costeo -- [ ] Exportaci贸n IMSS/INFONAVIT -- [ ] Logs de sincronizaci贸n - -### Database -- [ ] Schemas: hr, attendance, payroll -- [ ] Tablas de empleados, asistencia, costeo -- [ ] Funciones de c谩lculo -- [ ] Triggers de validaci贸n -- [ ] 脥ndices optimizados - -### Testing -- [ ] Unit tests >80% coverage -- [ ] E2E tests de flujo completo -- [ ] Integration tests con IMSS/INFONAVIT (sandbox) -- [ ] Tests de app m贸vil (offline, GPS, biom茅trico) - -### Deployment -- [ ] Variables de entorno (API keys, certificados) -- [ ] Secrets management (certificados IMSS encrypted) -- [ ] Deploy de app a TestFlight (iOS) -- [ ] Deploy de app a Google Play (Android) -- [ ] Configuraci贸n de monitoreo -- [ ] Documentaci贸n de integraci贸n - ---- - -## 馃摓 Pr贸ximos Pasos Inmediatos - -### Esta Semana -- [ ] **Aprobar roadmap ajustado** (7 茅picas, 16 semanas, $175K) -- [ ] Confirmar que RRHH en Fase 1 es correcto -- [ ] Validar presupuesto adicional de $25,000 MXN -- [ ] Revisar especificaciones de app m贸vil - -### Pr贸xima Semana -- [ ] Completar documentos RF, ET, US de MAI-006 -- [ ] Iniciar gesti贸n de certificados IMSS -- [ ] Solicitar acceso a APIs sandbox de IMSS/INFONAVIT -- [ ] Asignar mobile developer al equipo - -### Sprint 0 (Semana 1) -- [ ] Setup de repositorio -- [ ] Migraci贸n de componentes GAMILIT -- [ ] Configurar proyecto de app m贸vil (React Native + Expo) - ---- - -## 馃挕 Recomendaciones Finales - -### Priorizar Integraciones IMSS/INFONAVIT -- **Iniciar gesti贸n de certificados desde Sprint 1** -- Proceso puede tomar 2-4 semanas -- Solicitar acceso a sandboxes inmediatamente - -### App M贸vil Simplificada al Inicio -- **Sprint 7-8:** App con avances + incidencias (b谩sico) -- **Sprint 9-10:** Agregar asistencia biom茅trica -- No intentar todo en un solo sprint - -### Testing Exhaustivo de Modo Offline -- **Cr铆tico:** La obra no siempre tiene buena conexi贸n -- Probar escenarios: - - 100+ registros en cola - - Conflictos de sincronizaci贸n - - Bater铆a baja del dispositivo - -### UX Simple para Residentes -- **Target:** Residentes no son t茅cnicos -- UI debe ser intuitiva, con iconos grandes -- Flujo de 3-4 taps m谩ximo para registrar asistencia -- Confirmaciones visuales y h谩pticas - ---- - -## 馃搳 M茅tricas de 脡xito (Actualizadas) - -| KPI | Target | Medici贸n | -|-----|--------|----------| -| **Tiempo de desarrollo Fase 1** | 鈮 16 semanas | Tracking semanal | -| **Presupuesto Fase 1** | $175,000 卤5% | Tracking financiero | -| **Reducci贸n vs desde cero** | 鈮 25% | Comparaci贸n post-mortem | -| **C贸digo reutilizado de GAMILIT** | 鈮 50% | An谩lisis de c贸digo | -| **Coverage de tests** | 鈮 80% | CI/CD reports | -| **Bugs cr铆ticos en staging** | 0 | Issue tracker | -| **Integraci贸n IMSS funcionando** | 鉁 | Tests de integraci贸n | -| **Integraci贸n INFONAVIT funcionando** | 鉁 | Tests de integraci贸n | -| **App m贸vil en stores** | 鉁 | TestFlight + Play Store | - ---- - -## 馃摎 Documentos de Referencia - -### Generados en esta sesi贸n: -1. [ROADMAP-DETALLADO.md](./ROADMAP-DETALLADO.md) - Roadmap completo con 11 sprints -2. [MAI-007-rrhh-asistencias/_MAP.md](./MAI-007-rrhh-asistencias/_MAP.md) - 脡pica RRHH completa -3. [CAMBIOS-Y-ACTUALIZACIONES.md](./CAMBIOS-Y-ACTUALIZACIONES.md) - Este documento - -### Actualizados: -1. [README.md](./README.md) - 7 茅picas, $175K -2. [_MAP.md](./_MAP.md) - Tabla de 茅picas actualizada - -### Pendientes de actualizar: -1. [RESUMEN-EJECUTIVO.md](./RESUMEN-EJECUTIVO.md) - Reflejar 7 茅picas -2. [ANALISIS-REUTILIZACION-GAMILIT.md](./ANALISIS-REUTILIZACION-GAMILIT.md) - Agregar MAI-006 - ---- - -**Generado:** 2025-11-17 -**Versi贸n:** 2.0.0 -**Autor:** An谩lisis T茅cnico -**Estado:** 鉁 Completo - Listo para aprobaci贸n -**Pr贸xima acci贸n:** Aprobar roadmap y presupuesto ajustado diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/README.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/README.md deleted file mode 100644 index f9c168fe9..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/README.md +++ /dev/null @@ -1,167 +0,0 @@ -# MAA-017: Seguridad HSE (Health, Safety & Environment) - -## Informacion General - -| Atributo | Valor | -|----------|-------| -| **Codigo** | MAA-017 | -| **Nombre** | Seguridad HSE | -| **Fase** | 3 - Avanzada | -| **Prioridad** | P2 | -| **Estado** | Documentado | -| **Reutilizacion Core** | 20% | - -## Descripcion - -Modulo de gestion de seguridad industrial, salud ocupacional y medio ambiente para obras de construccion. Incluye control de incidentes, cumplimiento normativo STPS, capacitaciones, inspecciones de seguridad y gestion ambiental. - -## Alcance Funcional - -### Incluido -- Gestion de incidentes y accidentes -- Registro de capacitaciones (DC-3, DC-4) -- Inspecciones de seguridad -- Control de EPP (Equipo de Proteccion Personal) -- Cumplimiento normativo STPS -- Gestion ambiental (residuos, impacto) -- Permisos de trabajo peligroso -- Indicadores de seguridad (LTIR, TRIR) - -### Excluido -- Nomina de personal (ver MAI-007) -- Control de asistencias (ver MAI-007) -- Contratos de trabajo (ver MAI-012) - -## Dependencias - -### Depende de -- **MAI-001**: Fundamentos y Seguridad (autenticacion, roles) -- **MAI-002**: Proyectos y Estructura (fraccionamientos, obras) -- **MAI-007**: RRHH y Asistencias (personal, cuadrillas) - -### Es dependido por -- Ninguno (modulo terminal) - -## Requerimientos Funcionales - -| Codigo | Nombre | Prioridad | -|--------|--------|-----------| -| RF-MAA017-001 | Gestion de Incidentes | P0 | -| RF-MAA017-002 | Control de Capacitaciones | P0 | -| RF-MAA017-003 | Inspecciones de Seguridad | P1 | -| RF-MAA017-004 | Control de EPP | P1 | -| RF-MAA017-005 | Cumplimiento STPS | P0 | -| RF-MAA017-006 | Gestion Ambiental | P2 | -| RF-MAA017-007 | Permisos de Trabajo | P1 | -| RF-MAA017-008 | Indicadores HSE | P1 | - -## Especificaciones Tecnicas - -| Codigo | Nombre | Capa | -|--------|--------|------| -| ET-MAA017-DB-001 | Schema HSE Database | Database | -| ET-MAA017-BE-001 | Incidents Service | Backend | -| ET-MAA017-BE-002 | Training Service | Backend | -| ET-MAA017-BE-003 | Inspections Service | Backend | -| ET-MAA017-FE-001 | HSE Dashboard | Frontend | -| ET-MAA017-FE-002 | Incident Form | Frontend | -| ET-MAA017-FE-003 | Training Management | Frontend | - -## User Stories - -| Codigo | Titulo | Puntos | -|--------|--------|--------| -| US-MAA017-001 | Registrar incidente de seguridad | 5 | -| US-MAA017-002 | Investigar accidente | 8 | -| US-MAA017-003 | Programar capacitacion | 3 | -| US-MAA017-004 | Registrar asistencia a capacitacion | 2 | -| US-MAA017-005 | Realizar inspeccion de seguridad | 5 | -| US-MAA017-006 | Asignar EPP a trabajador | 3 | -| US-MAA017-007 | Generar reporte STPS | 5 | -| US-MAA017-008 | Solicitar permiso trabajo peligroso | 5 | -| US-MAA017-009 | Consultar indicadores HSE | 3 | -| US-MAA017-010 | Registrar manejo de residuos | 3 | - -**Total Story Points**: 42 - -## Schema de Base de Datos - -``` -Schema: hse -Tablas: -鈹溾攢鈹 incidentes (registro de incidentes/accidentes) -鈹溾攢鈹 incidente_involucrados (personas involucradas) -鈹溾攢鈹 incidente_investigacion (investigacion de causas) -鈹溾攢鈹 incidente_acciones (acciones correctivas) -鈹溾攢鈹 capacitaciones (catalogo de capacitaciones) -鈹溾攢鈹 capacitacion_sesiones (sesiones programadas) -鈹溾攢鈹 capacitacion_asistentes (asistencia a sesiones) -鈹溾攢鈹 constancias_dc3 (constancias STPS DC-3) -鈹溾攢鈹 inspecciones_seguridad (inspecciones realizadas) -鈹溾攢鈹 inspeccion_hallazgos (hallazgos de inspeccion) -鈹溾攢鈹 epp_catalogo (catalogo de EPP) -鈹溾攢鈹 epp_asignaciones (asignacion de EPP a personal) -鈹溾攢鈹 permisos_trabajo (permisos de trabajo peligroso) -鈹溾攢鈹 indicadores_hse (indicadores mensuales) -鈹溾攢鈹 residuos_peligrosos (registro de residuos) -鈹斺攢鈹 manifiestos_residuos (manifiestos de traslado) -``` - -## Normativas Aplicables - -- NOM-030-STPS-2009: Servicios preventivos de seguridad y salud -- NOM-017-STPS-2008: Equipo de proteccion personal -- NOM-019-STPS-2011: Comisiones de seguridad e higiene -- NOM-009-STPS-2011: Trabajos en altura -- NOM-031-STPS-2011: Construccion -- Ley General de Equilibrio Ecologico (LGEEPA) - -## Indicadores Clave - -| Indicador | Formula | Meta | -|-----------|---------|------| -| LTIR | (Accidentes incapacitantes x 200,000) / Horas trabajadas | < 1.0 | -| TRIR | (Total incidentes x 200,000) / Horas trabajadas | < 3.0 | -| Cumplimiento capacitacion | Capacitados / Personal total x 100 | > 95% | -| Inspecciones completadas | Realizadas / Programadas x 100 | > 90% | - -## Integraciones - -### Internas -- MAI-007: Obtener lista de personal activo -- MAI-002: Asociar incidentes a obras/fraccionamientos -- MAI-005: Vincular con bitacora de obra - -### Externas -- IMSS: Reporte de accidentes de trabajo -- STPS: Constancias DC-3, DC-4 -- SEMARNAT: Manifiestos de residuos peligrosos - -## Consideraciones de Implementacion - -1. **Offline**: Las inspecciones deben poder realizarse sin conexion -2. **Fotos**: Requeridas para incidentes e inspecciones -3. **Geolocation**: Registrar ubicacion de incidentes -4. **Alertas**: Notificaciones inmediatas para incidentes graves -5. **Auditoria**: Log completo de cambios para cumplimiento - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Subregistro de incidentes | Media | Alto | Capacitacion, cultura de reporte | -| Incumplimiento normativo | Baja | Critico | Alertas automaticas, dashboards | -| Datos incompletos | Media | Medio | Validaciones obligatorias | - -## Cronograma Sugerido - -- Sprint 15: RF-MAA017-001, RF-MAA017-002 (Incidentes y Capacitaciones) -- Sprint 16: RF-MAA017-003, RF-MAA017-004 (Inspecciones y EPP) -- Sprint 17: RF-MAA017-005, RF-MAA017-007 (STPS y Permisos) -- Sprint 18: RF-MAA017-006, RF-MAA017-008 (Ambiental e Indicadores) - ---- - -**Autor**: Requirements-Analyst -**Fecha**: 2025-12-06 -**Version**: 1.0.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/INDICE-RF-MAA017.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/INDICE-RF-MAA017.md deleted file mode 100644 index 53e5d34a5..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/INDICE-RF-MAA017.md +++ /dev/null @@ -1,53 +0,0 @@ -# INDICE DE REQUERIMIENTOS FUNCIONALES - MAA-017 - -## Modulo: Seguridad HSE - -| Codigo | Nombre | Prioridad | Estado | -|--------|--------|-----------|--------| -| RF-MAA017-001 | Gestion de Incidentes | P0 | Documentado | -| RF-MAA017-002 | Control de Capacitaciones | P0 | Documentado | -| RF-MAA017-003 | Inspecciones de Seguridad | P1 | Documentado | -| RF-MAA017-004 | Control de EPP | P1 | Documentado | -| RF-MAA017-005 | Cumplimiento STPS | P0 | Documentado | -| RF-MAA017-006 | Gestion Ambiental | P2 | Documentado | -| RF-MAA017-007 | Permisos de Trabajo | P1 | Documentado | -| RF-MAA017-008 | Indicadores HSE | P1 | Documentado | - -## Resumen - -- **Total RF**: 8 -- **Documentados**: 8 -- **Pendientes**: 0 -- **Prioridad P0**: 3 -- **Prioridad P1**: 4 -- **Prioridad P2**: 1 - -## Descripcion por RF - -### RF-MAA017-001: Gestion de Incidentes -Registro, investigacion y seguimiento de incidentes y accidentes de trabajo. - -### RF-MAA017-002: Control de Capacitaciones -Programacion, registro y seguimiento de capacitaciones de seguridad. Generacion de constancias DC-3. - -### RF-MAA017-003: Inspecciones de Seguridad -Ejecucion de inspecciones periodicas, registro de hallazgos y seguimiento de correcciones. - -### RF-MAA017-004: Control de EPP -Catalogo de EPP, asignacion a personal, control de vida util y renovaciones. - -### RF-MAA017-005: Cumplimiento STPS -Verificacion de cumplimiento normativo, generacion de reportes oficiales, alertas de vencimiento. - -### RF-MAA017-006: Gestion Ambiental -Manejo de residuos peligrosos, manifiestos de traslado, impacto ambiental. - -### RF-MAA017-007: Permisos de Trabajo -Solicitud y autorizacion de permisos para trabajos peligrosos (altura, espacios confinados, caliente). - -### RF-MAA017-008: Indicadores HSE -Dashboard de indicadores clave (LTIR, TRIR), tendencias, comparativos por obra. - ---- - -**Ultima actualizacion**: 2025-12-06 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-001-gestion-incidentes.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-001-gestion-incidentes.md deleted file mode 100644 index 8113f3f41..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-001-gestion-incidentes.md +++ /dev/null @@ -1,140 +0,0 @@ -# RF-MAA017-001: Gestion de Incidentes - -## Informacion General - -| Atributo | Valor | -|----------|-------| -| **Codigo** | RF-MAA017-001 | -| **Nombre** | Gestion de Incidentes | -| **Modulo** | MAA-017 Seguridad HSE | -| **Prioridad** | P0 - Critica | -| **Complejidad** | Alta | - -## Descripcion - -El sistema debe permitir el registro, seguimiento e investigacion de incidentes y accidentes de trabajo ocurridos en las obras. Incluye clasificacion por gravedad, investigacion de causas raiz, y seguimiento de acciones correctivas. - -## Requisitos Funcionales - -### RF-MAA017-001.1: Registro de Incidentes -- Capturar datos basicos: fecha, hora, ubicacion, descripcion -- Clasificar por tipo: accidente, incidente, casi-accidente -- Clasificar por gravedad: leve, moderado, grave, fatal -- Registrar personas involucradas (lesionados, testigos) -- Adjuntar fotografias del lugar/lesiones -- Capturar geolocalizacion automatica -- Generar numero de folio automatico - -### RF-MAA017-001.2: Investigacion de Accidentes -- Metodologia de investigacion (5 Por ques, Arbol de causas) -- Identificar causas inmediatas y basicas -- Clasificar factores: acto inseguro, condicion insegura -- Documentar declaraciones de testigos -- Adjuntar evidencia fotografica/documental -- Determinar acciones correctivas y preventivas - -### RF-MAA017-001.3: Seguimiento de Acciones -- Asignar responsables y fechas compromiso -- Enviar notificaciones de vencimiento -- Registrar avances y evidencias de cumplimiento -- Cerrar acciones con evidencia -- Calcular efectividad de acciones - -### RF-MAA017-001.4: Reportes IMSS -- Generar formato ST-7 (Aviso de accidente) -- Generar formato ST-9 (Recaida) -- Calcular dias de incapacidad -- Exportar en formato requerido - -## Reglas de Negocio - -1. Todo incidente debe registrarse en menos de 24 horas -2. Accidentes graves requieren investigacion en 48 horas -3. Accidentes fatales deben notificarse inmediatamente a gerencia -4. Las acciones correctivas tienen maximo 30 dias para implementarse -5. No se puede cerrar un incidente sin completar la investigacion -6. Los testigos deben firmar sus declaraciones - -## Criterios de Aceptacion - -- [ ] Usuario puede registrar incidente con todos los campos requeridos -- [ ] Sistema genera folio unico automaticamente -- [ ] Se pueden adjuntar hasta 10 fotos por incidente -- [ ] Geolocalizacion se captura automaticamente -- [ ] Investigacion sigue metodologia definida -- [ ] Acciones correctivas tienen seguimiento automatico -- [ ] Alertas se envian 3 dias antes del vencimiento -- [ ] Reportes IMSS se generan en formato oficial - -## Datos Requeridos - -### Incidente -``` -- folio: VARCHAR(20) AUTO -- fecha_hora: TIMESTAMP -- fraccionamiento_id: UUID FK -- ubicacion_descripcion: TEXT -- ubicacion_geo: GEOMETRY(Point) -- tipo: ENUM(accidente, incidente, casi_accidente) -- gravedad: ENUM(leve, moderado, grave, fatal) -- descripcion: TEXT -- causa_inmediata: TEXT -- causa_basica: TEXT -- estado: ENUM(abierto, en_investigacion, cerrado) -``` - -### Involucrado -``` -- incidente_id: UUID FK -- employee_id: UUID FK -- rol: ENUM(lesionado, testigo, responsable) -- descripcion_lesion: TEXT -- parte_cuerpo: VARCHAR(100) -- dias_incapacidad: INTEGER -``` - -## Mockups - -### Pantalla: Registro de Incidente -``` -+------------------------------------------+ -| REGISTRO DE INCIDENTE | -+------------------------------------------+ -| Folio: INC-2025-0001 (auto) | -| | -| Fecha/Hora: [____/__/__] [__:__] | -| Fraccionamiento: [_____________ v] | -| Ubicacion: [_________________________] | -| | -| Tipo: (o) Accidente | -| ( ) Incidente | -| ( ) Casi-accidente | -| | -| Gravedad: ( ) Leve (o) Moderado | -| ( ) Grave ( ) Fatal | -| | -| Descripcion: | -| [_____________________________________] | -| [_____________________________________] | -| | -| Fotos: [+ Agregar] [IMG1] [IMG2] | -| | -| [Cancelar] [Guardar] | -+------------------------------------------+ -``` - -## Especificaciones Relacionadas - -- ET-MAA017-DB-001: Schema HSE Database -- ET-MAA017-BE-001: Incidents Service -- ET-MAA017-FE-002: Incident Form - -## User Stories Relacionadas - -- US-MAA017-001: Registrar incidente de seguridad -- US-MAA017-002: Investigar accidente - ---- - -**Autor**: Requirements-Analyst -**Fecha**: 2025-12-06 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-002-control-capacitaciones.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-002-control-capacitaciones.md deleted file mode 100644 index 7a2dd71f0..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-002-control-capacitaciones.md +++ /dev/null @@ -1,512 +0,0 @@ -# RF-MAA017-002: Control de Capacitaciones - -**脡pica:** MAA-017 - Seguridad HSE -**Versi贸n:** 1.0 -**Fecha:** 2025-12-06 -**Responsable:** Requirements-Analyst - ---- - -## 1. Descripci贸n General - -Sistema integral para la gesti贸n de capacitaciones de seguridad industrial, salud ocupacional y medio ambiente. Permite programar, ejecutar, registrar asistencia y generar constancias oficiales (DC-3, DC-4) conforme a la normativa STPS. - -### Objetivos -- Asegurar que el 100% del personal cuente con capacitaci贸n requerida -- Cumplir con normativas STPS (NOM-030, NOM-017, NOM-009) -- Generar constancias DC-3 v谩lidas ante autoridades -- Mantener trazabilidad de competencias del personal - ---- - -## 2. Alcance Funcional - -### 2.1 Cat谩logo de Capacitaciones - -**Tipos de Capacitaci贸n:** -``` -鈹溾攢鈹 Inducci贸n General -鈹 鈹溾攢鈹 Inducci贸n empresa (obligatoria d铆a 1) -鈹 鈹溾攢鈹 Inducci贸n obra espec铆fica -鈹 鈹斺攢鈹 Inducci贸n subcontratistas -鈹溾攢鈹 Seguridad Industrial -鈹 鈹溾攢鈹 Trabajos en altura (NOM-009) -鈹 鈹溾攢鈹 Espacios confinados (NOM-033) -鈹 鈹溾攢鈹 Trabajo en caliente -鈹 鈹溾攢鈹 Manejo de materiales peligrosos -鈹 鈹溾攢鈹 Bloqueo y etiquetado (LOTO) -鈹 鈹斺攢鈹 Uso de EPP (NOM-017) -鈹溾攢鈹 Primeros Auxilios -鈹 鈹溾攢鈹 RCP b谩sico -鈹 鈹溾攢鈹 Primeros auxilios nivel 1 -鈹 鈹斺攢鈹 Brigadas de emergencia -鈹溾攢鈹 Prevenci贸n de Riesgos -鈹 鈹溾攢鈹 Identificaci贸n de peligros (IPER) -鈹 鈹溾攢鈹 An谩lisis de seguridad en el trabajo (AST) -鈹 鈹斺攢鈹 Reporte de condiciones inseguras -鈹斺攢鈹 Medio Ambiente - 鈹溾攢鈹 Manejo de residuos - 鈹溾攢鈹 Ahorro de agua y energ铆a - 鈹斺攢鈹 Respuesta a derrames -``` - -**Atributos de Capacitaci贸n:** -``` -- C贸digo 煤nico (CAP-{tipo}-{consecutivo}) -- Nombre -- Descripci贸n -- Duraci贸n (horas) -- Modalidad: presencial, virtual, mixta -- Vigencia (meses) -- Normativa aplicable (NOM referencia) -- Requiere evaluaci贸n: s铆/no -- Puntaje m铆nimo aprobatorio: 0-100 -- Requiere pr谩ctica: s铆/no -- Instructor requerido: interno/externo/certificado -- Costo estimado por persona -- Materiales requeridos -``` - -### 2.2 Programaci贸n de Capacitaciones - -**Matriz de Capacitaci贸n por Puesto:** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Puesto 鈹 Inducci贸n 鈹 Altura 鈹 EPP 鈹 P.Aux 鈹 Freq 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Residente de obra 鈹 鈼 鈹 鈼 鈹 鈼 鈹 鈼 鈹 Anual 鈹 -鈹 Supervisor 鈹 鈼 鈹 鈼 鈹 鈼 鈹 鈼 鈹 Anual 鈹 -鈹 Oficial alba帽il 鈹 鈼 鈹 鈼 鈹 鈼 鈹 鈼 鈹 Anual 鈹 -鈹 Oficial electricista鈹 鈼 鈹 鈼 鈹 鈼 鈹 鈼 鈹 Anual 鈹 -鈹 Ayudante general 鈹 鈼 鈹 鈼 鈹 鈼 鈹 鈼 鈹 Anual 鈹 -鈹 Operador maquinaria 鈹 鈼 鈹 鈼 鈹 鈼 鈹 鈼 鈹 Anual 鈹 -鈹 Almacenista 鈹 鈼 鈹 鈼 鈹 鈼 鈹 鈼 鈹 Anual 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈼 Obligatoria 鈼 Opcional/Recomendada -``` - -**Programaci贸n de Sesiones:** -``` -Sesi贸n de Capacitaci贸n: -- ID 煤nico -- Capacitaci贸n (FK) -- Fraccionamiento/Obra -- Fecha y hora inicio -- Fecha y hora fin -- Lugar (aula, obra, virtual) -- Instructor (interno/externo) -- Cupo m谩ximo -- Cupo disponible -- Estado: programada, en_curso, completada, cancelada -- Costo real -- Observaciones -``` - -### 2.3 Registro de Asistencia - -**Proceso de Registro:** -1. Lista de asistencia digital (tablet/m贸vil) -2. Firma electr贸nica del participante -3. Foto de evidencia (opcional) -4. Hora de entrada/salida -5. Validaci贸n biom茅trica (opcional) - -**Estados de Participante:** -- Inscrito -- Asisti贸 -- No asisti贸 (justificado/injustificado) -- Aprobado -- Reprobado -- Pendiente evaluaci贸n - -### 2.4 Evaluaci贸n de Conocimientos - -**Tipos de Evaluaci贸n:** -``` -鈹溾攢鈹 Examen te贸rico -鈹 鈹溾攢鈹 Opci贸n m煤ltiple (10-20 preguntas) -鈹 鈹溾攢鈹 Verdadero/Falso -鈹 鈹斺攢鈹 Casos pr谩cticos -鈹溾攢鈹 Evaluaci贸n pr谩ctica -鈹 鈹溾攢鈹 Demostraci贸n de habilidades -鈹 鈹溾攢鈹 Simulacro -鈹 鈹斺攢鈹 Checklist de competencias -鈹斺攢鈹 Evaluaci贸n combinada - 鈹斺攢鈹 Teor铆a (60%) + Pr谩ctica (40%) -``` - -**Banco de Preguntas:** -``` -Pregunta: -- Capacitaci贸n (FK) -- Texto de pregunta -- Tipo: multiple, boolean, open -- Opciones (si aplica) -- Respuesta correcta -- Puntos -- Nivel: b谩sico, intermedio, avanzado -- Activa: s铆/no -``` - -### 2.5 Constancias DC-3 - -**Formato DC-3 (STPS):** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CONSTANCIA DE COMPETENCIAS O HABILIDADES LABORALES 鈹 -鈹 FORMATO DC-3 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 DATOS DEL TRABAJADOR 鈹 -鈹 Nombre: ________________________________________________ 鈹 -鈹 CURP: __________________________________________________ 鈹 -鈹 Puesto: ________________________________________________ 鈹 -鈹 Empresa: _______________________________________________ 鈹 -鈹 鈹 -鈹 DATOS DE LA CAPACITACI脫N 鈹 -鈹 Nombre del curso: ______________________________________ 鈹 -鈹 Duraci贸n: _______ horas 鈹 -鈹 Per铆odo: Del __________ al __________ 鈹 -鈹 脕rea tem谩tica: _________________________________________ 鈹 -鈹 鈹 -鈹 DATOS DEL INSTRUCTOR 鈹 -鈹 Nombre: ________________________________________________ 鈹 -鈹 Registro STPS: _________________________________________ 鈹 -鈹 鈹 -鈹 DATOS DEL REPRESENTANTE LEGAL 鈹 -鈹 Nombre: ________________________________________________ 鈹 -鈹 Cargo: _________________________________________________ 鈹 -鈹 鈹 -鈹 Lugar y fecha: _________________, a __ de _______ de 20__ 鈹 -鈹 鈹 -鈹 鈹 -鈹 _____________________ _____________________ 鈹 -鈹 Firma del Instructor Firma Rep. Legal 鈹 -鈹 鈹 -鈹 Folio: DC3-2025-XXXXX 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Generaci贸n Autom谩tica:** -- Al aprobar capacitaci贸n -- Datos pre-llenados del sistema -- Folio 煤nico secuencial por a帽o -- PDF firmable electr贸nicamente -- QR de verificaci贸n - -### 2.6 Vencimientos y Renovaciones - -**Alertas Autom谩ticas:** -``` -D铆as antes de vencimiento 鈫 Acci贸n -90 d铆as 鈫 Notificaci贸n preventiva -60 d铆as 鈫 Alerta a supervisor -30 d铆as 鈫 Alerta a RRHH + empleado -15 d铆as 鈫 Escalamiento a gerencia -0 d铆as 鈫 Bloqueo de acceso a obra (configurable) -``` - -**Dashboard de Vencimientos:** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 PR脫XIMOS VENCIMIENTOS - Diciembre 2025 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 馃敶 Vencidos (5) 鈹 -鈹 鈥 Juan P茅rez - Trabajo en altura - Venci贸 01/12/2025 鈹 -鈹 鈥 Mar铆a L贸pez - EPP - Venci贸 28/11/2025 鈹 -鈹 鈹 -鈹 馃煛 Pr贸ximos 30 d铆as (12) 鈹 -鈹 鈥 Carlos Ruiz - Primeros auxilios - Vence 15/12/2025 鈹 -鈹 鈥 Ana Garc铆a - Espacios confinados - Vence 20/12/2025 鈹 -鈹 鈹 -鈹 馃煝 Vigentes (145) 鈹 -鈹 鈥 95% del personal al d铆a 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 3. Modelo de Datos - -```typescript -// Tabla: hse.capacitaciones (cat谩logo) -{ - id: UUID, - tenant_id: UUID, - code: VARCHAR(20) UNIQUE, - name: VARCHAR(200), - description: TEXT, - duration_hours: DECIMAL(4,1), - modality: ENUM('presencial', 'virtual', 'mixta'), - validity_months: INTEGER, - norm_reference: VARCHAR(50), // NOM-009-STPS-2011 - requires_evaluation: BOOLEAN, - min_passing_score: INTEGER, // 0-100 - requires_practice: BOOLEAN, - instructor_type: ENUM('interno', 'externo', 'certificado'), - estimated_cost: DECIMAL(10,2), - materials: TEXT, - is_mandatory: BOOLEAN, - applicable_positions: TEXT[], // Array de puestos - created_at: TIMESTAMP, - created_by: UUID, - updated_at: TIMESTAMP, - is_active: BOOLEAN -} - -// Tabla: hse.capacitacion_sesiones -{ - id: UUID, - tenant_id: UUID, - capacitacion_id: UUID FK, - fraccionamiento_id: UUID FK, - session_date: DATE, - start_time: TIME, - end_time: TIME, - location: VARCHAR(200), - location_type: ENUM('aula', 'obra', 'virtual'), - instructor_id: UUID, // Puede ser empleado o externo - instructor_name: VARCHAR(200), - instructor_stps_registration: VARCHAR(50), - max_capacity: INTEGER, - current_enrollment: INTEGER, - status: ENUM('programada', 'en_curso', 'completada', 'cancelada'), - actual_cost: DECIMAL(10,2), - notes: TEXT, - created_at: TIMESTAMP, - created_by: UUID -} - -// Tabla: hse.capacitacion_asistentes -{ - id: UUID, - tenant_id: UUID, - sesion_id: UUID FK, - employee_id: UUID FK, - enrollment_date: TIMESTAMP, - attendance_status: ENUM('inscrito', 'asistio', 'no_asistio', 'justificado'), - check_in_time: TIMESTAMP, - check_out_time: TIMESTAMP, - signature_url: VARCHAR(500), - photo_url: VARCHAR(500), - evaluation_score: INTEGER, - evaluation_status: ENUM('pendiente', 'aprobado', 'reprobado'), - certificate_number: VARCHAR(30), - certificate_date: DATE, - valid_until: DATE, - notes: TEXT, - created_at: TIMESTAMP -} - -// Tabla: hse.constancias_dc3 -{ - id: UUID, - tenant_id: UUID, - asistente_id: UUID FK, - folio: VARCHAR(20) UNIQUE, // DC3-2025-00001 - employee_name: VARCHAR(200), - employee_curp: VARCHAR(18), - employee_position: VARCHAR(100), - course_name: VARCHAR(200), - course_duration: DECIMAL(4,1), - course_start_date: DATE, - course_end_date: DATE, - thematic_area: VARCHAR(100), - instructor_name: VARCHAR(200), - instructor_stps_reg: VARCHAR(50), - legal_rep_name: VARCHAR(200), - legal_rep_position: VARCHAR(100), - issue_place: VARCHAR(100), - issue_date: DATE, - pdf_url: VARCHAR(500), - qr_verification_code: VARCHAR(50), - is_valid: BOOLEAN, - invalidation_reason: TEXT, - created_at: TIMESTAMP, - created_by: UUID -} - -// Tabla: hse.evaluacion_preguntas -{ - id: UUID, - tenant_id: UUID, - capacitacion_id: UUID FK, - question_text: TEXT, - question_type: ENUM('multiple', 'boolean', 'open'), - options: JSONB, // [{text, is_correct}, ...] - correct_answer: TEXT, - points: INTEGER, - difficulty: ENUM('basico', 'intermedio', 'avanzado'), - is_active: BOOLEAN, - created_at: TIMESTAMP -} - -// Tabla: hse.evaluacion_respuestas -{ - id: UUID, - tenant_id: UUID, - asistente_id: UUID FK, - pregunta_id: UUID FK, - answer_given: TEXT, - is_correct: BOOLEAN, - points_earned: INTEGER, - answered_at: TIMESTAMP -} -``` - ---- - -## 4. Casos de Uso - -### CU-001: Programar Sesi贸n de Capacitaci贸n -**Actor:** Coordinador HSE -**Flujo:** -1. Accede a "Programar Capacitaci贸n" -2. Selecciona capacitaci贸n del cat谩logo -3. Define: - - Obra/Fraccionamiento - - Fecha y hora - - Lugar (aula o virtual) - - Instructor - - Cupo m谩ximo -4. Sistema valida disponibilidad de instructor -5. Guarda sesi贸n con estado "Programada" -6. Notifica a supervisores de obra - -### CU-002: Registrar Asistencia -**Actor:** Instructor / Supervisor -**Flujo:** -1. Abre sesi贸n en tablet -2. Lista de inscritos aparece -3. Por cada asistente: - - Marca presente/ausente - - Captura firma digital - - Registra hora de entrada -4. Al finalizar: - - Registra hora de salida de todos - - Captura foto grupal (evidencia) -5. Cierra registro de asistencia - -### CU-003: Aplicar Evaluaci贸n -**Actor:** Instructor -**Flujo:** -1. Inicia evaluaci贸n de sesi贸n -2. Sistema genera examen: - - Selecciona preguntas aleatorias del banco - - 10 preguntas de opci贸n m煤ltiple -3. Participantes responden en tablet/m贸vil -4. Al terminar: - - Sistema califica autom谩ticamente - - Muestra resultados - - Marca aprobados/reprobados -5. Genera constancias DC-3 para aprobados - -### CU-004: Generar Constancia DC-3 -**Actor:** Sistema (autom谩tico) -**Flujo:** -1. Participante aprueba evaluaci贸n -2. Sistema genera: - - Folio 煤nico: DC3-2025-00001 - - PDF con datos completos - - C贸digo QR de verificaci贸n -3. Almacena en repositorio -4. Env铆a por email al participante -5. Notifica a RRHH - -### CU-005: Consultar Vencimientos -**Actor:** Gerente RRHH -**Flujo:** -1. Accede a "Dashboard de Capacitaci贸n" -2. Visualiza: - - Capacitaciones vencidas (rojo) - - Por vencer 30 d铆as (amarillo) - - Vigentes (verde) -3. Filtra por: - - Obra - - Tipo de capacitaci贸n - - Supervisor -4. Exporta reporte Excel -5. Programa sesiones de renovaci贸n - ---- - -## 5. Interfaces de Usuario - -### Vista: Cat谩logo de Capacitaciones -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Cat谩logo de Capacitaciones [+ Nueva] [鈿橾 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 馃攳 Buscar: [________________________] [馃攷] 鈹 -鈹 Tipo: [Todos 鈻糫 Modalidad: [Todos 鈻糫 Estado: [Activas 鈻糫 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 C贸digo 鈹 Nombre 鈹 Horas 鈹 Vigencia 鈹 鈰 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹 -鈹 CAP-IND-01 鈹 Inducci贸n General 鈹 4h 鈹 12 meses 鈹 鈰 鈹 -鈹 CAP-ALT-01 鈹 Trabajo en Altura NOM-009 鈹 8h 鈹 12 meses 鈹 鈰 鈹 -鈹 CAP-EPP-01 鈹 Uso de EPP NOM-017 鈹 2h 鈹 12 meses 鈹 鈰 鈹 -鈹 CAP-PAU-01 鈹 Primeros Auxilios B谩sico 鈹 8h 鈹 24 meses 鈹 鈰 鈹 -鈹 CAP-ESP-01 鈹 Espacios Confinados 鈹 8h 鈹 12 meses 鈹 鈰 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -### Vista: Registro de Asistencia (Tablet) -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Trabajo en Altura NOM-009 Fecha: 06/12/2025 14:00 鈹 -鈹 Instructor: Ing. Roberto S谩nchez Obra: Fracc. Los Pinos 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 # 鈹 Nombre 鈹 Puesto 鈹 Entrada 鈹 Firma 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 1 鈹 Juan P茅rez Garc铆a 鈹 Oficial 鈹 14:02 鈹 鉁 鈹 鈹 -鈹 鈹 2 鈹 Mar铆a L贸pez Ruiz 鈹 Ayudante 鈹 14:05 鈹 鉁 鈹 鈹 -鈹 鈹 3 鈹 Carlos Hern谩ndez 鈹 Oficial 鈹 --:-- 鈹 - 鈹 鈹 -鈹 鈹 4 鈹 Ana Mart铆nez 鈹 Superv. 鈹 14:01 鈹 鉁 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 Asistencia: 3/4 (75%) 鈹 -鈹 鈹 -鈹 [Agregar Participante] [Cerrar Asistencia] 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 6. Reglas de Negocio - -1. **Inducci贸n obligatoria**: Todo trabajador nuevo debe completar inducci贸n antes de ingresar a obra -2. **Vigencia estricta**: Personal con capacitaci贸n vencida no puede realizar trabajos de alto riesgo -3. **Instructor certificado**: Capacitaciones de NOM requieren instructor con registro STPS -4. **Evaluaci贸n requerida**: Capacitaciones de seguridad requieren evaluaci贸n con m铆nimo 80% -5. **DC-3 煤nico**: Un trabajador solo puede tener una DC-3 vigente por capacitaci贸n -6. **Asistencia m铆nima**: Se requiere 90% de asistencia para aprobar - ---- - -## 7. Criterios de Aceptaci贸n - -- [ ] Cat谩logo de capacitaciones con todos los atributos -- [ ] Programaci贸n de sesiones con validaci贸n de disponibilidad -- [ ] Registro de asistencia digital con firma -- [ ] Evaluaci贸n autom谩tica de conocimientos -- [ ] Generaci贸n de constancias DC-3 en PDF -- [ ] Dashboard de vencimientos con alertas -- [ ] Matriz de capacitaci贸n por puesto -- [ ] Reportes de cumplimiento por obra -- [ ] Integraci贸n con m贸dulo RRHH (empleados) -- [ ] Notificaciones autom谩ticas de vencimiento - ---- - -## 8. M茅tricas de 脡xito - -- **Cobertura**: 100% del personal con inducci贸n completada -- **Cumplimiento**: >95% de capacitaciones al d铆a -- **Eficiencia**: Generaci贸n de DC-3 en <5 minutos -- **Trazabilidad**: 100% de capacitaciones con evidencia - ---- - -**Estado:** 鉁 Ready for Development diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-003-inspecciones-seguridad.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-003-inspecciones-seguridad.md deleted file mode 100644 index cdb3ca0a6..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-003-inspecciones-seguridad.md +++ /dev/null @@ -1,405 +0,0 @@ -# RF-MAA017-003: Inspecciones de Seguridad - -## Informacion General - -| Atributo | Valor | -|----------|-------| -| **Codigo** | RF-MAA017-003 | -| **Nombre** | Inspecciones de Seguridad | -| **Modulo** | MAA-017 Seguridad HSE | -| **Prioridad** | P1 - Alta | -| **Complejidad** | Alta | - -## Descripcion - -El sistema debe permitir la ejecucion, registro y seguimiento de inspecciones de seguridad en obras de construccion. Incluye diferentes tipos de inspecciones (rutinarias, especiales, pre-inicio), registro de hallazgos, calificacion de areas y seguimiento de acciones correctivas hasta su cierre. - -## Requisitos Funcionales - -### RF-MAA017-003.1: Catalogo de Tipos de Inspeccion - -- Definir tipos de inspeccion con sus caracteristicas -- Tipos predefinidos: - - Inspeccion rutinaria diaria - - Inspeccion semanal de obra - - Inspeccion pre-inicio de actividad - - Inspeccion de equipos/maquinaria - - Inspeccion de andamios - - Inspeccion de excavaciones - - Inspeccion de espacios confinados - - Inspeccion ambiental -- Configurar checklist por tipo de inspeccion -- Definir frecuencia requerida por tipo -- Asignar categorias de evaluacion (cumple, no cumple, no aplica) -- Registrar norma STPS de referencia - -### RF-MAA017-003.2: Programacion de Inspecciones - -- Crear programa mensual de inspecciones por obra -- Asignar inspector responsable (calificado) -- Definir areas/zonas a inspeccionar -- Establecer fecha y hora programada -- Generar calendario visual de inspecciones -- Enviar recordatorios automaticos a inspectores -- Permitir reprogramacion con motivo -- Alertar sobre inspecciones vencidas - -### RF-MAA017-003.3: Ejecucion de Inspeccion - -- Cargar checklist predefinido del tipo de inspeccion -- Ejecutar en modo offline (sincronizar al conectar) -- Evaluar cada item: cumple, no cumple, no aplica -- Agregar observaciones por item -- Capturar fotografias de evidencia (minimo 3, maximo 20) -- Registrar ubicacion GPS automatica -- Registrar fecha/hora de inicio y fin -- Calcular porcentaje de cumplimiento automatico -- Permitir inspeccion parcial (guardar avance) -- Firma digital del inspector - -### RF-MAA017-003.4: Registro de Hallazgos - -- Crear hallazgo desde item no conforme -- Clasificar gravedad: critico, mayor, menor -- Clasificar tipo: condicion insegura, acto inseguro -- Describir hallazgo detalladamente -- Adjuntar fotos del hallazgo -- Marcar ubicacion exacta en plano de obra -- Asignar responsable de correccion -- Establecer fecha limite de correccion -- Generar accion correctiva automatica - -### RF-MAA017-003.5: Seguimiento y Cierre - -- Dashboard de hallazgos abiertos por obra -- Alertas de fechas proximas a vencer -- Registrar avances de correccion -- Adjuntar evidencia de correccion (fotos obligatorias) -- Verificar correccion en campo -- Cerrar hallazgo con firma de verificador -- Permitir reabrir si la correccion no es efectiva -- Calcular tiempo promedio de cierre - -### RF-MAA017-003.6: Reportes de Inspeccion - -- Generar reporte PDF de inspeccion con fotos -- Informe semanal de inspecciones por obra -- Reporte mensual de cumplimiento -- Comparativo de cumplimiento entre obras -- Top 10 de hallazgos recurrentes -- Tendencia de cumplimiento por area -- Exportar a Excel para analisis - -## Reglas de Negocio - -1. Inspecciones rutinarias deben realizarse diariamente antes de iniciar labores -2. Hallazgos criticos deben corregirse en maximo 24 horas -3. Hallazgos mayores tienen plazo maximo de 72 horas -4. No se puede cerrar hallazgo sin evidencia fotografica -5. Si un hallazgo critico no se corrige, se debe detener la actividad -6. Inspector no puede cerrar sus propios hallazgos (requiere verificador) -7. Obras con cumplimiento menor a 70% requieren plan de accion -8. Inspecciones vencidas mas de 3 dias generan alerta a gerencia - -## Criterios de Aceptacion - -- [ ] Usuario puede crear programa mensual de inspecciones -- [ ] Calendario muestra inspecciones programadas con colores por estado -- [ ] Checklist se carga automaticamente segun tipo de inspeccion -- [ ] Inspeccion funciona offline y sincroniza al conectar -- [ ] Fotos se capturan con geolocalizacion -- [ ] Porcentaje de cumplimiento se calcula en tiempo real -- [ ] Hallazgos se crean automaticamente de items no conformes -- [ ] Alertas se envian 24 horas antes de fecha limite -- [ ] Reporte PDF incluye fotos y firma digital -- [ ] Dashboard muestra metricas de cumplimiento - -## Modelo de Datos - -### Tabla: hse.tipos_inspeccion -``` -- id: UUID PK -- tenant_id: UUID FK (core.tenants) -- codigo: VARCHAR(20) UNIQUE -- nombre: VARCHAR(200) -- descripcion: TEXT -- frecuencia: ENUM(diaria, semanal, quincenal, mensual, eventual) -- norma_referencia: VARCHAR(50) -- duracion_estimada_min: INTEGER -- requiere_firma: BOOLEAN DEFAULT true -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.checklist_items -``` -- id: UUID PK -- tipo_inspeccion_id: UUID FK -- numero_orden: INTEGER -- categoria: VARCHAR(100) -- descripcion: TEXT -- criterio_cumplimiento: TEXT -- es_critico: BOOLEAN DEFAULT false -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.programa_inspecciones -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK (construction.fraccionamientos) -- tipo_inspeccion_id: UUID FK -- inspector_id: UUID FK (hr.employees) -- fecha_programada: DATE -- hora_programada: TIME -- zona_area: VARCHAR(200) -- estado: ENUM(programada, en_progreso, completada, cancelada, vencida) -- motivo_cancelacion: TEXT -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.inspecciones -``` -- id: UUID PK -- tenant_id: UUID FK -- programa_id: UUID FK (nullable si es no programada) -- tipo_inspeccion_id: UUID FK -- fraccionamiento_id: UUID FK -- inspector_id: UUID FK -- fecha_inicio: TIMESTAMPTZ -- fecha_fin: TIMESTAMPTZ -- ubicacion_geo: GEOMETRY(Point) -- items_evaluados: INTEGER -- items_cumple: INTEGER -- items_no_cumple: INTEGER -- items_no_aplica: INTEGER -- porcentaje_cumplimiento: DECIMAL(5,2) -- observaciones_generales: TEXT -- firma_inspector: TEXT -- estado: ENUM(borrador, completada, verificada) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.inspeccion_evaluaciones -``` -- id: UUID PK -- inspeccion_id: UUID FK -- checklist_item_id: UUID FK -- resultado: ENUM(cumple, no_cumple, no_aplica) -- observacion: TEXT -- genera_hallazgo: BOOLEAN DEFAULT false -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.hallazgos -``` -- id: UUID PK -- tenant_id: UUID FK -- inspeccion_id: UUID FK -- evaluacion_id: UUID FK -- folio: VARCHAR(20) UNIQUE -- gravedad: ENUM(critico, mayor, menor) -- tipo: ENUM(condicion_insegura, acto_inseguro) -- descripcion: TEXT -- ubicacion_descripcion: VARCHAR(500) -- ubicacion_geo: GEOMETRY(Point) -- responsable_correccion_id: UUID FK -- fecha_limite: DATE -- estado: ENUM(abierto, en_correccion, verificando, cerrado, reabierto) -- fecha_correccion: TIMESTAMPTZ -- descripcion_correccion: TEXT -- verificador_id: UUID FK -- fecha_verificacion: TIMESTAMPTZ -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.hallazgo_evidencias -``` -- id: UUID PK -- hallazgo_id: UUID FK -- tipo: ENUM(hallazgo, correccion) -- archivo_url: VARCHAR(500) -- descripcion: VARCHAR(200) -- ubicacion_geo: GEOMETRY(Point) -- created_at: TIMESTAMPTZ -- created_by: UUID FK -``` - -## Casos de Uso - -### CU-MAA017-003.1: Programar Inspecciones del Mes - -**Actor**: Coordinador HSE -**Precondicion**: Catalogo de tipos de inspeccion configurado - -**Flujo Principal**: -1. Coordinador selecciona obra y mes a programar -2. Sistema muestra calendario con dias habiles -3. Coordinador selecciona dia y tipo de inspeccion -4. Sistema muestra inspectores disponibles -5. Coordinador asigna inspector y zona -6. Sistema guarda y notifica al inspector -7. Sistema genera calendario visual del programa - -### CU-MAA017-003.2: Ejecutar Inspeccion en Campo - -**Actor**: Inspector de Seguridad -**Precondicion**: Inspeccion programada, inspector en obra - -**Flujo Principal**: -1. Inspector abre app movil y selecciona inspeccion -2. Sistema carga checklist del tipo de inspeccion -3. Inspector recorre area evaluando cada item -4. Por cada item, inspector marca resultado (cumple/no cumple/NA) -5. Si no cumple, inspector toma foto y registra observacion -6. Sistema crea hallazgo automaticamente -7. Al finalizar, inspector firma digitalmente -8. Sistema calcula porcentaje y guarda -9. Si hay conexion, sincroniza inmediatamente - -**Flujo Alterno - Sin Conexion**: -3a. Si no hay red, sistema trabaja en modo offline -9a. Sistema guarda localmente y marca para sincronizar -9b. Al recuperar conexion, sincroniza automaticamente - -### CU-MAA017-003.3: Cerrar Hallazgo - -**Actor**: Verificador HSE -**Precondicion**: Hallazgo corregido por responsable - -**Flujo Principal**: -1. Verificador consulta hallazgos pendientes de verificacion -2. Sistema muestra lista con evidencias de correccion -3. Verificador selecciona hallazgo y revisa evidencia -4. Verificador acude a campo a verificar fisicamente -5. Si correccion es efectiva, verificador cierra hallazgo -6. Sistema registra fecha y firma de verificacion -7. Sistema actualiza metricas de cumplimiento - -**Flujo Alterno - Correccion Insuficiente**: -5a. Si correccion no es efectiva, verificador reabre hallazgo -5b. Sistema notifica a responsable con nueva fecha limite - -## Mockups - -### Pantalla: Calendario de Inspecciones -``` -+--------------------------------------------------+ -| PROGRAMA DE INSPECCIONES - Enero 2025 | -| Obra: [Residencial Norte v] | -+--------------------------------------------------+ -| Lun Mar Mie Jue Vie Sab Dom | -+--------------------------------------------------+ -| 1 2 3 4 5 6 7 | -| [R] [R] [R] [R] [R] | -| [S] | -+--------------------------------------------------+ -| 8 9 10 11 12 13 14 | -| [R] [R] [R] [R] [R] | -| [E] [S] [A] | -+--------------------------------------------------+ -| Leyenda: [R]=Rutinaria [S]=Semanal | -| [E]=Equipos [A]=Andamios | -+--------------------------------------------------+ -| [+ Agregar Inspeccion] [Ver Lista] [Exportar] | -+--------------------------------------------------+ -``` - -### Pantalla: Ejecucion de Inspeccion (Movil) -``` -+----------------------------------+ -| < INSPECCION RUTINARIA | -| Obra: Residencial Norte | -| Zona: Edificio A - Nivel 3 | -+----------------------------------+ -| Progreso: 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻戔枒 80% | -| 16/20 items evaluados | -+----------------------------------+ -| ORDEN Y LIMPIEZA | -+----------------------------------+ -| 1. Areas de trabajo despejadas | -| [鉁 Cumple] [ ] No [ ] N/A | -| | -| 2. Materiales ordenados | -| [ ] Cumple [鉁揮 No [ ] N/A | -| Obs: [Varilla dispersa____] | -| [馃摲 Foto] | -| | -| 3. Rutas de evacuacion libres | -| [鉁 Cumple] [ ] No [ ] N/A | -+----------------------------------+ -| PROTECCION PERSONAL | -+----------------------------------+ -| 4. Uso de casco | -| [鉁 Cumple] [ ] No [ ] N/A | -+----------------------------------+ -| [Siguiente >>>] | -+----------------------------------+ -``` - -### Pantalla: Dashboard de Hallazgos -``` -+--------------------------------------------------+ -| HALLAZGOS ABIERTOS | -+--------------------------------------------------+ -| Filtros: [Todas las obras v] [Todos v] [30 dias] | -+--------------------------------------------------+ -| RESUMEN | -| +--------+ +--------+ +--------+ | -| | 12 | | 28 | | 45 | | -| |Criticos| |Mayores | |Menores | | -| +--------+ +--------+ +--------+ | -+--------------------------------------------------+ -| HALLAZGOS PROXIMOS A VENCER (3 dias) | -+--------------------------------------------------+ -| ! HAL-2025-0089 | CRITICO | Residencial Norte | -| Falta barandal en nivel 4 | -| Vence: HOY | Resp: Juan Perez | -| [Ver Detalle] | -+--------------------------------------------------+ -| ! HAL-2025-0092 | MAYOR | Torres del Valle | -| Cables electricos expuestos | -| Vence: Manana | Resp: Pedro Lopez | -| [Ver Detalle] | -+--------------------------------------------------+ -| [Ver Todos los Hallazgos] | -+--------------------------------------------------+ -``` - -## Especificaciones Tecnicas Relacionadas - -- ET-MAA017-DB-001: Schema HSE Database -- ET-MAA017-BE-003: Inspections Service -- ET-MAA017-BE-004: Findings Service -- ET-MAA017-FE-004: Inspections Calendar -- ET-MAA017-FE-005: Mobile Inspection Form -- ET-MAA017-FE-006: Findings Dashboard - -## User Stories Relacionadas - -- US-MAA017-005: Realizar inspeccion de seguridad -- US-MAA017-011: Programar inspecciones del mes -- US-MAA017-012: Registrar hallazgo de inspeccion -- US-MAA017-013: Cerrar hallazgo con evidencia - -## Integraciones - -### Internas -- MAI-002: Obtener lista de obras y fraccionamientos -- MAI-007: Obtener lista de personal para asignar responsables -- RF-MAA017-001: Vincular hallazgos criticos con incidentes - -### Externas -- Almacenamiento de fotos en sistema de archivos -- Notificaciones push para alertas - ---- - -**Autor**: Requirements-Analyst -**Fecha**: 2025-12-06 -**Version**: 1.0.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-004-control-epp.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-004-control-epp.md deleted file mode 100644 index bd69a2605..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-004-control-epp.md +++ /dev/null @@ -1,378 +0,0 @@ -# RF-MAA017-004: Control de EPP - -## Informacion General - -| Atributo | Valor | -|----------|-------| -| **Codigo** | RF-MAA017-004 | -| **Nombre** | Control de EPP (Equipo de Proteccion Personal) | -| **Modulo** | MAA-017 Seguridad HSE | -| **Prioridad** | P1 - Alta | -| **Complejidad** | Media | - -## Descripcion - -El sistema debe permitir la gestion completa del Equipo de Proteccion Personal (EPP) requerido en obras de construccion. Incluye catalogo de EPP, asignacion a trabajadores, control de vida util, renovaciones, y cumplimiento de NOM-017-STPS-2008. - -## Requisitos Funcionales - -### RF-MAA017-004.1: Catalogo de EPP - -- Registrar tipos de EPP con sus caracteristicas -- Categorias de EPP: - - Proteccion de cabeza (cascos) - - Proteccion de ojos y cara (lentes, caretas) - - Proteccion auditiva (tapones, orejeras) - - Proteccion respiratoria (mascarillas, respiradores) - - Proteccion de manos (guantes) - - Proteccion de pies (botas) - - Proteccion contra caidas (arnes, lineas de vida) - - Ropa de proteccion (chalecos, overoles) -- Definir vida util por tipo de EPP -- Registrar especificaciones tecnicas y certificaciones -- Vincular norma NOM-017-STPS aplicable -- Configurar alertas de renovacion (dias antes) - -### RF-MAA017-004.2: Matriz de EPP por Puesto - -- Definir EPP requerido por puesto de trabajo -- Configurar EPP obligatorio vs opcional -- Asociar EPP a actividades especificas (altura, soldadura, etc.) -- Generar lista de EPP necesario al asignar trabajador a obra -- Validar que trabajador tenga EPP completo antes de ingresar - -### RF-MAA017-004.3: Asignacion de EPP - -- Registrar entrega de EPP a trabajador -- Capturar firma digital de recepcion -- Registrar numero de serie/lote del EPP -- Adjuntar foto del EPP entregado -- Calcular fecha de vencimiento segun vida util -- Generar vale de entrega con detalle -- Notificar al trabajador sobre cuidado del equipo -- Registrar capacitacion de uso correcto - -### RF-MAA017-004.4: Control de Vida Util - -- Dashboard de EPP por vencer en proximos 30 dias -- Alertas automaticas a trabajador y supervisor -- Registro de inspecciones periodicas de EPP -- Marcar EPP como danado/perdido -- Proceso de renovacion de EPP vencido -- Baja de EPP por termino de vida util -- Historial de EPP por trabajador - -### RF-MAA017-004.5: Inventario de EPP - -- Stock de EPP disponible por almacen/obra -- Minimos y maximos de inventario -- Alertas de reabastecimiento -- Solicitudes de EPP por obra -- Transferencias entre almacenes -- Historial de movimientos - -### RF-MAA017-004.6: Reportes EPP - -- Reporte de cumplimiento EPP por obra -- Trabajadores con EPP vencido -- Costo de EPP por trabajador/obra -- Analisis de duracion real vs esperada -- Comparativo de proveedores por durabilidad -- Exportar datos para auditorias - -## Reglas de Negocio - -1. Todo trabajador debe tener EPP basico completo antes de ingresar a obra -2. EPP vencido debe reemplazarse inmediatamente -3. EPP danado debe reportarse en menos de 24 horas -4. La entrega de EPP requiere firma digital obligatoria -5. Trabajadores de altura requieren certificacion de arnes vigente -6. El costo de EPP danado por negligencia puede descontarse al trabajador -7. Inspecciones de EPP critico (arnes) deben ser mensuales -8. Proveedores de EPP deben tener certificaciones vigentes - -## Criterios de Aceptacion - -- [ ] Catalogo de EPP con al menos 20 tipos predefinidos -- [ ] Matriz de EPP asociada a puestos de trabajo -- [ ] Asignacion con firma digital funcional -- [ ] Alertas automaticas 15 dias antes de vencimiento -- [ ] Historial completo de EPP por trabajador -- [ ] Dashboard de EPP vencido/proximo a vencer -- [ ] Vale de entrega generado en PDF -- [ ] Integracion con inventario de almacen -- [ ] Reporte de cumplimiento NOM-017-STPS - -## Modelo de Datos - -### Tabla: hse.epp_catalogo -``` -- id: UUID PK -- tenant_id: UUID FK (core.tenants) -- codigo: VARCHAR(20) UNIQUE -- nombre: VARCHAR(200) -- categoria: ENUM(cabeza, ojos, auditiva, respiratoria, manos, pies, caidas, ropa) -- descripcion: TEXT -- especificaciones: TEXT -- vida_util_dias: INTEGER -- norma_referencia: VARCHAR(50) -- requiere_certificacion: BOOLEAN DEFAULT false -- requiere_inspeccion_periodica: BOOLEAN DEFAULT false -- frecuencia_inspeccion_dias: INTEGER -- alerta_dias_antes: INTEGER DEFAULT 15 -- imagen_url: VARCHAR(500) -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.epp_matriz_puesto -``` -- id: UUID PK -- tenant_id: UUID FK -- puesto_id: UUID FK (hr.puestos) -- epp_id: UUID FK (hse.epp_catalogo) -- es_obligatorio: BOOLEAN DEFAULT true -- actividad_especifica: VARCHAR(200) -- observaciones: TEXT -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.epp_asignaciones -``` -- id: UUID PK -- tenant_id: UUID FK -- employee_id: UUID FK (hr.employees) -- epp_id: UUID FK (hse.epp_catalogo) -- fraccionamiento_id: UUID FK -- fecha_entrega: DATE -- fecha_vencimiento: DATE -- numero_serie: VARCHAR(100) -- numero_lote: VARCHAR(100) -- firma_trabajador: TEXT -- foto_entrega_url: VARCHAR(500) -- capacitacion_uso: BOOLEAN DEFAULT false -- estado: ENUM(activo, vencido, danado, perdido, devuelto) -- costo_unitario: DECIMAL(10,2) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -- created_by: UUID FK -``` - -### Tabla: hse.epp_inspecciones -``` -- id: UUID PK -- asignacion_id: UUID FK (hse.epp_asignaciones) -- inspector_id: UUID FK -- fecha_inspeccion: DATE -- estado_epp: ENUM(bueno, regular, malo, danado) -- observaciones: TEXT -- requiere_reemplazo: BOOLEAN DEFAULT false -- foto_url: VARCHAR(500) -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.epp_bajas -``` -- id: UUID PK -- asignacion_id: UUID FK -- fecha_baja: DATE -- motivo: ENUM(vencimiento, danado, perdido, terminacion_laboral) -- descripcion: TEXT -- descuento_aplicado: BOOLEAN DEFAULT false -- monto_descuento: DECIMAL(10,2) -- autorizado_por: UUID FK -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.epp_inventario -``` -- id: UUID PK -- tenant_id: UUID FK -- epp_id: UUID FK -- almacen_id: UUID FK (inventory.almacenes) -- cantidad_disponible: INTEGER -- cantidad_minima: INTEGER -- cantidad_maxima: INTEGER -- costo_promedio: DECIMAL(10,2) -- ultima_entrada: DATE -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.epp_movimientos -``` -- id: UUID PK -- tenant_id: UUID FK -- epp_id: UUID FK -- almacen_origen_id: UUID FK -- almacen_destino_id: UUID FK -- tipo: ENUM(entrada, salida, transferencia, ajuste) -- cantidad: INTEGER -- referencia: VARCHAR(100) -- observaciones: TEXT -- created_at: TIMESTAMPTZ -- created_by: UUID FK -``` - -## Casos de Uso - -### CU-MAA017-004.1: Entregar EPP a Trabajador Nuevo - -**Actor**: Almacenista / Coordinador HSE -**Precondicion**: Trabajador dado de alta, puesto asignado - -**Flujo Principal**: -1. Sistema consulta matriz de EPP del puesto asignado -2. Sistema muestra lista de EPP requerido -3. Almacenista verifica disponibilidad en inventario -4. Almacenista registra entrega de cada item -5. Sistema captura numero de serie/lote -6. Trabajador firma digitalmente en tablet -7. Sistema calcula fechas de vencimiento -8. Sistema genera vale de entrega PDF -9. Sistema actualiza inventario -10. Sistema programa alertas de vencimiento - -**Flujo Alterno - EPP No Disponible**: -3a. Si algun EPP no esta en stock -3b. Sistema genera solicitud de compra -3c. Trabajador no puede ingresar hasta completar EPP - -### CU-MAA017-004.2: Renovar EPP Vencido - -**Actor**: Coordinador HSE -**Precondicion**: EPP proximo a vencer o vencido - -**Flujo Principal**: -1. Sistema genera alerta de EPP por vencer -2. Coordinador consulta trabajadores afectados -3. Coordinador programa fecha de renovacion -4. Sistema notifica a trabajadores -5. En fecha, se realiza entrega de nuevo EPP -6. Sistema da de baja EPP anterior -7. Sistema actualiza registro con nuevo EPP - -### CU-MAA017-004.3: Reportar EPP Danado - -**Actor**: Trabajador / Supervisor -**Precondicion**: EPP presenta dano - -**Flujo Principal**: -1. Trabajador/Supervisor reporta EPP danado -2. Sistema registra tipo de dano y evidencia -3. Supervisor evalua si fue por negligencia -4. Si fue negligencia, sistema calcula descuento -5. Sistema genera baja de EPP danado -6. Sistema genera solicitud de reemplazo -7. Trabajador no puede continuar sin EPP - -## Mockups - -### Pantalla: Catalogo de EPP -``` -+--------------------------------------------------+ -| CATALOGO DE EPP [+ Agregar] | -+--------------------------------------------------+ -| Buscar: [____________________] [Buscar] | -| Categoria: [Todas v] | -+--------------------------------------------------+ -| Codigo | Nombre | Categoria | Vida Util| -+--------------------------------------------------+ -| EPP-001| Casco 3M H-700 | Cabeza | 730 dias | -| EPP-002| Lentes Uvex | Ojos | 180 dias | -| EPP-003| Guantes Carnaza | Manos | 90 dias | -| EPP-004| Botas Ind. Dielectricas| Pies | 365 dias| -| EPP-005| Arnes DBI-Sala | Caidas | 1825 dias| -| EPP-006| Chaleco Reflect. | Ropa | 180 dias | -+--------------------------------------------------+ -``` - -### Pantalla: Asignacion de EPP -``` -+--------------------------------------------------+ -| ENTREGA DE EPP | -+--------------------------------------------------+ -| Trabajador: Juan Perez Martinez | -| Puesto: Albanil General | -| Obra: Residencial Norte | -+--------------------------------------------------+ -| EPP REQUERIDO SEGUN PUESTO: | -+--------------------------------------------------+ -| [鉁揮 Casco 3M H-700 Serie: [__________] | -| [鉁揮 Lentes Uvex Serie: [__________] | -| [鉁揮 Guantes Carnaza Lote: [__________] | -| [鉁揮 Botas Dielectricas Serie: [__________] | -| [鉁揮 Chaleco Reflejante Lote: [__________] | -+--------------------------------------------------+ -| [馃摲 Foto Entrega] | -| | -| Firma Trabajador: | -| +------------------+ | -| | | | -| | [Firma aqui] | | -| +------------------+ | -| | -| [Cancelar] [Confirmar Entrega] | -+--------------------------------------------------+ -``` - -### Pantalla: Dashboard EPP -``` -+--------------------------------------------------+ -| CONTROL DE EPP - Dashboard | -+--------------------------------------------------+ -| Obra: [Todas v] Periodo: [Enero 2025] | -+--------------------------------------------------+ -| RESUMEN | -| +----------+ +----------+ +----------+ | -| | 85 | | 12 | | 5 | | -| |Trabajador| |EPP por | |EPP | | -| |con EPP | |vencer | |vencido | | -| +----------+ +----------+ +----------+ | -+--------------------------------------------------+ -| EPP POR VENCER (15 dias) | -+--------------------------------------------------+ -| ! Juan Perez | Casco 3M | Vence: 15-Ene-2025 | -| ! Maria Garcia | Arnes | Vence: 18-Ene-2025 | -| ! Pedro Lopez | Lentes | Vence: 20-Ene-2025 | -+--------------------------------------------------+ -| CUMPLIMIENTO POR CATEGORIA | -| | -| Cabeza 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 100% | -| Ojos 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻戔枒鈻戔枒 85% | -| Manos 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻戔枒 92% | -| Pies 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅 100% | -| Caidas 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻戔枒鈻戔枒鈻戔枒 78% | -+--------------------------------------------------+ -``` - -## Especificaciones Tecnicas Relacionadas - -- ET-MAA017-DB-001: Schema HSE Database -- ET-MAA017-BE-005: EPP Management Service -- ET-MAA017-FE-007: EPP Catalog Admin -- ET-MAA017-FE-008: EPP Assignment Form -- ET-MAA017-FE-009: EPP Dashboard - -## User Stories Relacionadas - -- US-MAA017-006: Asignar EPP a trabajador -- US-MAA017-014: Consultar EPP por vencer -- US-MAA017-015: Renovar EPP vencido -- US-MAA017-016: Reportar EPP danado - -## Integraciones - -### Internas -- MAI-007: Obtener lista de trabajadores y puestos -- MAI-008: Vincular con inventario de almacen -- RF-MAA017-002: Registrar capacitacion de uso de EPP - -### Externas -- Sistema de nomina: Aplicar descuentos por EPP danado - ---- - -**Autor**: Requirements-Analyst -**Fecha**: 2025-12-06 -**Version**: 1.0.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-005-cumplimiento-stps.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-005-cumplimiento-stps.md deleted file mode 100644 index e28462995..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-005-cumplimiento-stps.md +++ /dev/null @@ -1,470 +0,0 @@ -# RF-MAA017-005: Cumplimiento STPS - -## Informacion General - -| Atributo | Valor | -|----------|-------| -| **Codigo** | RF-MAA017-005 | -| **Nombre** | Cumplimiento STPS | -| **Modulo** | MAA-017 Seguridad HSE | -| **Prioridad** | P0 - Critica | -| **Complejidad** | Alta | - -## Descripcion - -El sistema debe permitir verificar, documentar y mantener el cumplimiento de las Normas Oficiales Mexicanas (NOM) de la Secretaria del Trabajo y Prevision Social (STPS) aplicables a obras de construccion. Incluye generacion de documentos oficiales, alertas de vencimiento, matriz de cumplimiento y preparacion para auditorias. - -## Requisitos Funcionales - -### RF-MAA017-005.1: Catalogo de Normas STPS - -- Mantener catalogo de NOMs aplicables a construccion: - - NOM-001-STPS-2008: Edificios, locales e instalaciones - - NOM-002-STPS-2010: Prevencion y proteccion contra incendios - - NOM-004-STPS-1999: Sistemas de proteccion y dispositivos de seguridad en maquinaria - - NOM-005-STPS-1998: Manejo de sustancias quimicas peligrosas - - NOM-006-STPS-2014: Manejo y almacenamiento de materiales - - NOM-009-STPS-2011: Trabajos en altura - - NOM-011-STPS-2001: Ruido - - NOM-017-STPS-2008: Equipo de proteccion personal - - NOM-019-STPS-2011: Comisiones de seguridad e higiene - - NOM-026-STPS-2008: Senales de seguridad - - NOM-029-STPS-2011: Mantenimiento de instalaciones electricas - - NOM-030-STPS-2009: Servicios preventivos de seguridad y salud - - NOM-031-STPS-2011: Construccion -- Definir requisitos por norma -- Configurar vigencia y actualizaciones -- Vincular evidencias requeridas por requisito - -### RF-MAA017-005.2: Matriz de Cumplimiento por Obra - -- Generar matriz de NOMs aplicables por tipo de obra -- Evaluar cumplimiento por requisito (cumple, parcial, no cumple) -- Registrar evidencias de cumplimiento -- Calcular porcentaje de cumplimiento por norma -- Identificar brechas de cumplimiento -- Priorizar acciones correctivas -- Dashboard de estado de cumplimiento - -### RF-MAA017-005.3: Documentos Oficiales STPS - -- Generar Constancia DC-1: Habilidades laborales -- Generar Constancia DC-2: Lista de constancias -- Generar Constancia DC-3: Constancia de capacitacion -- Generar Constancia DC-4: Lista de instructores -- Generar formato ST-7: Aviso de accidente de trabajo -- Generar formato ST-9: Aviso de recaida -- Exportar en formatos oficiales (PDF/XML) -- Mantener consecutivo por tipo de documento - -### RF-MAA017-005.4: Comision de Seguridad e Higiene (NOM-019) - -- Registrar integrantes de la comision -- Documentar acta constitutiva -- Programar recorridos mensuales -- Registrar actas de recorridos -- Dar seguimiento a recomendaciones -- Alertar sobre reuniones programadas -- Generar reportes de la comision - -### RF-MAA017-005.5: Programa de Seguridad (NOM-030) - -- Crear programa anual de seguridad -- Definir objetivos y metas -- Programar actividades preventivas -- Asignar responsables y recursos -- Medir avance del programa -- Evaluar efectividad -- Generar informes periodicos - -### RF-MAA017-005.6: Alertas y Vencimientos - -- Alertas de vencimiento de constancias DC-3 -- Alertas de proximas reuniones de comision -- Alertas de fechas de recorridos -- Alertas de actualizaciones normativas -- Alertas de cumplimiento pendiente -- Calendario de obligaciones STPS -- Envio de notificaciones automaticas - -### RF-MAA017-005.7: Preparacion para Auditorias - -- Checklist de documentacion por norma -- Verificar completitud de expedientes -- Generar carpeta de auditoria con documentos -- Simular auditoria STPS -- Identificar no conformidades potenciales -- Plan de accion pre-auditoria -- Historico de auditorias recibidas - -## Reglas de Negocio - -1. Toda obra debe tener matriz de cumplimiento antes de iniciar -2. Las constancias DC-3 deben emitirse dentro de 20 dias habiles -3. La comision debe tener recorrido minimo mensual -4. No conformidades criticas deben cerrarse antes de 30 dias -5. El programa de seguridad debe actualizarse anualmente -6. Los documentos oficiales requieren firma electronica autorizada -7. Obras sin comision constituida no pueden operar -8. Auditorias simuladas deben realizarse trimestralmente - -## Criterios de Aceptacion - -- [ ] Catalogo incluye las 13 NOMs principales de construccion -- [ ] Matriz de cumplimiento calcula porcentajes automaticamente -- [ ] Constancias DC-3 se generan en formato oficial -- [ ] Formato ST-7 cumple especificaciones IMSS -- [ ] Alertas se envian 30 dias antes de vencimientos -- [ ] Dashboard muestra estado de cumplimiento en tiempo real -- [ ] Comision tiene calendario de recorridos automatizado -- [ ] Sistema genera carpeta de auditoria con un clic - -## Modelo de Datos - -### Tabla: hse.normas_stps -``` -- id: UUID PK -- codigo: VARCHAR(30) UNIQUE -- nombre: VARCHAR(300) -- descripcion: TEXT -- fecha_publicacion: DATE -- ultima_actualizacion: DATE -- aplica_construccion: BOOLEAN DEFAULT true -- documento_url: VARCHAR(500) -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.norma_requisitos -``` -- id: UUID PK -- norma_id: UUID FK (hse.normas_stps) -- numero: VARCHAR(20) -- descripcion: TEXT -- tipo_evidencia: VARCHAR(200) -- es_critico: BOOLEAN DEFAULT false -- aplica_a: VARCHAR(100) -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.cumplimiento_obra -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- norma_id: UUID FK -- requisito_id: UUID FK -- estado: ENUM(cumple, parcial, no_cumple, no_aplica) -- evidencia_url: VARCHAR(500) -- observaciones: TEXT -- fecha_evaluacion: DATE -- evaluador_id: UUID FK -- fecha_compromiso: DATE -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.comision_seguridad -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- fecha_constitucion: DATE -- numero_acta: VARCHAR(50) -- vigencia_inicio: DATE -- vigencia_fin: DATE -- estado: ENUM(activa, vencida, renovada) -- documento_acta_url: VARCHAR(500) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.comision_integrantes -``` -- id: UUID PK -- comision_id: UUID FK -- employee_id: UUID FK -- rol: ENUM(presidente, secretario, vocal_patronal, vocal_trabajador) -- representacion: ENUM(patronal, trabajadores) -- fecha_nombramiento: DATE -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.comision_recorridos -``` -- id: UUID PK -- comision_id: UUID FK -- fecha_programada: DATE -- fecha_realizada: DATE -- numero_acta: VARCHAR(50) -- areas_recorridas: TEXT -- hallazgos: TEXT -- recomendaciones: TEXT -- estado: ENUM(programado, realizado, cancelado, pendiente) -- documento_acta_url: VARCHAR(500) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.programa_seguridad -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- anio: INTEGER -- objetivo_general: TEXT -- metas: JSONB -- presupuesto: DECIMAL(12,2) -- estado: ENUM(borrador, activo, finalizado) -- aprobado_por: UUID FK -- fecha_aprobacion: DATE -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.programa_actividades -``` -- id: UUID PK -- programa_id: UUID FK -- actividad: VARCHAR(300) -- tipo: ENUM(capacitacion, inspeccion, simulacro, campana, otro) -- fecha_programada: DATE -- fecha_realizada: DATE -- responsable_id: UUID FK -- recursos: TEXT -- estado: ENUM(pendiente, en_progreso, completada, cancelada) -- evidencia_url: VARCHAR(500) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.documentos_stps -``` -- id: UUID PK -- tenant_id: UUID FK -- tipo: ENUM(dc1, dc2, dc3, dc4, st7, st9) -- folio: VARCHAR(30) UNIQUE -- fraccionamiento_id: UUID FK -- employee_id: UUID FK (nullable) -- fecha_emision: DATE -- fecha_vencimiento: DATE -- datos_documento: JSONB -- documento_url: VARCHAR(500) -- firmado: BOOLEAN DEFAULT false -- created_at: TIMESTAMPTZ -- created_by: UUID FK -``` - -### Tabla: hse.auditorias -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- tipo: ENUM(interna, simulada, stps, cliente, certificadora) -- fecha_programada: DATE -- fecha_realizada: DATE -- auditor: VARCHAR(200) -- resultado: ENUM(aprobada, aprobada_observaciones, no_aprobada) -- no_conformidades: INTEGER -- observaciones: TEXT -- informe_url: VARCHAR(500) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -## Casos de Uso - -### CU-MAA017-005.1: Evaluar Cumplimiento Normativo - -**Actor**: Coordinador HSE -**Precondicion**: Obra activa, normas configuradas - -**Flujo Principal**: -1. Coordinador selecciona obra a evaluar -2. Sistema genera matriz de normas aplicables -3. Por cada norma, sistema muestra requisitos -4. Coordinador evalua cada requisito -5. Coordinador adjunta evidencias disponibles -6. Sistema calcula porcentaje de cumplimiento -7. Sistema identifica brechas criticas -8. Sistema genera plan de accion sugerido -9. Coordinador asigna responsables y fechas -10. Sistema programa alertas de seguimiento - -### CU-MAA017-005.2: Generar Constancia DC-3 - -**Actor**: Coordinador HSE -**Precondicion**: Capacitacion completada, asistencia registrada - -**Flujo Principal**: -1. Sistema identifica capacitaciones sin constancia -2. Coordinador selecciona capacitacion -3. Sistema muestra lista de asistentes aprobados -4. Coordinador selecciona trabajadores -5. Sistema genera DC-3 con datos de NOM -6. Sistema asigna folio consecutivo -7. Coordinador verifica datos -8. Sistema genera PDF en formato oficial -9. Sistema registra emision y notifica a trabajadores - -### CU-MAA017-005.3: Preparar Carpeta de Auditoria - -**Actor**: Gerente HSE -**Precondicion**: Auditoria programada - -**Flujo Principal**: -1. Gerente selecciona tipo de auditoria -2. Sistema genera checklist de documentos requeridos -3. Sistema verifica documentos disponibles -4. Sistema indica documentos faltantes -5. Gerente genera acciones para completar -6. Al completar, sistema genera carpeta digital -7. Sistema organiza por norma/requisito -8. Sistema genera indice navegable -9. Sistema permite descargar carpeta completa - -### CU-MAA017-005.4: Registrar Recorrido de Comision - -**Actor**: Secretario de Comision -**Precondicion**: Comision constituida, recorrido programado - -**Flujo Principal**: -1. Secretario abre recorrido programado -2. Sistema muestra formato de acta -3. Durante recorrido, se registran areas visitadas -4. Se registran hallazgos encontrados -5. Se documentan recomendaciones -6. Integrantes firman digitalmente -7. Sistema genera acta en formato oficial -8. Sistema programa seguimiento a recomendaciones -9. Sistema actualiza calendario de comision - -## Mockups - -### Pantalla: Matriz de Cumplimiento -``` -+--------------------------------------------------+ -| MATRIZ DE CUMPLIMIENTO NORMATIVO | -| Obra: Residencial Norte | -+--------------------------------------------------+ -| Cumplimiento General: 78% | -| 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻戔枒鈻戔枒鈻戔枒 | -+--------------------------------------------------+ -| NORMA | REQUISITOS | CUMPLIMIENTO | -+--------------------------------------------------+ -| NOM-009-STPS-2011 | 15/18 | 83% 鈻堚枅鈻堚枅鈻 | -| Trabajos en altura| | | -+--------------------------------------------------+ -| NOM-017-STPS-2008 | 12/12 | 100% 鈻堚枅鈻堚枅鈻 | -| EPP | | | -+--------------------------------------------------+ -| NOM-031-STPS-2011 | 20/28 | 71% 鈻堚枅鈻堚枒鈻 | -| Construccion | | | -+--------------------------------------------------+ -| NOM-019-STPS-2011 | 8/10 | 80% 鈻堚枅鈻堚枅鈻 | -| Comision S&H | | | -+--------------------------------------------------+ -| [Ver Brechas] [Generar Plan] [Exportar] | -+--------------------------------------------------+ -``` - -### Pantalla: Generacion DC-3 -``` -+--------------------------------------------------+ -| GENERAR CONSTANCIAS DC-3 | -+--------------------------------------------------+ -| Capacitacion: Trabajos en Altura | -| Fecha: 15-Dic-2024 | -| Duracion: 8 horas | -| Norma: NOM-009-STPS-2011 | -+--------------------------------------------------+ -| SELECCIONAR TRABAJADORES APROBADOS: | -+--------------------------------------------------+ -| [鉁揮 Juan Perez Martinez | Calif: 90 | -| [鉁揮 Maria Garcia Lopez | Calif: 85 | -| [鉁揮 Pedro Sanchez Ruiz | Calif: 88 | -| [ ] Carlos Hernandez (Reprobado) | Calif: 55 | -+--------------------------------------------------+ -| Instructor: Ing. Roberto Mendez | -| Registro STPS: MERA-850612-TRB-0013 | -+--------------------------------------------------+ -| Vista previa DC-3: | -| +--------------------------------------------+ | -| | CONSTANCIA DE HABILIDADES LABORALES DC-3 | | -| | Folio: DC3-2025-0089 | | -| | [Vista previa del formato oficial] | | -| +--------------------------------------------+ | -+--------------------------------------------------+ -| [Cancelar] [Generar 3 Constancias] | -+--------------------------------------------------+ -``` - -### Pantalla: Comision de Seguridad -``` -+--------------------------------------------------+ -| COMISION DE SEGURIDAD E HIGIENE | -| Obra: Residencial Norte | -+--------------------------------------------------+ -| Estado: ACTIVA Vigencia: Ene-2025 a Dic-2025 | -| Acta Constitutiva: CSH-2025-001 | -+--------------------------------------------------+ -| INTEGRANTES | -+--------------------------------------------------+ -| Representantes Patronales: | -| Presidente: Ing. Carlos Mendez | -| Secretario: Lic. Ana Torres | -| | -| Representantes Trabajadores: | -| Vocal: Juan Perez Martinez | -| Vocal: Maria Garcia Lopez | -+--------------------------------------------------+ -| CALENDARIO DE RECORRIDOS 2025 | -+--------------------------------------------------+ -| Mes | Fecha | Estado | Acta | -+--------------------------------------------------+ -| Enero | 15-Ene | Completado | ACT-001 | -| Febrero | 15-Feb | Programado | - | -| Marzo | 15-Mar | Programado | - | -+--------------------------------------------------+ -| RECOMENDACIONES PENDIENTES: 3 | -| [Ver Detalle] | -+--------------------------------------------------+ -| [Nuevo Recorrido] [Renovar Comision] | -+--------------------------------------------------+ -``` - -## Especificaciones Tecnicas Relacionadas - -- ET-MAA017-DB-001: Schema HSE Database -- ET-MAA017-BE-006: STPS Compliance Service -- ET-MAA017-BE-007: Document Generation Service -- ET-MAA017-FE-010: Compliance Matrix View -- ET-MAA017-FE-011: DC3 Generator -- ET-MAA017-FE-012: Commission Management - -## User Stories Relacionadas - -- US-MAA017-007: Generar reporte STPS -- US-MAA017-017: Evaluar cumplimiento normativo -- US-MAA017-018: Constituir comision de seguridad -- US-MAA017-019: Registrar recorrido de comision -- US-MAA017-020: Preparar auditoria STPS - -## Integraciones - -### Internas -- RF-MAA017-002: Vincular capacitaciones para DC-3 -- RF-MAA017-003: Vincular inspecciones a requisitos normativos -- RF-MAA017-004: Vincular EPP a NOM-017 - -### Externas -- STPS: Formatos oficiales DC-1 a DC-4 -- IMSS: Formatos ST-7, ST-9 - ---- - -**Autor**: Requirements-Analyst -**Fecha**: 2025-12-06 -**Version**: 1.0.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-006-gestion-ambiental.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-006-gestion-ambiental.md deleted file mode 100644 index 1f09caca1..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-006-gestion-ambiental.md +++ /dev/null @@ -1,429 +0,0 @@ -# RF-MAA017-006: Gestion Ambiental - -## Informacion General - -| Atributo | Valor | -|----------|-------| -| **Codigo** | RF-MAA017-006 | -| **Nombre** | Gestion Ambiental | -| **Modulo** | MAA-017 Seguridad HSE | -| **Prioridad** | P2 - Media | -| **Complejidad** | Media | - -## Descripcion - -El sistema debe permitir la gestion de aspectos ambientales en obras de construccion, incluyendo manejo de residuos peligrosos y no peligrosos, generacion de manifiestos de traslado, evaluacion de impacto ambiental, y cumplimiento de normatividad SEMARNAT y LGEEPA. - -## Requisitos Funcionales - -### RF-MAA017-006.1: Catalogo de Residuos - -- Registrar tipos de residuos generados en obra: - - Residuos peligrosos (RP): - - Aceites usados - - Solventes - - Pinturas y barnices - - Baterias - - Envases contaminados - - Trapos y estopas impregnados - - Residuos de soldadura - - Residuos de manejo especial (RME): - - Escombro y cascajo - - Tierra contaminada - - Material de demolicion - - Residuos solidos urbanos (RSU): - - Carton y papel - - Plasticos - - Organicos -- Clasificar segun NOM-052-SEMARNAT-2005 -- Definir caracteristicas CRETIB -- Indicar manejo requerido por tipo -- Vincular transportistas autorizados - -### RF-MAA017-006.2: Registro de Generacion de Residuos - -- Registrar volumen/peso de residuos generados por obra -- Capturar fecha y area de generacion -- Identificar fuente generadora -- Adjuntar fotos del residuo -- Etiquetar contenedores segun norma -- Control de almacenamiento temporal -- Tiempo maximo de almacenamiento (6 meses RP) -- Alertas de aproximacion a limite - -### RF-MAA017-006.3: Manifiestos de Traslado - -- Generar manifiesto para residuos peligrosos -- Datos del generador (empresa) -- Datos del transportista autorizado -- Datos del destino final (reciclador/confinamiento) -- Descripcion del residuo y cantidad -- Numero de autorizacion SEMARNAT -- Folio de manifiesto -- Firmas digitales de responsables -- Seguimiento de entrega a destino -- Archivo de manifiestos por 5 anos - -### RF-MAA017-006.4: Proveedores Autorizados - -- Catalogo de transportistas autorizados SEMARNAT -- Catalogo de sitios de disposicion final -- Verificar vigencia de autorizaciones -- Registrar numero de autorizacion -- Alertas de vencimiento de autorizaciones -- Historial de servicios por proveedor - -### RF-MAA017-006.5: Impacto Ambiental - -- Evaluacion de aspectos ambientales por obra -- Identificar impactos potenciales: - - Ruido - - Polvo - - Vibraciones - - Descarga de aguas residuales - - Emision de particulas - - Afectacion de vegetacion -- Medidas de mitigacion por impacto -- Registro de quejas vecinales -- Plan de manejo ambiental -- Monitoreo de indicadores - -### RF-MAA017-006.6: Reportes Ambientales - -- Cedula de Operacion Anual (COA) -- Bitacora de residuos peligrosos -- Inventario de residuos acumulados -- Reportes de disposicion final -- Costos de gestion ambiental -- Comparativo entre obras -- Exportar para auditorias PROFEPA - -## Reglas de Negocio - -1. Residuos peligrosos no pueden almacenarse mas de 6 meses -2. Todo traslado de RP requiere manifiesto -3. Solo transportistas autorizados pueden mover RP -4. Manifiestos deben archivarse minimo 5 anos -5. Generador es responsable hasta disposicion final -6. Contenedores de RP deben estar etiquetados -7. Area de almacenamiento temporal debe tener contencion -8. Derrames deben reportarse en 24 horas - -## Criterios de Aceptacion - -- [ ] Catalogo de residuos segun NOM-052-SEMARNAT -- [ ] Registro de generacion con fotos y geolocalizacion -- [ ] Manifiestos generados en formato oficial -- [ ] Alertas 30 dias antes de limite de almacenamiento -- [ ] Proveedores con verificacion de autorizacion vigente -- [ ] Dashboard de residuos por obra -- [ ] Exportar COA en formato requerido -- [ ] Historial de 5 anos accesible - -## Modelo de Datos - -### Tabla: hse.residuos_catalogo -``` -- id: UUID PK -- codigo: VARCHAR(20) UNIQUE -- nombre: VARCHAR(200) -- categoria: ENUM(peligroso, manejo_especial, urbano) -- caracteristicas_cretib: VARCHAR(6) -- norma_referencia: VARCHAR(50) -- manejo_requerido: TEXT -- tiempo_max_almacen_dias: INTEGER -- requiere_manifiesto: BOOLEAN DEFAULT false -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.residuos_generacion -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- residuo_id: UUID FK (hse.residuos_catalogo) -- fecha_generacion: DATE -- cantidad: DECIMAL(10,2) -- unidad: ENUM(kg, litros, m3, piezas) -- area_generacion: VARCHAR(200) -- fuente: VARCHAR(200) -- contenedor_id: VARCHAR(50) -- foto_url: VARCHAR(500) -- ubicacion_geo: GEOMETRY(Point) -- estado: ENUM(almacenado, en_transito, dispuesto) -- created_at: TIMESTAMPTZ -- created_by: UUID FK -``` - -### Tabla: hse.almacen_temporal -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- nombre: VARCHAR(100) -- ubicacion: VARCHAR(200) -- capacidad_m3: DECIMAL(8,2) -- tiene_contencion: BOOLEAN DEFAULT true -- tiene_techo: BOOLEAN DEFAULT true -- senalizacion_ok: BOOLEAN DEFAULT true -- estado: ENUM(operativo, lleno, mantenimiento) -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.proveedores_ambientales -``` -- id: UUID PK -- tenant_id: UUID FK -- razon_social: VARCHAR(300) -- rfc: VARCHAR(13) -- tipo: ENUM(transportista, reciclador, confinamiento) -- numero_autorizacion: VARCHAR(50) -- entidad_autorizadora: VARCHAR(100) -- fecha_autorizacion: DATE -- fecha_vencimiento: DATE -- servicios: TEXT -- contacto_nombre: VARCHAR(200) -- contacto_telefono: VARCHAR(20) -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.manifiestos_residuos -``` -- id: UUID PK -- tenant_id: UUID FK -- folio: VARCHAR(30) UNIQUE -- fraccionamiento_id: UUID FK -- transportista_id: UUID FK (hse.proveedores_ambientales) -- destino_id: UUID FK (hse.proveedores_ambientales) -- fecha_recoleccion: DATE -- fecha_entrega: DATE -- estado: ENUM(emitido, en_transito, entregado, cerrado) -- observaciones: TEXT -- documento_url: VARCHAR(500) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.manifiesto_detalle -``` -- id: UUID PK -- manifiesto_id: UUID FK -- residuo_id: UUID FK -- generacion_ids: UUID[] -- cantidad: DECIMAL(10,2) -- unidad: ENUM(kg, litros, m3) -- descripcion: TEXT -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.impacto_ambiental -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- aspecto: VARCHAR(200) -- tipo_impacto: ENUM(ruido, polvo, vibraciones, agua, emision, vegetacion, otro) -- severidad: ENUM(bajo, medio, alto) -- probabilidad: ENUM(baja, media, alta) -- nivel_riesgo: ENUM(tolerable, moderado, significativo) -- medidas_mitigacion: TEXT -- responsable_id: UUID FK -- estado: ENUM(identificado, mitigando, controlado) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.quejas_ambientales -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- fecha_queja: TIMESTAMPTZ -- origen: ENUM(vecino, autoridad, interno, anonimo) -- tipo: ENUM(ruido, polvo, olores, agua, otro) -- descripcion: TEXT -- nombre_quejoso: VARCHAR(200) -- contacto_quejoso: VARCHAR(100) -- acciones_tomadas: TEXT -- estado: ENUM(recibida, atendiendo, cerrada) -- fecha_cierre: DATE -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -## Casos de Uso - -### CU-MAA017-006.1: Registrar Generacion de Residuo Peligroso - -**Actor**: Encargado de Obra / Almacenista -**Precondicion**: Catalogo de residuos configurado - -**Flujo Principal**: -1. Usuario identifica residuo generado en obra -2. Sistema muestra catalogo de residuos -3. Usuario selecciona tipo de residuo -4. Usuario registra cantidad y unidad -5. Usuario indica area y fuente de generacion -6. Usuario toma foto del residuo -7. Sistema asigna contenedor de almacen temporal -8. Sistema calcula fecha limite de almacenamiento -9. Sistema programa alertas de vencimiento -10. Sistema actualiza inventario de almacen - -### CU-MAA017-006.2: Generar Manifiesto de Traslado - -**Actor**: Coordinador HSE -**Precondicion**: Residuos acumulados, transportista disponible - -**Flujo Principal**: -1. Coordinador selecciona obra y residuos a trasladar -2. Sistema muestra inventario de residuos almacenados -3. Coordinador selecciona registros a incluir -4. Sistema verifica limite de tiempo no excedido -5. Coordinador selecciona transportista autorizado -6. Sistema verifica autorizacion vigente -7. Coordinador selecciona destino final -8. Sistema genera manifiesto con folio -9. Coordinador revisa y firma digitalmente -10. Sistema genera PDF y registra emision -11. Al entregar, se cierra manifiesto con firma destino - -### CU-MAA017-006.3: Atender Queja Ambiental - -**Actor**: Residente de Obra / Coordinador HSE -**Precondicion**: Queja recibida - -**Flujo Principal**: -1. Usuario registra queja con origen y tipo -2. Sistema asigna folio de seguimiento -3. Usuario documenta descripcion detallada -4. Sistema vincula a impactos ambientales -5. Usuario define acciones inmediatas -6. Sistema notifica a responsables -7. Usuario ejecuta acciones de mitigacion -8. Usuario registra evidencia de atencion -9. Usuario cierra queja con resolucion -10. Sistema actualiza indicadores ambientales - -## Mockups - -### Pantalla: Inventario de Residuos -``` -+--------------------------------------------------+ -| INVENTARIO DE RESIDUOS | -| Obra: Residencial Norte | -+--------------------------------------------------+ -| Almacen Temporal: [Almacen RP-01 v] | -| Capacidad: 80% ocupado | -+--------------------------------------------------+ -| RESIDUOS PELIGROSOS ALMACENADOS | -+--------------------------------------------------+ -| Tipo | Cantidad | Ingreso | Limite | -+--------------------------------------------------+ -| Aceite usado | 200 L | 01-Oct | 01-Abr | -| Solventes | 50 L | 15-Oct | 15-Abr | -| Envases cont. | 30 kg | 01-Nov | 01-May | -| Estopas imp. | 25 kg | 20-Nov | 20-May | -+--------------------------------------------------+ -| ! ALERTA: Aceite usado a 60 dias de limite | -+--------------------------------------------------+ -| [Registrar Nuevo] [Generar Manifiesto] | -+--------------------------------------------------+ -``` - -### Pantalla: Generar Manifiesto -``` -+--------------------------------------------------+ -| MANIFIESTO DE RESIDUOS PELIGROSOS | -+--------------------------------------------------+ -| Folio: MAN-RP-2025-0015 (auto) | -| Fecha: 06-Dic-2025 | -+--------------------------------------------------+ -| DATOS DEL GENERADOR | -| Empresa: Constructora ABC S.A. de C.V. | -| RFC: CAB-950612-XX1 | -| Obra: Residencial Norte | -+--------------------------------------------------+ -| RESIDUOS A TRASLADAR | -| [鉁揮 Aceite usado - 200 L | -| [鉁揮 Solventes - 50 L | -| [ ] Envases contaminados - 30 kg | -+--------------------------------------------------+ -| TRANSPORTISTA | -| [Transportes Ecologicos SA v] | -| Autorizacion: SCT-2024-0089 (Vigente) | -+--------------------------------------------------+ -| DESTINO FINAL | -| [Recicladora Ambiental SA v] | -| Autorizacion: SEMARNAT-2023-1234 (Vigente) | -+--------------------------------------------------+ -| [Cancelar] [Generar Manifiesto] | -+--------------------------------------------------+ -``` - -### Pantalla: Dashboard Ambiental -``` -+--------------------------------------------------+ -| GESTION AMBIENTAL - Dashboard | -+--------------------------------------------------+ -| Periodo: [2025 v] Obra: [Todas v] | -+--------------------------------------------------+ -| RESIDUOS GENERADOS (Toneladas) | -| | -| RP RME RSU | -| 0.8 45 12 [Grafica barras] | -| | -+--------------------------------------------------+ -| MANIFIESTOS EMITIDOS | -| +--------+ +--------+ +--------+ | -| | 15 | | 12 | | 3 | | -| | Total | |Cerrados| |Abiertos| | -| +--------+ +--------+ +--------+ | -+--------------------------------------------------+ -| QUEJAS AMBIENTALES | -| Este mes: 2 Pendientes: 1 | -+--------------------------------------------------+ -| IMPACTOS POR CONTROLAR | -| ! Ruido - Nivel Alto - Edificio A | -| ! Polvo - Nivel Medio - Excavaciones | -+--------------------------------------------------+ -| [Ver Detalle] [Exportar COA] | -+--------------------------------------------------+ -``` - -## Especificaciones Tecnicas Relacionadas - -- ET-MAA017-DB-001: Schema HSE Database -- ET-MAA017-BE-008: Environmental Management Service -- ET-MAA017-BE-009: Manifest Generator Service -- ET-MAA017-FE-013: Waste Inventory View -- ET-MAA017-FE-014: Manifest Form -- ET-MAA017-FE-015: Environmental Dashboard - -## User Stories Relacionadas - -- US-MAA017-010: Registrar manejo de residuos -- US-MAA017-021: Generar manifiesto de traslado -- US-MAA017-022: Registrar queja ambiental -- US-MAA017-023: Evaluar impacto ambiental -- US-MAA017-024: Generar COA - -## Integraciones - -### Internas -- MAI-002: Obtener lista de obras/fraccionamientos -- MAI-008: Vincular con proveedores de servicios - -### Externas -- SEMARNAT: Consulta de autorizaciones -- SEMARNAT: Formato COA -- PROFEPA: Preparacion de auditorias - ---- - -**Autor**: Requirements-Analyst -**Fecha**: 2025-12-06 -**Version**: 1.0.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-007-permisos-trabajo.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-007-permisos-trabajo.md deleted file mode 100644 index a15b9d795..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-007-permisos-trabajo.md +++ /dev/null @@ -1,470 +0,0 @@ -# RF-MAA017-007: Permisos de Trabajo - -## Informacion General - -| Atributo | Valor | -|----------|-------| -| **Codigo** | RF-MAA017-007 | -| **Nombre** | Permisos de Trabajo | -| **Modulo** | MAA-017 Seguridad HSE | -| **Prioridad** | P1 - Alta | -| **Complejidad** | Alta | - -## Descripcion - -El sistema debe permitir la solicitud, autorizacion y control de permisos de trabajo para actividades peligrosas en obras de construccion. Incluye permisos para trabajos en altura, espacios confinados, trabajos en caliente, excavaciones, y otros trabajos de alto riesgo, con verificacion de requisitos previos y controles durante la ejecucion. - -## Requisitos Funcionales - -### RF-MAA017-007.1: Tipos de Permisos de Trabajo - -- Catalogo de tipos de permiso con requisitos: - - **Trabajo en Altura** (>1.8m): - - Arnes certificado vigente - - Capacitacion NOM-009-STPS - - Linea de vida/punto de anclaje - - Casco con barbiquejo - - Supervision continua - - **Espacio Confinado**: - - Monitoreo de atmosfera - - Ventilacion forzada - - Vigia exterior - - Equipo de rescate - - Capacitacion especifica - - **Trabajo en Caliente** (soldadura, corte): - - Extintor disponible - - Area despejada de combustibles - - Vigia de fuego (fire watch) - - EPP especial - - **Excavaciones** (>1.5m): - - Estudio de suelos - - Ademe o talud - - Escaleras de acceso - - Detector de lineas subterraneas - - **Trabajos Electricos**: - - Bloqueo/Etiquetado (LOTO) - - Guantes dielelectricos - - Detector de tension - - Capacitacion NOM-029 - - **Izaje de Cargas**: - - Plan de izaje - - Grua certificada - - Operador calificado - - Senalero designado -- Configurar vigencia maxima por tipo -- Definir autorizadores requeridos -- Establecer documentos obligatorios - -### RF-MAA017-007.2: Solicitud de Permiso - -- Solicitante selecciona tipo de trabajo -- Describir trabajo a realizar -- Indicar ubicacion exacta en obra -- Definir fecha y horario del trabajo -- Listar personal involucrado -- Sistema verifica requisitos del personal: - - Capacitaciones vigentes - - EPP asignado y vigente - - Aptitud medica -- Adjuntar documentos de soporte -- Describir medidas de seguridad a aplicar -- Firma digital del solicitante - -### RF-MAA017-007.3: Proceso de Autorizacion - -- Flujo de aprobacion configurable: - 1. Supervisor de obra (primera firma) - 2. Coordinador HSE (segunda firma) - 3. Residente de obra (tercera firma - trabajos criticos) -- Verificacion automatica de requisitos -- Checklist de condiciones previas -- Registro de observaciones por autorizador -- Rechazo con motivo obligatorio -- Notificaciones en cada paso -- Permiso valido solo con todas las firmas - -### RF-MAA017-007.4: Control Durante Ejecucion - -- Hora de inicio real registrada -- Verificacion de condiciones antes de iniciar -- Monitoreo periodico durante trabajo -- Registro de eventos/anomalias -- Suspension por condiciones inseguras -- Extension de permiso si es necesario -- Cierre formal del permiso al terminar - -### RF-MAA017-007.5: Cierre de Permiso - -- Verificacion de condiciones post-trabajo -- Confirmacion de area segura -- Retiro de senalizaciones temporales -- Firma de cierre por responsable -- Registro de incidentes ocurridos -- Fotos de area al finalizar -- Archivo digital del permiso completo - -### RF-MAA017-007.6: Reportes y Seguimiento - -- Dashboard de permisos activos -- Permisos por vencer en proximas horas -- Historial de permisos por obra/trabajador -- Estadisticas de tipos de permiso -- Permisos rechazados y motivos -- Tiempo promedio de autorizacion -- Cumplimiento de proceso - -## Reglas de Negocio - -1. No se puede iniciar trabajo de alto riesgo sin permiso autorizado -2. Permiso de altura valido maximo 8 horas continuas -3. Permiso de espacio confinado valido maximo 4 horas -4. Trabajo en caliente requiere vigilancia 30 min post-trabajo -5. Personal sin capacitacion vigente no puede incluirse -6. Permiso vencido requiere nueva solicitud -7. Suspension de permiso es inmediata y definitiva -8. Minimo 2 personas para trabajo en espacio confinado -9. Monitoreo de atmosfera cada 2 horas en espacio confinado -10. Area de trabajo en caliente debe despejarse 10m radio - -## Criterios de Aceptacion - -- [ ] Catalogo incluye 6 tipos principales de permisos -- [ ] Solicitud verifica automaticamente requisitos del personal -- [ ] Flujo de 3 niveles de autorizacion funcional -- [ ] Alertas enviadas 30 min antes de vencimiento -- [ ] Permiso no puede iniciarse sin todas las firmas -- [ ] Suspension registra motivo y notifica a todos -- [ ] Dashboard muestra permisos activos en tiempo real -- [ ] Historial mantiene 5 anos de registros -- [ ] Proceso completo funciona en modo offline - -## Modelo de Datos - -### Tabla: hse.tipos_permiso_trabajo -``` -- id: UUID PK -- tenant_id: UUID FK -- codigo: VARCHAR(20) UNIQUE -- nombre: VARCHAR(200) -- descripcion: TEXT -- norma_referencia: VARCHAR(50) -- vigencia_max_horas: INTEGER -- requiere_autorizacion_nivel: INTEGER DEFAULT 2 -- documentos_requeridos: JSONB -- requisitos_personal: JSONB -- equipos_requeridos: JSONB -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.permisos_trabajo -``` -- id: UUID PK -- tenant_id: UUID FK -- folio: VARCHAR(30) UNIQUE -- tipo_permiso_id: UUID FK -- fraccionamiento_id: UUID FK -- solicitante_id: UUID FK -- descripcion_trabajo: TEXT -- ubicacion: VARCHAR(300) -- ubicacion_geo: GEOMETRY(Point) -- fecha_inicio_programada: TIMESTAMPTZ -- fecha_fin_programada: TIMESTAMPTZ -- fecha_inicio_real: TIMESTAMPTZ -- fecha_fin_real: TIMESTAMPTZ -- estado: ENUM(borrador, solicitado, aprobado_parcial, autorizado, en_ejecucion, suspendido, cerrado, rechazado, vencido) -- motivo_rechazo: TEXT -- motivo_suspension: TEXT -- observaciones: TEXT -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.permiso_personal -``` -- id: UUID PK -- permiso_id: UUID FK -- employee_id: UUID FK -- rol: ENUM(ejecutor, supervisor, vigia, operador, senalero) -- verificacion_capacitacion: BOOLEAN DEFAULT false -- verificacion_epp: BOOLEAN DEFAULT false -- verificacion_aptitud: BOOLEAN DEFAULT false -- observaciones: TEXT -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.permiso_autorizaciones -``` -- id: UUID PK -- permiso_id: UUID FK -- nivel: INTEGER -- autorizador_id: UUID FK -- rol_autorizador: VARCHAR(100) -- decision: ENUM(aprobado, rechazado) -- observaciones: TEXT -- firma_digital: TEXT -- fecha_decision: TIMESTAMPTZ -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.permiso_checklist -``` -- id: UUID PK -- permiso_id: UUID FK -- momento: ENUM(pre_trabajo, durante, post_trabajo) -- item_verificacion: VARCHAR(300) -- cumple: BOOLEAN -- observacion: TEXT -- verificador_id: UUID FK -- fecha_verificacion: TIMESTAMPTZ -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.permiso_monitoreos -``` -- id: UUID PK -- permiso_id: UUID FK -- fecha_hora: TIMESTAMPTZ -- tipo: VARCHAR(100) -- valor_medicion: VARCHAR(50) -- unidad: VARCHAR(20) -- dentro_rango: BOOLEAN DEFAULT true -- observaciones: TEXT -- responsable_id: UUID FK -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.permiso_eventos -``` -- id: UUID PK -- permiso_id: UUID FK -- fecha_hora: TIMESTAMPTZ -- tipo_evento: ENUM(inicio, suspension, reanudacion, extension, anomalia, cierre) -- descripcion: TEXT -- accion_tomada: TEXT -- responsable_id: UUID FK -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.permiso_documentos -``` -- id: UUID PK -- permiso_id: UUID FK -- tipo_documento: VARCHAR(100) -- nombre: VARCHAR(200) -- archivo_url: VARCHAR(500) -- fecha_subida: TIMESTAMPTZ -- subido_por: UUID FK -``` - -## Casos de Uso - -### CU-MAA017-007.1: Solicitar Permiso de Trabajo en Altura - -**Actor**: Supervisor de cuadrilla -**Precondicion**: Actividad de altura programada - -**Flujo Principal**: -1. Supervisor accede a modulo de permisos -2. Selecciona tipo "Trabajo en Altura" -3. Sistema muestra requisitos del permiso -4. Supervisor describe el trabajo a realizar -5. Supervisor indica ubicacion y horario -6. Supervisor agrega personal involucrado -7. Sistema verifica por cada trabajador: - - Capacitacion NOM-009 vigente - - Arnes asignado y vigente - - Aptitud medica -8. Sistema marca trabajadores que cumplen/no cumplen -9. Supervisor ajusta lista si hay faltantes -10. Supervisor marca medidas de seguridad a aplicar -11. Supervisor firma digitalmente solicitud -12. Sistema envia a flujo de autorizacion -13. Sistema notifica al Coordinador HSE - -**Flujo Alterno - Personal sin Requisitos**: -8a. Si trabajador no cumple requisitos -8b. Sistema indica requisito faltante -8c. Supervisor puede excluir o programar cumplimiento -8d. Permiso no avanza hasta resolver - -### CU-MAA017-007.2: Autorizar Permiso de Trabajo - -**Actor**: Coordinador HSE -**Precondicion**: Permiso solicitado pendiente - -**Flujo Principal**: -1. Coordinador recibe notificacion de permiso -2. Sistema muestra detalle y requisitos -3. Coordinador verifica cumplimiento de cada item -4. Coordinador realiza inspeccion en campo (opcional) -5. Coordinador registra observaciones -6. Si todo cumple, Coordinador aprueba -7. Coordinador firma digitalmente -8. Sistema actualiza estado del permiso -9. Si es ultimo nivel, permiso queda autorizado -10. Sistema notifica a solicitante -11. Sistema genera documento de permiso PDF - -**Flujo Alterno - Rechazo**: -6a. Si hay incumplimientos criticos -6b. Coordinador rechaza con motivo -6c. Sistema notifica rechazo a solicitante -6d. Solicitante debe corregir y resolicitar - -### CU-MAA017-007.3: Ejecutar y Cerrar Permiso - -**Actor**: Supervisor / Vigia -**Precondicion**: Permiso autorizado - -**Flujo Principal**: -1. Supervisor marca inicio de trabajo -2. Sistema registra hora real de inicio -3. Sistema inicia contador de vigencia -4. Durante trabajo, vigia registra monitoreos -5. Sistema alerta 30 min antes de vencimiento -6. Al terminar, supervisor verifica area segura -7. Supervisor completa checklist post-trabajo -8. Supervisor firma cierre del permiso -9. Sistema registra hora de cierre -10. Sistema archiva permiso completo - -**Flujo Alterno - Suspension**: -4a. Si se detecta condicion insegura -4b. Vigia/Supervisor suspende permiso -4c. Sistema registra motivo de suspension -4d. Sistema notifica a todos los involucrados -4e. Trabajo debe detenerse inmediatamente - -## Mockups - -### Pantalla: Solicitud de Permiso -``` -+--------------------------------------------------+ -| SOLICITUD DE PERMISO DE TRABAJO | -+--------------------------------------------------+ -| Tipo: [Trabajo en Altura v] | -| Norma: NOM-009-STPS-2011 | -| Vigencia maxima: 8 horas | -+--------------------------------------------------+ -| DESCRIPCION DEL TRABAJO | -| [Instalacion de canceleria en nivel 5, | -| fachada norte del edificio A____________] | -| | -| Ubicacion: [Edificio A - Nivel 5 - Fachada N] | -| Fecha: [06/12/2025] Hora: [08:00] a [16:00] | -+--------------------------------------------------+ -| PERSONAL INVOLUCRADO | -+--------------------------------------------------+ -| [+ Agregar Personal] | -| | -| Nombre | Rol | NOM-009 | Arnes | -+--------------------------------------------------+ -| Juan Perez | Ejecutor | 鉁 | 鉁 | -| Pedro Lopez | Ejecutor | 鉁 | 鉁 | -| Maria Garcia | Vigia | 鉁 | N/A | -+--------------------------------------------------+ -| MEDIDAS DE SEGURIDAD | -| [鉁揮 Linea de vida instalada | -| [鉁揮 Puntos de anclaje verificados | -| [鉁揮 Area acordonada abajo | -| [鉁揮 Comunicacion radio disponible | -+--------------------------------------------------+ -| [Cancelar] [Firmar y Enviar] | -+--------------------------------------------------+ -``` - -### Pantalla: Autorizacion de Permiso -``` -+--------------------------------------------------+ -| AUTORIZACION DE PERMISO | -+--------------------------------------------------+ -| Folio: PTR-ALT-2025-0089 | -| Tipo: Trabajo en Altura | -| Solicitante: Sup. Carlos Mendez | -| Estado: Pendiente Nivel 2 (HSE) | -+--------------------------------------------------+ -| VERIFICACION DE REQUISITOS | -+--------------------------------------------------+ -| [鉁揮 Personal con capacitacion vigente | -| [鉁揮 EPP completo y vigente | -| [鉁揮 Equipo de proteccion disponible | -| [鉁揮 Area senalizada | -| [ ] Verificacion en campo realizada | -+--------------------------------------------------+ -| Observaciones: | -| [Verificar punto de anclaje #3 antes de_____] | -| [iniciar, presenta oxido visible_____________] | -+--------------------------------------------------+ -| Nivel 1 (Supervisor): 鉁 Juan Perez - 05/12 14:00| -| Nivel 2 (HSE): Pendiente | -+--------------------------------------------------+ -| [Rechazar] [Aprobar y Firmar] | -+--------------------------------------------------+ -``` - -### Pantalla: Dashboard de Permisos -``` -+--------------------------------------------------+ -| PERMISOS DE TRABAJO - Control | -+--------------------------------------------------+ -| Obra: [Todas v] Fecha: [Hoy] | -+--------------------------------------------------+ -| PERMISOS ACTIVOS AHORA | -+--------------------------------------------------+ -| +--------+ +--------+ +--------+ +--------+ | -| | 3 | | 1 | | 0 | | 5 | | -| | Altura | | Conf. | |Caliente| |Pendient| | -| +--------+ +--------+ +--------+ +--------+ | -+--------------------------------------------------+ -| EN EJECUCION | -+--------------------------------------------------+ -| ! PTR-ALT-0089 | Altura | Edif A Niv 5 | -| Vence en: 2:30 hrs | Ejecutor: J. Perez | -| [Ver] [Monitorear] | -+--------------------------------------------------+ -| ! PTR-CON-0023 | Esp. Confinado | Cisterna | -| Vence en: 1:15 hrs | Ejecutor: P. Lopez | -| Ultimo O2: 20.8% (14:30) | -| [Ver] [Monitorear] | -+--------------------------------------------------+ -| PROXIMOS A VENCER (30 min) | -| ! PTR-ALT-0088 | Altura | Vence 15:00 | -+--------------------------------------------------+ -| [Nuevo Permiso] [Ver Historial] [Reportes] | -+--------------------------------------------------+ -``` - -## Especificaciones Tecnicas Relacionadas - -- ET-MAA017-DB-001: Schema HSE Database -- ET-MAA017-BE-010: Work Permits Service -- ET-MAA017-BE-011: Authorization Workflow Service -- ET-MAA017-FE-016: Work Permit Request Form -- ET-MAA017-FE-017: Authorization View -- ET-MAA017-FE-018: Permit Monitoring Dashboard - -## User Stories Relacionadas - -- US-MAA017-008: Solicitar permiso trabajo peligroso -- US-MAA017-025: Autorizar permiso de trabajo -- US-MAA017-026: Monitorear trabajo en ejecucion -- US-MAA017-027: Cerrar permiso de trabajo -- US-MAA017-028: Consultar historial de permisos - -## Integraciones - -### Internas -- RF-MAA017-002: Verificar capacitaciones del personal -- RF-MAA017-004: Verificar EPP asignado -- RF-MAA017-001: Vincular incidentes durante ejecucion -- MAI-007: Obtener datos del personal - -### Externas -- Ninguna requerida - ---- - -**Autor**: Requirements-Analyst -**Fecha**: 2025-12-06 -**Version**: 1.0.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-008-indicadores-hse.md b/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-008-indicadores-hse.md deleted file mode 100644 index 216aa6a3e..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAA-017-seguridad-hse/requerimientos/RF-MAA017-008-indicadores-hse.md +++ /dev/null @@ -1,453 +0,0 @@ -# RF-MAA017-008: Indicadores HSE - -## Informacion General - -| Atributo | Valor | -|----------|-------| -| **Codigo** | RF-MAA017-008 | -| **Nombre** | Indicadores HSE | -| **Modulo** | MAA-017 Seguridad HSE | -| **Prioridad** | P1 - Alta | -| **Complejidad** | Media | - -## Descripcion - -El sistema debe calcular, visualizar y analizar indicadores clave de desempeno (KPIs) en seguridad, salud ocupacional y medio ambiente. Incluye indicadores reactivos (accidentabilidad) y proactivos (cumplimiento de actividades), dashboards ejecutivos, tendencias historicas y comparativos entre obras. - -## Requisitos Funcionales - -### RF-MAA017-008.1: Indicadores de Accidentabilidad (Reactivos) - -- **LTIR** (Lost Time Incident Rate): - - Formula: (Accidentes con tiempo perdido x 200,000) / Horas trabajadas - - Meta tipica: < 1.0 -- **TRIR** (Total Recordable Incident Rate): - - Formula: (Total incidentes registrables x 200,000) / Horas trabajadas - - Meta tipica: < 3.0 -- **Indice de Frecuencia (IF)**: - - Formula: (Accidentes x 1,000,000) / Horas trabajadas -- **Indice de Gravedad (IG)**: - - Formula: (Dias perdidos x 1,000,000) / Horas trabajadas -- **Dias sin accidentes**: - - Contador desde ultimo accidente incapacitante -- **Near Miss Rate**: - - Formula: (Casi-accidentes x 200,000) / Horas trabajadas -- Calcular por periodo: diario, semanal, mensual, anual -- Calcular por obra, division, empresa - -### RF-MAA017-008.2: Indicadores Proactivos - -- **Cumplimiento de capacitacion**: - - Formula: Trabajadores capacitados / Total trabajadores x 100 - - Meta: > 95% -- **Cumplimiento de inspecciones**: - - Formula: Inspecciones realizadas / Inspecciones programadas x 100 - - Meta: > 90% -- **Cierre de hallazgos**: - - Formula: Hallazgos cerrados / Total hallazgos x 100 - - Tiempo promedio de cierre -- **Cumplimiento EPP**: - - Formula: Personal con EPP completo / Total personal x 100 - - Meta: 100% -- **Recorridos de comision**: - - Formula: Recorridos realizados / Recorridos programados x 100 -- **Permisos de trabajo emitidos vs rechazados** -- **Cumplimiento normativo STPS**: - - Porcentaje de cumplimiento de matriz - -### RF-MAA017-008.3: Indicadores Ambientales - -- **Residuos peligrosos generados** (toneladas/mes) -- **Tasa de reciclaje**: - - Formula: Residuos reciclados / Total residuos x 100 -- **Manifiestos emitidos en tiempo** -- **Quejas ambientales**: - - Numero por periodo - - Tiempo de respuesta promedio -- **Cumplimiento ambiental**: - - Porcentaje de requisitos SEMARNAT - -### RF-MAA017-008.4: Dashboard Ejecutivo - -- Vista general de todos los KPIs -- Semaforos de cumplimiento (verde, amarillo, rojo) -- Graficas de tendencia temporal -- Comparativo entre obras -- Comparativo vs meta -- Ranking de obras por desempeno -- Drill-down por indicador -- Filtros por periodo y obra - -### RF-MAA017-008.5: Analisis y Tendencias - -- Tendencia historica por indicador -- Proyeccion a fin de periodo -- Identificacion de patrones -- Analisis de correlacion: - - Capacitacion vs accidentes - - Inspecciones vs hallazgos - - Cumplimiento vs incidentes -- Alertas de desviacion de meta -- Benchmarking interno - -### RF-MAA017-008.6: Reportes Periodicos - -- Reporte semanal de seguridad -- Reporte mensual HSE ejecutivo -- Reporte trimestral de indicadores -- Informe anual de desempeno -- Exportar en PDF, Excel, PowerPoint -- Envio automatico programado -- Personalizacion de contenido - -## Reglas de Negocio - -1. Horas trabajadas se obtienen de sistema de asistencia -2. LTIR y TRIR se calculan con base 200,000 horas (100 trabajadores x 50 semanas x 40 horas) -3. Un accidente incapacitante reinicia contador de dias sin accidentes -4. Metas deben definirse por tipo de obra y tamano -5. Indicadores se recalculan cada 24 horas automaticamente -6. Alertas se envian cuando indicador supera meta en 10% -7. Solo incidentes cerrados se consideran para calculos -8. Historico debe mantenerse minimo 5 anos - -## Criterios de Aceptacion - -- [ ] LTIR y TRIR calculados correctamente segun formula OSHA -- [ ] Dashboard muestra todos los indicadores principales -- [ ] Semaforos cambian segun umbrales configurados -- [ ] Graficas de tendencia muestran 12 meses -- [ ] Comparativo entre obras funcional -- [ ] Drill-down permite ver detalle de cada indicador -- [ ] Reportes exportables en PDF y Excel -- [ ] Alertas automaticas configuradas -- [ ] Datos refrescados cada 24 horas - -## Modelo de Datos - -### Tabla: hse.indicadores_config -``` -- id: UUID PK -- tenant_id: UUID FK -- codigo: VARCHAR(20) UNIQUE -- nombre: VARCHAR(200) -- descripcion: TEXT -- formula: TEXT -- unidad: VARCHAR(50) -- tipo: ENUM(reactivo, proactivo, ambiental) -- meta_global: DECIMAL(10,4) -- umbral_verde: DECIMAL(10,4) -- umbral_amarillo: DECIMAL(10,4) -- umbral_rojo: DECIMAL(10,4) -- frecuencia_calculo: ENUM(diario, semanal, mensual) -- activo: BOOLEAN DEFAULT true -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.indicadores_meta_obra -``` -- id: UUID PK -- indicador_id: UUID FK -- fraccionamiento_id: UUID FK -- anio: INTEGER -- meta: DECIMAL(10,4) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.indicadores_valores -``` -- id: UUID PK -- tenant_id: UUID FK -- indicador_id: UUID FK -- fraccionamiento_id: UUID FK (null para empresa) -- periodo_tipo: ENUM(diario, semanal, mensual, anual) -- periodo_fecha: DATE -- valor: DECIMAL(15,4) -- numerador: DECIMAL(15,4) -- denominador: DECIMAL(15,4) -- estado: ENUM(verde, amarillo, rojo) -- calculado_at: TIMESTAMPTZ -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.horas_trabajadas -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- fecha: DATE -- horas_totales: DECIMAL(12,2) -- trabajadores_promedio: INTEGER -- fuente: ENUM(asistencia, manual) -- created_at: TIMESTAMPTZ -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.dias_sin_accidente -``` -- id: UUID PK -- tenant_id: UUID FK -- fraccionamiento_id: UUID FK -- fecha_inicio_conteo: DATE -- dias_acumulados: INTEGER -- record_historico: INTEGER -- ultimo_incidente_id: UUID FK -- updated_at: TIMESTAMPTZ -``` - -### Tabla: hse.reportes_programados -``` -- id: UUID PK -- tenant_id: UUID FK -- nombre: VARCHAR(200) -- tipo_reporte: ENUM(semanal, mensual, trimestral, anual) -- indicadores: UUID[] -- fraccionamientos: UUID[] -- destinatarios: VARCHAR[] -- dia_envio: INTEGER -- hora_envio: TIME -- formato: ENUM(pdf, excel, ambos) -- activo: BOOLEAN DEFAULT true -- ultimo_envio: TIMESTAMPTZ -- created_at: TIMESTAMPTZ -``` - -### Tabla: hse.alertas_indicadores -``` -- id: UUID PK -- tenant_id: UUID FK -- indicador_id: UUID FK -- fraccionamiento_id: UUID FK -- tipo_alerta: ENUM(meta_superada, tendencia_negativa, sin_datos) -- mensaje: TEXT -- valor_actual: DECIMAL(10,4) -- valor_meta: DECIMAL(10,4) -- leida: BOOLEAN DEFAULT false -- fecha_alerta: TIMESTAMPTZ -- created_at: TIMESTAMPTZ -``` - -## Casos de Uso - -### CU-MAA017-008.1: Consultar Dashboard de Indicadores - -**Actor**: Gerente de Obra / Director HSE -**Precondicion**: Datos de periodos anteriores disponibles - -**Flujo Principal**: -1. Usuario accede a dashboard de indicadores -2. Sistema muestra vista general de KPIs -3. Usuario visualiza semaforos de cumplimiento -4. Usuario puede filtrar por: - - Periodo (mes, trimestre, ano) - - Obra/fraccionamiento - - Tipo de indicador -5. Sistema actualiza graficas segun filtros -6. Usuario selecciona indicador para drill-down -7. Sistema muestra detalle y tendencia -8. Usuario puede exportar vista actual - -### CU-MAA017-008.2: Analizar Tendencia de Accidentabilidad - -**Actor**: Director HSE / Gerente General -**Precondicion**: Historico de al menos 6 meses - -**Flujo Principal**: -1. Usuario accede a seccion de analisis -2. Usuario selecciona indicador (ej: LTIR) -3. Sistema muestra grafica de tendencia 12 meses -4. Usuario puede comparar: - - Vs periodo anterior - - Vs meta - - Entre obras -5. Sistema identifica patron (mejora/empeora) -6. Sistema muestra correlaciones relevantes -7. Usuario genera informe de analisis -8. Sistema exporta con comentarios - -### CU-MAA017-008.3: Configurar Reporte Automatico - -**Actor**: Coordinador HSE -**Precondicion**: Indicadores configurados - -**Flujo Principal**: -1. Coordinador accede a configuracion de reportes -2. Selecciona tipo de reporte (semanal/mensual) -3. Selecciona indicadores a incluir -4. Selecciona obras a reportar -5. Ingresa destinatarios (emails) -6. Define dia y hora de envio -7. Sistema programa envio automatico -8. Sistema envia reporte en fecha/hora - -### CU-MAA017-008.4: Actualizar Horas Trabajadas - -**Actor**: Sistema / Administrador -**Precondicion**: Sistema de asistencia integrado - -**Flujo Principal**: -1. Sistema ejecuta tarea programada diaria -2. Sistema consulta horas de asistencia por obra -3. Sistema calcula total de horas del dia -4. Sistema actualiza tabla de horas trabajadas -5. Sistema recalcula indicadores afectados -6. Sistema evalua cumplimiento de metas -7. Si hay desviacion, sistema genera alerta -8. Sistema notifica a responsables - -## Mockups - -### Pantalla: Dashboard Principal HSE -``` -+------------------------------------------------------------------+ -| DASHBOARD HSE Periodo: [Dic 2025 v] | -| Obra: [Todas v] | -+------------------------------------------------------------------+ -| INDICADORES REACTIVOS | -+------------------------------------------------------------------+ -| +----------+ +----------+ +----------+ +----------+ | -| | 0.45 | | 1.85 | | 156 | | 23 | | -| | LTIR | | TRIR | | Dias | | Near Miss| | -| | Meta:<1.0| | Meta:<3.0| |Sin Accid | | | | -| | [VERDE] | | [VERDE] | | [META] | | | | -| +----------+ +----------+ +----------+ +----------+ | -+------------------------------------------------------------------+ -| TENDENCIA LTIR (12 meses) | -| 1.5 | | -| 1.0 |----*----*----*----*----*----*----*----*----*---- Meta | -| 0.5 | * * * * * * * * | | -| 0.0 +---+----+----+----+----+----+----+----+----+----+--- | -| Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic | -+------------------------------------------------------------------+ -| INDICADORES PROACTIVOS | -+------------------------------------------------------------------+ -| Capacitacion: 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻戔枒 92% [AMARILLO] | -| Inspecciones: 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻 98% [VERDE] | -| Hallazgos: 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻戔枒鈻戔枒 78% [ROJO] | -| EPP Completo: 鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻堚枅鈻 100% [VERDE] | -+------------------------------------------------------------------+ -| COMPARATIVO POR OBRA | -+------------------------------------------------------------------+ -| Obra | LTIR | TRIR | Capacit | Inspec | -+------------------------------------------------------------------+ -| Residencial Norte | 0.32 | 1.45 | 95% | 100% | -| Torres del Valle | 0.58 | 2.10 | 88% | 92% | -| Industrial Poniente | 0.45 | 1.90 | 91% | 98% | -+------------------------------------------------------------------+ -| [Exportar PDF] [Exportar Excel] [Enviar por Email] | -+------------------------------------------------------------------+ -``` - -### Pantalla: Detalle de Indicador -``` -+------------------------------------------------------------------+ -| DETALLE: Lost Time Incident Rate (LTIR) | -+------------------------------------------------------------------+ -| Formula: (Accidentes incapacitantes x 200,000) / Horas trabajadas| -| Meta: < 1.0 | -+------------------------------------------------------------------+ -| VALOR ACTUAL: 0.45 | -| Estado: [VERDE] Cumple meta | -+------------------------------------------------------------------+ -| DATOS DEL CALCULO (Diciembre 2025) | -| Accidentes incapacitantes: 2 | -| Horas trabajadas: 890,000 | -| Calculo: (2 x 200,000) / 890,000 = 0.45 | -+------------------------------------------------------------------+ -| TENDENCIA ANUAL | -| | -| 1.2 | * | -| 1.0 |----*----*---------------------------------------- Meta | -| 0.8 | * | -| 0.6 | * * | -| 0.4 | * * * * * * | -| 0.2 | | -| 0.0 +---+----+----+----+----+----+----+----+----+----+--- | -| Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic | -+------------------------------------------------------------------+ -| PROYECCION FIN DE ANO: 0.52 (Cumple meta) | -+------------------------------------------------------------------+ -| CORRELACIONES DETECTADAS | -| - Aumento de capacitacion = Reduccion 15% en LTIR | -| - Obras con >95% inspeccion tienen LTIR 30% menor | -+------------------------------------------------------------------+ -| [Volver al Dashboard] [Exportar Analisis] | -+------------------------------------------------------------------+ -``` - -### Pantalla: Configuracion de Reportes -``` -+--------------------------------------------------+ -| CONFIGURAR REPORTE AUTOMATICO | -+--------------------------------------------------+ -| Nombre: [Reporte Mensual HSE Ejecutivo____] | -| | -| Tipo: ( ) Semanal (x) Mensual ( ) Trimestral | -| | -| Dia de envio: [Primer dia habil v] | -| Hora: [08:00 v] | -+--------------------------------------------------+ -| INDICADORES A INCLUIR | -| [鉁揮 LTIR | -| [鉁揮 TRIR | -| [鉁揮 Dias sin accidente | -| [鉁揮 Cumplimiento capacitacion | -| [鉁揮 Cumplimiento inspecciones | -| [ ] Cierre de hallazgos | -| [鉁揮 Indicadores ambientales | -+--------------------------------------------------+ -| OBRAS A REPORTAR | -| [鉁揮 Residencial Norte | -| [鉁揮 Torres del Valle | -| [鉁揮 Industrial Poniente | -| [ ] Comercial Centro (En pausa) | -+--------------------------------------------------+ -| DESTINATARIOS | -| [+ Agregar email] | -| - director@empresa.com | -| - gerente.hse@empresa.com | -| - gerente.ops@empresa.com | -+--------------------------------------------------+ -| Formato: [鉁揮 PDF [鉁揮 Excel | -+--------------------------------------------------+ -| [Cancelar] [Vista Previa] [Guardar] | -+--------------------------------------------------+ -``` - -## Especificaciones Tecnicas Relacionadas - -- ET-MAA017-DB-001: Schema HSE Database -- ET-MAA017-BE-012: KPI Calculation Service -- ET-MAA017-BE-013: Report Generation Service -- ET-MAA017-FE-019: HSE Dashboard -- ET-MAA017-FE-020: Indicator Detail View -- ET-MAA017-FE-021: Report Configuration - -## User Stories Relacionadas - -- US-MAA017-009: Consultar indicadores HSE -- US-MAA017-029: Analizar tendencia de accidentabilidad -- US-MAA017-030: Comparar desempeno entre obras -- US-MAA017-031: Configurar reporte automatico -- US-MAA017-032: Recibir alertas de desviacion - -## Integraciones - -### Internas -- RF-MAA017-001: Datos de incidentes para calculo -- RF-MAA017-002: Datos de capacitaciones -- RF-MAA017-003: Datos de inspecciones y hallazgos -- RF-MAA017-004: Datos de EPP -- RF-MAA017-006: Datos ambientales -- MAI-007: Horas trabajadas de asistencia - -### Externas -- Email: Envio de reportes automaticos -- BI Tools: Exportacion para analisis avanzado (opcional) - ---- - -**Autor**: Requirements-Analyst -**Fecha**: 2025-12-06 -**Version**: 1.0.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-008-contabilidad/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-008-contabilidad/implementacion/TRACEABILITY.yml deleted file mode 100644 index 962c0ba13..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-008-contabilidad/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,577 +0,0 @@ -# TRACEABILITY.yml - MAE-008: Contabilidad -# Matriz de trazabilidad: Documentacion -> Codigo -# Ubicacion: apps/verticales/construccion/docs/02-definicion-modulos/MAE-008-contabilidad/implementacion/ - -epic_code: MAE-008 -epic_name: Contabilidad -vertical: construccion -phase: 2 -phase_name: Modulos de Negocio -story_points: 30 -status: rf_documented - -# ============================================================================= -# REUTILIZACION DE CORE -# ============================================================================= - -reusability: - core_module: MGN-010 - core_module_name: Financial - reuse_percentage: 65 - strategy: extension - description: | - Este modulo extiende la funcionalidad base de MGN-010 (Financial) para - adaptarla a las necesidades especificas del vertical de construccion. - Se reutiliza la infraestructura contable base (cuentas, asientos, periodos) - y se extiende con funcionalidad especifica para construccion. - - reused_components: - database: - - chart_of_accounts - - accounts - - account_balances - - currencies - - exchange_rates - - fiscal_years - - fiscal_periods - - journal_entries - - journal_lines - - cost_centers - - services: - - ChartOfAccountsService - - AccountsService - - CurrenciesService - - ExchangeRateService - - FiscalPeriodService - - JournalService - - endpoints: - - GET /api/v1/financial/charts - - POST /api/v1/financial/charts - - GET /api/v1/financial/charts/:id/accounts - - POST /api/v1/financial/charts/:id/accounts - - GET /api/v1/financial/currencies - - POST /api/v1/financial/currencies/convert - - GET /api/v1/financial/fiscal-years - - POST /api/v1/financial/fiscal-years - - GET /api/v1/financial/journal - - POST /api/v1/financial/journal - - POST /api/v1/financial/journal/:id/post - - extended_components: - - component: cost_centers - extension: Vinculacion con obras y frentes de trabajo - - component: journal_entries - extension: Integracion con estimaciones y valuaciones - - component: account_balances - extension: Reportes por obra y centro de costos - - component: accounts - extension: Plantilla de cuentas especifica para construccion - -# ============================================================================= -# DOCUMENTACION -# ============================================================================= - -documentation: - - requirements: - - id: RF-CONT-001 - title: Catalogo de Cuentas Contables - file: ../requerimientos/RF-CONT-001.md - priority: P0 - story_points: 8 - status: documented - reuses: RF-FIN-001 - extensions: - - Plantilla de cuentas especifica para construccion - - Cuentas predefinidas para estimaciones y anticipos - - Cuentas de costos por tipo de obra - - Integracion con catalogo de obras (MAE-002) - traces_to: - tables: [chart_of_accounts, accounts, account_balances] - services: [ChartOfAccountsService, AccountsService, ConstructionAccountsService] - endpoints: - - GET /api/v1/construccion/contabilidad/catalogo-cuentas - - POST /api/v1/construccion/contabilidad/catalogo-cuentas/:id/accounts - - GET /api/v1/construccion/contabilidad/plantillas/construccion - - - id: RF-CONT-002 - title: Polizas Contables - file: ../requerimientos/RF-CONT-002.md - priority: P0 - story_points: 8 - status: documented - reuses: RF-FIN-004 - extensions: - - Polizas automaticas desde estimaciones - - Polizas automaticas desde valuaciones de obra - - Polizas de anticipos a proveedores - - Polizas de retenciones (5 al millar, IMSS patronal) - - Vinculacion con obras y subcontratos - traces_to: - tables: [journal_entries, journal_lines, cost_centers, construction_journal_sources] - services: [JournalService, ConstructionJournalService, EstimationJournalService] - endpoints: - - GET /api/v1/construccion/contabilidad/polizas - - POST /api/v1/construccion/contabilidad/polizas - - POST /api/v1/construccion/contabilidad/polizas/desde-estimacion - - POST /api/v1/construccion/contabilidad/polizas/desde-valuacion - - POST /api/v1/construccion/contabilidad/polizas/:id/mayorizar - - - id: RF-CONT-003 - title: Balanzas y Estados Financieros - file: ../requerimientos/RF-CONT-003.md - priority: P0 - story_points: 8 - status: documented - reuses: RF-FIN-001 - extensions: - - Balanza por obra - - Balanza por centro de costos - - Estado de resultados por obra - - Comparativos presupuesto vs real - - Exportacion a Excel con formatos fiscales - traces_to: - tables: [account_balances, fiscal_periods, construction_balance_views] - services: [AccountsService, BalanceService, ConstructionBalanceService] - endpoints: - - GET /api/v1/construccion/contabilidad/balanza - - GET /api/v1/construccion/contabilidad/balanza/por-obra - - GET /api/v1/construccion/contabilidad/estados-financieros - - GET /api/v1/construccion/contabilidad/estados-financieros/por-obra - - POST /api/v1/construccion/contabilidad/exportar/balanza - - - id: RF-CONT-004 - title: Centro de Costos - file: ../requerimientos/RF-CONT-004.md - priority: P0 - story_points: 6 - status: documented - reuses: RF-FIN-004 - extensions: - - Centros de costo vinculados a obras - - Centros de costo vinculados a frentes de trabajo - - Centros de costo por tipo de actividad - - Jerarquia de centros de costos - - Reporte de costos por centro - traces_to: - tables: [cost_centers, construction_cost_centers] - services: [CostCenterService, ConstructionCostCenterService] - endpoints: - - GET /api/v1/construccion/contabilidad/centros-costo - - POST /api/v1/construccion/contabilidad/centros-costo - - GET /api/v1/construccion/contabilidad/centros-costo/:id/costos - - GET /api/v1/construccion/contabilidad/centros-costo/por-obra/:obraId - - specifications: [] - # Pendiente de documentacion - - user_stories: [] - # Pendiente de documentacion - -# ============================================================================= -# IMPLEMENTACION -# ============================================================================= - -implementation: - - database: - schema: construccion_contabilidad - path: apps/database/ddl/schemas/construccion_contabilidad/ - status: pending - note: Extiende core_financial con tablas especificas de construccion - - tables: - # Tablas extendidas de core - - name: construction_cost_centers - file: apps/database/ddl/schemas/construccion_contabilidad/tables/construction_cost_centers.sql - status: pending - requirement: RF-CONT-004 - extends: cost_centers - columns: - - {name: id, type: UUID, pk: true, fk: cost_centers} - - {name: obra_id, type: UUID, fk: obras} - - {name: frente_trabajo_id, type: UUID, fk: frentes_trabajo, nullable: true} - - {name: activity_type, type: VARCHAR(50)} - - {name: budget_amount, type: "DECIMAL(18,4)"} - - {name: actual_amount, type: "DECIMAL(18,4)", default: 0} - - {name: variance, type: "DECIMAL(18,4)", default: 0} - - {name: created_at, type: TIMESTAMPTZ} - - {name: updated_at, type: TIMESTAMPTZ} - - - name: construction_journal_sources - file: apps/database/ddl/schemas/construccion_contabilidad/tables/construction_journal_sources.sql - status: pending - requirement: RF-CONT-002 - columns: - - {name: id, type: UUID, pk: true} - - {name: journal_entry_id, type: UUID, fk: journal_entries} - - {name: source_module, type: VARCHAR(50)} - - {name: estimacion_id, type: UUID, fk: estimaciones, nullable: true} - - {name: valuacion_id, type: UUID, fk: valuaciones, nullable: true} - - {name: obra_id, type: UUID, fk: obras, nullable: true} - - {name: subcontrato_id, type: UUID, fk: subcontratos, nullable: true} - - {name: created_at, type: TIMESTAMPTZ} - - - name: construction_account_templates - file: apps/database/ddl/schemas/construccion_contabilidad/tables/construction_account_templates.sql - status: pending - requirement: RF-CONT-001 - columns: - - {name: id, type: UUID, pk: true} - - {name: code, type: VARCHAR(50)} - - {name: name, type: VARCHAR(255)} - - {name: account_type, type: VARCHAR(50)} - - {name: purpose, type: VARCHAR(100)} - - {name: is_default, type: BOOLEAN, default: false} - - {name: created_at, type: TIMESTAMPTZ} - - # Vistas para reportes - - name: construction_balance_views - file: apps/database/ddl/schemas/construccion_contabilidad/views/construction_balance_views.sql - status: pending - requirement: RF-CONT-003 - type: VIEW - description: Vistas materializadas para balanzas por obra y centro de costos - - backend: - module: construccion/contabilidad - path: apps/verticales/construccion/backend/src/modules/contabilidad/ - framework: NestJS - status: pending - note: Extiende modulos de core_financial - - entities: - # Entidades extendidas - - name: ConstructionCostCenter - file: apps/verticales/construccion/backend/src/modules/contabilidad/entities/construction-cost-center.entity.ts - status: pending - requirement: RF-CONT-004 - extends: CostCenter - - - name: ConstructionJournalSource - file: apps/verticales/construccion/backend/src/modules/contabilidad/entities/construction-journal-source.entity.ts - status: pending - requirement: RF-CONT-002 - - - name: ConstructionAccountTemplate - file: apps/verticales/construccion/backend/src/modules/contabilidad/entities/construction-account-template.entity.ts - status: pending - requirement: RF-CONT-001 - - services: - - name: ConstructionAccountsService - file: apps/verticales/construccion/backend/src/modules/contabilidad/construction-accounts.service.ts - status: pending - requirement: RF-CONT-001 - extends: AccountsService - methods: - - {name: createFromTemplate, description: Crear catalogo desde plantilla construccion} - - {name: getConstructionAccounts, description: Obtener cuentas especificas de construccion} - - {name: validateConstructionCode, description: Validar codigo segun estandar construccion} - - - name: ConstructionJournalService - file: apps/verticales/construccion/backend/src/modules/contabilidad/construction-journal.service.ts - status: pending - requirement: RF-CONT-002 - extends: JournalService - methods: - - {name: createFromEstimacion, description: Crear poliza desde estimacion} - - {name: createFromValuacion, description: Crear poliza desde valuacion} - - {name: createAnticipo, description: Crear poliza de anticipo} - - {name: createRetencion, description: Crear poliza de retenciones} - - {name: getByObra, description: Obtener polizas por obra} - - - name: EstimationJournalService - file: apps/verticales/construccion/backend/src/modules/contabilidad/estimation-journal.service.ts - status: pending - requirement: RF-CONT-002 - methods: - - {name: generateJournalFromEstimacion, description: Generar asientos automaticos de estimacion} - - {name: calculateRetenciones, description: Calcular retenciones (5 al millar, IMSS)} - - {name: applyAnticipo, description: Aplicar anticipo a estimacion} - - - name: ConstructionBalanceService - file: apps/verticales/construccion/backend/src/modules/contabilidad/construction-balance.service.ts - status: pending - requirement: RF-CONT-003 - methods: - - {name: getBalanceByObra, description: Obtener balanza por obra} - - {name: getBalanceByCostCenter, description: Obtener balanza por centro costos} - - {name: getEstadoResultadosByObra, description: Estado de resultados por obra} - - {name: getComparativoBudget, description: Comparativo presupuesto vs real} - - {name: exportToExcel, description: Exportar balanza a Excel} - - - name: ConstructionCostCenterService - file: apps/verticales/construccion/backend/src/modules/contabilidad/construction-cost-center.service.ts - status: pending - requirement: RF-CONT-004 - methods: - - {name: createByObra, description: Crear centro de costos por obra} - - {name: createByFrente, description: Crear centro costos por frente} - - {name: getCostosByCenter, description: Obtener costos acumulados} - - {name: getVariance, description: Calcular variacion presupuesto vs real} - - controllers: - - name: ConstructionAccountsController - file: apps/verticales/construccion/backend/src/modules/contabilidad/controllers/construction-accounts.controller.ts - status: pending - endpoints: - - method: GET - path: /api/v1/construccion/contabilidad/catalogo-cuentas - description: Listar catalogo de cuentas - requirement: RF-CONT-001 - - - method: POST - path: /api/v1/construccion/contabilidad/catalogo-cuentas - description: Crear catalogo desde plantilla - requirement: RF-CONT-001 - - - method: GET - path: /api/v1/construccion/contabilidad/plantillas/construccion - description: Obtener plantilla de cuentas para construccion - requirement: RF-CONT-001 - - - method: POST - path: /api/v1/construccion/contabilidad/catalogo-cuentas/:id/accounts - description: Crear cuenta en catalogo - requirement: RF-CONT-001 - - - name: ConstructionJournalController - file: apps/verticales/construccion/backend/src/modules/contabilidad/controllers/construction-journal.controller.ts - status: pending - endpoints: - - method: GET - path: /api/v1/construccion/contabilidad/polizas - description: Listar polizas contables - requirement: RF-CONT-002 - - - method: POST - path: /api/v1/construccion/contabilidad/polizas - description: Crear poliza manual - requirement: RF-CONT-002 - - - method: POST - path: /api/v1/construccion/contabilidad/polizas/desde-estimacion - description: Generar poliza desde estimacion - requirement: RF-CONT-002 - - - method: POST - path: /api/v1/construccion/contabilidad/polizas/desde-valuacion - description: Generar poliza desde valuacion - requirement: RF-CONT-002 - - - method: POST - path: /api/v1/construccion/contabilidad/polizas/:id/mayorizar - description: Mayorizar poliza - requirement: RF-CONT-002 - - - method: GET - path: /api/v1/construccion/contabilidad/polizas/obra/:obraId - description: Obtener polizas por obra - requirement: RF-CONT-002 - - - name: ConstructionBalanceController - file: apps/verticales/construccion/backend/src/modules/contabilidad/controllers/construction-balance.controller.ts - status: pending - endpoints: - - method: GET - path: /api/v1/construccion/contabilidad/balanza - description: Obtener balanza general - requirement: RF-CONT-003 - - - method: GET - path: /api/v1/construccion/contabilidad/balanza/por-obra - description: Obtener balanza por obra - requirement: RF-CONT-003 - - - method: GET - path: /api/v1/construccion/contabilidad/estados-financieros - description: Obtener estados financieros - requirement: RF-CONT-003 - - - method: GET - path: /api/v1/construccion/contabilidad/estados-financieros/por-obra - description: Estados financieros por obra - requirement: RF-CONT-003 - - - method: POST - path: /api/v1/construccion/contabilidad/exportar/balanza - description: Exportar balanza a Excel - requirement: RF-CONT-003 - - - name: ConstructionCostCenterController - file: apps/verticales/construccion/backend/src/modules/contabilidad/controllers/construction-cost-center.controller.ts - status: pending - endpoints: - - method: GET - path: /api/v1/construccion/contabilidad/centros-costo - description: Listar centros de costo - requirement: RF-CONT-004 - - - method: POST - path: /api/v1/construccion/contabilidad/centros-costo - description: Crear centro de costo - requirement: RF-CONT-004 - - - method: GET - path: /api/v1/construccion/contabilidad/centros-costo/:id/costos - description: Obtener costos acumulados - requirement: RF-CONT-004 - - - method: GET - path: /api/v1/construccion/contabilidad/centros-costo/por-obra/:obraId - description: Centros de costo por obra - requirement: RF-CONT-004 - - frontend: - module: construccion/contabilidad - path: apps/verticales/construccion/frontend/src/modules/contabilidad/ - framework: Angular - status: pending - note: Reutiliza componentes de core_financial - - components: - - name: CatalogoCuentasComponent - file: apps/verticales/construccion/frontend/src/modules/contabilidad/components/catalogo-cuentas.component.ts - status: pending - requirement: RF-CONT-001 - - - name: PolizasListComponent - file: apps/verticales/construccion/frontend/src/modules/contabilidad/components/polizas-list.component.ts - status: pending - requirement: RF-CONT-002 - - - name: PolizaFormComponent - file: apps/verticales/construccion/frontend/src/modules/contabilidad/components/poliza-form.component.ts - status: pending - requirement: RF-CONT-002 - - - name: BalanzaComponent - file: apps/verticales/construccion/frontend/src/modules/contabilidad/components/balanza.component.ts - status: pending - requirement: RF-CONT-003 - - - name: EstadosFinancierosComponent - file: apps/verticales/construccion/frontend/src/modules/contabilidad/components/estados-financieros.component.ts - status: pending - requirement: RF-CONT-003 - - - name: CentrosCostoComponent - file: apps/verticales/construccion/frontend/src/modules/contabilidad/components/centros-costo.component.ts - status: pending - requirement: RF-CONT-004 - -# ============================================================================= -# DEPENDENCIAS -# ============================================================================= - -dependencies: - depends_on: - # Dependencias de core - - module: MGN-010 - type: hard - reason: Reutiliza funcionalidad base de contabilidad (65%) - - module: MGN-001 - type: hard - reason: Autenticacion requerida - - module: MGN-004 - type: hard - reason: Aislamiento por tenant - - module: MGN-005 - type: soft - reason: Catalogos de monedas, tipos cuenta - - # Dependencias del vertical construccion - - module: MAE-002 - type: hard - reason: Vinculacion con obras para centros de costo y reportes - - module: MAE-004 - type: hard - reason: Generacion de polizas desde estimaciones - - module: MAE-005 - type: soft - reason: Generacion de polizas desde subcontratos - - required_by: - - module: MAE-009 - type: soft - reason: Tesoreria requiere polizas contables - - module: MAE-010 - type: soft - reason: Reportes financieros consolidan informacion contable - -# ============================================================================= -# INTEGRACIONES -# ============================================================================= - -integrations: - - module: MAE-004 - name: Estimaciones - integration_points: - - Generacion automatica de polizas desde estimaciones - - Contabilizacion de anticipos y retenciones - - Vinculacion estimacion -> poliza -> obra - - - module: MAE-002 - name: Obras - integration_points: - - Centros de costo por obra - - Balanzas y estados financieros por obra - - Comparativos presupuesto vs real - - - module: MAE-005 - name: Subcontratos - integration_points: - - Polizas de subcontratos - - Retenciones a subcontratistas - -# ============================================================================= -# METRICAS -# ============================================================================= - -metrics: - story_points: - estimated: 30 - actual: null - note: Reducido vs MGN-010 (45) por reutilizacion del 65% - - documentation: - requirements: 4 - specifications: 0 - user_stories: 0 - - files: - database: 5 - backend: 15 - frontend: 8 - total: 28 - reused_from_core: 30 - - reusability: - core_tables_reused: 10 - core_services_reused: 6 - core_endpoints_reused: 11 - new_tables: 3 - new_services: 5 - new_endpoints: 14 - -# ============================================================================= -# HISTORIAL -# ============================================================================= - -history: - - date: "2025-12-06" - action: "Creacion de estructura GAMILIT" - author: Requirements-Analyst - changes: - - "Creacion de TRACEABILITY.yml para MAE-008" - - "Definicion de estructura basada en MGN-010" - - "Definicion de reutilizacion del 65% de core" - - "Definicion de 4 RF especificos de construccion" - - "RF-CONT-001: Catalogo de Cuentas Contables" - - "RF-CONT-002: Polizas Contables" - - "RF-CONT-003: Balanzas y Estados Financieros" - - "RF-CONT-004: Centro de Costos" - - "Definicion de integraciones con MAE-002, MAE-004, MAE-005" diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-009-facturacion/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-009-facturacion/implementacion/TRACEABILITY.yml deleted file mode 100644 index d4dd3536e..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-009-facturacion/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,1420 +0,0 @@ -# ============================================================================ -# MATRIZ DE TRAZABILIDAD - M脫DULO MAE-009: FACTURACI脫N -# ============================================================================ -# Proyecto: ERP Suite - Vertical Construcci贸n -# M贸dulo: MAE-009 - Facturaci贸n Electr贸nica -# Versi贸n: 1.0.0 -# Fecha: 2025-12-06 -# ============================================================================ - -metadata: - module_id: MAE-009 - module_name: Facturaci贸n - vertical: construccion - version: 1.0.0 - status: planned - owner: Equipo Construcci贸n - created_date: 2025-12-06 - last_updated: 2025-12-06 - description: | - M贸dulo de facturaci贸n electr贸nica para el sector construcci贸n. - Gestiona la emisi贸n, cancelaci贸n y recepci贸n de CFDI, as铆 como - complementos de pago seg煤n la normativa del SAT mexicano. - -# ============================================================================ -# REQUERIMIENTOS FUNCIONALES -# ============================================================================ - -functional_requirements: - - # -------------------------------------------------------------------------- - # RF-001: EMISI脫N DE CFDI - # -------------------------------------------------------------------------- - - id: RF-MAE-009-001 - name: Emisi贸n de CFDI - description: | - Emisi贸n de Comprobantes Fiscales Digitales por Internet (CFDI) seg煤n - la normativa del SAT mexicano. Incluye validaci贸n de datos fiscales, - timbrado mediante PAC y almacenamiento de XML y PDF. - priority: critical - status: pending - business_rules: - - id: BR-MAE-009-001-01 - description: El RFC del emisor debe estar registrado y activo en el SAT - - id: BR-MAE-009-001-02 - description: Todos los CFDI deben timbarse con un PAC autorizado antes de ser v谩lidos - - id: BR-MAE-009-001-03 - description: Los datos fiscales del receptor deben validarse contra el SAT - - id: BR-MAE-009-001-04 - description: El CFDI debe cumplir con el esquema XML vigente del SAT (versi贸n 4.0) - - id: BR-MAE-009-001-05 - description: Cada CFDI debe incluir UUID 煤nico proporcionado por el PAC - - id: BR-MAE-009-001-06 - description: Se debe generar representaci贸n impresa (PDF) del CFDI - - id: BR-MAE-009-001-07 - description: Los CFDI de obra deben incluir informaci贸n de la obra/proyecto - - id: BR-MAE-009-001-08 - description: Validar que la forma de pago coincida con el m茅todo de pago - - user_stories: - - id: US-MAE-009-001-01 - title: Crear CFDI de ingreso desde contrato - description: Como contador, quiero generar un CFDI de ingreso a partir de un contrato de obra para facturar estimaciones - acceptance_criteria: - - El sistema carga autom谩ticamente los datos del contrato - - Se pre-llena la informaci贸n fiscal del cliente - - Se permite seleccionar conceptos de la estimaci贸n - - Se calcula autom谩ticamente IVA y retenciones - - Se valida el RFC del receptor contra el SAT - priority: high - story_points: 8 - - - id: US-MAE-009-001-02 - title: Timbrar CFDI con PAC - description: Como contador, quiero timbrar el CFDI generado mediante el PAC autorizado para obtener el sello digital - acceptance_criteria: - - El sistema env铆a el XML pre-validado al PAC - - Se recibe y almacena el UUID del comprobante - - Se guarda el XML timbrado con cadena original - - Se genera PDF con c贸digo QR y sello digital - - Se notifica al usuario del 茅xito o error del timbrado - priority: critical - story_points: 13 - - - id: US-MAE-009-001-03 - title: Enviar CFDI por correo electr贸nico - description: Como contador, quiero enviar el CFDI timbrado al cliente por correo electr贸nico con XML y PDF adjuntos - acceptance_criteria: - - Se adjuntan XML y PDF del CFDI - - El correo incluye resumen de la factura - - Se registra el env铆o en el historial - - Se permite reenv铆o de CFDI - - Se valida el formato del correo electr贸nico - priority: medium - story_points: 5 - - - id: US-MAE-009-001-04 - title: Gestionar addendas de clientes - description: Como contador, quiero agregar addendas espec铆ficas de clientes corporativos al CFDI para cumplir sus requerimientos - acceptance_criteria: - - Se pueden configurar addendas por cliente - - Las addendas se incluyen en el XML del CFDI - - Se valida la estructura XML de la addenda - - Se mantiene compatibilidad con esquema SAT - priority: low - story_points: 8 - - technical_requirements: - - id: TR-MAE-009-001-01 - description: Implementar servicio de validaci贸n de RFC contra API del SAT - component: backend/services/sat-validation - technology: Node.js, Axios - - - id: TR-MAE-009-001-02 - description: Integrar con PAC autorizado (ej. Finkok, PAC SAT) - component: backend/integrations/pac - technology: SOAP/REST, XML - - - id: TR-MAE-009-001-03 - description: Generar XML seg煤n esquema CFDI 4.0 - component: backend/services/cfdi-generator - technology: Node.js, xml2js - - - id: TR-MAE-009-001-04 - description: Generar PDF con representaci贸n impresa del CFDI - component: backend/services/pdf-generator - technology: PDFKit, QRCode - - - id: TR-MAE-009-001-05 - description: Implementar interfaz de captura de CFDI - component: frontend/modules/facturacion - technology: React, TypeScript - - dependencies: - internal: - - module: MGN-001 - requirement: Gesti贸n de usuarios y autenticaci贸n - - module: MGN-002 - requirement: Control de permisos para emisi贸n de CFDI - - module: MGN-003 - requirement: Aislamiento de datos por tenant/empresa - - module: MAE-001 - requirement: Datos de contratos y clientes - - external: - - service: PAC - description: Proveedor Autorizado de Certificaci贸n para timbrado - type: SOAP/REST API - - service: SAT - description: Servicio de Administraci贸n Tributaria - Validaci贸n de RFC - type: Web Service - - test_cases: - - id: TC-MAE-009-001-01 - description: Validar generaci贸n correcta de XML CFDI 4.0 - type: unit - status: pending - - - id: TC-MAE-009-001-02 - description: Probar timbrado exitoso con PAC en ambiente de pruebas - type: integration - status: pending - - - id: TC-MAE-009-001-03 - description: Verificar generaci贸n de PDF con todos los elementos legales - type: functional - status: pending - - - id: TC-MAE-009-001-04 - description: Validar flujo completo de emisi贸n de CFDI - type: e2e - status: pending - - implementation: - files: - - path: backend/src/modules/facturacion/services/cfdi-emision.service.ts - status: pending - - path: backend/src/modules/facturacion/controllers/cfdi.controller.ts - status: pending - - path: backend/src/integrations/pac/pac-client.ts - status: pending - - path: frontend/src/modules/facturacion/components/CFDIForm.tsx - status: pending - - database_changes: - - table: cfdi - type: create - description: Almacena los CFDI emitidos - - table: cfdi_conceptos - type: create - description: Detalle de conceptos del CFDI - - table: cfdi_impuestos - type: create - description: Desglose de impuestos - - api_endpoints: - - method: POST - path: /api/v1/facturacion/cfdi - description: Crear y timbrar nuevo CFDI - - method: GET - path: /api/v1/facturacion/cfdi/:id - description: Obtener detalles de un CFDI - - method: GET - path: /api/v1/facturacion/cfdi/:id/xml - description: Descargar XML del CFDI - - method: GET - path: /api/v1/facturacion/cfdi/:id/pdf - description: Descargar PDF del CFDI - - # -------------------------------------------------------------------------- - # RF-002: CANCELACI脫N DE CFDI - # -------------------------------------------------------------------------- - - id: RF-MAE-009-002 - name: Cancelaci贸n de CFDI - description: | - Proceso de cancelaci贸n de CFDI previamente timbrados mediante el PAC, - incluyendo solicitud de cancelaci贸n, gesti贸n de aceptaci贸n por parte - del receptor y actualizaci贸n de estatus en el SAT. - priority: critical - status: pending - business_rules: - - id: BR-MAE-009-002-01 - description: Solo se pueden cancelar CFDI previamente timbrados - - id: BR-MAE-009-002-02 - description: La cancelaci贸n debe registrarse ante el SAT a trav茅s del PAC - - id: BR-MAE-009-002-03 - description: Para CFDI mayores a $5,000 se requiere aceptaci贸n del receptor - - id: BR-MAE-009-002-04 - description: Se debe especificar el motivo de cancelaci贸n seg煤n cat谩logo SAT - - id: BR-MAE-009-002-05 - description: Si se especifica UUID de sustituci贸n, este debe ser v谩lido - - id: BR-MAE-009-002-06 - description: No se pueden cancelar CFDI ya cancelados - - id: BR-MAE-009-002-07 - description: Validar per铆odo permitido para cancelaci贸n seg煤n reglas SAT - - user_stories: - - id: US-MAE-009-002-01 - title: Solicitar cancelaci贸n de CFDI - description: Como contador, quiero solicitar la cancelaci贸n de un CFDI emitido especificando el motivo para corregir errores - acceptance_criteria: - - Se muestra lista de motivos de cancelaci贸n del cat谩logo SAT - - Se permite indicar UUID de sustituci贸n si aplica - - El sistema valida que el CFDI no est茅 ya cancelado - - Se env铆a solicitud al PAC - - Se registra el intento de cancelaci贸n - priority: high - story_points: 8 - - - id: US-MAE-009-002-02 - title: Gestionar aceptaci贸n de cancelaci贸n - description: Como receptor, quiero recibir notificaci贸n de solicitud de cancelaci贸n y poder aceptarla o rechazarla - acceptance_criteria: - - Se notifica por correo la solicitud de cancelaci贸n - - Se proporciona enlace para aceptar/rechazar - - Se registra la respuesta del receptor - - Se actualiza el estatus del CFDI - - Se notifica al emisor de la decisi贸n - priority: high - story_points: 8 - - - id: US-MAE-009-002-03 - title: Consultar estatus de cancelaci贸n - description: Como contador, quiero consultar el estatus de una solicitud de cancelaci贸n para dar seguimiento - acceptance_criteria: - - Se muestra el estatus actual (pendiente, aceptada, rechazada) - - Se muestra la fecha de solicitud - - Se indica si requiere aceptaci贸n del receptor - - Se muestra historial de la solicitud - priority: medium - story_points: 5 - - technical_requirements: - - id: TR-MAE-009-002-01 - description: Implementar servicio de cancelaci贸n v铆a PAC - component: backend/services/cfdi-cancelacion - technology: Node.js, SOAP/REST - - - id: TR-MAE-009-002-02 - description: Crear flujo de aceptaci贸n/rechazo de cancelaci贸n - component: backend/services/cancelacion-workflow - technology: Node.js, Estado de m谩quina - - - id: TR-MAE-009-002-03 - description: Implementar notificaciones de cancelaci贸n - component: backend/services/notifications - technology: Email, Push notifications - - - id: TR-MAE-009-002-04 - description: Interfaz de gesti贸n de cancelaciones - component: frontend/modules/facturacion/cancelacion - technology: React, TypeScript - - dependencies: - internal: - - module: RF-MAE-009-001 - requirement: CFDI debe estar previamente emitido - - module: MGN-008 - requirement: Sistema de notificaciones - - external: - - service: PAC - description: Cancelaci贸n de CFDI ante el SAT - type: SOAP/REST API - - service: SAT - description: Consulta de estatus de cancelaci贸n - type: Web Service - - test_cases: - - id: TC-MAE-009-002-01 - description: Validar cancelaci贸n exitosa de CFDI menor a $5,000 - type: integration - status: pending - - - id: TC-MAE-009-002-02 - description: Probar flujo de aceptaci贸n de cancelaci贸n por receptor - type: functional - status: pending - - - id: TC-MAE-009-002-03 - description: Verificar rechazo de cancelaci贸n de CFDI ya cancelado - type: unit - status: pending - - implementation: - files: - - path: backend/src/modules/facturacion/services/cfdi-cancelacion.service.ts - status: pending - - path: backend/src/modules/facturacion/controllers/cancelacion.controller.ts - status: pending - - path: frontend/src/modules/facturacion/components/CancelacionForm.tsx - status: pending - - database_changes: - - table: cfdi_cancelaciones - type: create - description: Registro de solicitudes de cancelaci贸n - - table: cfdi - type: alter - description: Agregar campos de estatus de cancelaci贸n - - api_endpoints: - - method: POST - path: /api/v1/facturacion/cfdi/:id/cancelar - description: Solicitar cancelaci贸n de CFDI - - method: GET - path: /api/v1/facturacion/cfdi/:id/cancelacion/status - description: Consultar estatus de cancelaci贸n - - method: POST - path: /api/v1/facturacion/cancelacion/:id/aceptar - description: Aceptar cancelaci贸n como receptor - - method: POST - path: /api/v1/facturacion/cancelacion/:id/rechazar - description: Rechazar cancelaci贸n como receptor - - # -------------------------------------------------------------------------- - # RF-003: RECEPCI脫N DE CFDI - # -------------------------------------------------------------------------- - - id: RF-MAE-009-003 - name: Recepci贸n de CFDI - description: | - Gesti贸n de CFDI recibidos de proveedores, incluyendo carga de XML, - validaci贸n de timbrado, verificaci贸n ante el SAT y registro contable - de documentos por pagar. - priority: high - status: pending - business_rules: - - id: BR-MAE-009-003-01 - description: El XML debe contener un UUID v谩lido y verificable ante el SAT - - id: BR-MAE-009-003-02 - description: El RFC receptor debe corresponder a la empresa activa - - id: BR-MAE-009-003-03 - description: Se debe validar el sello digital del PAC - - id: BR-MAE-009-003-04 - description: Verificar que el CFDI no est茅 cancelado en el SAT - - id: BR-MAE-009-003-05 - description: Los CFDI recibidos deben asociarse a 贸rdenes de compra o contratos - - id: BR-MAE-009-003-06 - description: Detectar y alertar sobre CFDI duplicados (mismo UUID) - - id: BR-MAE-009-003-07 - description: Validar que los montos coincidan con la orden de compra asociada - - user_stories: - - id: US-MAE-009-003-01 - title: Cargar CFDI recibido - description: Como auxiliar contable, quiero cargar el XML de un CFDI recibido para registrarlo en el sistema - acceptance_criteria: - - Se permite carga individual o masiva de archivos XML - - El sistema extrae autom谩ticamente datos del XML - - Se valida la estructura y sello del XML - - Se detectan XML duplicados - - Se almacena el XML original - priority: high - story_points: 8 - - - id: US-MAE-009-003-02 - title: Validar CFDI ante el SAT - description: Como contador, quiero validar autom谩ticamente el CFDI ante el SAT para verificar su autenticidad y vigencia - acceptance_criteria: - - Se consulta el estatus del UUID ante el SAT - - Se verifica que el CFDI no est茅 cancelado - - Se valida la cadena original y sello digital - - Se muestra resultado de la validaci贸n - - Se registra fecha de validaci贸n - priority: high - story_points: 8 - - - id: US-MAE-009-003-03 - title: Asociar CFDI a orden de compra - description: Como auxiliar contable, quiero asociar un CFDI recibido a su orden de compra correspondiente para conciliar - acceptance_criteria: - - Se sugieren 贸rdenes de compra pendientes del proveedor - - Se comparan montos entre CFDI y orden de compra - - Se permite asociaci贸n manual o autom谩tica - - Se actualiza estatus de la orden de compra - - Se alerta si hay discrepancias en montos - priority: medium - story_points: 5 - - - id: US-MAE-009-003-04 - title: Consultar CFDI recibidos - description: Como contador, quiero consultar y filtrar los CFDI recibidos para revisi贸n y auditor铆a - acceptance_criteria: - - Filtros por proveedor, fecha, monto, estatus - - Visualizaci贸n de datos fiscales principales - - Descarga de XML y PDF - - Exportaci贸n a Excel - - Indicadores visuales de estatus - priority: medium - story_points: 5 - - technical_requirements: - - id: TR-MAE-009-003-01 - description: Implementar parser de XML CFDI 4.0 y versiones anteriores - component: backend/services/cfdi-parser - technology: Node.js, xml2js - - - id: TR-MAE-009-003-02 - description: Servicio de validaci贸n de CFDI ante SAT - component: backend/services/sat-validator - technology: Node.js, SOAP - - - id: TR-MAE-009-003-03 - description: Almacenamiento y gesti贸n de archivos XML - component: backend/services/file-storage - technology: S3, MinIO - - - id: TR-MAE-009-003-04 - description: Interfaz de carga y gesti贸n de CFDI recibidos - component: frontend/modules/facturacion/recepcion - technology: React, TypeScript - - dependencies: - internal: - - module: MGN-003 - requirement: Validaci贸n de RFC de empresa - - module: MAE-002 - requirement: 脫rdenes de compra para asociaci贸n - - external: - - service: SAT - description: Validaci贸n de vigencia de CFDI - type: Web Service - - test_cases: - - id: TC-MAE-009-003-01 - description: Validar parsing correcto de XML CFDI 4.0 - type: unit - status: pending - - - id: TC-MAE-009-003-02 - description: Probar validaci贸n de CFDI vigente ante SAT - type: integration - status: pending - - - id: TC-MAE-009-003-03 - description: Verificar detecci贸n de CFDI duplicados - type: functional - status: pending - - - id: TC-MAE-009-003-04 - description: Validar asociaci贸n correcta con orden de compra - type: functional - status: pending - - implementation: - files: - - path: backend/src/modules/facturacion/services/cfdi-recepcion.service.ts - status: pending - - path: backend/src/modules/facturacion/parsers/cfdi-parser.ts - status: pending - - path: frontend/src/modules/facturacion/components/RecepcionCFDI.tsx - status: pending - - database_changes: - - table: cfdi_recibidos - type: create - description: Almacena CFDI recibidos de proveedores - - table: cfdi_recibidos_conceptos - type: create - description: Detalle de conceptos de CFDI recibidos - - table: cfdi_validaciones - type: create - description: Historial de validaciones ante SAT - - api_endpoints: - - method: POST - path: /api/v1/facturacion/cfdi/recibidos - description: Cargar CFDI recibido - - method: POST - path: /api/v1/facturacion/cfdi/recibidos/validar - description: Validar CFDI ante SAT - - method: GET - path: /api/v1/facturacion/cfdi/recibidos - description: Listar CFDI recibidos - - method: POST - path: /api/v1/facturacion/cfdi/recibidos/:id/asociar - description: Asociar CFDI a orden de compra - - # -------------------------------------------------------------------------- - # RF-004: COMPLEMENTOS DE PAGO - # -------------------------------------------------------------------------- - - id: RF-MAE-009-004 - name: Complementos de Pago - description: | - Emisi贸n de CFDI con complemento de pago para relacionar pagos - recibidos con facturas previamente emitidas, cumpliendo con - la normativa del SAT para documentar la forma de pago. - priority: high - status: pending - business_rules: - - id: BR-MAE-009-004-01 - description: El complemento de pago debe relacionarse con uno o m谩s CFDI de ingreso previos - - id: BR-MAE-009-004-02 - description: La suma de pagos no debe exceder el total del CFDI relacionado - - id: BR-MAE-009-004-03 - description: Se debe especificar la forma de pago (transferencia, cheque, etc.) - - id: BR-MAE-009-004-04 - description: Incluir datos bancarios si el pago es por transferencia - - id: BR-MAE-009-004-05 - description: El tipo de cambio es obligatorio para pagos en moneda extranjera - - id: BR-MAE-009-004-06 - description: Validar que los CFDI relacionados no est茅n cancelados - - id: BR-MAE-009-004-07 - description: El monto del pago debe ser mayor a cero - - user_stories: - - id: US-MAE-009-004-01 - title: Generar complemento de pago - description: Como contador, quiero generar un complemento de pago para documentar un pago recibido de un cliente - acceptance_criteria: - - Se seleccionan los CFDI pendientes de pago del cliente - - Se captura monto, fecha y forma de pago - - Se calculan parcialidades autom谩ticamente - - Se valida que no se exceda el saldo pendiente - - Se genera y timbra el complemento - priority: high - story_points: 13 - - - id: US-MAE-009-004-02 - title: Aplicar pago parcial a m煤ltiples facturas - description: Como contador, quiero aplicar un pago que cubre parcialmente varias facturas para documentar correctamente - acceptance_criteria: - - Se distribuye autom谩ticamente el pago entre facturas - - Se permite ajuste manual de distribuci贸n - - Se muestra saldo pendiente de cada factura - - Se valida que la suma sea correcta - - Se generan parcialidades correctas en el XML - priority: high - story_points: 8 - - - id: US-MAE-009-004-03 - title: Consultar complementos de pago emitidos - description: Como contador, quiero consultar los complementos de pago emitidos para verificar aplicaci贸n de pagos - acceptance_criteria: - - Lista de complementos con filtros - - Visualizaci贸n de facturas relacionadas - - Descarga de XML y PDF - - Indicador de estatus (vigente/cancelado) - - Integraci贸n con estado de cuenta del cliente - priority: medium - story_points: 5 - - - id: US-MAE-009-004-04 - title: Cancelar complemento de pago - description: Como contador, quiero cancelar un complemento de pago en caso de error para corregir la aplicaci贸n - acceptance_criteria: - - Se restaura el saldo de las facturas relacionadas - - Se solicita cancelaci贸n al PAC - - Se actualiza estado de cuenta del cliente - - Se registra motivo de cancelaci贸n - - Se notifica al cliente - priority: medium - story_points: 5 - - technical_requirements: - - id: TR-MAE-009-004-01 - description: Implementar generaci贸n de complemento de pago seg煤n anexo 20 - component: backend/services/complemento-pago - technology: Node.js, xml2js - - - id: TR-MAE-009-004-02 - description: Servicio de c谩lculo de parcialidades - component: backend/services/parcialidades-calculator - technology: Node.js - - - id: TR-MAE-009-004-03 - description: Interfaz de aplicaci贸n de pagos - component: frontend/modules/facturacion/pagos - technology: React, TypeScript - - - id: TR-MAE-009-004-04 - description: Actualizaci贸n de saldos de clientes - component: backend/services/cuentas-corrientes - technology: Node.js, PostgreSQL - - dependencies: - internal: - - module: RF-MAE-009-001 - requirement: CFDI de ingreso previamente emitidos - - module: MAE-003 - requirement: Gesti贸n de cuentas por cobrar - - external: - - service: PAC - description: Timbrado de complementos de pago - type: SOAP/REST API - - service: Banxico - description: Tipo de cambio para pagos en moneda extranjera - type: REST API - - test_cases: - - id: TC-MAE-009-004-01 - description: Validar generaci贸n correcta de XML complemento de pago - type: unit - status: pending - - - id: TC-MAE-009-004-02 - description: Probar c谩lculo de parcialidades para pago m煤ltiple - type: unit - status: pending - - - id: TC-MAE-009-004-03 - description: Verificar actualizaci贸n de saldos tras aplicar pago - type: integration - status: pending - - - id: TC-MAE-009-004-04 - description: Validar flujo completo de complemento de pago - type: e2e - status: pending - - implementation: - files: - - path: backend/src/modules/facturacion/services/complemento-pago.service.ts - status: pending - - path: backend/src/modules/facturacion/controllers/pagos.controller.ts - status: pending - - path: frontend/src/modules/facturacion/components/ComplementoPagoForm.tsx - status: pending - - database_changes: - - table: complementos_pago - type: create - description: Registro de complementos de pago emitidos - - table: complementos_pago_documentos - type: create - description: Relaci贸n con documentos relacionados - - table: cfdi - type: alter - description: Agregar campos de control de saldo y parcialidades - - api_endpoints: - - method: POST - path: /api/v1/facturacion/complementos-pago - description: Generar complemento de pago - - method: GET - path: /api/v1/facturacion/complementos-pago - description: Listar complementos de pago - - method: GET - path: /api/v1/facturacion/cfdi/:id/saldo - description: Consultar saldo pendiente de un CFDI - - method: POST - path: /api/v1/facturacion/complementos-pago/:id/cancelar - description: Cancelar complemento de pago - -# ============================================================================ -# INTEGRACIONES EXTERNAS -# ============================================================================ - -external_integrations: - - - id: INT-MAE-009-001 - name: Integraci贸n con PAC - description: | - Integraci贸n con Proveedor Autorizado de Certificaci贸n para timbrado - y cancelaci贸n de CFDI ante el SAT. - provider: PAC Autorizado (Finkok, PAC SAT, etc.) - type: SOAP/REST API - authentication: API Key + Certificado Digital - endpoints: - - name: Timbrado de CFDI - method: POST - description: Env铆o de XML para timbrado - - name: Cancelaci贸n de CFDI - method: POST - description: Solicitud de cancelaci贸n - - name: Consulta de estatus - method: GET - description: Verificaci贸n de estatus de CFDI - requirements: - - Certificado de sello digital vigente (CSD) - - Cuenta activa con el PAC - - Configuraci贸n de credenciales de API - error_handling: - - Timeout: Reintentar hasta 3 veces - - Error de validaci贸n: Mostrar mensaje espec铆fico - - Servicio no disponible: Encolar para reintento - - - id: INT-MAE-009-002 - name: Validaci贸n ante SAT - description: | - Servicios web del SAT para validaci贸n de RFC, estatus de CFDI - y consulta de datos fiscales. - provider: Servicio de Administraci贸n Tributaria - type: Web Service (SOAP) - authentication: P煤blica (sin autenticaci贸n para consultas) - endpoints: - - name: Validaci贸n de RFC - method: GET - description: Verificar existencia y estatus de RFC - - name: Validaci贸n de CFDI - method: GET - description: Consultar vigencia de UUID - - name: Consulta de estatus tributario - method: GET - description: Verificar situaci贸n fiscal del contribuyente - requirements: - - Conexi贸n a internet estable - - Manejo de certificados SSL - error_handling: - - Cache de RFC validados (24 horas) - - Modo offline para consultas no cr铆ticas - - - id: INT-MAE-009-003 - name: Consulta de tipo de cambio - description: | - Obtenci贸n de tipo de cambio oficial para CFDI en moneda extranjera - y complementos de pago. - provider: Banco de M茅xico (Banxico) - type: REST API - authentication: API Token - endpoints: - - name: Tipo de cambio USD - method: GET - description: Obtener tipo de cambio USD/MXN - requirements: - - Token de API de Banxico - - Actualizaci贸n diaria de tipos de cambio - error_handling: - - Cache del 煤ltimo tipo de cambio conocido - - Alerta si no se puede actualizar por 3 d铆as - -# ============================================================================ -# MODELO DE DATOS -# ============================================================================ - -data_model: - - tables: - - name: cfdi - description: Cat谩logo principal de CFDI emitidos - columns: - - name: id - type: UUID - primary_key: true - - name: tenant_id - type: UUID - nullable: false - foreign_key: tenants.id - - name: uuid - type: VARCHAR(36) - nullable: false - unique: true - description: UUID del CFDI asignado por el PAC - - name: folio - type: VARCHAR(20) - nullable: false - - name: serie - type: VARCHAR(10) - nullable: true - - name: tipo_comprobante - type: VARCHAR(1) - nullable: false - description: I=Ingreso, E=Egreso, T=Traslado, N=N贸mina, P=Pago - - name: fecha_emision - type: TIMESTAMP - nullable: false - - name: fecha_timbrado - type: TIMESTAMP - nullable: true - - name: emisor_rfc - type: VARCHAR(13) - nullable: false - - name: emisor_nombre - type: VARCHAR(300) - nullable: false - - name: receptor_rfc - type: VARCHAR(13) - nullable: false - - name: receptor_nombre - type: VARCHAR(300) - nullable: false - - name: receptor_email - type: VARCHAR(100) - nullable: true - - name: subtotal - type: DECIMAL(18,2) - nullable: false - - name: total - type: DECIMAL(18,2) - nullable: false - - name: moneda - type: VARCHAR(3) - nullable: false - default: MXN - - name: tipo_cambio - type: DECIMAL(10,6) - nullable: true - - name: forma_pago - type: VARCHAR(2) - nullable: true - - name: metodo_pago - type: VARCHAR(3) - nullable: false - - name: uso_cfdi - type: VARCHAR(3) - nullable: false - - name: estatus - type: VARCHAR(20) - nullable: false - description: borrador, timbrado, cancelado, cancelacion_pendiente - - name: xml_path - type: VARCHAR(500) - nullable: true - - name: pdf_path - type: VARCHAR(500) - nullable: true - - name: saldo_pendiente - type: DECIMAL(18,2) - nullable: true - description: Para control de complementos de pago - - name: contrato_id - type: UUID - nullable: true - foreign_key: contratos.id - - name: created_at - type: TIMESTAMP - default: NOW() - - name: updated_at - type: TIMESTAMP - default: NOW() - indexes: - - columns: [uuid] - unique: true - - columns: [tenant_id, folio, serie] - - columns: [receptor_rfc] - - columns: [estatus] - - columns: [fecha_emision] - - - name: cfdi_conceptos - description: Conceptos/partidas de los CFDI - columns: - - name: id - type: UUID - primary_key: true - - name: cfdi_id - type: UUID - nullable: false - foreign_key: cfdi.id - - name: numero - type: INTEGER - nullable: false - - name: clave_prod_serv - type: VARCHAR(8) - nullable: false - - name: clave_unidad - type: VARCHAR(3) - nullable: false - - name: unidad - type: VARCHAR(20) - nullable: true - - name: descripcion - type: TEXT - nullable: false - - name: cantidad - type: DECIMAL(18,6) - nullable: false - - name: valor_unitario - type: DECIMAL(18,6) - nullable: false - - name: importe - type: DECIMAL(18,2) - nullable: false - - name: descuento - type: DECIMAL(18,2) - nullable: true - - name: objeto_imp - type: VARCHAR(2) - nullable: false - indexes: - - columns: [cfdi_id] - - - name: cfdi_impuestos - description: Desglose de impuestos por concepto - columns: - - name: id - type: UUID - primary_key: true - - name: concepto_id - type: UUID - nullable: false - foreign_key: cfdi_conceptos.id - - name: tipo - type: VARCHAR(10) - nullable: false - description: traslado o retencion - - name: impuesto - type: VARCHAR(3) - nullable: false - description: 001=ISR, 002=IVA, 003=IEPS - - name: tipo_factor - type: VARCHAR(10) - nullable: false - - name: tasa_o_cuota - type: DECIMAL(6,6) - nullable: true - - name: base - type: DECIMAL(18,2) - nullable: false - - name: importe - type: DECIMAL(18,2) - nullable: false - indexes: - - columns: [concepto_id] - - - name: cfdi_cancelaciones - description: Registro de solicitudes de cancelaci贸n - columns: - - name: id - type: UUID - primary_key: true - - name: cfdi_id - type: UUID - nullable: false - foreign_key: cfdi.id - - name: fecha_solicitud - type: TIMESTAMP - nullable: false - default: NOW() - - name: motivo - type: VARCHAR(2) - nullable: false - description: Cat谩logo de motivos SAT - - name: uuid_sustitucion - type: VARCHAR(36) - nullable: true - - name: estatus - type: VARCHAR(20) - nullable: false - description: pendiente, aceptada, rechazada - - name: fecha_respuesta - type: TIMESTAMP - nullable: true - - name: respuesta_receptor - type: VARCHAR(20) - nullable: true - - name: pac_acuse - type: TEXT - nullable: true - - name: created_by - type: UUID - nullable: false - foreign_key: users.id - indexes: - - columns: [cfdi_id] - - columns: [estatus] - - - name: cfdi_recibidos - description: CFDI recibidos de proveedores - columns: - - name: id - type: UUID - primary_key: true - - name: tenant_id - type: UUID - nullable: false - foreign_key: tenants.id - - name: uuid - type: VARCHAR(36) - nullable: false - unique: true - - name: folio - type: VARCHAR(20) - nullable: true - - name: serie - type: VARCHAR(10) - nullable: true - - name: fecha_emision - type: TIMESTAMP - nullable: false - - name: emisor_rfc - type: VARCHAR(13) - nullable: false - - name: emisor_nombre - type: VARCHAR(300) - nullable: false - - name: subtotal - type: DECIMAL(18,2) - nullable: false - - name: total - type: DECIMAL(18,2) - nullable: false - - name: moneda - type: VARCHAR(3) - nullable: false - - name: estatus - type: VARCHAR(20) - nullable: false - description: pendiente_validar, vigente, cancelado - - name: xml_path - type: VARCHAR(500) - nullable: false - - name: pdf_path - type: VARCHAR(500) - nullable: true - - name: validado_sat - type: BOOLEAN - default: false - - name: fecha_validacion - type: TIMESTAMP - nullable: true - - name: orden_compra_id - type: UUID - nullable: true - foreign_key: ordenes_compra.id - - name: created_at - type: TIMESTAMP - default: NOW() - indexes: - - columns: [uuid] - unique: true - - columns: [tenant_id, emisor_rfc] - - columns: [estatus] - - - name: complementos_pago - description: Complementos de pago emitidos - columns: - - name: id - type: UUID - primary_key: true - - name: cfdi_id - type: UUID - nullable: false - foreign_key: cfdi.id - description: CFDI tipo P que contiene el complemento - - name: fecha_pago - type: DATE - nullable: false - - name: forma_pago - type: VARCHAR(2) - nullable: false - - name: moneda - type: VARCHAR(3) - nullable: false - - name: tipo_cambio - type: DECIMAL(10,6) - nullable: true - - name: monto - type: DECIMAL(18,2) - nullable: false - - name: num_operacion - type: VARCHAR(100) - nullable: true - - name: rfc_banco_ordenante - type: VARCHAR(13) - nullable: true - - name: cuenta_ordenante - type: VARCHAR(50) - nullable: true - - name: rfc_banco_beneficiario - type: VARCHAR(13) - nullable: true - - name: cuenta_beneficiario - type: VARCHAR(50) - nullable: true - - name: created_at - type: TIMESTAMP - default: NOW() - indexes: - - columns: [cfdi_id] - - - name: complementos_pago_documentos - description: Documentos relacionados en complemento de pago - columns: - - name: id - type: UUID - primary_key: true - - name: complemento_pago_id - type: UUID - nullable: false - foreign_key: complementos_pago.id - - name: cfdi_relacionado_id - type: UUID - nullable: false - foreign_key: cfdi.id - - name: uuid_documento - type: VARCHAR(36) - nullable: false - - name: serie - type: VARCHAR(10) - nullable: true - - name: folio - type: VARCHAR(20) - nullable: false - - name: moneda - type: VARCHAR(3) - nullable: false - - name: equivalencia_dr - type: DECIMAL(10,6) - nullable: true - - name: num_parcialidad - type: INTEGER - nullable: false - - name: imp_saldo_anterior - type: DECIMAL(18,2) - nullable: false - - name: imp_pagado - type: DECIMAL(18,2) - nullable: false - - name: imp_saldo_insoluto - type: DECIMAL(18,2) - nullable: false - - name: objeto_imp_dr - type: VARCHAR(2) - nullable: false - indexes: - - columns: [complemento_pago_id] - - columns: [cfdi_relacionado_id] - -# ============================================================================ -# PLAN DE IMPLEMENTACI脫N -# ============================================================================ - -implementation_plan: - - phases: - - phase: 1 - name: Configuraci贸n y infraestructura - duration: 2 semanas - tasks: - - Configurar integraci贸n con PAC de pruebas - - Implementar servicio de validaci贸n SAT - - Configurar almacenamiento de archivos XML/PDF - - Crear estructura de base de datos - - Configurar certificados de sello digital - deliverables: - - Integraci贸n con PAC funcional en ambiente de pruebas - - Base de datos creada y migrada - - Servicio de almacenamiento configurado - - - phase: 2 - name: Emisi贸n de CFDI (RF-001) - duration: 3 semanas - tasks: - - Implementar generador de XML CFDI 4.0 - - Desarrollar servicio de timbrado con PAC - - Crear generador de PDF con representaci贸n impresa - - Desarrollar interfaz de captura de CFDI - - Implementar env铆o por correo electr贸nico - - Pruebas de timbrado en ambiente de pruebas - deliverables: - - M贸dulo de emisi贸n de CFDI completo - - Casos de prueba ejecutados - - Documentaci贸n de usuario - - - phase: 3 - name: Cancelaci贸n de CFDI (RF-002) - duration: 2 semanas - tasks: - - Implementar servicio de cancelaci贸n v铆a PAC - - Desarrollar flujo de aceptaci贸n/rechazo - - Crear interfaz de gesti贸n de cancelaciones - - Implementar notificaciones de cancelaci贸n - - Pruebas de cancelaci贸n - deliverables: - - M贸dulo de cancelaci贸n funcional - - Sistema de notificaciones integrado - - Casos de prueba ejecutados - - - phase: 4 - name: Recepci贸n de CFDI (RF-003) - duration: 2 semanas - tasks: - - Implementar parser de XML CFDI - - Desarrollar validador ante SAT - - Crear interfaz de carga de XML - - Implementar asociaci贸n con 贸rdenes de compra - - Desarrollar consultas y reportes - deliverables: - - M贸dulo de recepci贸n completo - - Validaci贸n autom谩tica ante SAT - - Reportes de CFDI recibidos - - - phase: 5 - name: Complementos de Pago (RF-004) - duration: 2 semanas - tasks: - - Implementar generador de complemento de pago - - Desarrollar calculadora de parcialidades - - Crear interfaz de aplicaci贸n de pagos - - Implementar actualizaci贸n de saldos - - Integrar con cuentas por cobrar - - Pruebas de complementos - deliverables: - - M贸dulo de complementos de pago funcional - - Integraci贸n con cuentas por cobrar - - Actualizaci贸n autom谩tica de saldos - - - phase: 6 - name: Pruebas integrales y despliegue - duration: 1 semana - tasks: - - Pruebas de integraci贸n end-to-end - - Pruebas de rendimiento - - Migraci贸n a ambiente de producci贸n - - Configuraci贸n de PAC en producci贸n - - Capacitaci贸n a usuarios - - Documentaci贸n final - deliverables: - - Sistema probado y validado - - Ambiente de producci贸n configurado - - Usuarios capacitados - - Documentaci贸n completa - - total_duration: 12 semanas - - resources: - - role: Tech Lead - allocation: 100% - - role: Backend Developer - allocation: 100% - count: 2 - - role: Frontend Developer - allocation: 100% - - role: QA Engineer - allocation: 50% - - role: DevOps Engineer - allocation: 25% - -# ============================================================================ -# M脡TRICAS Y KPIs -# ============================================================================ - -metrics: - - business_metrics: - - name: CFDI emitidos por mes - description: N煤mero total de CFDI timbrados exitosamente - target: "> 1000" - measurement: Conteo mensual - - - name: Tasa de 茅xito de timbrado - description: Porcentaje de CFDI timbrados exitosamente vs. intentos - target: "> 98%" - measurement: (Timbrados exitosos / Total intentos) * 100 - - - name: Tiempo promedio de emisi贸n - description: Tiempo desde captura hasta timbrado exitoso - target: "< 30 segundos" - measurement: Promedio de tiempo por CFDI - - - name: Tasa de cancelaci贸n - description: Porcentaje de CFDI cancelados vs. emitidos - target: "< 5%" - measurement: (Cancelados / Emitidos) * 100 - - - name: CFDI recibidos validados - description: Porcentaje de CFDI recibidos validados ante SAT - target: "100%" - measurement: (Validados / Total recibidos) * 100 - - technical_metrics: - - name: Disponibilidad del servicio - description: Uptime del m贸dulo de facturaci贸n - target: "> 99.5%" - measurement: Monitoreo continuo - - - name: Tiempo de respuesta API - description: Tiempo promedio de respuesta de endpoints - target: "< 500ms" - measurement: Promedio de p95 - - - name: Tasa de errores - description: Porcentaje de requests con error - target: "< 1%" - measurement: (Errores / Total requests) * 100 - - - name: Cobertura de pruebas - description: Porcentaje de c贸digo cubierto por pruebas - target: "> 80%" - measurement: An谩lisis de cobertura - -# ============================================================================ -# RIESGOS Y MITIGACIONES -# ============================================================================ - -risks: - - - id: RISK-MAE-009-001 - description: Indisponibilidad del servicio del PAC - probability: medium - impact: high - mitigation: | - - Implementar sistema de cola para reintentos autom谩ticos - - Configurar PAC de respaldo - - Alertas autom谩ticas al equipo t茅cnico - - SLA con proveedor PAC - - - id: RISK-MAE-009-002 - description: Cambios en normativa SAT - probability: high - impact: high - mitigation: | - - Monitoreo continuo de publicaciones del SAT - - Arquitectura flexible para adaptaci贸n r谩pida - - Mantener compatibilidad con versiones anteriores - - Equipo de compliance fiscal - - - id: RISK-MAE-009-003 - description: Problemas con certificados de sello digital - probability: low - impact: critical - mitigation: | - - Alertas de vencimiento 60 d铆as antes - - Proceso documentado de renovaci贸n - - Respaldo de certificados en vault seguro - - Procedimiento de emergencia - - - id: RISK-MAE-009-004 - description: Volumen alto de facturaci贸n (cierre de mes) - probability: high - impact: medium - mitigation: | - - Arquitectura escalable horizontalmente - - Auto-scaling en cloud - - Cache de validaciones frecuentes - - Pruebas de carga peri贸dicas - - - id: RISK-MAE-009-005 - description: P茅rdida de archivos XML/PDF - probability: low - impact: high - mitigation: | - - Almacenamiento redundante (S3 con replicaci贸n) - - Backups diarios autom谩ticos - - Registro en blockchain (opcional) - - Posibilidad de re-descarga desde PAC - -# ============================================================================ -# NOTAS Y CONSIDERACIONES -# ============================================================================ - -notes: | - 1. CUMPLIMIENTO NORMATIVO: - - Todas las funcionalidades deben cumplir con el Anexo 20 del SAT - - Mantener actualizaci贸n con versi贸n vigente de CFDI (actualmente 4.0) - - Consultar regularmente el Diario Oficial de la Federaci贸n - - 2. SEGURIDAD: - - Los certificados de sello digital (CSD) deben almacenarse encriptados - - Implementar logs de auditor铆a para todas las operaciones - - Control de acceso basado en roles para emisi贸n de CFDI - - Encriptaci贸n en tr谩nsito y en reposo para archivos XML - - 3. INTEGRACIONES: - - Priorizar PAC con mejor uptime y soporte - - Considerar integraci贸n con m煤ltiples PAC para redundancia - - Mantener ambiente de pruebas separado (sandbox PAC) - - 4. EXPERIENCIA DE USUARIO: - - Formularios con autocompletado de cat谩logos SAT - - Validaciones en tiempo real antes de timbrar - - Mensajes de error claros y accionables - - Descarga masiva de XML y PDF - - 5. RENDIMIENTO: - - Optimizar para alta concurrencia en cierre de mes - - Implementar procesamiento as铆ncrono para timbrado - - Cache de cat谩logos SAT (actualizaci贸n semanal) - - 6. REPORTES Y ANALYTICS: - - Dashboard de facturaci贸n con m茅tricas clave - - Reportes de CFDI para contabilidad - - Integraci贸n con sistema contable - - Exportaci贸n a formatos est谩ndar (Excel, PDF, XML) - -# ============================================================================ -# HISTORIAL DE CAMBIOS -# ============================================================================ - -change_log: - - version: 1.0.0 - date: 2025-12-06 - author: Sistema - changes: - - Creaci贸n inicial del archivo de trazabilidad - - Definici贸n de 4 requerimientos funcionales principales - - Especificaci贸n de integraciones con PAC y SAT - - Definici贸n de modelo de datos - - Plan de implementaci贸n en 6 fases diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-010-tesoreria/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-010-tesoreria/implementacion/TRACEABILITY.yml deleted file mode 100644 index 2181466d9..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-010-tesoreria/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,1060 +0,0 @@ -# ============================================================================ -# MATRIZ DE TRAZABILIDAD - MAE-010 Tesorer铆a -# ============================================================================ -# Proyecto: ERP Suite - Construcci贸n -# M贸dulo: MAE-010 - Gesti贸n de Tesorer铆a -# Versi贸n: 1.0.0 -# 脷ltima actualizaci贸n: 2025-12-06 -# ============================================================================ - -metadata: - module_id: MAE-010 - module_name: Gesti贸n de Tesorer铆a - vertical: Construcci贸n - phase: Fase Core Business - version: 1.0.0 - status: in_design - last_updated: 2025-12-06 - owner: Equipo Construcci贸n - - # M茅tricas de reutilizaci贸n - reuse_metrics: - core_reuse_percentage: 70 - custom_development_percentage: 30 - estimated_effort_reduction: 65 - core_components_used: 12 - custom_components: 5 - - # Dependencias del Core - core_dependencies: - - MGN-001 # Gesti贸n de Usuarios - - MGN-002 # Control de Acceso (RBAC) - - MGN-003 # Multi-Tenancy - - MGN-004 # Multi-Empresa - - MGN-005 # Cat谩logos - - MGN-006 # Configuraci贸n - - MGN-007 # Auditor铆a - - MGN-008 # Notificaciones - - MGN-009 # Reportes - - MGN-010 # Financiero (Core) - -# ============================================================================ -# REQUERIMIENTOS FUNCIONALES -# ============================================================================ - -functional_requirements: - - # -------------------------------------------------------------------------- - # RF-001: Gesti贸n de Bancos - # -------------------------------------------------------------------------- - - id: RF-MAE-010-001 - name: Gesti贸n de Bancos - description: Administraci贸n completa de cuentas bancarias y entidades financieras - priority: CRITICAL - status: pending - complexity: HIGH - - business_rules: - - id: BR-001-001 - description: Cada cuenta bancaria debe estar asociada a una empresa y proyecto - validation: Validaci贸n obligatoria de empresa y proyecto al crear cuenta - - - id: BR-001-002 - description: Control de saldos en tiempo real por cuenta bancaria - validation: Actualizaci贸n autom谩tica de saldos con cada transacci贸n - - - id: BR-001-003 - description: L铆mites de saldo m铆nimo y m谩ximo configurables - validation: Alertas autom谩ticas al exceder l铆mites configurados - - - id: BR-001-004 - description: Soporte para m煤ltiples monedas por cuenta - validation: Conversi贸n autom谩tica seg煤n tipo de cambio del d铆a - - - id: BR-001-005 - description: Control de cuentas activas/inactivas con bloqueo de transacciones - validation: Validaci贸n de estado antes de permitir movimientos - - user_stories: - - id: US-001-001 - title: Registrar nueva cuenta bancaria - as_a: Gerente Financiero - i_want: Registrar una nueva cuenta bancaria - so_that: Pueda gestionar los fondos del proyecto - acceptance_criteria: - - Formulario con datos de banco y cuenta - - Asociaci贸n obligatoria a empresa y proyecto - - Configuraci贸n de moneda y saldos iniciales - - Definici贸n de l铆mites de operaci贸n - - Validaci贸n de n煤mero de cuenta 煤nico - priority: HIGH - story_points: 5 - - - id: US-001-002 - title: Consultar saldos bancarios - as_a: Tesorero - i_want: Consultar saldos actuales de todas las cuentas - so_that: Pueda conocer la disponibilidad de efectivo - acceptance_criteria: - - Dashboard con saldos por cuenta - - Filtros por empresa, proyecto y banco - - Visualizaci贸n en m煤ltiples monedas - - Indicadores de alertas por l铆mites - - Exportaci贸n de reportes de saldos - priority: HIGH - story_points: 8 - - - id: US-001-003 - title: Configurar l铆mites de cuentas - as_a: Director Financiero - i_want: Configurar l铆mites m铆nimos y m谩ximos por cuenta - so_that: Pueda controlar el flujo de efectivo autom谩ticamente - acceptance_criteria: - - Definici贸n de saldo m铆nimo requerido - - Definici贸n de saldo m谩ximo permitido - - Configuraci贸n de alertas por umbrales - - Notificaciones autom谩ticas a responsables - - Hist贸rico de cambios en l铆mites - priority: MEDIUM - story_points: 5 - - technical_requirements: - - id: TR-001-001 - description: API REST para gesti贸n CRUD de cuentas bancarias - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-010 - - - id: TR-001-002 - description: Modelo de datos extendido para cuentas por proyecto - component: database - technology: PostgreSQL - core_reuse: true - customization: Campos adicionales para proyecto y l铆mites - - - id: TR-001-003 - description: Servicio de c谩lculo de saldos en tiempo real - component: backend - technology: NestJS + Redis - core_reuse: true - core_module: MGN-010 - - - id: TR-001-004 - description: UI para gesti贸n de cuentas bancarias - component: frontend - technology: Angular - core_reuse: true - customization: Campos espec铆ficos de construcci贸n - - - id: TR-001-005 - description: Sistema de alertas por l铆mites de saldo - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-008 - - test_cases: - - id: TC-001-001 - description: Crear cuenta bancaria con datos v谩lidos - type: functional - priority: HIGH - - - id: TC-001-002 - description: Validar unicidad de n煤mero de cuenta - type: functional - priority: HIGH - - - id: TC-001-003 - description: Verificar actualizaci贸n de saldos en tiempo real - type: functional - priority: CRITICAL - - - id: TC-001-004 - description: Probar alertas por l铆mites de saldo - type: functional - priority: MEDIUM - - - id: TC-001-005 - description: Validar conversi贸n de monedas - type: functional - priority: HIGH - - dependencies: - core_modules: - - MGN-003 # Multi-Tenancy - - MGN-004 # Multi-Empresa - - MGN-005 # Cat谩logos (bancos, monedas) - - MGN-008 # Notificaciones - - MGN-010 # Financiero Core - custom_modules: [] - external_services: - - name: Servicio de Tipos de Cambio - type: API externa - required: true - - # -------------------------------------------------------------------------- - # RF-002: Flujo de Efectivo - # -------------------------------------------------------------------------- - - id: RF-MAE-010-002 - name: Flujo de Efectivo - description: Proyecci贸n y control del flujo de efectivo por proyecto - priority: CRITICAL - status: pending - complexity: VERY_HIGH - - business_rules: - - id: BR-002-001 - description: Proyecci贸n de flujo basada en estimaciones de obra - validation: Integraci贸n con cronograma y presupuesto de proyecto - - - id: BR-002-002 - description: Actualizaci贸n autom谩tica con ingresos y egresos reales - validation: Sincronizaci贸n con m贸dulos de facturaci贸n y pagos - - - id: BR-002-003 - description: An谩lisis de variaciones entre flujo proyectado y real - validation: C谩lculo autom谩tico de desviaciones y alertas - - - id: BR-002-004 - description: Proyecci贸n multi-per铆odo (semanal, mensual, trimestral) - validation: Configuraci贸n flexible de per铆odos de an谩lisis - - - id: BR-002-005 - description: Control de flujo negativo con alertas tempranas - validation: Notificaciones 15 d铆as antes de flujo negativo proyectado - - user_stories: - - id: US-002-001 - title: Proyectar flujo de efectivo - as_a: Gerente de Proyecto - i_want: Proyectar el flujo de efectivo de mi obra - so_that: Pueda anticipar necesidades de financiamiento - acceptance_criteria: - - Integraci贸n con presupuesto del proyecto - - Definici贸n de ingresos esperados por estimaci贸n - - Programaci贸n de egresos por partida - - Visualizaci贸n en l铆nea de tiempo - - Escenarios optimista/realista/pesimista - priority: CRITICAL - story_points: 13 - - - id: US-002-002 - title: Monitorear flujo real vs proyectado - as_a: Director Financiero - i_want: Comparar el flujo real contra el proyectado - so_that: Pueda identificar desviaciones y tomar acciones - acceptance_criteria: - - Dashboard comparativo real vs proyectado - - Indicadores de variaci贸n por per铆odo - - An谩lisis de causas de desviaci贸n - - Alertas autom谩ticas por desviaciones >15% - - Reportes de tendencias - priority: HIGH - story_points: 13 - - - id: US-002-003 - title: Simular escenarios de flujo - as_a: Tesorero - i_want: Simular diferentes escenarios de flujo de efectivo - so_that: Pueda planificar estrategias financieras - acceptance_criteria: - - Creaci贸n de escenarios personalizados - - Ajuste de variables clave (pagos, cobros, plazos) - - Comparaci贸n entre escenarios - - An谩lisis de sensibilidad - - Exportaci贸n de simulaciones - priority: MEDIUM - story_points: 8 - - technical_requirements: - - id: TR-002-001 - description: Motor de proyecci贸n de flujo de efectivo - component: backend - technology: NestJS - core_reuse: false - custom_development: true - - - id: TR-002-002 - description: Integraci贸n con m贸dulo de presupuestos - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-010 - - - id: TR-002-003 - description: Servicio de an谩lisis de variaciones - component: backend - technology: NestJS + Python (Analytics) - core_reuse: false - custom_development: true - - - id: TR-002-004 - description: UI de visualizaci贸n de flujo de efectivo - component: frontend - technology: Angular + Chart.js - core_reuse: false - custom_development: true - - - id: TR-002-005 - description: Sistema de alertas por flujo negativo - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-008 - - test_cases: - - id: TC-002-001 - description: Proyectar flujo basado en presupuesto - type: functional - priority: CRITICAL - - - id: TC-002-002 - description: Actualizar flujo con transacciones reales - type: functional - priority: CRITICAL - - - id: TC-002-003 - description: Calcular variaciones real vs proyectado - type: functional - priority: HIGH - - - id: TC-002-004 - description: Generar alertas por flujo negativo - type: functional - priority: HIGH - - - id: TC-002-005 - description: Simular escenarios con diferentes variables - type: functional - priority: MEDIUM - - dependencies: - core_modules: - - MGN-008 # Notificaciones - - MGN-009 # Reportes - - MGN-010 # Financiero Core - custom_modules: - - MAE-007 # Presupuestos - - MAE-008 # Estimaciones - external_services: [] - - # -------------------------------------------------------------------------- - # RF-003: Conciliaciones Bancarias - # -------------------------------------------------------------------------- - - id: RF-MAE-010-003 - name: Conciliaciones Bancarias - description: Proceso de conciliaci贸n autom谩tica y manual de movimientos bancarios - priority: HIGH - status: pending - complexity: HIGH - - business_rules: - - id: BR-003-001 - description: Conciliaci贸n autom谩tica por importe y fecha - validation: Matching autom谩tico con tolerancia configurable - - - id: BR-003-002 - description: Gesti贸n de partidas en tr谩nsito - validation: Identificaci贸n y seguimiento de movimientos pendientes - - - id: BR-003-003 - description: Control de diferencias por investigar - validation: Registro y seguimiento de discrepancias - - - id: BR-003-004 - description: Cierre mensual obligatorio de conciliaciones - validation: Bloqueo de per铆odo anterior al aprobar conciliaci贸n - - - id: BR-003-005 - description: Importaci贸n de estados de cuenta bancarios - validation: Soporte para formatos XML, CSV, PDF - - user_stories: - - id: US-003-001 - title: Importar estado de cuenta bancario - as_a: Contador - i_want: Importar el estado de cuenta del banco - so_that: Pueda iniciar el proceso de conciliaci贸n - acceptance_criteria: - - Importaci贸n desde XML, CSV o PDF - - Mapeo autom谩tico de campos - - Validaci贸n de formato y datos - - Vista previa antes de confirmar - - Registro de archivo importado - priority: HIGH - story_points: 8 - - - id: US-003-002 - title: Conciliar autom谩ticamente movimientos - as_a: Contador - i_want: Ejecutar conciliaci贸n autom谩tica - so_that: Ahorre tiempo en el proceso de conciliaci贸n - acceptance_criteria: - - Matching por importe exacto y fecha 卤3 d铆as - - Matching por referencia bancaria - - Tolerancia configurable de diferencias - - Reporte de movimientos conciliados - - Lista de partidas sin conciliar - priority: HIGH - story_points: 13 - - - id: US-003-003 - title: Conciliar manualmente movimientos - as_a: Contador - i_want: Conciliar manualmente movimientos no autom谩ticos - so_that: Pueda completar la conciliaci贸n del per铆odo - acceptance_criteria: - - Vista lado a lado: banco vs sistema - - Selecci贸n manual de movimientos a conciliar - - Registro de notas y observaciones - - Creaci贸n de ajustes contables - - Confirmaci贸n de conciliaci贸n - priority: HIGH - story_points: 8 - - - id: US-003-004 - title: Cerrar per铆odo de conciliaci贸n - as_a: Gerente Financiero - i_want: Cerrar y aprobar la conciliaci贸n mensual - so_that: Pueda garantizar la integridad de la informaci贸n - acceptance_criteria: - - Validaci贸n de conciliaci贸n completa - - Reporte de diferencias pendientes - - Workflow de aprobaci贸n - - Bloqueo de per铆odo conciliado - - Generaci贸n de reporte final - priority: MEDIUM - story_points: 5 - - technical_requirements: - - id: TR-003-001 - description: Servicio de importaci贸n de estados de cuenta - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-010 - - - id: TR-003-002 - description: Motor de conciliaci贸n autom谩tica - component: backend - technology: NestJS + Algoritmos de matching - core_reuse: true - core_module: MGN-010 - customization: Reglas espec铆ficas de construcci贸n - - - id: TR-003-003 - description: API para conciliaci贸n manual - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-010 - - - id: TR-003-004 - description: UI de conciliaci贸n bancaria - component: frontend - technology: Angular - core_reuse: true - customization: Vista espec铆fica para proyectos - - - id: TR-003-005 - description: Servicio de cierre y aprobaci贸n de conciliaciones - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-010 - - test_cases: - - id: TC-003-001 - description: Importar estado de cuenta en formato XML - type: functional - priority: HIGH - - - id: TC-003-002 - description: Conciliar autom谩ticamente por importe exacto - type: functional - priority: CRITICAL - - - id: TC-003-003 - description: Conciliar manualmente con diferencias - type: functional - priority: HIGH - - - id: TC-003-004 - description: Cerrar per铆odo con validaciones - type: functional - priority: HIGH - - - id: TC-003-005 - description: Verificar bloqueo de per铆odo cerrado - type: functional - priority: MEDIUM - - dependencies: - core_modules: - - MGN-007 # Auditor铆a - - MGN-010 # Financiero Core - custom_modules: [] - external_services: - - name: Parser de documentos PDF - type: Librer铆a externa - required: true - - # -------------------------------------------------------------------------- - # RF-004: Pagos Programados - # -------------------------------------------------------------------------- - - id: RF-MAE-010-004 - name: Pagos Programados - description: Gesti贸n y automatizaci贸n de pagos recurrentes y programados - priority: HIGH - status: pending - complexity: MEDIUM - - business_rules: - - id: BR-004-001 - description: Programaci贸n de pagos recurrentes (semanales, mensuales) - validation: Configuraci贸n de frecuencia y fechas de ejecuci贸n - - - id: BR-004-002 - description: Aprobaci贸n multi-nivel seg煤n monto del pago - validation: Workflow de aprobaci贸n configurable por rangos - - - id: BR-004-003 - description: Generaci贸n autom谩tica de 贸rdenes de pago - validation: Creaci贸n de documentos seg煤n calendario - - - id: BR-004-004 - description: Control de disponibilidad de fondos antes de ejecutar - validation: Validaci贸n de saldo disponible al momento del pago - - - id: BR-004-005 - description: Notificaciones previas a ejecuci贸n de pagos - validation: Alertas 24 horas antes del pago programado - - user_stories: - - id: US-004-001 - title: Programar pagos recurrentes - as_a: Tesorero - i_want: Programar pagos recurrentes - so_that: Automatice los pagos regulares a proveedores - acceptance_criteria: - - Selecci贸n de proveedor y concepto - - Definici贸n de monto y moneda - - Configuraci贸n de frecuencia (semanal/mensual) - - Definici贸n de cuenta bancaria origen - - Fecha de inicio y fin de programaci贸n - priority: HIGH - story_points: 8 - - - id: US-004-002 - title: Aprobar pagos programados - as_a: Gerente Financiero - i_want: Aprobar o rechazar pagos programados - so_that: Pueda mantener control sobre los egresos - acceptance_criteria: - - Bandeja de pagos pendientes de aprobaci贸n - - Visualizaci贸n de detalles del pago - - Verificaci贸n de disponibilidad de fondos - - Opci贸n de aprobar/rechazar con comentarios - - Notificaci贸n al solicitante - priority: HIGH - story_points: 5 - - - id: US-004-003 - title: Ejecutar pagos autom谩ticamente - as_a: Sistema - i_want: Ejecutar pagos programados autom谩ticamente - so_that: Se procesen los pagos en las fechas configuradas - acceptance_criteria: - - Job programado que revisa pagos del d铆a - - Validaci贸n de aprobaciones requeridas - - Verificaci贸n de saldo disponible - - Generaci贸n de orden de pago - - Notificaci贸n de ejecuci贸n exitosa - priority: HIGH - story_points: 8 - - - id: US-004-004 - title: Consultar calendario de pagos - as_a: Director Financiero - i_want: Consultar el calendario de pagos programados - so_that: Pueda planificar la disponibilidad de efectivo - acceptance_criteria: - - Vista de calendario mensual - - Lista de pagos por fecha - - Filtros por proyecto, proveedor, estado - - Exportaci贸n a Excel - - Indicadores de alertas por fondos insuficientes - priority: MEDIUM - story_points: 5 - - technical_requirements: - - id: TR-004-001 - description: API para gesti贸n de pagos programados - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-010 - - - id: TR-004-002 - description: Scheduler para ejecuci贸n autom谩tica de pagos - component: backend - technology: NestJS + Bull Queue - core_reuse: true - core_module: MGN-010 - - - id: TR-004-003 - description: Workflow de aprobaci贸n de pagos - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-002 - - - id: TR-004-004 - description: UI de calendario y gesti贸n de pagos - component: frontend - technology: Angular + FullCalendar - core_reuse: true - customization: Vista espec铆fica de construcci贸n - - - id: TR-004-005 - description: Sistema de notificaciones de pagos - component: backend - technology: NestJS - core_reuse: true - core_module: MGN-008 - - test_cases: - - id: TC-004-001 - description: Crear pago programado recurrente - type: functional - priority: HIGH - - - id: TC-004-002 - description: Aprobar pago con workflow multi-nivel - type: functional - priority: HIGH - - - id: TC-004-003 - description: Ejecutar pago autom谩ticamente en fecha programada - type: functional - priority: CRITICAL - - - id: TC-004-004 - description: Validar fondos insuficientes al ejecutar - type: functional - priority: HIGH - - - id: TC-004-005 - description: Generar notificaciones previas a ejecuci贸n - type: functional - priority: MEDIUM - - dependencies: - core_modules: - - MGN-002 # RBAC (aprobaciones) - - MGN-008 # Notificaciones - - MGN-010 # Financiero Core - custom_modules: - - MAE-009 # Proveedores - external_services: [] - -# ============================================================================ -# MATRIZ DE TRAZABILIDAD -# ============================================================================ - -traceability_matrix: - - # RF-001: Gesti贸n de Bancos - - requirement_id: RF-MAE-010-001 - requirement_name: Gesti贸n de Bancos - business_rules: [BR-001-001, BR-001-002, BR-001-003, BR-001-004, BR-001-005] - user_stories: [US-001-001, US-001-002, US-001-003] - technical_requirements: [TR-001-001, TR-001-002, TR-001-003, TR-001-004, TR-001-005] - test_cases: [TC-001-001, TC-001-002, TC-001-003, TC-001-004, TC-001-005] - implementation_status: pending - test_coverage: 0 - - # RF-002: Flujo de Efectivo - - requirement_id: RF-MAE-010-002 - requirement_name: Flujo de Efectivo - business_rules: [BR-002-001, BR-002-002, BR-002-003, BR-002-004, BR-002-005] - user_stories: [US-002-001, US-002-002, US-002-003] - technical_requirements: [TR-002-001, TR-002-002, TR-002-003, TR-002-004, TR-002-005] - test_cases: [TC-002-001, TC-002-002, TC-002-003, TC-002-004, TC-002-005] - implementation_status: pending - test_coverage: 0 - - # RF-003: Conciliaciones Bancarias - - requirement_id: RF-MAE-010-003 - requirement_name: Conciliaciones Bancarias - business_rules: [BR-003-001, BR-003-002, BR-003-003, BR-003-004, BR-003-005] - user_stories: [US-003-001, US-003-002, US-003-003, US-003-004] - technical_requirements: [TR-003-001, TR-003-002, TR-003-003, TR-003-004, TR-003-005] - test_cases: [TC-003-001, TC-003-002, TC-003-003, TC-003-004, TC-003-005] - implementation_status: pending - test_coverage: 0 - - # RF-004: Pagos Programados - - requirement_id: RF-MAE-010-004 - requirement_name: Pagos Programados - business_rules: [BR-004-001, BR-004-002, BR-004-003, BR-004-004, BR-004-005] - user_stories: [US-004-001, US-004-002, US-004-003, US-004-004] - technical_requirements: [TR-004-001, TR-004-002, TR-004-003, TR-004-004, TR-004-005] - test_cases: [TC-004-001, TC-004-002, TC-004-003, TC-004-004, TC-004-005] - implementation_status: pending - test_coverage: 0 - -# ============================================================================ -# COMPONENTES REUTILIZADOS DEL CORE -# ============================================================================ - -core_components_reused: - - # M贸dulo Financiero Core - - component_id: MGN-010-ACCOUNTS - component_name: Gesti贸n de Cuentas Bancarias - core_module: MGN-010 - reuse_percentage: 80 - customization_needed: - - Asociaci贸n a proyectos de construcci贸n - - L铆mites de saldo por proyecto - - Campos espec铆ficos de construcci贸n - - - component_id: MGN-010-TRANSACTIONS - component_name: Registro de Transacciones Bancarias - core_module: MGN-010 - reuse_percentage: 90 - customization_needed: - - Clasificaci贸n por tipo de gasto de obra - - - component_id: MGN-010-RECONCILIATION - component_name: Motor de Conciliaci贸n Bancaria - core_module: MGN-010 - reuse_percentage: 85 - customization_needed: - - Reglas de matching espec铆ficas - - Integraci贸n con estimaciones - - - component_id: MGN-010-PAYMENTS - component_name: Gesti贸n de Pagos - core_module: MGN-010 - reuse_percentage: 75 - customization_needed: - - Workflow de aprobaci贸n por proyecto - - Integraci贸n con retenciones de obra - - # M贸dulo de Notificaciones - - component_id: MGN-008-ALERTS - component_name: Sistema de Alertas - core_module: MGN-008 - reuse_percentage: 100 - customization_needed: [] - - # M贸dulo de Reportes - - component_id: MGN-009-FINANCIAL-REPORTS - component_name: Reportes Financieros - core_module: MGN-009 - reuse_percentage: 70 - customization_needed: - - Reportes espec铆ficos de flujo de obra - - Dashboards de tesorer铆a por proyecto - - # M贸dulo de Cat谩logos - - component_id: MGN-005-BANKS - component_name: Cat谩logo de Bancos - core_module: MGN-005 - reuse_percentage: 100 - customization_needed: [] - - - component_id: MGN-005-CURRENCIES - component_name: Cat谩logo de Monedas - core_module: MGN-005 - reuse_percentage: 100 - customization_needed: [] - - # M贸dulo de Auditor铆a - - component_id: MGN-007-AUDIT-LOG - component_name: Registro de Auditor铆a - core_module: MGN-007 - reuse_percentage: 100 - customization_needed: [] - - # M贸dulo RBAC - - component_id: MGN-002-WORKFLOWS - component_name: Workflows de Aprobaci贸n - core_module: MGN-002 - reuse_percentage: 90 - customization_needed: - - Niveles de aprobaci贸n por monto en construcci贸n - - # M贸dulo Multi-Tenancy - - component_id: MGN-003-TENANT-ISOLATION - component_name: Aislamiento de Datos - core_module: MGN-003 - reuse_percentage: 100 - customization_needed: [] - - # M贸dulo Multi-Empresa - - component_id: MGN-004-COMPANY-CONTEXT - component_name: Contexto de Empresa - core_module: MGN-004 - reuse_percentage: 100 - customization_needed: [] - -# ============================================================================ -# COMPONENTES CUSTOM -# ============================================================================ - -custom_components: - - - component_id: MAE-010-CASHFLOW-ENGINE - component_name: Motor de Proyecci贸n de Flujo de Efectivo - description: Componente espec铆fico para proyectar flujo basado en cronograma de obra - complexity: VERY_HIGH - estimated_effort: 21 story_points - technologies: - - NestJS - - Python (Analytics) - - PostgreSQL - integration_points: - - MAE-007 # Presupuestos - - MAE-008 # Estimaciones - - MAE-003 # Cronograma - - - component_id: MAE-010-CASHFLOW-UI - component_name: UI de Visualizaci贸n de Flujo de Efectivo - description: Interfaz especializada para an谩lisis de flujo de construcci贸n - complexity: HIGH - estimated_effort: 13 story_points - technologies: - - Angular - - Chart.js - - D3.js - integration_points: - - MAE-010-CASHFLOW-ENGINE - - - component_id: MAE-010-VARIANCE-ANALYSIS - component_name: An谩lisis de Variaciones - description: Componente para an谩lisis de desviaciones real vs proyectado - complexity: MEDIUM - estimated_effort: 8 story_points - technologies: - - NestJS - - Python (Pandas) - integration_points: - - MAE-010-CASHFLOW-ENGINE - - - component_id: MAE-010-SCENARIO-SIMULATOR - component_name: Simulador de Escenarios - description: Herramienta para simular diferentes escenarios de flujo - complexity: MEDIUM - estimated_effort: 8 story_points - technologies: - - NestJS - - Angular - integration_points: - - MAE-010-CASHFLOW-ENGINE - - - component_id: MAE-010-PROJECT-ACCOUNTS - component_name: Extensi贸n de Cuentas por Proyecto - description: L贸gica espec铆fica para manejo de cuentas por proyecto - complexity: LOW - estimated_effort: 5 story_points - technologies: - - NestJS - - PostgreSQL - integration_points: - - MGN-010 # Financiero Core - - MAE-001 # Proyectos - -# ============================================================================ -# PLAN DE IMPLEMENTACI脫N -# ============================================================================ - -implementation_plan: - - phases: - - phase: 1 - name: Configuraci贸n Base - duration_weeks: 2 - requirements: - - RF-MAE-010-001 # Gesti贸n de Bancos - components: - - MAE-010-PROJECT-ACCOUNTS - - MGN-010-ACCOUNTS (extensi贸n) - story_points: 18 - - - phase: 2 - name: Pagos y Transacciones - duration_weeks: 2 - requirements: - - RF-MAE-010-004 # Pagos Programados - components: - - MGN-010-PAYMENTS (extensi贸n) - - MGN-002-WORKFLOWS (configuraci贸n) - story_points: 21 - - - phase: 3 - name: Conciliaci贸n Bancaria - duration_weeks: 2 - requirements: - - RF-MAE-010-003 # Conciliaciones Bancarias - components: - - MGN-010-RECONCILIATION (extensi贸n) - story_points: 21 - - - phase: 4 - name: Flujo de Efectivo - duration_weeks: 4 - requirements: - - RF-MAE-010-002 # Flujo de Efectivo - components: - - MAE-010-CASHFLOW-ENGINE - - MAE-010-CASHFLOW-UI - - MAE-010-VARIANCE-ANALYSIS - - MAE-010-SCENARIO-SIMULATOR - story_points: 34 - - total_duration_weeks: 10 - total_story_points: 94 - estimated_team_size: 4 - - milestones: - - milestone: M1 - name: Gesti贸n B谩sica de Tesorer铆a - week: 4 - deliverables: - - Gesti贸n de cuentas bancarias por proyecto - - Pagos programados funcionales - - Integraciones con Core completadas - - - milestone: M2 - name: Conciliaci贸n y Control - week: 6 - deliverables: - - Conciliaci贸n bancaria autom谩tica y manual - - Cierre de per铆odos - - Reportes de conciliaci贸n - - - milestone: M3 - name: An谩lisis de Flujo de Efectivo - week: 10 - deliverables: - - Proyecci贸n de flujo por proyecto - - An谩lisis de variaciones - - Simulador de escenarios - - Dashboard completo de tesorer铆a - -# ============================================================================ -# RIESGOS Y MITIGACIONES -# ============================================================================ - -risks: - - id: RISK-001 - description: Complejidad del motor de proyecci贸n de flujo - probability: MEDIUM - impact: HIGH - mitigation: | - - Validar algoritmos con casos reales antes de implementar - - Considerar soluci贸n en fases incrementales - - Consultar con expertos en construcci贸n - - - id: RISK-002 - description: Integraci贸n con m煤ltiples m贸dulos (presupuesto, estimaciones) - probability: MEDIUM - impact: MEDIUM - mitigation: | - - Definir contratos de integraci贸n claros - - Implementar mocks para desarrollo paralelo - - Pruebas de integraci贸n continuas - - - id: RISK-003 - description: Precisi贸n de las proyecciones de flujo - probability: HIGH - impact: HIGH - mitigation: | - - Calibraci贸n con datos hist贸ricos - - Ajustes manuales permitidos - - M煤ltiples escenarios (optimista/realista/pesimista) - - - id: RISK-004 - description: Importaci贸n de estados de cuenta en m煤ltiples formatos - probability: LOW - impact: MEDIUM - mitigation: | - - Comenzar con formatos m谩s comunes (XML, CSV) - - Validaci贸n exhaustiva de parsers - - Manual de formatos soportados - -# ============================================================================ -# M脡TRICAS DE 脡XITO -# ============================================================================ - -success_metrics: - - technical: - - metric: Cobertura de pruebas - target: ">= 80%" - - - metric: Tiempo de respuesta API - target: "< 200ms (p95)" - - - metric: Disponibilidad del servicio - target: ">= 99.5%" - - - metric: Tasa de conciliaci贸n autom谩tica - target: ">= 85%" - - business: - - metric: Precisi贸n de proyecci贸n de flujo - target: "Desviaci贸n < 10% vs real" - - - metric: Tiempo de cierre de conciliaci贸n - target: "< 2 horas por per铆odo" - - - metric: Adopci贸n por usuarios - target: ">= 90% tesoreros activos" - - - metric: Reducci贸n de errores manuales - target: ">= 70%" - - reuse: - - metric: Porcentaje de c贸digo reutilizado - target: ">= 70%" - - - metric: Componentes del core utilizados - target: ">= 10" - - - metric: Reducci贸n de tiempo de desarrollo - target: ">= 60%" - -# ============================================================================ -# APROBACIONES Y CONTROL DE VERSIONES -# ============================================================================ - -approvals: - - role: Product Owner - name: Pendiente - date: null - status: pending - - - role: Arquitecto de Software - name: Pendiente - date: null - status: pending - - - role: Tech Lead - name: Pendiente - date: null - status: pending - -version_history: - - version: 1.0.0 - date: 2025-12-06 - author: Sistema - changes: - - Creaci贸n inicial del documento - - Definici贸n de 4 requerimientos funcionales - - Especificaci贸n de componentes reutilizados (70%) - - Plan de implementaci贸n en 4 fases diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-011-nomina/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-011-nomina/implementacion/TRACEABILITY.yml deleted file mode 100644 index 623c7ad23..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-011-nomina/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,833 +0,0 @@ -# TRACEABILITY - MAE-011: N贸mina -metadata: - modulo: MAE-011 - nombre: N贸mina - version: 1.0.0 - fecha: 2025-12-06 - reutilizacion_core: 65% - descripcion: Gesti贸n integral de n贸mina para personal de obra, incluyendo c谩lculo de n贸mina, timbrado CFDI, deducciones y percepciones, finiquitos e integraci贸n con checador biom茅trico - dependencias: - - MGN-001 # Usuarios y Autenticaci贸n - - MGN-002 # RBAC - - MGN-003 # Multi-tenancy - - MGN-005 # Cat谩logos - - MAI-001 # Proyectos de Construcci贸n - apps_moviles: - - MOB-001 # Checador Biom茅trico - -requerimientos: - - id: RF-NOM-001 - titulo: C谩lculo de N贸mina - descripcion: Procesamiento autom谩tico de n贸mina semanal, quincenal o mensual con c谩lculo de percepciones, deducciones e impuestos - prioridad: alta - especificaciones: - - id: ESP-NOM-001-01 - descripcion: Configurar per铆odos de n贸mina (semanal, quincenal, mensual) - - id: ESP-NOM-001-02 - descripcion: Calcular salarios base seg煤n contrato y d铆as trabajados - - id: ESP-NOM-001-03 - descripcion: Calcular tiempo extra (simple y doble) desde checador biom茅trico - - id: ESP-NOM-001-04 - descripcion: Calcular prestaciones (prima dominical, vacaciones, aguinaldo) - - id: ESP-NOM-001-05 - descripcion: Aplicar deducciones autom谩ticas (IMSS, ISR, infonavit, pensiones) - - id: ESP-NOM-001-06 - descripcion: Calcular cuotas patronales para provisiones - - id: ESP-NOM-001-07 - descripcion: Generar dispersi贸n bancaria por CLABE - - id: ESP-NOM-001-08 - descripcion: Validar n贸mina antes de procesamiento final - historias_usuario: - - id: US-NOM-001-01 - titulo: Como jefe de RH necesito procesar la n贸mina del per铆odo - criterios_aceptacion: - - Seleccionar per铆odo de n贸mina (semanal/quincenal/mensual) - - Importar asistencias desde checador biom茅trico (MOB-001) - - Revisar y ajustar incidencias (faltas, retardos, permisos) - - Calcular autom谩ticamente percepciones y deducciones - - Validar totales antes de autorizar - - id: US-NOM-001-02 - titulo: Como jefe de RH necesito calcular tiempo extra autom谩ticamente - criterios_aceptacion: - - Obtener horas trabajadas desde MOB-001 - - Identificar horas extra simples (despu茅s de 8 hrs diarias) - - Identificar horas extra dobles (despu茅s de 9 hrs o en descanso) - - Aplicar factor de pago seg煤n tipo de tiempo extra - - Generar reporte de tiempo extra por empleado - - id: US-NOM-001-03 - titulo: Como contador necesito generar dispersi贸n bancaria - criterios_aceptacion: - - Generar archivo de dispersi贸n en formato bancario - - Incluir CLABE, nombre completo y monto neto - - Validar que suma de dispersi贸n coincida con n贸mina - - Exportar en formato del banco (BBVA, Santander, etc) - - Marcar n贸mina como dispersada - - id: US-NOM-001-04 - titulo: Como empleado necesito consultar mi recibo de n贸mina - criterios_aceptacion: - - Ver listado de per铆odos de n贸mina - - Descargar recibo en PDF - - Ver desglose de percepciones y deducciones - - Consultar hist贸rico de pagos - tablas: - - payroll_management.periodos_nomina - - payroll_management.nominas - - payroll_management.nominas_detalle - - payroll_management.nominas_percepciones - - payroll_management.nominas_deducciones - - payroll_management.nominas_provision_patronal - - payroll_management.dispersiones_bancarias - endpoints: - - POST /api/v1/nomina/periodos - - GET /api/v1/nomina/periodos - - POST /api/v1/nomina/calcular - - GET /api/v1/nomina/:periodo_id - - PUT /api/v1/nomina/:periodo_id - - POST /api/v1/nomina/:periodo_id/validar - - POST /api/v1/nomina/:periodo_id/autorizar - - GET /api/v1/nomina/:periodo_id/dispersion - - POST /api/v1/nomina/:periodo_id/dispersar - - GET /api/v1/nomina/empleado/:empleado_id/recibos - - GET /api/v1/nomina/empleado/:empleado_id/recibo/:periodo_id/pdf - componentes_ui: - - NominasListView - - NominaCalculoWizard - - NominaDetalleTable - - NominaValidacionPanel - - NominaDispersionGenerator - - ReciboNominaPDFViewer - - EmpleadoRecibosHistory - - TiempoExtraCalculator - integraciones: - - modulo: MOB-001 - descripcion: Importaci贸n de asistencias y horas trabajadas desde checador biom茅trico - - modulo: MAI-001 - descripcion: Vinculaci贸n de empleados con obras de construcci贸n - - modulo: MGN-005 - descripcion: Cat谩logo de empleados, departamentos y puestos - estado: pendiente - - - id: RF-NOM-002 - titulo: Timbrado CFDI N贸mina - descripcion: Generaci贸n y timbrado de comprobantes fiscales digitales (CFDI) de n贸mina cumpliendo normativa SAT - prioridad: alta - especificaciones: - - id: ESP-NOM-002-01 - descripcion: Generar XML de n贸mina seg煤n est谩ndar SAT vigente - - id: ESP-NOM-002-02 - descripcion: Integraci贸n con PAC (Proveedor Autorizado de Certificaci贸n) - - id: ESP-NOM-002-03 - descripcion: Timbrado autom谩tico al autorizar n贸mina - - id: ESP-NOM-002-04 - descripcion: Almacenar XML y PDF timbrados - - id: ESP-NOM-002-05 - descripcion: Env铆o autom谩tico de CFDI por email a empleados - - id: ESP-NOM-002-06 - descripcion: Consulta de estatus de timbrado en PAC - - id: ESP-NOM-002-07 - descripcion: Cancelaci贸n de CFDI de n贸mina - - id: ESP-NOM-002-08 - descripcion: Registro de n贸minas extraordinarias (aguinaldo, PTU, bonos) - historias_usuario: - - id: US-NOM-002-01 - titulo: Como jefe de RH necesito timbrar recibos de n贸mina - criterios_aceptacion: - - Autorizar n贸mina calculada - - Generar XML de cada recibo seg煤n est谩ndar SAT - - Enviar lote a PAC para timbrado - - Recibir UUID de timbrado - - Almacenar XML y PDF timbrados - - Enviar CFDI por email a empleados - - id: US-NOM-002-02 - titulo: Como contador necesito cancelar CFDIs de n贸mina - criterios_aceptacion: - - Seleccionar CFDI a cancelar - - Especificar motivo de cancelaci贸n - - Solicitar cancelaci贸n al PAC - - Recibir acuse de cancelaci贸n - - Marcar recibo como cancelado - - Registrar en bit谩cora de auditor铆a - - id: US-NOM-002-03 - titulo: Como empleado necesito descargar mi CFDI de n贸mina - criterios_aceptacion: - - Ver listado de CFDIs recibidos - - Descargar XML timbrado - - Descargar PDF con c贸digo QR - - Verificar autenticidad en portal SAT - - id: US-NOM-002-04 - titulo: Como jefe de RH necesito procesar n贸minas extraordinarias - criterios_aceptacion: - - Crear per铆odo extraordinario (aguinaldo, PTU, bono) - - Calcular montos por empleado - - Generar CFDI con tipo de n贸mina extraordinaria - - Timbrar y dispersar - tablas: - - payroll_management.cfdi_nomina - - payroll_management.cfdi_timbrado_log - - payroll_management.cfdi_cancelaciones - - payroll_management.nominas_extraordinarias - - payroll_management.pac_configuration - endpoints: - - POST /api/v1/nomina/:periodo_id/timbrar - - GET /api/v1/nomina/cfdi/:uuid - - GET /api/v1/nomina/cfdi/:uuid/xml - - GET /api/v1/nomina/cfdi/:uuid/pdf - - POST /api/v1/nomina/cfdi/:uuid/cancelar - - POST /api/v1/nomina/cfdi/:uuid/reenviar-email - - GET /api/v1/nomina/cfdi/estatus/:uuid - - POST /api/v1/nomina/extraordinaria - - GET /api/v1/nomina/pac/saldo-timbres - componentes_ui: - - CFDITimbradoPanel - - CFDIStatusMonitor - - CFDICancelacionModal - - CFDIDownloadViewer - - NominaExtraordinariaForm - - PACConfigurationPanel - - TimbradoLogTable - - CFDIValidationTool - integraciones: - - servicio_externo: PAC (Finkok, SW Sapien, etc) - descripcion: Timbrado y cancelaci贸n de CFDIs - - servicio_externo: SMTP - descripcion: Env铆o de CFDIs por email - - modulo: MGN-007 - descripcion: Auditor铆a de timbrado y cancelaciones - estado: pendiente - - - id: RF-NOM-003 - titulo: Deducciones y Percepciones - descripcion: Gesti贸n completa de conceptos de deducciones y percepciones con c谩lculos autom谩ticos y manuales - prioridad: alta - especificaciones: - - id: ESP-NOM-003-01 - descripcion: Cat谩logo de percepciones (salario, tiempo extra, prima dominical, vacaciones, etc) - - id: ESP-NOM-003-02 - descripcion: Cat谩logo de deducciones (IMSS, ISR, infonavit, pr茅stamos, pensi贸n alimenticia) - - id: ESP-NOM-003-03 - descripcion: Configurar f贸rmulas de c谩lculo por concepto - - id: ESP-NOM-003-04 - descripcion: Aplicar deducciones fijas y variables por empleado - - id: ESP-NOM-003-05 - descripcion: Gestionar pr茅stamos de n贸mina con amortizaci贸n autom谩tica - - id: ESP-NOM-003-06 - descripcion: Calcular ISR con tablas y subsidios vigentes - - id: ESP-NOM-003-07 - descripcion: Calcular IMSS, infonavit y otras cuotas obrero-patronales - - id: ESP-NOM-003-08 - descripcion: Deducciones judiciales (pensi贸n alimenticia) - historias_usuario: - - id: US-NOM-003-01 - titulo: Como jefe de RH necesito gestionar pr茅stamos de empleados - criterios_aceptacion: - - Registrar pr茅stamo con monto y plazo - - Calcular amortizaci贸n quincenal o mensual - - Aplicar descuento autom谩tico en cada n贸mina - - Ver saldo pendiente de cada pr茅stamo - - Liquidar anticipadamente pr茅stamos - - Generar reporte de pr茅stamos activos - - id: US-NOM-003-02 - titulo: Como contador necesito configurar tablas ISR actualizadas - criterios_aceptacion: - - Cargar tablas de ISR vigentes por per铆odo fiscal - - Configurar l铆mites inferiores y superiores - - Definir porcentajes y cuotas fijas - - Aplicar subsidio al empleo autom谩ticamente - - Validar c谩lculo de ISR por empleado - - id: US-NOM-003-03 - titulo: Como jefe de RH necesito aplicar deducciones especiales - criterios_aceptacion: - - Crear deducci贸n manual para un empleado - - Especificar si es 煤nica o recurrente - - Definir monto fijo o porcentaje - - Aplicar en per铆odo espec铆fico - - Ver hist贸rico de deducciones por empleado - - id: US-NOM-003-04 - titulo: Como jefe de RH necesito gestionar pensiones alimenticias - criterios_aceptacion: - - Registrar orden judicial de pensi贸n - - Configurar porcentaje o monto fijo a descontar - - Aplicar autom谩ticamente en cada n贸mina - - Generar reporte mensual para autoridad - - Exportar dispersi贸n a cuenta del acreedor - tablas: - - payroll_management.conceptos_percepciones - - payroll_management.conceptos_deducciones - - payroll_management.formulas_calculo - - payroll_management.empleado_percepciones_fijas - - payroll_management.empleado_deducciones_fijas - - payroll_management.prestamos_empleados - - payroll_management.amortizaciones_prestamos - - payroll_management.tablas_isr - - payroll_management.pensiones_alimenticias - endpoints: - - GET /api/v1/nomina/conceptos/percepciones - - POST /api/v1/nomina/conceptos/percepciones - - GET /api/v1/nomina/conceptos/deducciones - - POST /api/v1/nomina/conceptos/deducciones - - POST /api/v1/nomina/empleado/:id/prestamo - - GET /api/v1/nomina/empleado/:id/prestamos - - PUT /api/v1/nomina/prestamo/:id/liquidar - - POST /api/v1/nomina/empleado/:id/deduccion-especial - - GET /api/v1/nomina/empleado/:id/deducciones - - POST /api/v1/nomina/tablas-isr - - GET /api/v1/nomina/tablas-isr/vigente - - POST /api/v1/nomina/empleado/:id/pension-alimenticia - - GET /api/v1/nomina/pensiones-alimenticias/reporte - componentes_ui: - - ConceptosNominaManager - - FormulaCalculoEditor - - PrestamosEmpleadoPanel - - PrestamoFormModal - - AmortizacionesTable - - DeduccionEspecialModal - - TablasISRManager - - PensionAlimenticiaForm - - PercepcionesDeduccionesReport - - CalculadoraISR - integraciones: - - modulo: MGN-005 - descripcion: Cat谩logo de empleados - - modulo: MAI-001 - descripcion: Vinculaci贸n de costos con obras - - modulo: MGN-010 - descripcion: Provisiones contables de n贸mina - estado: pendiente - - - id: RF-NOM-004 - titulo: Finiquitos - descripcion: C谩lculo y generaci贸n de finiquitos y liquidaciones por terminaci贸n de relaci贸n laboral - prioridad: media - especificaciones: - - id: ESP-NOM-004-01 - descripcion: Calcular finiquitos por renuncia voluntaria - - id: ESP-NOM-004-02 - descripcion: Calcular liquidaciones por despido sin causa - - id: ESP-NOM-004-03 - descripcion: Calcular indemnizaciones por despido justificado - - id: ESP-NOM-004-04 - descripcion: Incluir vacaciones no disfrutadas y prima vacacional - - id: ESP-NOM-004-05 - descripcion: Calcular aguinaldo proporcional - - id: ESP-NOM-004-06 - descripcion: Aplicar antig眉edad para prima de antig眉edad - - id: ESP-NOM-004-07 - descripcion: Generar CFDI de n贸mina tipo egreso - - id: ESP-NOM-004-08 - descripcion: Generar documento de finiquito para firma - historias_usuario: - - id: US-NOM-004-01 - titulo: Como jefe de RH necesito calcular finiquito por renuncia - criterios_aceptacion: - - Seleccionar empleado y fecha de baja - - Calcular d铆as trabajados del 煤ltimo per铆odo - - Calcular vacaciones pendientes y prima vacacional - - Calcular aguinaldo proporcional - - Calcular prima de antig眉edad si aplica - - Ver desglose completo antes de autorizar - - id: US-NOM-004-02 - titulo: Como jefe de RH necesito procesar liquidaci贸n por despido - criterios_aceptacion: - - Especificar tipo de terminaci贸n (con/sin causa) - - Calcular indemnizaci贸n de 3 meses si aplica - - Calcular 20 d铆as por a帽o de antig眉edad - - Incluir prestaciones proporcionales - - Generar formato de finiquito - - Timbrar CFDI de egreso - - id: US-NOM-004-03 - titulo: Como empleado necesito consultar mi finiquito - criterios_aceptacion: - - Ver desglose de conceptos de finiquito - - Descargar PDF de finiquito - - Descargar CFDI timbrado - - Consultar fecha estimada de pago - - id: US-NOM-004-04 - titulo: Como contador necesito generar reportes de finiquitos - criterios_aceptacion: - - Ver finiquitos procesados por per铆odo - - Exportar para p贸liza contable - - Generar dispersi贸n de finiquitos - - Consultar provisiones de finiquitos - tablas: - - payroll_management.finiquitos - - payroll_management.finiquitos_detalle - - payroll_management.finiquitos_conceptos - - payroll_management.tipos_terminacion - - payroll_management.cfdi_finiquitos - endpoints: - - POST /api/v1/nomina/finiquito - - GET /api/v1/nomina/finiquito/:id - - PUT /api/v1/nomina/finiquito/:id - - POST /api/v1/nomina/finiquito/:id/calcular - - POST /api/v1/nomina/finiquito/:id/autorizar - - POST /api/v1/nomina/finiquito/:id/timbrar - - GET /api/v1/nomina/finiquito/:id/pdf - - GET /api/v1/nomina/finiquitos - - GET /api/v1/nomina/finiquitos/reporte - - POST /api/v1/nomina/finiquito/:id/dispersar - componentes_ui: - - FiniquitoCalculator - - FiniquitoFormWizard - - FiniquitoDetailView - - FiniquitoConceptosTable - - FiniquitoPDFGenerator - - FiniquitosListView - - TipoTerminacionSelector - - FiniquitoAutorizacionPanel - integraciones: - - modulo: RF-NOM-002 - descripcion: Timbrado de CFDI de finiquito - - modulo: MGN-005 - descripcion: Cat谩logo de empleados - - modulo: MGN-010 - descripcion: Registro contable de finiquitos - - modulo: MGN-007 - descripcion: Auditor铆a de bajas y finiquitos - estado: pendiente - - - id: RF-NOM-005 - titulo: Integraci贸n Checador Biom茅trico - descripcion: Integraci贸n con app m贸vil de checador biom茅trico para control de asistencias y c谩lculo de n贸mina - prioridad: alta - especificaciones: - - id: ESP-NOM-005-01 - descripcion: Importar asistencias desde checador biom茅trico (MOB-001) - - id: ESP-NOM-005-02 - descripcion: Procesar entradas y salidas diarias - - id: ESP-NOM-005-03 - descripcion: Calcular horas trabajadas por d铆a y semana - - id: ESP-NOM-005-04 - descripcion: Identificar retardos seg煤n tolerancia configurada - - id: ESP-NOM-005-05 - descripcion: Detectar faltas injustificadas - - id: ESP-NOM-005-06 - descripcion: Calcular tiempo extra autom谩ticamente - - id: ESP-NOM-005-07 - descripcion: Gestionar incidencias (permisos, vacaciones, incapacidades) - - id: ESP-NOM-005-08 - descripcion: Sincronizaci贸n bidireccional con app m贸vil - - id: ESP-NOM-005-09 - descripcion: Reportes de asistencia por empleado y obra - historias_usuario: - - id: US-NOM-005-01 - titulo: Como jefe de RH necesito importar asistencias del checador - criterios_aceptacion: - - Conectar con MOB-001 v铆a API - - Importar registros de entrada/salida por per铆odo - - Validar integridad de datos importados - - Detectar registros duplicados o inconsistentes - - Ver resumen de asistencias importadas - - id: US-NOM-005-02 - titulo: Como jefe de RH necesito revisar incidencias detectadas - criterios_aceptacion: - - Ver listado de retardos del per铆odo - - Ver listado de faltas injustificadas - - Ver tiempo extra acumulado por empleado - - Ajustar o justificar incidencias manualmente - - Aprobar incidencias para c谩lculo de n贸mina - - id: US-NOM-005-03 - titulo: Como residente de obra necesito gestionar incidencias en checador m贸vil - criterios_aceptacion: - - Registrar permisos desde MOB-001 - - Justificar retardos o faltas - - Aprobar salidas anticipadas - - Sincronizar con sistema de n贸mina - - id: US-NOM-005-04 - titulo: Como jefe de RH necesito generar reportes de asistencia - criterios_aceptacion: - - Reporte de asistencias por empleado - - Reporte de puntualidad por obra - - Reporte de tiempo extra por departamento - - Exportar a Excel para an谩lisis - - Filtrar por fechas, obra, empleado - - id: US-NOM-005-05 - titulo: Como empleado necesito consultar mis asistencias desde app m贸vil - criterios_aceptacion: - - Ver mis entradas/salidas del mes (MOB-001) - - Consultar saldo de vacaciones - - Ver incidencias reportadas - - Solicitar correcci贸n de registro err贸neo - tablas: - - payroll_management.asistencias - - payroll_management.registros_checador - - payroll_management.incidencias - - payroll_management.tipo_incidencia - - payroll_management.horarios_trabajo - - payroll_management.tolerancia_retardos - - payroll_management.horas_extra_calculadas - endpoints: - - POST /api/v1/nomina/asistencias/importar - - GET /api/v1/nomina/asistencias - - GET /api/v1/nomina/asistencias/empleado/:id - - GET /api/v1/nomina/asistencias/periodo/:periodo_id - - POST /api/v1/nomina/incidencias - - GET /api/v1/nomina/incidencias - - PUT /api/v1/nomina/incidencias/:id - - DELETE /api/v1/nomina/incidencias/:id - - POST /api/v1/nomina/incidencias/:id/aprobar - - GET /api/v1/nomina/tiempo-extra/calculado - - GET /api/v1/nomina/reportes/asistencia - - GET /api/v1/nomina/reportes/puntualidad - - POST /api/v1/nomina/horarios - - GET /api/v1/nomina/horarios - componentes_ui: - - AsistenciasImportPanel - - AsistenciasListView - - IncidenciasManager - - IncidenciaFormModal - - TiempoExtraReport - - AsistenciasPorEmpleadoView - - PuntualidadChart - - HorariosTrabajoCRUD - - ToleranciaConfigPanel - - RegistrosChecadorTable - app_movil: MOB-001 - funcionalidades_movil: - - Registro biom茅trico de entrada/salida - - Registro de ubicaci贸n GPS - - Foto de evidencia en check-in - - Solicitud de permisos e incidencias - - Consulta de asistencias personales - - Notificaciones de incidencias - - Modo offline con sincronizaci贸n - integraciones: - - modulo: MOB-001 - descripcion: Importaci贸n de registros de checador biom茅trico - tipo: bidireccional - endpoints: - - GET /api/v1/checador/registros - - POST /api/v1/checador/incidencias - - GET /api/v1/checador/empleado/:id/asistencias - - modulo: RF-NOM-001 - descripcion: Provisi贸n de horas trabajadas para c谩lculo de n贸mina - - modulo: MAI-001 - descripcion: Vinculaci贸n de asistencias con obras de construcci贸n - - modulo: MGN-008 - descripcion: Notificaciones de incidencias y aprobaciones - estado: pendiente - -# Aplicaci贸n M贸vil -app_movil: - id: MOB-001 - nombre: Checador Biom茅trico - descripcion: Aplicaci贸n m贸vil para registro de asistencias con verificaci贸n biom茅trica - plataformas: - - Android - - iOS - tecnologias: - - React Native - - Expo - - SQLite (offline) - - Axios (sync) - - React Native Biometrics - - React Native Geolocation - funcionalidades: - - autenticacion_biometrica: Verificaci贸n por huella digital o reconocimiento facial - - registro_entrada_salida: Check-in/check-out con timestamp - - captura_ubicacion: GPS y geofencing por obra - - foto_evidencia: Captura de foto en cada registro - - consulta_asistencias: Historial personal de asistencias - - gestion_incidencias: Solicitud de permisos y justificaciones - - notificaciones_push: Alertas de aprobaciones y rechazos - - sincronizacion: Sync autom谩tica y manual con backend - - modo_offline: Operaci贸n sin conexi贸n con queue de sincronizaci贸n - requerimientos_vinculados: - - RF-NOM-005 # Integraci贸n Checador Biom茅trico - - RF-NOM-001 # Provisi贸n de datos para c谩lculo de n贸mina - estado: pendiente - -# Esquemas de Base de Datos -esquemas: - - nombre: payroll_management - descripcion: Esquema para gesti贸n integral de n贸mina - tablas: - # RF-NOM-001: C谩lculo de N贸mina - - periodos_nomina - - nominas - - nominas_detalle - - nominas_percepciones - - nominas_deducciones - - nominas_provision_patronal - - dispersiones_bancarias - # RF-NOM-002: Timbrado CFDI - - cfdi_nomina - - cfdi_timbrado_log - - cfdi_cancelaciones - - nominas_extraordinarias - - pac_configuration - # RF-NOM-003: Deducciones y Percepciones - - conceptos_percepciones - - conceptos_deducciones - - formulas_calculo - - empleado_percepciones_fijas - - empleado_deducciones_fijas - - prestamos_empleados - - amortizaciones_prestamos - - tablas_isr - - pensiones_alimenticias - # RF-NOM-004: Finiquitos - - finiquitos - - finiquitos_detalle - - finiquitos_conceptos - - tipos_terminacion - - cfdi_finiquitos - # RF-NOM-005: Integraci贸n Checador - - asistencias - - registros_checador - - incidencias - - tipo_incidencia - - horarios_trabajo - - tolerancia_retardos - - horas_extra_calculadas - -# Pol铆ticas RLS -rls_policies: - archivo: ET-NOM-rls-policies.sql - descripcion: Pol铆ticas de seguridad a nivel de fila para multi-tenancy en n贸mina - tablas_protegidas: - - payroll_management.nominas - - payroll_management.cfdi_nomina - - payroll_management.asistencias - - payroll_management.finiquitos - - payroll_management.prestamos_empleados - reglas: - - Los empleados solo pueden ver sus propios recibos - - RH puede ver n贸mina de su constructora/tenant - - Contador puede acceder a informaci贸n fiscal - - Aislamiento total entre diferentes constructoras - -# Reportes -reportes: - - id: REP-NOM-001 - nombre: Recibo de N贸mina Individual - descripcion: Recibo detallado de n贸mina por empleado - parametros: [empleado_id, periodo_id] - - id: REP-NOM-002 - nombre: Resumen de N贸mina por Per铆odo - descripcion: Consolidado de n贸mina con totales por concepto - parametros: [periodo_id, obra_id] - - id: REP-NOM-003 - nombre: Dispersi贸n Bancaria - descripcion: Archivo de dispersi贸n en formato bancario - parametros: [periodo_id, formato_banco] - - id: REP-NOM-004 - nombre: Provisi贸n de Cuotas Patronales - descripcion: C谩lculo de IMSS, infonavit, SAR para provisiones - parametros: [periodo_id] - - id: REP-NOM-005 - nombre: Reporte de Tiempo Extra - descripcion: Detalle de horas extra por empleado y obra - parametros: [fecha_inicio, fecha_fin, obra_id] - - id: REP-NOM-006 - nombre: Hist贸rico de Pr茅stamos - descripcion: Pr茅stamos activos y liquidados con saldos - parametros: [empleado_id, estado] - - id: REP-NOM-007 - nombre: Reporte de Finiquitos - descripcion: Finiquitos procesados en per铆odo - parametros: [fecha_inicio, fecha_fin, tipo_terminacion] - - id: REP-NOM-008 - nombre: Reporte de Asistencias - descripcion: Asistencias, retardos y faltas por empleado - parametros: [fecha_inicio, fecha_fin, empleado_id, obra_id] - - id: REP-NOM-009 - nombre: An谩lisis de Puntualidad - descripcion: Indicadores de puntualidad por obra y departamento - parametros: [fecha_inicio, fecha_fin, obra_id] - - id: REP-NOM-010 - nombre: Costo de N贸mina por Obra - descripcion: Distribuci贸n de costo de n贸mina por proyecto - parametros: [periodo_id, obra_id] - -# Notificaciones -notificaciones: - - evento: nomina_calculada - destinatarios: [jefe_rh, contador] - canal: [email, app] - - evento: nomina_autorizada - destinatarios: [contador, empleados] - canal: [email, app] - - evento: cfdi_timbrado - destinatarios: [empleado] - canal: [email] - - evento: dispersion_generada - destinatarios: [contador, jefe_rh] - canal: [email] - - evento: prestamo_otorgado - destinatarios: [empleado, jefe_rh] - canal: [email, app] - - evento: finiquito_calculado - destinatarios: [empleado, jefe_rh, contador] - canal: [email] - - evento: incidencia_pendiente - destinatarios: [jefe_rh, residente_obra] - canal: [app, push_movil] - - evento: retardo_detectado - destinatarios: [empleado, supervisor] - canal: [app] - - evento: falta_registrada - destinatarios: [empleado, jefe_rh] - canal: [email, app] - - evento: tiempo_extra_aprobacion - destinatarios: [jefe_rh] - canal: [app] - -# M茅tricas y KPIs -metricas: - - nombre: tiempo_procesamiento_nomina - descripcion: Tiempo promedio para calcular y autorizar n贸mina - unidad: horas - objetivo: "< 4 horas" - - nombre: tasa_timbrado_exitoso - descripcion: Porcentaje de CFDIs timbrados sin error - unidad: porcentaje - objetivo: "> 99%" - - nombre: morosidad_prestamos - descripcion: Porcentaje de pr茅stamos con atrasos - unidad: porcentaje - objetivo: "< 5%" - - nombre: tasa_puntualidad - descripcion: Porcentaje de empleados sin retardos - unidad: porcentaje - objetivo: "> 85%" - - nombre: ausentismo - descripcion: Porcentaje de faltas injustificadas - unidad: porcentaje - objetivo: "< 3%" - - nombre: tiempo_extra_acumulado - descripcion: Promedio de horas extra por empleado - unidad: horas/mes - objetivo: "< 20 horas" - - nombre: costo_nomina_vs_presupuesto - descripcion: Desviaci贸n del costo de n贸mina vs presupuestado - unidad: porcentaje - objetivo: "< 5%" - -# Validaciones y Reglas de Negocio -reglas_negocio: - - codigo: RN-NOM-001 - descripcion: No se puede autorizar n贸mina sin validar totales - - codigo: RN-NOM-002 - descripcion: Los CFDIs deben timbrarse antes de dispersi贸n bancaria - - codigo: RN-NOM-003 - descripcion: El ISR se calcula seg煤n tablas vigentes del SAT - - codigo: RN-NOM-004 - descripcion: Los pr茅stamos no pueden exceder 4 meses de salario - - codigo: RN-NOM-005 - descripcion: La amortizaci贸n de pr茅stamo no puede exceder 30% del salario neto - - codigo: RN-NOM-006 - descripcion: Los finiquitos deben incluir vacaciones y aguinaldo proporcional - - codigo: RN-NOM-007 - descripcion: Las asistencias deben importarse desde MOB-001 para evitar manipulaci贸n - - codigo: RN-NOM-008 - descripcion: El tiempo extra doble aplica despu茅s de 9 horas o en d铆a de descanso - - codigo: RN-NOM-009 - descripcion: Las incidencias deben ser aprobadas por supervisor antes de c谩lculo - - codigo: RN-NOM-010 - descripcion: Los registros de checador con ubicaci贸n fuera de geofence requieren justificaci贸n - -# Seguridad -seguridad: - permisos: - - nomina.periodos.crear - - nomina.periodos.leer - - nomina.nomina.calcular - - nomina.nomina.validar - - nomina.nomina.autorizar - - nomina.nomina.dispersar - - nomina.cfdi.timbrar - - nomina.cfdi.cancelar - - nomina.cfdi.consultar - - nomina.conceptos.gestionar - - nomina.prestamos.crear - - nomina.prestamos.aprobar - - nomina.prestamos.consultar - - nomina.finiquitos.calcular - - nomina.finiquitos.autorizar - - nomina.asistencias.importar - - nomina.asistencias.consultar - - nomina.incidencias.gestionar - - nomina.incidencias.aprobar - - nomina.reportes.generar - - nomina.empleado.ver_propios_recibos - roles_sugeridos: - - nombre: jefe_rh - permisos: [nomina.*, asistencias.*, incidencias.*, prestamos.*, finiquitos.*] - - nombre: contador - permisos: [nomina.autorizar, nomina.dispersar, nomina.cfdi.*, nomina.reportes.*] - - nombre: empleado - permisos: [nomina.empleado.ver_propios_recibos, asistencias.consultar_propias] - - nombre: residente_obra - permisos: [incidencias.gestionar, asistencias.consultar] - - nombre: auditor - permisos: [nomina.reportes.generar, cfdi.consultar, finiquitos.consultar] - -# Pruebas -pruebas: - unitarias: - - calcular_isr_con_tablas_sat - - calcular_tiempo_extra_simple_doble - - calcular_aguinaldo_proporcional - - calcular_prima_antiguedad - - amortizar_prestamo_empleado - - validar_existencia_suficiente_para_prestamo - - aplicar_pension_alimenticia - - calcular_cuotas_imss_infonavit - integracion: - - flujo_completo_calculo_nomina - - importacion_asistencias_desde_mob001 - - timbrado_cfdi_con_pac - - generacion_dispersion_bancaria - - calculo_finiquito_completo - - sincronizacion_bidireccional_checador - e2e: - - proceso_nomina_completo_calculo_a_pago - - ciclo_vida_prestamo_empleado - - proceso_finiquito_con_timbrado - - registro_asistencia_app_a_nomina - -# Cronograma Estimado -cronograma: - - fase: An谩lisis y Dise帽o - duracion: 2 semanas - entregables: [modelos_datos, diagramas_flujo, especificaciones_api, integracion_pac] - - fase: Desarrollo Backend - RF-NOM-001 y RF-NOM-003 - duracion: 4 semanas - entregables: [api_calculo_nomina, api_conceptos, calculos_isr_imss, rls_policies] - - fase: Desarrollo Backend - RF-NOM-002 - duracion: 2 semanas - entregables: [integracion_pac, generacion_xml_cfdi, timbrado_automatico] - - fase: Desarrollo Backend - RF-NOM-004 y RF-NOM-005 - duracion: 3 semanas - entregables: [api_finiquitos, api_asistencias, integracion_mob001] - - fase: Desarrollo Frontend - duracion: 4 semanas - entregables: [ui_nomina, ui_cfdi, ui_prestamos, ui_finiquitos, ui_asistencias] - - fase: Desarrollo App M贸vil MOB-001 - duracion: 4 semanas - entregables: [app_checador, biometria, geofencing, sincronizacion] - - fase: Integraci贸n PAC y Pruebas - duracion: 2 semanas - entregables: [pruebas_timbrado, pruebas_integracion, validacion_sat] - - fase: Pruebas de Usuario y Ajustes - duracion: 2 semanas - entregables: [pruebas_usuario, ajustes_feedback, optimizaciones] - - fase: Documentaci贸n y Capacitaci贸n - duracion: 1 semana - entregables: [manual_usuario, manual_tecnico, videos_capacitacion] - -# Notas de Implementaci贸n -notas: - - Priorizar RF-NOM-001 (C谩lculo) y RF-NOM-005 (Checador) como base del m贸dulo - - Integraci贸n con PAC debe ser configurable para m煤ltiples proveedores - - Desarrollar MOB-001 (Checador Biom茅trico) en paralelo con backend de asistencias - - Implementar cach茅 de tablas ISR para optimizar c谩lculos - - Considerar migraci贸n de datos hist贸ricos de sistema legacy - - Planificar sincronizaci贸n offline-online robusta para MOB-001 - - Validar cumplimiento normativo SAT antes de producci贸n - - Implementar auditor铆a completa de todas las operaciones de n贸mina - - Considerar integraci贸n futura con bancos para confirmaci贸n de dispersi贸n - - Evaluar uso de firma electr贸nica para autorizaci贸n de n贸mina - - El m贸dulo MOB-001 es cr铆tico para la integridad de datos de asistencias - - Implementar geofencing estricto para evitar registros fraudulentos - - Considerar funcionalidad de n贸mina retroactiva para ajustes diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-012-compras/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-012-compras/implementacion/TRACEABILITY.yml deleted file mode 100644 index b1346eed8..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-012-compras/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,640 +0,0 @@ -# TRACEABILITY - MAE-012: Compras -metadata: - modulo: MAE-012 - nombre: Compras - tipo: Maestro - version: 1.0.0 - fecha: 2025-12-06 - reutilizacion_core: 85% - descripcion: Gesti贸n integral del proceso de compras incluyendo requisiciones, cotizaciones, 贸rdenes de compra y recepci贸n de materiales - dependencias: - - MGN-001 # Usuarios y Autenticaci贸n - - MGN-002 # RBAC - - MGN-003 # Multi-tenancy - - MGN-005 # Cat谩logos - - MAE-013 # Inventarios - apps_moviles: - - MOB-002 # Almacenista (para recepci贸n en campo) - -# ============================================================================ -# REQUERIMIENTOS FUNCIONALES -# ============================================================================ - -requerimientos: - - id: RF-COMP-001 - titulo: Requisiciones de Compra - descripcion: Gesti贸n de solicitudes de compra de materiales y servicios - prioridad: alta - especificaciones: - - id: ESP-COMP-001-01 - descripcion: Crear requisiciones de compra con m煤ltiples partidas - - id: ESP-COMP-001-02 - descripcion: Definir proveedor sugerido, cantidad, unidad y fecha requerida - - id: ESP-COMP-001-03 - descripcion: Flujo de aprobaci贸n multinivel configurable - - id: ESP-COMP-001-04 - descripcion: Estados (borrador, enviada, aprobada, rechazada, convertida, cancelada) - - id: ESP-COMP-001-05 - descripcion: Conversi贸n de requisiciones aprobadas a 贸rdenes de compra - - id: ESP-COMP-001-06 - descripcion: Vinculaci贸n con proyectos y centros de costo - historias_usuario: - - id: US-COMP-001-01 - titulo: Como usuario autorizado necesito crear requisiciones de compra - criterios_aceptacion: - - Seleccionar tipo de requisici贸n (material, servicio, activo) - - Agregar partidas con descripci贸n, cantidad y especificaciones - - Definir proveedor sugerido (opcional) - - Establecer fecha requerida y prioridad - - Adjuntar documentos de soporte - - Guardar como borrador o enviar a aprobaci贸n - - id: US-COMP-001-02 - titulo: Como aprobador necesito revisar y aprobar/rechazar requisiciones - criterios_aceptacion: - - Ver lista de requisiciones pendientes de mi aprobaci贸n - - Revisar detalle completo de la requisici贸n - - Aprobar, rechazar o solicitar modificaciones - - Agregar comentarios y observaciones - - Recibir notificaci贸n de nuevas requisiciones - - id: US-COMP-001-03 - titulo: Como comprador necesito consultar requisiciones aprobadas - criterios_aceptacion: - - Ver requisiciones aprobadas pendientes de proceso - - Filtrar por fecha, solicitante, tipo, prioridad - - Agrupar requisiciones para 贸rdenes consolidadas - - Exportar listados para an谩lisis - tablas: - - purchasing_management.requisiciones - - purchasing_management.requisiciones_detalle - - purchasing_management.requisiciones_aprobaciones - - purchasing_management.requisiciones_documentos - endpoints: - - POST /api/v1/requisiciones - - GET /api/v1/requisiciones - - GET /api/v1/requisiciones/:id - - PUT /api/v1/requisiciones/:id - - DELETE /api/v1/requisiciones/:id - - POST /api/v1/requisiciones/:id/enviar - - POST /api/v1/requisiciones/:id/aprobar - - POST /api/v1/requisiciones/:id/rechazar - - POST /api/v1/requisiciones/:id/cancelar - - GET /api/v1/requisiciones/pendientes-aprobacion - componentes_ui: - - RequisicionesListView - - RequisicionFormModal - - RequisicionDetailView - - RequisicionApprovalPanel - - RequisicionItemsTable - - RequisicionWorkflowTimeline - - RequisicionAttachmentsPanel - integraciones: - - modulo: MGN-005 - descripcion: Cat谩logo de proveedores, materiales y servicios - - modulo: MAE-013 - descripcion: Validaci贸n de existencias antes de requisicionar - estado: pendiente - - - id: RF-COMP-002 - titulo: 脫rdenes de Compra - descripcion: Gesti贸n completa del ciclo de vida de 贸rdenes de compra - prioridad: alta - especificaciones: - - id: ESP-COMP-002-01 - descripcion: Crear 贸rdenes de compra manual o desde requisiciones - - id: ESP-COMP-002-02 - descripcion: Definir condiciones comerciales (precio, descuentos, plazo, forma de pago) - - id: ESP-COMP-002-03 - descripcion: Generar folio consecutivo autom谩tico por ejercicio - - id: ESP-COMP-002-04 - descripcion: C谩lculo autom谩tico de subtotales, impuestos y total - - id: ESP-COMP-002-05 - descripcion: Estados (borrador, enviada, confirmada, recibida parcial, recibida total, cancelada) - - id: ESP-COMP-002-06 - descripcion: Generaci贸n de documento PDF con formato personalizable - - id: ESP-COMP-002-07 - descripcion: Env铆o autom谩tico por email al proveedor - - id: ESP-COMP-002-08 - descripcion: Control de recepciones parciales y totales - historias_usuario: - - id: US-COMP-002-01 - titulo: Como comprador necesito crear 贸rdenes de compra - criterios_aceptacion: - - Seleccionar proveedor del cat谩logo - - Agregar partidas manualmente o desde requisiciones - - Definir precio unitario, descuentos y condiciones - - Calcular impuestos seg煤n configuraci贸n fiscal - - Especificar condiciones de pago y entrega - - Agregar notas y observaciones - - Guardar como borrador o enviar - - id: US-COMP-002-02 - titulo: Como comprador necesito enviar 贸rdenes de compra a proveedores - criterios_aceptacion: - - Generar PDF con formato corporativo - - Previsualizar documento antes de enviar - - Enviar por email al proveedor autom谩ticamente - - Registrar fecha y hora de env铆o - - Reenviar si es necesario - - id: US-COMP-002-03 - titulo: Como comprador necesito dar seguimiento a 贸rdenes de compra - criterios_aceptacion: - - Ver dashboard con estado de todas las 贸rdenes - - Consultar 贸rdenes pendientes de recepci贸n - - Identificar 贸rdenes con atraso en entrega - - Generar reportes de 贸rdenes por per铆odo - - Filtrar por proveedor, estado, proyecto - tablas: - - purchasing_management.ordenes_compra - - purchasing_management.ordenes_compra_detalle - - purchasing_management.ordenes_compra_condiciones - - purchasing_management.ordenes_compra_documentos - - purchasing_management.ordenes_compra_seguimiento - endpoints: - - POST /api/v1/ordenes-compra - - GET /api/v1/ordenes-compra - - GET /api/v1/ordenes-compra/:id - - PUT /api/v1/ordenes-compra/:id - - DELETE /api/v1/ordenes-compra/:id - - POST /api/v1/ordenes-compra/:id/enviar - - POST /api/v1/ordenes-compra/:id/confirmar - - POST /api/v1/ordenes-compra/:id/cancelar - - GET /api/v1/ordenes-compra/:id/pdf - - POST /api/v1/ordenes-compra/:id/enviar-email - - GET /api/v1/ordenes-compra/pendientes-recepcion - componentes_ui: - - OrdenesCompraListView - - OrdenCompraFormModal - - OrdenCompraDetailView - - OrdenCompraItemsTable - - OrdenCompraPDFPreview - - OrdenCompraStatusBadge - - OrdenCompraTimelineView - - OrdenCompraEmailModal - integraciones: - - modulo: RF-COMP-001 - descripcion: Generaci贸n desde requisiciones aprobadas - - modulo: RF-COMP-004 - descripcion: Vinculaci贸n con recepciones de material - - modulo: MGN-005 - descripcion: Cat谩logo de proveedores y productos - estado: pendiente - - - id: RF-COMP-003 - titulo: Cotizaciones - descripcion: Gesti贸n del proceso de solicitud y comparaci贸n de cotizaciones - prioridad: media - especificaciones: - - id: ESP-COMP-003-01 - descripcion: Crear solicitudes de cotizaci贸n a m煤ltiples proveedores - - id: ESP-COMP-003-02 - descripcion: Definir partidas a cotizar con especificaciones t茅cnicas - - id: ESP-COMP-003-03 - descripcion: Establecer fecha l铆mite de recepci贸n de cotizaciones - - id: ESP-COMP-003-04 - descripcion: Registrar cotizaciones recibidas de proveedores - - id: ESP-COMP-003-05 - descripcion: Comparar cotizaciones en tabla comparativa - - id: ESP-COMP-003-06 - descripcion: Seleccionar proveedor ganador y generar orden de compra - - id: ESP-COMP-003-07 - descripcion: Estados (borrador, enviada, recibiendo, evaluaci贸n, adjudicada, cancelada) - historias_usuario: - - id: US-COMP-003-01 - titulo: Como comprador necesito solicitar cotizaciones a proveedores - criterios_aceptacion: - - Crear solicitud con partidas a cotizar - - Definir especificaciones t茅cnicas requeridas - - Seleccionar proveedores a invitar - - Establecer fecha l铆mite de respuesta - - Generar y enviar documento de solicitud - - id: US-COMP-003-02 - titulo: Como comprador necesito registrar cotizaciones recibidas - criterios_aceptacion: - - Capturar cotizaci贸n recibida por cada proveedor - - Registrar precios, plazos y condiciones - - Adjuntar documento de cotizaci贸n (PDF) - - Marcar cotizaci贸n como recibida - - Notificar recepci贸n al solicitante - - id: US-COMP-003-03 - titulo: Como comprador necesito comparar cotizaciones - criterios_aceptacion: - - Ver tabla comparativa de todas las cotizaciones - - Comparar precios unitarios y totales - - Evaluar condiciones de pago y entrega - - Identificar mejor opci贸n por partida - - Generar cuadro comparativo para aprobaci贸n - - Seleccionar proveedor ganador - - Convertir a orden de compra - tablas: - - purchasing_management.solicitudes_cotizacion - - purchasing_management.solicitudes_cotizacion_detalle - - purchasing_management.solicitudes_cotizacion_proveedores - - purchasing_management.cotizaciones - - purchasing_management.cotizaciones_detalle - - purchasing_management.comparativo_cotizaciones - endpoints: - - POST /api/v1/cotizaciones/solicitudes - - GET /api/v1/cotizaciones/solicitudes - - GET /api/v1/cotizaciones/solicitudes/:id - - PUT /api/v1/cotizaciones/solicitudes/:id - - POST /api/v1/cotizaciones/solicitudes/:id/enviar - - POST /api/v1/cotizaciones - - GET /api/v1/cotizaciones - - GET /api/v1/cotizaciones/:id - - PUT /api/v1/cotizaciones/:id - - GET /api/v1/cotizaciones/comparativo/:solicitud_id - - POST /api/v1/cotizaciones/:id/adjudicar - componentes_ui: - - SolicitudCotizacionListView - - SolicitudCotizacionFormModal - - SolicitudCotizacionDetailView - - CotizacionFormModal - - CotizacionDetailView - - ComparativoCotizacionesTable - - CotizacionAdjudicacionModal - integraciones: - - modulo: RF-COMP-001 - descripcion: Generaci贸n desde requisiciones - - modulo: RF-COMP-002 - descripcion: Conversi贸n a 贸rdenes de compra - - modulo: MGN-005 - descripcion: Cat谩logo de proveedores - estado: pendiente - - - id: RF-COMP-004 - titulo: Recepci贸n de Materiales - descripcion: Registro y control de recepci贸n de materiales en almac茅n y campo - prioridad: alta - especificaciones: - - id: ESP-COMP-004-01 - descripcion: Registrar recepci贸n completa o parcial de 贸rdenes de compra - - id: ESP-COMP-004-02 - descripcion: Validar cantidad recibida vs cantidad ordenada - - id: ESP-COMP-004-03 - descripcion: Registrar diferencias, faltantes o da帽os - - id: ESP-COMP-004-04 - descripcion: Captura de informaci贸n de lote, serie, caducidad - - id: ESP-COMP-004-05 - descripcion: Generaci贸n autom谩tica de entrada a inventario - - id: ESP-COMP-004-06 - descripcion: Firma digital del almacenista receptor - - id: ESP-COMP-004-07 - descripcion: Modo offline en app m贸vil con sincronizaci贸n posterior - - id: ESP-COMP-004-08 - descripcion: Actualizaci贸n autom谩tica del estado de la orden de compra - historias_usuario: - - id: US-COMP-004-01 - titulo: Como almacenista necesito registrar recepciones desde escritorio - criterios_aceptacion: - - Ver lista de 贸rdenes pendientes de recepci贸n - - Seleccionar orden de compra a recibir - - Registrar cantidad recibida por partida - - Capturar informaci贸n de lote/serie si aplica - - Documentar diferencias o da帽os - - Generar comprobante de recepci贸n - - Actualizar autom谩ticamente inventario - - id: US-COMP-004-02 - titulo: Como almacenista necesito registrar recepciones desde app m贸vil (MOB-002) - criterios_aceptacion: - - Consultar 贸rdenes pendientes en dispositivo m贸vil - - Escanear c贸digo de barras de productos - - Registrar cantidad recibida con teclado num茅rico - - Capturar fotos de material recibido - - Registrar firma digital del proveedor - - Documentar incidencias (faltantes, da帽os) - - Trabajar en modo offline si no hay conexi贸n - - Sincronizar recepciones al recuperar conexi贸n - - Recibir confirmaci贸n de registro exitoso - - id: US-COMP-004-03 - titulo: Como comprador necesito consultar recepciones realizadas - criterios_aceptacion: - - Ver historial de recepciones por orden - - Identificar recepciones parciales pendientes - - Consultar diferencias reportadas - - Generar reporte de materiales recibidos - - Dar seguimiento a incidencias - tablas: - - purchasing_management.recepciones - - purchasing_management.recepciones_detalle - - purchasing_management.recepciones_incidencias - - purchasing_management.recepciones_documentos - endpoints: - - POST /api/v1/recepciones - - GET /api/v1/recepciones - - GET /api/v1/recepciones/:id - - PUT /api/v1/recepciones/:id - - GET /api/v1/recepciones/orden-compra/:oc_id - - POST /api/v1/recepciones/:id/incidencia - - POST /api/v1/recepciones/:id/firmar - - POST /api/v1/recepciones/sync # Para sincronizaci贸n m贸vil - componentes_ui: - - RecepcionesListView - - RecepcionFormModal - - RecepcionDetailView - - RecepcionItemsTable - - RecepcionIncidenciasPanel - - RecepcionSignaturePad - - RecepcionPhotoGallery - app_movil: MOB-002 - funcionalidades_movil: - - Consultar 贸rdenes pendientes de recepci贸n - - Escanear c贸digos de barras/QR - - Registrar cantidades recibidas - - Capturar fotos de materiales - - Registrar firma digital del proveedor - - Documentar incidencias (faltantes, da帽os) - - Modo offline con almacenamiento local - - Sincronizaci贸n autom谩tica en background - - Notificaciones push de nuevas 贸rdenes - integraciones: - - modulo: RF-COMP-002 - descripcion: Actualizaci贸n de estado de 贸rdenes de compra - - modulo: MAE-013 - descripcion: Generaci贸n de entradas a inventario - - modulo: MOB-002 - descripcion: Registro de recepciones en campo - estado: pendiente - -# ============================================================================ -# APLICACI脫N M脫VIL -# ============================================================================ - -app_movil: - id: MOB-002 - nombre: Almacenista - descripcion: Aplicaci贸n m贸vil para almacenistas en campo y almac茅n - plataformas: - - Android - - iOS - tecnologias: - - React Native - - Expo - - SQLite (offline storage) - - Axios (API sync) - - React Native Camera - - React Native Signature Capture - funcionalidades: - - login_offline: Autenticaci贸n con modo offline - - consulta_ordenes: Consulta de 贸rdenes pendientes de recepci贸n - - escaneo_codigos: Escaneo de c贸digos de barras y QR - - registro_recepciones: Registro de recepciones con cantidades - - captura_fotos: Captura de fotograf铆as de materiales - - firma_digital: Captura de firma del proveedor - - documentar_incidencias: Registro de faltantes y da帽os - - sincronizacion: Sincronizaci贸n autom谩tica con servidor - - notificaciones_push: Notificaciones de nuevas 贸rdenes - - modo_offline: Funcionamiento sin conexi贸n - requerimientos_vinculados: - - RF-COMP-002 # Consulta de 贸rdenes de compra - - RF-COMP-004 # Recepci贸n de materiales - estado: pendiente - -# ============================================================================ -# ESQUEMAS DE BASE DE DATOS -# ============================================================================ - -esquemas: - - nombre: purchasing_management - descripcion: Esquema para gesti贸n de compras - tablas: - - requisiciones - - requisiciones_detalle - - requisiciones_aprobaciones - - requisiciones_documentos - - ordenes_compra - - ordenes_compra_detalle - - ordenes_compra_condiciones - - ordenes_compra_documentos - - ordenes_compra_seguimiento - - solicitudes_cotizacion - - solicitudes_cotizacion_detalle - - solicitudes_cotizacion_proveedores - - cotizaciones - - cotizaciones_detalle - - comparativo_cotizaciones - - recepciones - - recepciones_detalle - - recepciones_incidencias - - recepciones_documentos - -# ============================================================================ -# POL脥TICAS RLS -# ============================================================================ - -rls_policies: - archivo: ET-COMP-rls-policies.sql - descripcion: Pol铆ticas de seguridad a nivel de fila para multi-tenancy - tablas_protegidas: - - purchasing_management.requisiciones - - purchasing_management.ordenes_compra - - purchasing_management.cotizaciones - - purchasing_management.recepciones - -# ============================================================================ -# REPORTES -# ============================================================================ - -reportes: - - id: REP-COMP-001 - nombre: Requisiciones por Estado - descripcion: Listado de requisiciones agrupadas por estado - parametros: [fecha_inicio, fecha_fin, estado, solicitante_id] - - id: REP-COMP-002 - nombre: 脫rdenes de Compra Emitidas - descripcion: Detalle de 贸rdenes de compra emitidas por per铆odo - parametros: [fecha_inicio, fecha_fin, proveedor_id, proyecto_id] - - id: REP-COMP-003 - nombre: 脫rdenes Pendientes de Recepci贸n - descripcion: 脫rdenes con recepciones parciales o pendientes - parametros: [dias_atraso, proveedor_id, almacen_id] - - id: REP-COMP-004 - nombre: Comparativo de Cotizaciones - descripcion: Cuadro comparativo de cotizaciones recibidas - parametros: [solicitud_cotizacion_id] - - id: REP-COMP-005 - nombre: Incidencias en Recepciones - descripcion: Reporte de faltantes y da帽os en recepciones - parametros: [fecha_inicio, fecha_fin, tipo_incidencia, proveedor_id] - - id: REP-COMP-006 - nombre: An谩lisis de Compras por Proveedor - descripcion: Estad铆sticas de compras realizadas por proveedor - parametros: [fecha_inicio, fecha_fin, proveedor_id] - -# ============================================================================ -# NOTIFICACIONES -# ============================================================================ - -notificaciones: - - evento: requisicion_enviada - destinatarios: [aprobadores, jefe_compras] - canal: [email, app] - - evento: requisicion_aprobada - destinatarios: [solicitante, comprador_asignado] - canal: [email, app] - - evento: requisicion_rechazada - destinatarios: [solicitante] - canal: [email, app] - - evento: orden_compra_creada - destinatarios: [proveedor, solicitante] - canal: [email] - - evento: orden_compra_enviada - destinatarios: [proveedor, almacenista] - canal: [email] - - evento: orden_pendiente_recepcion - destinatarios: [almacenista] - canal: [app, push_movil] - - evento: recepcion_registrada - destinatarios: [comprador, solicitante] - canal: [app] - - evento: incidencia_recepcion - destinatarios: [comprador, jefe_almacen] - canal: [email, app] - - evento: cotizacion_venciendo - destinatarios: [comprador] - canal: [email, app] - -# ============================================================================ -# M脡TRICAS Y KPIs -# ============================================================================ - -metricas: - - nombre: tiempo_aprobacion_requisiciones - descripcion: Tiempo promedio de aprobaci贸n de requisiciones - unidad: horas - objetivo: "< 24 horas" - - nombre: tasa_rechazo_requisiciones - descripcion: Porcentaje de requisiciones rechazadas - unidad: porcentaje - objetivo: "< 10%" - - nombre: cumplimiento_entregas - descripcion: Porcentaje de 贸rdenes entregadas a tiempo - unidad: porcentaje - objetivo: "> 90%" - - nombre: ahorro_por_cotizaciones - descripcion: Ahorro obtenido por proceso de cotizaci贸n - unidad: porcentaje - objetivo: "> 8%" - - nombre: incidencias_recepcion - descripcion: Porcentaje de recepciones con incidencias - unidad: porcentaje - objetivo: "< 5%" - - nombre: tiempo_registro_recepcion - descripcion: Tiempo promedio de registro de recepci贸n - unidad: minutos - objetivo: "< 15 minutos" - -# ============================================================================ -# VALIDACIONES Y REGLAS DE NEGOCIO -# ============================================================================ - -reglas_negocio: - - codigo: RN-COMP-001 - descripcion: Toda requisici贸n debe tener al menos un aprobador asignado - - codigo: RN-COMP-002 - descripcion: No se puede crear orden de compra sin requisici贸n aprobada o autorizaci贸n especial - - codigo: RN-COMP-003 - descripcion: El total de la orden de compra debe calcularse con impuestos seg煤n configuraci贸n fiscal - - codigo: RN-COMP-004 - descripcion: Las cotizaciones deben tener al menos 3 proveedores para compras mayores a monto configurado - - codigo: RN-COMP-005 - descripcion: La cantidad recibida puede ser menor o igual a la ordenada, nunca mayor - - codigo: RN-COMP-006 - descripcion: Toda recepci贸n con incidencias debe generar notificaci贸n al comprador - - codigo: RN-COMP-007 - descripcion: Las recepciones parciales deben actualizar el estado de la orden a "recibida parcial" - -# ============================================================================ -# SEGURIDAD -# ============================================================================ - -seguridad: - permisos: - - compras.requisiciones.crear - - compras.requisiciones.leer - - compras.requisiciones.editar - - compras.requisiciones.eliminar - - compras.requisiciones.aprobar - - compras.ordenes.crear - - compras.ordenes.leer - - compras.ordenes.editar - - compras.ordenes.eliminar - - compras.ordenes.enviar - - compras.cotizaciones.crear - - compras.cotizaciones.leer - - compras.cotizaciones.evaluar - - compras.recepciones.crear - - compras.recepciones.leer - - compras.recepciones.firmar - - compras.reportes.generar - roles_sugeridos: - - nombre: solicitante - permisos: [compras.requisiciones.crear, compras.requisiciones.leer] - - nombre: aprobador - permisos: [compras.requisiciones.leer, compras.requisiciones.aprobar] - - nombre: comprador - permisos: [compras.requisiciones.*, compras.ordenes.*, compras.cotizaciones.*] - - nombre: almacenista - permisos: [compras.ordenes.leer, compras.recepciones.*] - - nombre: jefe_compras - permisos: [compras.*] - -# ============================================================================ -# PRUEBAS -# ============================================================================ - -pruebas: - unitarias: - - validar_flujo_aprobacion_requisiciones - - calcular_totales_orden_compra - - validar_cantidad_recibida_vs_ordenada - - procesar_sincronizacion_movil - integracion: - - flujo_completo_requisicion_a_orden - - proceso_cotizacion_y_adjudicacion - - recepcion_y_actualizacion_inventario - - sincronizacion_app_movil_offline - e2e: - - proceso_completo_compra_material - - aprobacion_multinivel_requisiciones - - recepcion_movil_con_incidencias - - comparativo_cotizaciones_y_adjudicacion - -# ============================================================================ -# CRONOGRAMA ESTIMADO -# ============================================================================ - -cronograma: - - fase: An谩lisis y Dise帽o - duracion: 2 semanas - entregables: [modelos_datos, diagramas_flujo, especificaciones_api] - - fase: Desarrollo Backend - duracion: 5 semanas - entregables: [api_requisiciones, api_ordenes, api_cotizaciones, api_recepciones, rls_policies] - - fase: Desarrollo Frontend - duracion: 4 semanas - entregables: [ui_requisiciones, ui_ordenes, ui_cotizaciones, ui_recepciones] - - fase: Desarrollo App M贸vil - duracion: 3 semanas - entregables: [app_almacenista_mob002, modo_offline, sincronizacion] - - fase: Integraci贸n - duracion: 2 semanas - entregables: [integracion_inventarios, integracion_catalogos] - - fase: Pruebas - duracion: 2 semanas - entregables: [pruebas_unitarias, pruebas_integracion, pruebas_usuario, pruebas_movil] - - fase: Documentaci贸n y Capacitaci贸n - duracion: 1 semana - entregables: [manual_usuario, manual_tecnico, manual_app_movil, videos_capacitacion] - -# ============================================================================ -# NOTAS DE IMPLEMENTACI脫N -# ============================================================================ - -notas: - - Priorizar desarrollo de RF-COMP-001 y RF-COMP-002 (flujo b谩sico de compras) - - Desarrollar app m贸vil MOB-002 en paralelo con RF-COMP-004 (recepciones) - - Implementar sincronizaci贸n offline-online robusta para app m贸vil - - Considerar integraci贸n futura con proveedores v铆a EDI o API - - Evaluar uso de c贸digos QR para trazabilidad de materiales - - La app MOB-002 es cr铆tica para recepci贸n en campo, priorizar su estabilidad - - Documentar protocolo de resoluci贸n de conflictos en sincronizaci贸n offline - - Implementar versionado de recepciones para auditor铆a completa diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-013-inventarios/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-013-inventarios/implementacion/TRACEABILITY.yml deleted file mode 100644 index ba335a958..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-013-inventarios/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,757 +0,0 @@ -# TRACEABILITY - MAE-013: Inventarios -metadata: - modulo: MAE-013 - nombre: Inventarios - version: 1.0.0 - fecha: 2025-12-06 - reutilizacion_core: 75% - descripcion: Sistema integral de gesti贸n de inventarios para construcci贸n con control de almacenes, movimientos, conteos f铆sicos y traspasos entre obras - dependencias: - - MGN-001 # Usuarios y Autenticaci贸n - - MGN-002 # RBAC - - MGN-003 # Multi-tenancy - - MGN-005 # Cat谩logos - - MAE-012 # Compras (para recepciones) - apps_moviles: - - MOB-002 # Almacenista - -requerimientos: - - id: RF-INV-001 - titulo: Almacenes y Ubicaciones - descripcion: Gesti贸n de almacenes con ubicaciones f铆sicas y control de responsables - prioridad: alta - especificaciones: - - id: ESP-INV-001-01 - descripcion: Crear y configurar almacenes por obra o sede central - - id: ESP-INV-001-02 - descripcion: Definir estructura de ubicaciones (zonas, pasillos, estantes, niveles) - - id: ESP-INV-001-03 - descripcion: Asignar responsables y almacenistas por almac茅n - - id: ESP-INV-001-04 - descripcion: Clasificar almacenes por tipo (general, herramientas, peligrosos, temporal) - - id: ESP-INV-001-05 - descripcion: Establecer niveles m铆nimos y m谩ximos de inventario por material - - id: ESP-INV-001-06 - descripcion: Control de capacidad y espacio disponible - - id: ESP-INV-001-07 - descripcion: Alertas autom谩ticas de existencias bajas o sobre-stock - historias_usuario: - - id: US-INV-001-01 - titulo: Como jefe de obra necesito crear almacenes en el proyecto - criterios_aceptacion: - - Registrar almac茅n con nombre, ubicaci贸n f铆sica y tipo - - Asignar almacenista responsable del cat谩logo de usuarios - - Definir horarios de operaci贸n y pol铆ticas de acceso - - Activar/desactivar almacenes seg煤n necesidad - - id: US-INV-001-02 - titulo: Como almacenista necesito organizar ubicaciones dentro del almac茅n - criterios_aceptacion: - - Crear jerarqu铆a de ubicaciones (zona > pasillo > estante > nivel) - - Asignar c贸digo 煤nico a cada ubicaci贸n - - Marcar capacidad y dimensiones de ubicaciones - - Generar mapa visual del almac茅n - - id: US-INV-001-03 - titulo: Como jefe de compras necesito establecer niveles de inventario - criterios_aceptacion: - - Definir nivel m铆nimo por material y almac茅n - - Definir nivel m谩ximo para control de sobre-stock - - Configurar punto de reorden autom谩tico - - Recibir alertas cuando se alcancen los niveles cr铆ticos - - id: US-INV-001-04 - titulo: Como residente necesito consultar disponibilidad en almacenes - criterios_aceptacion: - - Ver listado de almacenes de la obra - - Consultar existencias actuales por almac茅n - - Identificar ubicaci贸n f铆sica de materiales - - Verificar responsable de cada almac茅n - tablas: - - inventory_management.almacenes - - inventory_management.tipos_almacen - - inventory_management.ubicaciones - - inventory_management.niveles_inventario - - inventory_management.almacenes_responsables - endpoints: - - POST /api/v1/almacenes - - GET /api/v1/almacenes - - GET /api/v1/almacenes/:id - - PUT /api/v1/almacenes/:id - - DELETE /api/v1/almacenes/:id - - GET /api/v1/almacenes/obra/:obra_id - - POST /api/v1/almacenes/:id/ubicaciones - - GET /api/v1/almacenes/:id/ubicaciones - - PUT /api/v1/almacenes/:id/ubicaciones/:ubicacion_id - - DELETE /api/v1/almacenes/:id/ubicaciones/:ubicacion_id - - POST /api/v1/almacenes/:id/niveles-inventario - - GET /api/v1/almacenes/:id/alertas-stock - - GET /api/v1/almacenes/:id/capacidad - componentes_ui: - - AlmacenesListView - - AlmacenFormModal - - AlmacenDetailView - - UbicacionesTreeView - - UbicacionFormModal - - NivelesInventarioTable - - AlertasStockPanel - - AlmacenResponsableSelector - - MapaAlmacenView - app_movil: MOB-002 - integraciones: - - modulo: MGN-005 - descripcion: Cat谩logo de materiales y unidades de medida - - modulo: MGN-001 - descripcion: Asignaci贸n de almacenistas y responsables - - modulo: MAI-001 - descripcion: Vinculaci贸n con obras y proyectos - estado: pendiente - - - id: RF-INV-002 - titulo: Movimientos de Inventario - descripcion: Registro y control de entradas, salidas y ajustes de inventario - prioridad: alta - especificaciones: - - id: ESP-INV-002-01 - descripcion: Registrar entradas de inventario desde 贸rdenes de compra - - id: ESP-INV-002-02 - descripcion: Registrar salidas de inventario para consumo en obra - - id: ESP-INV-002-03 - descripcion: Procesar devoluciones de material (proveedor y obra) - - id: ESP-INV-002-04 - descripcion: Realizar ajustes de inventario (mermas, faltantes, sobrantes) - - id: ESP-INV-002-05 - descripcion: Trazabilidad completa de movimientos por lote y n煤mero de serie - - id: ESP-INV-002-06 - descripcion: Validaci贸n autom谩tica de existencias antes de salidas - - id: ESP-INV-002-07 - descripcion: C谩lculo de costos por m茅todo PEPS o promedio ponderado - - id: ESP-INV-002-08 - descripcion: Generaci贸n de documentos de entrada/salida con firma digital - historias_usuario: - - id: US-INV-002-01 - titulo: Como almacenista necesito registrar entradas de material desde app m贸vil - criterios_aceptacion: - - Seleccionar orden de compra o entrada manual - - Escanear c贸digo de barras/QR del material - - Registrar cantidad, lote y fecha de caducidad - - Asignar ubicaci贸n f铆sica en almac茅n - - Capturar fotos del material recibido - - Generar comprobante de entrada con firma - - id: US-INV-002-02 - titulo: Como residente necesito solicitar salidas de material - criterios_aceptacion: - - Seleccionar materiales del inventario disponible - - Especificar cantidad y partida presupuestal destino - - Indicar ubicaci贸n de entrega en obra - - Enviar solicitud a almacenista para aprobaci贸n - - Recibir notificaci贸n de aprobaci贸n/rechazo - - id: US-INV-002-03 - titulo: Como almacenista necesito procesar salidas de material en campo - criterios_aceptacion: - - Ver solicitudes pendientes en app m贸vil - - Verificar existencias disponibles en tiempo real - - Aprobar o rechazar solicitud con comentarios - - Registrar entrega con firma digital del receptor - - Actualizar inventario autom谩ticamente - - Generar vale de salida en PDF - - id: US-INV-002-04 - titulo: Como jefe de almac茅n necesito realizar ajustes de inventario - criterios_aceptacion: - - Registrar tipo de ajuste (merma, faltante, sobrante, correcci贸n) - - Especificar material, cantidad y ubicaci贸n - - Documentar motivo y evidencia del ajuste - - Requerir aprobaci贸n para ajustes mayores - - Actualizar kardex con el movimiento - - id: US-INV-002-05 - titulo: Como contador necesito consultar valuaci贸n de inventario - criterios_aceptacion: - - Ver valor total de inventario por almac茅n - - Consultar costo promedio por material - - Generar reporte de kardex detallado - - Exportar movimientos para contabilidad - - Comparar valor contable vs f铆sico - tablas: - - inventory_management.movimientos_inventario - - inventory_management.entradas_inventario - - inventory_management.salidas_inventario - - inventory_management.ajustes_inventario - - inventory_management.solicitudes_salida - - inventory_management.kardex - - inventory_management.lotes - - inventory_management.existencias_por_ubicacion - endpoints: - - POST /api/v1/inventario/entradas - - GET /api/v1/inventario/entradas - - GET /api/v1/inventario/entradas/:id - - POST /api/v1/inventario/salidas - - GET /api/v1/inventario/salidas - - GET /api/v1/inventario/salidas/:id - - POST /api/v1/inventario/solicitudes-salida - - GET /api/v1/inventario/solicitudes-salida - - POST /api/v1/inventario/solicitudes-salida/:id/aprobar - - POST /api/v1/inventario/solicitudes-salida/:id/rechazar - - POST /api/v1/inventario/ajustes - - GET /api/v1/inventario/ajustes - - GET /api/v1/inventario/movimientos - - GET /api/v1/inventario/kardex/:material_id - - GET /api/v1/inventario/existencias - - GET /api/v1/inventario/existencias/:material_id/ubicaciones - - GET /api/v1/inventario/valuacion - - GET /api/v1/inventario/lotes/:material_id - componentes_ui: - - MovimientosInventarioListView - - EntradaInventarioFormModal - - SalidaInventarioFormModal - - SolicitudSalidaFormModal - - SolicitudesSalidaPanel - - AjusteInventarioFormModal - - KardexMaterialView - - ValuacionInventarioReport - - MovimientoDetailView - - LotesTable - - ExistenciasPorUbicacionView - - FirmaDigitalCapture - app_movil: MOB-002 - funcionalidades_movil: - - Consultar existencias en tiempo real - - Escanear c贸digos de barras/QR - - Registrar entradas con captura de fotos - - Procesar solicitudes de salida - - Captura de firma digital - - Modo offline con sincronizaci贸n - - Geolocalizaci贸n de entregas - - Historial de movimientos - integraciones: - - modulo: MAE-012 - descripcion: Entradas desde 贸rdenes de compra - - modulo: RF-INV-001 - descripcion: Actualizaci贸n de existencias por ubicaci贸n - - modulo: MGN-005 - descripcion: Cat谩logo de materiales y unidades - estado: pendiente - - - id: RF-INV-003 - titulo: Conteos F铆sicos - descripcion: Gesti贸n de inventarios f铆sicos c铆clicos y anuales con cuadre de diferencias - prioridad: media - especificaciones: - - id: ESP-INV-003-01 - descripcion: Programar conteos f铆sicos c铆clicos por almac茅n o categor铆a - - id: ESP-INV-003-02 - descripcion: Generar hojas de conteo con existencias del sistema - - id: ESP-INV-003-03 - descripcion: Captura de conteo f铆sico desde app m贸vil - - id: ESP-INV-003-04 - descripcion: Comparaci贸n autom谩tica de conteo f铆sico vs sistema - - id: ESP-INV-003-05 - descripcion: Identificaci贸n y an谩lisis de diferencias - - id: ESP-INV-003-06 - descripcion: Aprobaci贸n de ajustes resultantes del conteo - - id: ESP-INV-003-07 - descripcion: Bloqueo de movimientos durante conteo f铆sico - - id: ESP-INV-003-08 - descripcion: Reporte de exactitud de inventario - historias_usuario: - - id: US-INV-003-01 - titulo: Como jefe de almac茅n necesito programar conteos f铆sicos - criterios_aceptacion: - - Crear programa de conteo c铆clico (semanal, mensual, trimestral) - - Seleccionar almacenes, ubicaciones o categor铆as a contar - - Asignar responsables del conteo - - Definir fecha y hora del conteo - - Bloquear movimientos en 谩reas durante conteo - - id: US-INV-003-02 - titulo: Como almacenista necesito realizar conteo f铆sico con app m贸vil - criterios_aceptacion: - - Descargar hoja de conteo asignada en app m贸vil - - Ver lista de materiales a contar con existencia te贸rica - - Escanear c贸digos de barras durante conteo - - Registrar cantidad f铆sica encontrada - - Capturar fotos de materiales con diferencias - - Trabajar en modo offline - - Sincronizar resultados al finalizar - - id: US-INV-003-03 - titulo: Como jefe de almac茅n necesito analizar diferencias de conteo - criterios_aceptacion: - - Ver comparativo de existencia te贸rica vs f铆sica - - Identificar materiales con diferencias significativas - - Consultar detalles y fotos de diferencias - - Solicitar reconteo de materiales espec铆ficos - - Aprobar o rechazar ajustes sugeridos - - Generar reporte de exactitud de inventario - - id: US-INV-003-04 - titulo: Como auditor necesito consultar historial de conteos - criterios_aceptacion: - - Ver historial de conteos f铆sicos realizados - - Consultar resultados y diferencias encontradas - - Analizar tendencias de exactitud por almac茅n - - Identificar materiales problem谩ticos recurrentes - - Exportar reportes de auditor铆a - tablas: - - inventory_management.conteos_fisicos - - inventory_management.conteos_detalle - - inventory_management.conteos_diferencias - - inventory_management.conteos_aprobaciones - - inventory_management.programas_conteo_ciclico - endpoints: - - POST /api/v1/inventario/conteos - - GET /api/v1/inventario/conteos - - GET /api/v1/inventario/conteos/:id - - PUT /api/v1/inventario/conteos/:id - - POST /api/v1/inventario/conteos/:id/iniciar - - POST /api/v1/inventario/conteos/:id/finalizar - - GET /api/v1/inventario/conteos/:id/hoja-conteo - - POST /api/v1/inventario/conteos/:id/capturar-conteo - - GET /api/v1/inventario/conteos/:id/diferencias - - POST /api/v1/inventario/conteos/:id/aprobar-ajustes - - POST /api/v1/inventario/conteos/:id/rechazar-ajustes - - GET /api/v1/inventario/conteos/:id/reporte-exactitud - - POST /api/v1/inventario/programas-conteo-ciclico - - GET /api/v1/inventario/programas-conteo-ciclico - componentes_ui: - - ConteosListView - - ConteoFormModal - - ConteoDetailView - - HojaConteoView - - CapturaConteoPanel - - DiferenciasTable - - AprobacionAjustesModal - - ReporteExactitudView - - ProgramaConteoCiclicoForm - - HistorialConteosChart - app_movil: MOB-002 - funcionalidades_movil: - - Descargar hojas de conteo - - Escanear c贸digos durante conteo - - Captura de cantidades f铆sicas - - Fotograf铆as de diferencias - - Modo offline completo - - Sincronizaci贸n de resultados - - Vista de materiales pendientes - integraciones: - - modulo: RF-INV-001 - descripcion: Conteos por almac茅n y ubicaci贸n - - modulo: RF-INV-002 - descripcion: Generaci贸n de ajustes de inventario - - modulo: MGN-005 - descripcion: Cat谩logo de materiales - estado: pendiente - - - id: RF-INV-004 - titulo: Traspasos entre Almacenes/Obras - descripcion: Transferencias de material entre almacenes de diferentes obras o sedes - prioridad: alta - especificaciones: - - id: ESP-INV-004-01 - descripcion: Crear solicitudes de traspaso entre almacenes - - id: ESP-INV-004-02 - descripcion: Aprobaci贸n multinivel de traspasos (origen, destino, direcci贸n) - - id: ESP-INV-004-03 - descripcion: Registro de salida en almac茅n origen - - id: ESP-INV-004-04 - descripcion: Registro de entrada en almac茅n destino - - id: ESP-INV-004-05 - descripcion: Documentaci贸n de material en tr谩nsito - - id: ESP-INV-004-06 - descripcion: Control de transportista y gu铆a de remisi贸n - - id: ESP-INV-004-07 - descripcion: Seguimiento de estatus del traspaso - - id: ESP-INV-004-08 - descripcion: Validaci贸n de cantidades recibidas vs enviadas - historias_usuario: - - id: US-INV-004-01 - titulo: Como residente necesito solicitar traspaso de material de otra obra - criterios_aceptacion: - - Seleccionar almac茅n origen y destino - - Buscar materiales disponibles en origen - - Especificar cantidad requerida y fecha necesaria - - Justificar la solicitud de traspaso - - Enviar solicitud para aprobaci贸n - - Recibir notificaci贸n de respuesta - - id: US-INV-004-02 - titulo: Como jefe de obra necesito aprobar/rechazar traspasos desde mi almac茅n - criterios_aceptacion: - - Ver solicitudes de traspaso pendientes - - Consultar impacto en mis existencias - - Verificar disponibilidad de material - - Aprobar o rechazar con comentarios - - Notificar al solicitante y destino - - id: US-INV-004-03 - titulo: Como almacenista origen necesito procesar env铆o de traspaso - criterios_aceptacion: - - Ver traspasos aprobados pendientes de env铆o - - Preparar material para env铆o - - Registrar salida con cantidades reales - - Generar documento de traspaso (remisi贸n) - - Capturar datos de transportista - - Marcar traspaso como "en tr谩nsito" - - Actualizar inventario de origen - - id: US-INV-004-04 - titulo: Como almacenista destino necesito recibir traspasos en app m贸vil - criterios_aceptacion: - - Ver traspasos en tr谩nsito esperados - - Verificar documentaci贸n de env铆o - - Registrar cantidades recibidas - - Identificar diferencias vs cantidad enviada - - Capturar fotos del material recibido - - Asignar ubicaci贸n en almac茅n destino - - Generar entrada de traspaso - - Actualizar inventario de destino - - id: US-INV-004-05 - titulo: Como director de operaciones necesito monitorear traspasos entre obras - criterios_aceptacion: - - Ver dashboard de traspasos activos - - Consultar material en tr谩nsito por obra - - Identificar traspasos retrasados - - Ver costos de traspasos realizados - - Generar reporte de movimientos entre obras - tablas: - - inventory_management.traspasos - - inventory_management.traspasos_detalle - - inventory_management.traspasos_aprobaciones - - inventory_management.traspasos_documentos - - inventory_management.material_en_transito - endpoints: - - POST /api/v1/inventario/traspasos - - GET /api/v1/inventario/traspasos - - GET /api/v1/inventario/traspasos/:id - - PUT /api/v1/inventario/traspasos/:id - - DELETE /api/v1/inventario/traspasos/:id - - POST /api/v1/inventario/traspasos/:id/aprobar-origen - - POST /api/v1/inventario/traspasos/:id/aprobar-destino - - POST /api/v1/inventario/traspasos/:id/aprobar-direccion - - POST /api/v1/inventario/traspasos/:id/rechazar - - POST /api/v1/inventario/traspasos/:id/procesar-envio - - POST /api/v1/inventario/traspasos/:id/procesar-recepcion - - GET /api/v1/inventario/traspasos/:id/documento - - GET /api/v1/inventario/traspasos/almacen/:almacen_id - - GET /api/v1/inventario/traspasos/en-transito - - GET /api/v1/inventario/traspasos/pendientes-recepcion - componentes_ui: - - TraspasosListView - - TraspasoFormModal - - TraspasoDetailView - - TraspasoAprobacionPanel - - TraspasoEnvioModal - - TraspasoRecepcionModal - - MaterialEnTransitoTable - - TraspasoDocumentoView - - TraspasosMapView - - TraspasosStatusTimeline - app_movil: MOB-002 - funcionalidades_movil: - - Ver traspasos pendientes de recepci贸n - - Escanear remisi贸n de traspaso - - Registrar recepci贸n con cantidades - - Captura de fotos del material - - Firma digital de recepci贸n - - Registro de diferencias - - Asignaci贸n a ubicaciones - - Notificaciones push de traspasos - integraciones: - - modulo: RF-INV-001 - descripcion: Actualizaci贸n de inventarios origen y destino - - modulo: RF-INV-002 - descripcion: Generaci贸n de movimientos de salida y entrada - - modulo: MGN-001 - descripcion: Aprobadores y responsables - - modulo: MGN-005 - descripcion: Cat谩logo de materiales - estado: pendiente - -# Aplicaci贸n M贸vil -app_movil: - id: MOB-002 - nombre: Almacenista - descripcion: Aplicaci贸n m贸vil para control de inventario en campo - plataformas: - - Android - - iOS - tecnologias: - - React Native - - Expo - - SQLite (offline) - - Axios (sync) - funcionalidades: - - login_offline: Autenticaci贸n con modo offline - - consulta_inventario: Consulta de existencias en tiempo real - - registro_entradas: Registro de entradas de material - - procesamiento_salidas: Aprobaci贸n y entrega de salidas - - escaneo_codigos: Escaneo de c贸digos de barras y QR - - firma_digital: Captura de firma en entregas - - conteo_fisico: Captura de conteo f铆sico - - recepcion_traspasos: Recepci贸n de traspasos entre almacenes - - captura_fotos: Captura de fotos de materiales - - geolocalizacion: Registro de ubicaci贸n en entregas - - sincronizacion: Sincronizaci贸n autom谩tica con servidor - - notificaciones_push: Alertas y notificaciones en tiempo real - requerimientos_vinculados: - - RF-INV-001 # Consulta de almacenes y ubicaciones - - RF-INV-002 # Movimientos de inventario - - RF-INV-003 # Conteos f铆sicos - - RF-INV-004 # Recepci贸n de traspasos - estado: pendiente - -# Esquemas de Base de Datos -esquemas: - - nombre: inventory_management - descripcion: Esquema para gesti贸n integral de inventarios - tablas: - - almacenes - - tipos_almacen - - ubicaciones - - niveles_inventario - - almacenes_responsables - - movimientos_inventario - - entradas_inventario - - salidas_inventario - - ajustes_inventario - - solicitudes_salida - - kardex - - lotes - - existencias_por_ubicacion - - conteos_fisicos - - conteos_detalle - - conteos_diferencias - - conteos_aprobaciones - - programas_conteo_ciclico - - traspasos - - traspasos_detalle - - traspasos_aprobaciones - - traspasos_documentos - - material_en_transito - -# Pol铆ticas RLS -rls_policies: - archivo: ET-INV-rls-policies.sql - descripcion: Pol铆ticas de seguridad a nivel de fila para multi-tenancy - tablas_protegidas: - - inventory_management.almacenes - - inventory_management.movimientos_inventario - - inventory_management.traspasos - - inventory_management.conteos_fisicos - -# Reportes -reportes: - - id: REP-INV-001 - nombre: Inventario Valorizado - descripcion: Valuaci贸n de inventario por almac茅n, obra o categor铆a - parametros: [fecha_corte, obra_id, almacen_id, categoria_id] - - id: REP-INV-002 - nombre: Kardex de Material - descripcion: Movimientos detallados por material - parametros: [material_id, fecha_inicio, fecha_fin, almacen_id] - - id: REP-INV-003 - nombre: Existencias por Almac茅n - descripcion: Listado de existencias actuales por almac茅n - parametros: [almacen_id, categoria_id, solo_con_existencia] - - id: REP-INV-004 - nombre: Materiales con Existencias Bajas - descripcion: Materiales que han alcanzado el nivel m铆nimo - parametros: [obra_id, almacen_id, categoria_id] - - id: REP-INV-005 - nombre: Movimientos por Per铆odo - descripcion: Detalle de entradas, salidas y ajustes por per铆odo - parametros: [fecha_inicio, fecha_fin, almacen_id, tipo_movimiento] - - id: REP-INV-006 - nombre: Exactitud de Inventario - descripcion: An谩lisis de diferencias en conteos f铆sicos - parametros: [periodo, almacen_id, categoria_id] - - id: REP-INV-007 - nombre: Traspasos entre Obras - descripcion: Reporte de traspasos realizados entre almacenes - parametros: [fecha_inicio, fecha_fin, obra_origen, obra_destino] - - id: REP-INV-008 - nombre: Material en Tr谩nsito - descripcion: Listado de material en proceso de traspaso - parametros: [fecha_corte, obra_destino] - - id: REP-INV-009 - nombre: Rotaci贸n de Inventario - descripcion: An谩lisis de rotaci贸n por material y categor铆a - parametros: [periodo, categoria_id, almacen_id] - - id: REP-INV-010 - nombre: Inventario Inmovilizado - descripcion: Materiales sin movimiento en per铆odo definido - parametros: [dias_sin_movimiento, almacen_id, valor_minimo] - -# Notificaciones -notificaciones: - - evento: existencias_bajas - destinatarios: [almacenista, residente_obra, compras] - canal: [email, app, push_movil] - - evento: solicitud_salida_pendiente - destinatarios: [almacenista] - canal: [app, push_movil] - - evento: salida_aprobada - destinatarios: [solicitante] - canal: [app] - - evento: salida_rechazada - destinatarios: [solicitante] - canal: [app] - - evento: entrada_registrada - destinatarios: [residente_obra, jefe_compras] - canal: [app] - - evento: ajuste_inventario_pendiente - destinatarios: [jefe_almacen, auditor] - canal: [email, app] - - evento: conteo_asignado - destinatarios: [almacenista] - canal: [app, push_movil] - - evento: diferencias_conteo_significativas - destinatarios: [jefe_almacen, auditor] - canal: [email, app] - - evento: traspaso_solicitado - destinatarios: [jefe_obra_origen, almacenista_origen] - canal: [email, app] - - evento: traspaso_aprobado - destinatarios: [solicitante, almacenista_origen, almacenista_destino] - canal: [app, push_movil] - - evento: traspaso_rechazado - destinatarios: [solicitante] - canal: [app] - - evento: traspaso_enviado - destinatarios: [almacenista_destino, jefe_obra_destino] - canal: [app, push_movil] - - evento: traspaso_recibido - destinatarios: [almacenista_origen, jefe_obra_origen] - canal: [app] - -# M茅tricas y KPIs -metricas: - - nombre: exactitud_inventario - descripcion: Porcentaje de exactitud en conteos f铆sicos - unidad: porcentaje - objetivo: "> 95%" - - nombre: rotacion_inventario - descripcion: Veces que rota el inventario por per铆odo - unidad: veces/mes - objetivo: "> 2" - - nombre: valor_inventario_inmovilizado - descripcion: Valor de materiales sin movimiento > 90 d铆as - unidad: moneda - objetivo: "< 5% del total" - - nombre: tiempo_procesamiento_salidas - descripcion: Tiempo promedio para procesar solicitudes de salida - unidad: horas - objetivo: "< 4 horas" - - nombre: diferencias_traspasos - descripcion: Porcentaje de diferencias en cantidades de traspasos - unidad: porcentaje - objetivo: "< 2%" - - nombre: nivel_servicio_almacen - descripcion: Porcentaje de solicitudes atendidas a tiempo - unidad: porcentaje - objetivo: "> 95%" - - nombre: costo_almacenamiento - descripcion: Costo de almacenamiento como % del valor inventario - unidad: porcentaje - objetivo: "< 8%" - -# Validaciones y Reglas de Negocio -reglas_negocio: - - codigo: RN-INV-001 - descripcion: No se permiten salidas de inventario si no hay existencias suficientes - - codigo: RN-INV-002 - descripcion: Las salidas de inventario deben estar vinculadas a una partida presupuestal o destino espec铆fico - - codigo: RN-INV-003 - descripcion: Los ajustes de inventario mayores al 5% requieren aprobaci贸n de jefe de almac茅n - - codigo: RN-INV-004 - descripcion: Los traspasos deben ser aprobados por los responsables de ambos almacenes - - codigo: RN-INV-005 - descripcion: No se permiten movimientos en almacenes durante conteo f铆sico activo - - codigo: RN-INV-006 - descripcion: El costo de materiales debe actualizarse seg煤n m茅todo PEPS o promedio ponderado - - codigo: RN-INV-007 - descripcion: Los conteos f铆sicos con diferencias > 10% requieren reconteo - - codigo: RN-INV-008 - descripcion: Los traspasos deben registrar salida antes de poder registrar entrada - - codigo: RN-INV-009 - descripcion: Las ubicaciones no pueden exceder su capacidad m谩xima definida - - codigo: RN-INV-010 - descripcion: Los materiales con fecha de caducidad deben alertar 30 d铆as antes - -# Seguridad -seguridad: - permisos: - - inventario.almacenes.crear - - inventario.almacenes.leer - - inventario.almacenes.editar - - inventario.almacenes.eliminar - - inventario.ubicaciones.gestionar - - inventario.movimientos.crear - - inventario.movimientos.leer - - inventario.entradas.registrar - - inventario.salidas.solicitar - - inventario.salidas.aprobar - - inventario.salidas.rechazar - - inventario.ajustes.crear - - inventario.ajustes.aprobar - - inventario.conteos.programar - - inventario.conteos.ejecutar - - inventario.conteos.aprobar - - inventario.traspasos.solicitar - - inventario.traspasos.aprobar - - inventario.traspasos.procesar - - inventario.reportes.generar - - inventario.reportes.exportar - roles_sugeridos: - - nombre: almacenista - permisos: [inventario.movimientos.*, inventario.entradas.*, inventario.salidas.aprobar, inventario.conteos.ejecutar] - - nombre: jefe_almacen - permisos: [inventario.*, -inventario.almacenes.eliminar] - - nombre: residente_obra - permisos: [inventario.almacenes.leer, inventario.salidas.solicitar, inventario.movimientos.leer] - - nombre: auditor - permisos: [inventario.*.leer, inventario.conteos.*, inventario.reportes.*] - -# Pruebas -pruebas: - unitarias: - - calcular_costo_promedio_ponderado - - validar_existencias_disponibles - - calcular_diferencias_conteo - - validar_capacidad_ubicacion - - calcular_rotacion_inventario - integracion: - - flujo_completo_entrada_inventario - - flujo_completo_salida_inventario - - proceso_conteo_fisico_con_ajustes - - proceso_traspaso_completo - - sincronizacion_app_movil - e2e: - - ciclo_vida_material_entrada_salida - - proceso_conteo_fisico_app_movil - - proceso_traspaso_entre_obras - - gestion_existencias_multiples_ubicaciones - -# Cronograma Estimado -cronograma: - - fase: An谩lisis y Dise帽o - duracion: 2 semanas - entregables: [modelos_datos, diagramas_flujo, especificaciones_api] - - fase: Desarrollo Backend - duracion: 5 semanas - entregables: [api_almacenes, api_movimientos, api_conteos, api_traspasos, rls_policies] - - fase: Desarrollo Frontend - duracion: 5 semanas - entregables: [ui_almacenes, ui_movimientos, ui_conteos, ui_traspasos, reportes] - - fase: Desarrollo App M贸vil - duracion: 4 semanas - entregables: [app_almacenista, modo_offline, sincronizacion, escaneo_codigos] - - fase: Pruebas - duracion: 2 semanas - entregables: [pruebas_unitarias, pruebas_integracion, pruebas_usuario, pruebas_campo] - - fase: Documentaci贸n y Capacitaci贸n - duracion: 1 semana - entregables: [manual_usuario, manual_tecnico, manual_app_movil, videos_capacitacion] - -# Notas de Implementaci贸n -notas: - - Priorizar desarrollo de RF-INV-001 y RF-INV-002 (funcionalidad b谩sica de almacenes y movimientos) - - Desarrollar app m贸vil MOB-002 en paralelo con backend de movimientos - - Implementar sincronizaci贸n offline-online robusta para app m贸vil - - Considerar integraci贸n con lectores de c贸digos de barras industriales - - Evaluar implementaci贸n de tecnolog铆a RFID para almacenes grandes - - Planificar estrategia de migraci贸n de saldos iniciales de inventario - - Definir pol铆ticas de conteo c铆clico antes de implementar RF-INV-003 - - Considerar integraci贸n futura con sistemas de transporte para tracking de traspasos - - Implementar dashboard de m茅tricas en tiempo real para jefes de almac茅n - - Validar m茅todo de costeo (PEPS vs Promedio) con departamento contable diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-crm/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-crm/implementacion/TRACEABILITY.yml deleted file mode 100644 index 4c0954c36..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-crm/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,1583 +0,0 @@ -# TRACEABILITY.yml - MAE-014: CRM Clientes -# Matriz completa de trazabilidad: Requerimientos 鈫 Especificaciones 鈫 Historias 鈫 Implementaci贸n - -epic_code: MAE-014 -epic_name: CRM Clientes -phase: 2 -phase_name: Comercializaci贸n y Ventas -budget_mxn: 85000 -story_points: 120 -status: planned -sprint: 8-11 -period: "Semanas 8-11" -reused_from_gamilit: 40% - -# ============================================================================ -# DOCUMENTACI脫N -# ============================================================================ - -documentation: - requirements: - - id: RF-CRM-001 - file: requerimientos/RF-CRM-001-gestion-prospectos.md - title: Gesti贸n de Prospectos - status: planned - reused_from: null - adaptations: - - "Sistema de captura y calificaci贸n de leads" - - "Scoring de prospectos seg煤n criterios de construcci贸n" - - "Pipeline de ventas espec铆fico para proyectos inmobiliarios" - - "Seguimiento de fuentes de captaci贸n" - - - id: RF-CRM-002 - file: requerimientos/RF-CRM-002-seguimiento-comercial.md - title: Seguimiento Comercial - status: planned - reused_from: null - adaptations: - - "Agenda de visitas a desarrollos" - - "Registro de interacciones con prospectos" - - "Embudo de conversi贸n por proyecto" - - "Asignaci贸n de vendedores por desarrollo" - - "KPIs comerciales por vendedor y proyecto" - - - id: RF-CRM-003 - file: requerimientos/RF-CRM-003-cotizaciones.md - title: Sistema de Cotizaciones - status: planned - reused_from: null - adaptations: - - "Cotizaciones de viviendas con configuraciones personalizadas" - - "C谩lculo de financiamiento y esquemas de pago" - - "Integraci贸n con cat谩logo de prototipos y acabados" - - "Generaci贸n de propuestas econ贸micas en PDF" - - "Historial de versiones de cotizaci贸n" - - - id: RF-CRM-004 - file: requerimientos/RF-CRM-004-portal-cliente.md - title: Portal del Cliente - status: planned - reused_from: null - adaptations: - - "Acceso web para compradores" - - "Visualizaci贸n de avance de obra de su vivienda" - - "Consulta de estados de cuenta y pagos" - - "Solicitud de servicios post-venta" - - "Integraci贸n con MOB-005 (Derechohabiente)" - - "Notificaciones de hitos de construcci贸n" - - specifications: - - id: ET-CRM-001 - file: especificaciones/ET-CRM-001-gestion-prospectos.md - rf: RF-CRM-001 - title: Implementaci贸n de Gesti贸n de Prospectos - status: planned - reused_from: null - adaptations: - - "Schema crm.prospectos con campos espec铆ficos" - - "Sistema de scoring autom谩tico" - - "Estados del lead: nuevo, contactado, calificado, hot, cold, perdido" - - "Asignaci贸n autom谩tica seg煤n reglas de distribuci贸n" - - - id: ET-CRM-002 - file: especificaciones/ET-CRM-002-seguimiento-comercial.md - rf: RF-CRM-002 - title: Implementaci贸n de Seguimiento Comercial - status: planned - reused_from: null - adaptations: - - "Schema crm.interacciones para registro de contactos" - - "Calendario de actividades comerciales" - - "Dashboard de pipeline y conversi贸n" - - "Reportes de actividad por vendedor" - - - id: ET-CRM-003 - file: especificaciones/ET-CRM-003-cotizaciones.md - rf: RF-CRM-003 - title: Implementaci贸n de Sistema de Cotizaciones - status: planned - reused_from: null - adaptations: - - "Schema crm.cotizaciones con versionamiento" - - "Motor de c谩lculo de financiamiento" - - "Plantillas de cotizaci贸n en PDF" - - "Integraci贸n con cat谩logos de prototipos y acabados" - - - id: ET-CRM-004 - file: especificaciones/ET-CRM-004-portal-cliente.md - rf: RF-CRM-004 - title: Implementaci贸n de Portal del Cliente - status: planned - reused_from: null - adaptations: - - "Frontend separado para clientes (portal-cliente app)" - - "Schema crm.clientes_portal para configuraci贸n" - - "Integraci贸n con MOB-005 para datos de derechohabiente" - - "API p煤blica para consulta de avance de obra" - - "Sistema de notificaciones por email/SMS" - - user_stories: - # RF-CRM-001: Gesti贸n de Prospectos - - id: US-CRM-001 - file: historias-usuario/US-CRM-001-captura-prospectos.md - title: Captura de Prospectos - rf: RF-CRM-001 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Sistema de captura con origen y scoring inicial"] - - - id: US-CRM-002 - file: historias-usuario/US-CRM-002-calificacion-leads.md - title: Calificaci贸n y Scoring de Leads - rf: RF-CRM-001 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Motor de scoring basado en criterios configurables"] - - - id: US-CRM-003 - file: historias-usuario/US-CRM-003-asignacion-prospectos.md - title: Asignaci贸n de Prospectos a Vendedores - rf: RF-CRM-001 - story_points: 5 - status: planned - reused_from: null - adaptations: ["Reglas de asignaci贸n por proyecto, zona, carga"] - - # RF-CRM-002: Seguimiento Comercial - - id: US-CRM-004 - file: historias-usuario/US-CRM-004-agenda-visitas.md - title: Agenda de Visitas y Seguimiento - rf: RF-CRM-002 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Calendario de citas en sala de ventas y obra"] - - - id: US-CRM-005 - file: historias-usuario/US-CRM-005-registro-interacciones.md - title: Registro de Interacciones con Clientes - rf: RF-CRM-002 - story_points: 5 - status: planned - reused_from: null - adaptations: ["Log de llamadas, emails, visitas, WhatsApp"] - - - id: US-CRM-006 - file: historias-usuario/US-CRM-006-pipeline-ventas.md - title: Pipeline de Ventas y Embudo - rf: RF-CRM-002 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Dashboard de embudo por proyecto y vendedor"] - - - id: US-CRM-007 - file: historias-usuario/US-CRM-007-kpis-comerciales.md - title: KPIs y M茅tricas Comerciales - rf: RF-CRM-002 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Dashboard de conversi贸n, cierre, tiempo promedio"] - - # RF-CRM-003: Cotizaciones - - id: US-CRM-008 - file: historias-usuario/US-CRM-008-crear-cotizacion.md - title: Creaci贸n de Cotizaciones - rf: RF-CRM-003 - story_points: 13 - status: planned - reused_from: null - adaptations: ["Wizard de cotizaci贸n con selecci贸n de prototipo y extras"] - - - id: US-CRM-009 - file: historias-usuario/US-CRM-009-calculo-financiamiento.md - title: C谩lculo de Esquemas de Financiamiento - rf: RF-CRM-003 - story_points: 13 - status: planned - reused_from: null - adaptations: ["Motor de c谩lculo: contado, cr茅dito, mixto"] - - - id: US-CRM-010 - file: historias-usuario/US-CRM-010-generar-propuesta-pdf.md - title: Generaci贸n de Propuesta Econ贸mica en PDF - rf: RF-CRM-003 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Template profesional con branding de constructora"] - - - id: US-CRM-011 - file: historias-usuario/US-CRM-011-versiones-cotizacion.md - title: Versionamiento de Cotizaciones - rf: RF-CRM-003 - story_points: 5 - status: planned - reused_from: null - adaptations: ["Historial de cambios y comparaci贸n de versiones"] - - # RF-CRM-004: Portal del Cliente - - id: US-CRM-012 - file: historias-usuario/US-CRM-012-portal-autenticacion.md - title: Autenticaci贸n en Portal del Cliente - rf: RF-CRM-004 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Login con email/tel茅fono, recuperaci贸n de contrase帽a"] - - - id: US-CRM-013 - file: historias-usuario/US-CRM-013-dashboard-cliente.md - title: Dashboard del Cliente - rf: RF-CRM-004 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Vista general de su vivienda y estatus"] - - - id: US-CRM-014 - file: historias-usuario/US-CRM-014-avance-obra-cliente.md - title: Visualizaci贸n de Avance de Obra - rf: RF-CRM-004 - story_points: 13 - status: planned - reused_from: null - adaptations: ["Integraci贸n con MOB-005, fotos, hitos, % avance"] - - - id: US-CRM-015 - file: historias-usuario/US-CRM-015-estados-cuenta-cliente.md - title: Consulta de Estados de Cuenta - rf: RF-CRM-004 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Saldos, pagos realizados, pr贸ximos vencimientos"] - - - id: US-CRM-016 - file: historias-usuario/US-CRM-016-solicitudes-postventa.md - title: Solicitudes de Servicio Post-Venta - rf: RF-CRM-004 - story_points: 8 - status: planned - reused_from: null - adaptations: ["Formulario de garant铆as y mantenimiento"] - -# ============================================================================ -# IMPLEMENTACI脫N - BASE DE DATOS -# ============================================================================ - -implementation: - database: - schemas: - - name: crm - path: apps/database/ddl/schemas/crm/ - description: Schema de CRM (prospectos, clientes, cotizaciones) - reused_from_gamilit: false - note: "Nuevo schema espec铆fico de CRM para construcci贸n" - - - name: crm_comercial - path: apps/database/ddl/schemas/crm_comercial/ - description: Schema de seguimiento comercial (interacciones, pipeline) - reused_from_gamilit: false - note: "Nuevo schema para actividades comerciales" - - - name: crm_portal - path: apps/database/ddl/schemas/crm_portal/ - description: Schema de portal del cliente - reused_from_gamilit: false - note: "Nuevo schema para portal de clientes" - - enums: - - name: lead_status - schema: crm - file: apps/database/ddl/schemas/crm/00-enums.sql - lines: "1-10" - values: [nuevo, contactado, calificado, hot, cold, perdido, convertido] - rf: RF-CRM-001 - reused_from: null - note: "Estados del prospecto en el pipeline" - - - name: lead_source - schema: crm - file: apps/database/ddl/schemas/crm/00-enums.sql - lines: "12-22" - values: [web, facebook, instagram, referido, evento, stand, llamada, walk_in] - rf: RF-CRM-001 - reused_from: null - note: "Fuentes de captaci贸n de prospectos" - - - name: interaction_type - schema: crm_comercial - file: apps/database/ddl/schemas/crm_comercial/00-enums.sql - lines: "1-10" - values: [llamada, email, whatsapp, visita_sala, visita_obra, reunion, video_llamada] - rf: RF-CRM-002 - reused_from: null - note: "Tipos de interacci贸n con clientes" - - - name: cotizacion_status - schema: crm - file: apps/database/ddl/schemas/crm/00-enums.sql - lines: "24-32" - values: [borrador, enviada, en_negociacion, aceptada, rechazada, vencida, facturada] - rf: RF-CRM-003 - reused_from: null - note: "Estados de la cotizaci贸n" - - - name: financing_type - schema: crm - file: apps/database/ddl/schemas/crm/00-enums.sql - lines: "34-42" - values: [contado, credito_infonavit, credito_bancario, credito_cofinavit, credito_constructora, mixto] - rf: RF-CRM-003 - reused_from: null - note: "Tipos de financiamiento para cotizaciones" - - - name: portal_user_status - schema: crm_portal - file: apps/database/ddl/schemas/crm_portal/00-enums.sql - lines: "1-8" - values: [active, suspended, pending_activation, inactive] - rf: RF-CRM-004 - reused_from: null - note: "Estados de usuario del portal" - - tables: - # RF-CRM-001: Gesti贸n de Prospectos - - name: prospectos - schema: crm - file: apps/database/ddl/schemas/crm/tables/01-prospectos.sql - lines: 180 - description: Cat谩logo de prospectos (leads) - rf: RF-CRM-001 - reused_from_gamilit: false - note: "Tabla principal de leads con scoring" - columns: - - id (UUID, PK) - - constructora_id (UUID, FK) - - proyecto_id (UUID, FK, nullable) - - nombre (TEXT) - - apellidos (TEXT) - - email (TEXT) - - telefono (TEXT) - - telefono_secundario (TEXT) - - lead_source (lead_source ENUM) - - lead_status (lead_status ENUM) - - scoring (INTEGER, 0-100) - - presupuesto_min (NUMERIC) - - presupuesto_max (NUMERIC) - - fecha_contacto_inicial (DATE) - - fecha_conversion (DATE, nullable) - - vendedor_asignado_id (UUID, FK, nullable) - - notas (TEXT) - - metadata (JSONB) - - created_at (TIMESTAMPTZ) - - updated_at (TIMESTAMPTZ) - indexes: - - idx_prospectos_constructora_status (constructora_id, lead_status) - - idx_prospectos_vendedor (vendedor_asignado_id) - - idx_prospectos_scoring (scoring DESC) - - - name: criterios_scoring - schema: crm - file: apps/database/ddl/schemas/crm/tables/02-criterios_scoring.sql - lines: 90 - description: Configuraci贸n de criterios de scoring de prospectos - rf: RF-CRM-001 - reused_from_gamilit: false - note: "Criterios personalizables por constructora" - columns: - - id (UUID, PK) - - constructora_id (UUID, FK) - - criterio (TEXT) - - peso (INTEGER, 0-100) - - condiciones (JSONB) - - active (BOOLEAN) - - created_at (TIMESTAMPTZ) - - # RF-CRM-002: Seguimiento Comercial - - name: interacciones - schema: crm_comercial - file: apps/database/ddl/schemas/crm_comercial/tables/01-interacciones.sql - lines: 140 - description: Registro de interacciones con prospectos/clientes - rf: RF-CRM-002 - reused_from_gamilit: false - note: "Log de todas las comunicaciones" - columns: - - id (UUID, PK) - - constructora_id (UUID, FK) - - prospecto_id (UUID, FK, nullable) - - cliente_id (UUID, FK, nullable) - - tipo (interaction_type ENUM) - - fecha (TIMESTAMPTZ) - - duracion_minutos (INTEGER) - - vendedor_id (UUID, FK) - - asunto (TEXT) - - notas (TEXT) - - resultado (TEXT) - - siguiente_accion (TEXT) - - fecha_siguiente_accion (TIMESTAMPTZ, nullable) - - archivos_adjuntos (JSONB) - - created_at (TIMESTAMPTZ) - indexes: - - idx_interacciones_prospecto (prospecto_id, fecha DESC) - - idx_interacciones_cliente (cliente_id, fecha DESC) - - idx_interacciones_vendedor (vendedor_id, fecha DESC) - - - name: pipeline_configuracion - schema: crm_comercial - file: apps/database/ddl/schemas/crm_comercial/tables/02-pipeline_configuracion.sql - lines: 100 - description: Configuraci贸n de etapas del pipeline de ventas - rf: RF-CRM-002 - reused_from_gamilit: false - note: "Etapas personalizables del embudo" - columns: - - id (UUID, PK) - - constructora_id (UUID, FK) - - etapa (TEXT) - - orden (INTEGER) - - probabilidad_cierre (INTEGER, 0-100) - - color (TEXT) - - active (BOOLEAN) - - - name: metas_comerciales - schema: crm_comercial - file: apps/database/ddl/schemas/crm_comercial/tables/03-metas_comerciales.sql - lines: 110 - description: Metas de ventas por vendedor y per铆odo - rf: RF-CRM-002 - reused_from_gamilit: false - note: "KPIs y objetivos comerciales" - columns: - - id (UUID, PK) - - constructora_id (UUID, FK) - - vendedor_id (UUID, FK) - - proyecto_id (UUID, FK, nullable) - - periodo_inicio (DATE) - - periodo_fin (DATE) - - meta_prospectos (INTEGER) - - meta_visitas (INTEGER) - - meta_cotizaciones (INTEGER) - - meta_cierres (INTEGER) - - meta_monto (NUMERIC) - - created_at (TIMESTAMPTZ) - - # RF-CRM-003: Cotizaciones - - name: cotizaciones - schema: crm - file: apps/database/ddl/schemas/crm/tables/10-cotizaciones.sql - lines: 220 - description: Cotizaciones de viviendas - rf: RF-CRM-003 - reused_from_gamilit: false - note: "Cotizaciones con versionamiento" - columns: - - id (UUID, PK) - - constructora_id (UUID, FK) - - numero_cotizacion (TEXT UNIQUE) - - version (INTEGER) - - prospecto_id (UUID, FK) - - proyecto_id (UUID, FK) - - prototipo_id (UUID, FK) - - lote_id (UUID, FK, nullable) - - vendedor_id (UUID, FK) - - status (cotizacion_status ENUM) - - fecha_emision (DATE) - - fecha_vencimiento (DATE) - - precio_base (NUMERIC) - - extras (JSONB) - - total_extras (NUMERIC) - - descuentos (JSONB) - - total_descuentos (NUMERIC) - - precio_final (NUMERIC) - - tipo_financiamiento (financing_type ENUM) - - enganche_porcentaje (NUMERIC) - - enganche_monto (NUMERIC) - - financiado_monto (NUMERIC) - - plazo_meses (INTEGER) - - tasa_interes (NUMERIC) - - mensualidad (NUMERIC) - - esquema_pagos (JSONB) - - condiciones (TEXT) - - notas_internas (TEXT) - - pdf_url (TEXT, nullable) - - enviada_cliente (BOOLEAN) - - fecha_envio (TIMESTAMPTZ, nullable) - - aceptada_fecha (TIMESTAMPTZ, nullable) - - rechazada_fecha (TIMESTAMPTZ, nullable) - - razon_rechazo (TEXT, nullable) - - created_at (TIMESTAMPTZ) - - updated_at (TIMESTAMPTZ) - indexes: - - idx_cotizaciones_numero (numero_cotizacion) - - idx_cotizaciones_prospecto (prospecto_id) - - idx_cotizaciones_proyecto_status (proyecto_id, status) - - idx_cotizaciones_vendedor (vendedor_id, fecha_emision DESC) - - - name: cotizacion_versiones - schema: crm - file: apps/database/ddl/schemas/crm/tables/11-cotizacion_versiones.sql - lines: 90 - description: Historial de versiones de cotizaciones - rf: RF-CRM-003 - reused_from_gamilit: false - note: "Auditor铆a de cambios en cotizaciones" - columns: - - id (UUID, PK) - - cotizacion_id (UUID, FK) - - version (INTEGER) - - cambios (JSONB) - - user_id (UUID, FK) - - motivo (TEXT) - - snapshot (JSONB) - - created_at (TIMESTAMPTZ) - - - name: plantillas_cotizacion - schema: crm - file: apps/database/ddl/schemas/crm/tables/12-plantillas_cotizacion.sql - lines: 80 - description: Plantillas para generaci贸n de PDFs de cotizaci贸n - rf: RF-CRM-003 - reused_from_gamilit: false - note: "Templates HTML/PDF personalizables" - columns: - - id (UUID, PK) - - constructora_id (UUID, FK) - - nombre (TEXT) - - descripcion (TEXT) - - template_html (TEXT) - - estilos_css (TEXT) - - active (BOOLEAN) - - default (BOOLEAN) - - created_at (TIMESTAMPTZ) - - updated_at (TIMESTAMPTZ) - - # RF-CRM-004: Portal del Cliente - - name: clientes_portal - schema: crm_portal - file: apps/database/ddl/schemas/crm_portal/tables/01-clientes_portal.sql - lines: 150 - description: Usuarios del portal de clientes - rf: RF-CRM-004 - reused_from_gamilit: false - note: "Autenticaci贸n separada para clientes" - columns: - - id (UUID, PK) - - constructora_id (UUID, FK) - - cliente_id (UUID, FK) - - derechohabiente_id (UUID, FK, nullable) - - email (TEXT UNIQUE) - - password_hash (TEXT) - - telefono (TEXT) - - status (portal_user_status ENUM) - - activacion_token (TEXT, nullable) - - activacion_fecha (TIMESTAMPTZ, nullable) - - ultimo_acceso (TIMESTAMPTZ, nullable) - - preferencias (JSONB) - - created_at (TIMESTAMPTZ) - - updated_at (TIMESTAMPTZ) - indexes: - - idx_portal_email (email) - - idx_portal_cliente (cliente_id) - - idx_portal_derechohabiente (derechohabiente_id) - - - name: portal_notificaciones - schema: crm_portal - file: apps/database/ddl/schemas/crm_portal/tables/02-portal_notificaciones.sql - lines: 120 - description: Notificaciones enviadas a clientes del portal - rf: RF-CRM-004 - reused_from_gamilit: false - note: "Sistema de notificaciones por email/SMS" - columns: - - id (UUID, PK) - - cliente_portal_id (UUID, FK) - - tipo (TEXT) - - titulo (TEXT) - - mensaje (TEXT) - - canal (TEXT[]) - - enviado (BOOLEAN) - - fecha_envio (TIMESTAMPTZ, nullable) - - leido (BOOLEAN) - - fecha_lectura (TIMESTAMPTZ, nullable) - - metadata (JSONB) - - created_at (TIMESTAMPTZ) - indexes: - - idx_portal_notif_cliente (cliente_portal_id, created_at DESC) - - - name: portal_solicitudes_servicio - schema: crm_portal - file: apps/database/ddl/schemas/crm_portal/tables/03-portal_solicitudes_servicio.sql - lines: 140 - description: Solicitudes de servicio post-venta desde el portal - rf: RF-CRM-004 - reused_from_gamilit: false - note: "Garant铆as y mantenimiento solicitados por cliente" - columns: - - id (UUID, PK) - - cliente_portal_id (UUID, FK) - - vivienda_id (UUID, FK) - - tipo_solicitud (TEXT) - - categoria (TEXT) - - descripcion (TEXT) - - prioridad (TEXT) - - status (TEXT) - - fecha_solicitud (TIMESTAMPTZ) - - fecha_atencion (TIMESTAMPTZ, nullable) - - fecha_resolucion (TIMESTAMPTZ, nullable) - - tecnico_asignado_id (UUID, FK, nullable) - - notas_tecnico (TEXT) - - fotos (JSONB) - - created_at (TIMESTAMPTZ) - - updated_at (TIMESTAMPTZ) - - functions: - - name: calcular_scoring_prospecto - schema: crm - file: apps/database/ddl/schemas/crm/functions/calcular_scoring_prospecto.sql - lines: "1-50" - description: Calcula el scoring de un prospecto seg煤n criterios configurados - rf: RF-CRM-001 - reused_from_gamilit: false - note: "Motor de scoring autom谩tico" - parameters: - - prospecto_id UUID - returns: INTEGER - - - name: asignar_prospecto_vendedor - schema: crm - file: apps/database/ddl/schemas/crm/functions/asignar_prospecto_vendedor.sql - lines: "1-60" - description: Asigna prospectos a vendedores seg煤n reglas de distribuci贸n - rf: RF-CRM-001 - reused_from_gamilit: false - note: "Asignaci贸n autom谩tica o manual" - parameters: - - prospecto_id UUID - - vendedor_id UUID (nullable) - returns: BOOLEAN - - - name: calcular_financiamiento - schema: crm - file: apps/database/ddl/schemas/crm/functions/calcular_financiamiento.sql - lines: "1-100" - description: Calcula esquema de financiamiento para cotizaci贸n - rf: RF-CRM-003 - reused_from_gamilit: false - note: "Motor de c谩lculo de mensualidades y amortizaci贸n" - parameters: - - monto NUMERIC - - enganche NUMERIC - - plazo INTEGER - - tasa NUMERIC - returns: JSONB - - - name: generar_numero_cotizacion - schema: crm - file: apps/database/ddl/schemas/crm/functions/generar_numero_cotizacion.sql - lines: "1-30" - description: Genera n煤mero secuencial 煤nico de cotizaci贸n - rf: RF-CRM-003 - reused_from_gamilit: false - note: "Formato: COT-{CONSTRUCTORA}-{YEAR}-{SEQ}" - parameters: - - constructora_id UUID - returns: TEXT - - - name: get_avance_obra_cliente - schema: crm_portal - file: apps/database/ddl/schemas/crm_portal/functions/get_avance_obra_cliente.sql - lines: "1-80" - description: Obtiene avance de obra para un cliente desde MOB-005 - rf: RF-CRM-004 - reused_from_gamilit: false - note: "Integraci贸n con m贸dulo MOB-005 (Derechohabiente)" - parameters: - - cliente_portal_id UUID - returns: JSONB - - - name: enviar_notificacion_portal - schema: crm_portal - file: apps/database/ddl/schemas/crm_portal/functions/enviar_notificacion_portal.sql - lines: "1-60" - description: Env铆a notificaci贸n a cliente del portal - rf: RF-CRM-004 - reused_from_gamilit: false - note: "Dispara email/SMS seg煤n preferencias" - parameters: - - cliente_portal_id UUID - - tipo TEXT - - titulo TEXT - - mensaje TEXT - returns: UUID - - views: - - name: vw_pipeline_ventas - schema: crm_comercial - file: apps/database/ddl/schemas/crm_comercial/views/01-vw_pipeline_ventas.sql - lines: 60 - description: Vista de pipeline de ventas por etapa - rf: RF-CRM-002 - reused_from_gamilit: false - note: "Dashboard de embudo de conversi贸n" - - - name: vw_kpis_vendedor - schema: crm_comercial - file: apps/database/ddl/schemas/crm_comercial/views/02-vw_kpis_vendedor.sql - lines: 80 - description: Vista de KPIs por vendedor - rf: RF-CRM-002 - reused_from_gamilit: false - note: "M茅tricas de desempe帽o comercial" - - - name: vw_cotizaciones_activas - schema: crm - file: apps/database/ddl/schemas/crm/views/01-vw_cotizaciones_activas.sql - lines: 50 - description: Vista de cotizaciones activas con informaci贸n completa - rf: RF-CRM-003 - reused_from_gamilit: false - note: "Dashboard de cotizaciones pendientes" - - rls_policies: - - table: crm.prospectos - policy: prospectos_select_own_constructora - description: Usuarios solo ven prospectos de su constructora - rf: RF-CRM-001 - reused_from_gamilit: false - sql: | - CREATE POLICY "prospectos_select_own_constructora" ON crm.prospectos - FOR SELECT - TO authenticated - USING ( - constructora_id = get_current_constructora_id() - ); - - - table: crm.prospectos - policy: vendedores_see_assigned - description: Vendedores ven solo sus prospectos asignados - rf: RF-CRM-001 - reused_from_gamilit: false - sql: | - CREATE POLICY "vendedores_see_assigned" ON crm.prospectos - FOR SELECT - TO authenticated - USING ( - CASE - WHEN get_current_user_role() IN ('director', 'post_sales') - THEN constructora_id = get_current_constructora_id() - ELSE vendedor_asignado_id = get_current_user_id() - END - ); - - - table: crm.cotizaciones - policy: cotizaciones_select_own_constructora - description: Usuarios solo ven cotizaciones de su constructora - rf: RF-CRM-003 - reused_from_gamilit: false - - - table: crm_portal.clientes_portal - policy: portal_users_own_data - description: Usuarios del portal solo ven sus propios datos - rf: RF-CRM-004 - reused_from_gamilit: false - sql: | - CREATE POLICY "portal_users_own_data" ON crm_portal.clientes_portal - FOR SELECT - TO authenticated - USING (id = get_current_portal_user_id()); - -# ============================================================================ -# IMPLEMENTACI脫N - BACKEND -# ============================================================================ - - backend: - modules: - - name: crm - path: apps/backend/src/modules/crm/ - description: M贸dulo principal de CRM - rf: [RF-CRM-001, RF-CRM-002, RF-CRM-003] - reused_from_gamilit: false - note: "Nuevo m贸dulo espec铆fico de CRM" - - - name: crm-portal - path: apps/backend/src/modules/crm-portal/ - description: M贸dulo de portal del cliente - rf: RF-CRM-004 - reused_from_gamilit: false - note: "API p煤blica para portal de clientes" - - services: - - name: ProspectosService - path: apps/backend/src/modules/crm/services/prospectos.service.ts - description: L贸gica de gesti贸n de prospectos - rf: RF-CRM-001 - reused_from_gamilit: false - methods: - - create() - - update() - - delete() - - findAll() - - findById() - - calcularScoring() - - asignarVendedor() - - cambiarEstado() - - - name: ScoringService - path: apps/backend/src/modules/crm/services/scoring.service.ts - description: Motor de scoring de prospectos - rf: RF-CRM-001 - reused_from_gamilit: false - methods: - - calcularScoringAutomatico() - - evaluarCriterios() - - actualizarCriterios() - - - name: InteraccionesService - path: apps/backend/src/modules/crm/services/interacciones.service.ts - description: Gesti贸n de interacciones comerciales - rf: RF-CRM-002 - reused_from_gamilit: false - methods: - - registrarInteraccion() - - findByProspecto() - - findByVendedor() - - programarSeguimiento() - - - name: PipelineService - path: apps/backend/src/modules/crm/services/pipeline.service.ts - description: Gesti贸n de pipeline de ventas - rf: RF-CRM-002 - reused_from_gamilit: false - methods: - - getEmbudoVentas() - - getKPIsVendedor() - - getMetasComerciales() - - getConversionRates() - - - name: CotizacionesService - path: apps/backend/src/modules/crm/services/cotizaciones.service.ts - description: Gesti贸n de cotizaciones - rf: RF-CRM-003 - reused_from_gamilit: false - methods: - - crear() - - actualizar() - - calcularPrecio() - - calcularFinanciamiento() - - generarPDF() - - enviarCliente() - - aceptar() - - rechazar() - - duplicar() - - - name: FinanciamientoService - path: apps/backend/src/modules/crm/services/financiamiento.service.ts - description: Motor de c谩lculo de financiamiento - rf: RF-CRM-003 - reused_from_gamilit: false - methods: - - calcularMensualidad() - - generarTablaAmortizacion() - - calcularEnganche() - - validarCreditoInfonavit() - - - name: PDFGeneratorService - path: apps/backend/src/modules/crm/services/pdf-generator.service.ts - description: Generaci贸n de PDFs de cotizaci贸n - rf: RF-CRM-003 - reused_from_gamilit: false - note: "Usa biblioteca como puppeteer o pdfmake" - methods: - - generarCotizacionPDF() - - generarPropuestaEconomica() - - - name: PortalClienteService - path: apps/backend/src/modules/crm-portal/services/portal-cliente.service.ts - description: L贸gica del portal del cliente - rf: RF-CRM-004 - reused_from_gamilit: false - methods: - - register() - - login() - - activarCuenta() - - recuperarPassword() - - getDashboard() - - getAvanceObra() - - getEstadoCuenta() - - solicitarServicio() - - - name: NotificacionesPortalService - path: apps/backend/src/modules/crm-portal/services/notificaciones-portal.service.ts - description: Env铆o de notificaciones a clientes - rf: RF-CRM-004 - reused_from_gamilit: false - methods: - - enviarEmail() - - enviarSMS() - - notificarHitoObra() - - notificarPagoProximo() - - - name: IntegracionMOB005Service - path: apps/backend/src/modules/crm-portal/services/integracion-mob005.service.ts - description: Integraci贸n con m贸dulo MOB-005 (Derechohabiente) - rf: RF-CRM-004 - reused_from_gamilit: false - note: "Consume API de MOB-005 para datos de avance" - methods: - - getDerechohabienteInfo() - - getAvanceObraDerechohabiente() - - getFotosObra() - - getHitosCompletados() - - controllers: - - name: ProspectosController - path: apps/backend/src/modules/crm/controllers/prospectos.controller.ts - description: Endpoints de prospectos - rf: RF-CRM-001 - reused_from_gamilit: false - endpoints: - - GET /api/crm/prospectos - - GET /api/crm/prospectos/:id - - POST /api/crm/prospectos - - PUT /api/crm/prospectos/:id - - DELETE /api/crm/prospectos/:id - - POST /api/crm/prospectos/:id/asignar-vendedor - - PUT /api/crm/prospectos/:id/estado - - - name: InteraccionesController - path: apps/backend/src/modules/crm/controllers/interacciones.controller.ts - description: Endpoints de interacciones - rf: RF-CRM-002 - reused_from_gamilit: false - endpoints: - - GET /api/crm/interacciones - - POST /api/crm/interacciones - - GET /api/crm/interacciones/prospecto/:id - - GET /api/crm/interacciones/vendedor/:id - - - name: PipelineController - path: apps/backend/src/modules/crm/controllers/pipeline.controller.ts - description: Endpoints de pipeline y KPIs - rf: RF-CRM-002 - reused_from_gamilit: false - endpoints: - - GET /api/crm/pipeline - - GET /api/crm/pipeline/vendedor/:id - - GET /api/crm/kpis/vendedor/:id - - GET /api/crm/kpis/proyecto/:id - - - name: CotizacionesController - path: apps/backend/src/modules/crm/controllers/cotizaciones.controller.ts - description: Endpoints de cotizaciones - rf: RF-CRM-003 - reused_from_gamilit: false - endpoints: - - GET /api/crm/cotizaciones - - GET /api/crm/cotizaciones/:id - - POST /api/crm/cotizaciones - - PUT /api/crm/cotizaciones/:id - - POST /api/crm/cotizaciones/:id/generar-pdf - - POST /api/crm/cotizaciones/:id/enviar-cliente - - POST /api/crm/cotizaciones/:id/aceptar - - POST /api/crm/cotizaciones/:id/rechazar - - POST /api/crm/cotizaciones/:id/duplicar - - GET /api/crm/cotizaciones/:id/versiones - - - name: PortalClienteController - path: apps/backend/src/modules/crm-portal/controllers/portal-cliente.controller.ts - description: Endpoints p煤blicos del portal del cliente - rf: RF-CRM-004 - reused_from_gamilit: false - note: "API p煤blica sin autenticaci贸n interna" - endpoints: - - POST /api/portal/register - - POST /api/portal/login - - POST /api/portal/activar - - POST /api/portal/recuperar-password - - GET /api/portal/dashboard - - GET /api/portal/avance-obra - - GET /api/portal/estado-cuenta - - POST /api/portal/solicitar-servicio - - GET /api/portal/notificaciones - - enums: - - name: LeadStatus - path: apps/backend/src/modules/crm/enums/lead-status.enum.ts - description: Enum de estados de prospecto - rf: RF-CRM-001 - reused_from: null - values: - - NUEVO = 'nuevo' - - CONTACTADO = 'contactado' - - CALIFICADO = 'calificado' - - HOT = 'hot' - - COLD = 'cold' - - PERDIDO = 'perdido' - - CONVERTIDO = 'convertido' - - - name: InteractionType - path: apps/backend/src/modules/crm/enums/interaction-type.enum.ts - description: Enum de tipos de interacci贸n - rf: RF-CRM-002 - reused_from: null - - - name: CotizacionStatus - path: apps/backend/src/modules/crm/enums/cotizacion-status.enum.ts - description: Enum de estados de cotizaci贸n - rf: RF-CRM-003 - reused_from: null - - - name: FinancingType - path: apps/backend/src/modules/crm/enums/financing-type.enum.ts - description: Enum de tipos de financiamiento - rf: RF-CRM-003 - reused_from: null - - dtos: - - name: CreateProspectoDto - path: apps/backend/src/modules/crm/dto/create-prospecto.dto.ts - rf: RF-CRM-001 - - - name: UpdateProspectoDto - path: apps/backend/src/modules/crm/dto/update-prospecto.dto.ts - rf: RF-CRM-001 - - - name: CreateInteraccionDto - path: apps/backend/src/modules/crm/dto/create-interaccion.dto.ts - rf: RF-CRM-002 - - - name: CreateCotizacionDto - path: apps/backend/src/modules/crm/dto/create-cotizacion.dto.ts - rf: RF-CRM-003 - - - name: CalcularFinanciamientoDto - path: apps/backend/src/modules/crm/dto/calcular-financiamiento.dto.ts - rf: RF-CRM-003 - - - name: PortalRegisterDto - path: apps/backend/src/modules/crm-portal/dto/portal-register.dto.ts - rf: RF-CRM-004 - - - name: PortalLoginDto - path: apps/backend/src/modules/crm-portal/dto/portal-login.dto.ts - rf: RF-CRM-004 - -# ============================================================================ -# IMPLEMENTACI脫N - FRONTEND -# ============================================================================ - - frontend: - features: - - name: crm - path: apps/frontend/src/features/crm/ - description: Feature de CRM (prospectos, cotizaciones) - rf: [RF-CRM-001, RF-CRM-002, RF-CRM-003] - reused_from_gamilit: false - - - name: portal-cliente - path: apps/portal-cliente/src/ - description: Aplicaci贸n separada para portal del cliente - rf: RF-CRM-004 - reused_from_gamilit: false - note: "App independiente Next.js para clientes" - - components: - # RF-CRM-001: Gesti贸n de Prospectos - - name: ProspectosList - path: apps/frontend/src/features/crm/components/ProspectosList.tsx - description: Lista de prospectos con filtros - rf: RF-CRM-001 - reused_from_gamilit: false - - - name: ProspectoForm - path: apps/frontend/src/features/crm/components/ProspectoForm.tsx - description: Formulario de captura/edici贸n de prospecto - rf: RF-CRM-001 - reused_from_gamilit: false - - - name: ProspectoDetailPanel - path: apps/frontend/src/features/crm/components/ProspectoDetailPanel.tsx - description: Panel lateral con detalle de prospecto - rf: RF-CRM-001 - reused_from_gamilit: false - - - name: ScoringIndicator - path: apps/frontend/src/features/crm/components/ScoringIndicator.tsx - description: Indicador visual de scoring - rf: RF-CRM-001 - reused_from_gamilit: false - - # RF-CRM-002: Seguimiento Comercial - - name: InteraccionesList - path: apps/frontend/src/features/crm/components/InteraccionesList.tsx - description: Timeline de interacciones - rf: RF-CRM-002 - reused_from_gamilit: false - - - name: InteraccionForm - path: apps/frontend/src/features/crm/components/InteraccionForm.tsx - description: Formulario de registro de interacci贸n - rf: RF-CRM-002 - reused_from_gamilit: false - - - name: PipelineBoard - path: apps/frontend/src/features/crm/components/PipelineBoard.tsx - description: Tablero Kanban de pipeline - rf: RF-CRM-002 - reused_from_gamilit: false - note: "Drag & drop de prospectos entre etapas" - - - name: KPIDashboard - path: apps/frontend/src/features/crm/components/KPIDashboard.tsx - description: Dashboard de KPIs comerciales - rf: RF-CRM-002 - reused_from_gamilit: false - note: "Gr谩ficas de conversi贸n, embudo, metas" - - - name: CalendarioActividades - path: apps/frontend/src/features/crm/components/CalendarioActividades.tsx - description: Calendario de visitas y seguimientos - rf: RF-CRM-002 - reused_from_gamilit: false - - # RF-CRM-003: Cotizaciones - - name: CotizacionesList - path: apps/frontend/src/features/crm/components/CotizacionesList.tsx - description: Lista de cotizaciones - rf: RF-CRM-003 - reused_from_gamilit: false - - - name: CotizacionWizard - path: apps/frontend/src/features/crm/components/CotizacionWizard.tsx - description: Wizard de creaci贸n de cotizaci贸n - rf: RF-CRM-003 - reused_from_gamilit: false - note: "5 pasos: Prospecto, Prototipo, Extras, Financiamiento, Resumen" - - - name: FinanciamientoCalculator - path: apps/frontend/src/features/crm/components/FinanciamientoCalculator.tsx - description: Calculadora de financiamiento - rf: RF-CRM-003 - reused_from_gamilit: false - - - name: CotizacionPDFViewer - path: apps/frontend/src/features/crm/components/CotizacionPDFViewer.tsx - description: Visor de PDF de cotizaci贸n - rf: RF-CRM-003 - reused_from_gamilit: false - - - name: CotizacionVersiones - path: apps/frontend/src/features/crm/components/CotizacionVersiones.tsx - description: Historial de versiones de cotizaci贸n - rf: RF-CRM-003 - reused_from_gamilit: false - - # RF-CRM-004: Portal del Cliente - - name: PortalLoginPage - path: apps/portal-cliente/src/components/PortalLoginPage.tsx - description: P谩gina de login del portal - rf: RF-CRM-004 - reused_from_gamilit: false - - - name: PortalDashboard - path: apps/portal-cliente/src/components/PortalDashboard.tsx - description: Dashboard principal del cliente - rf: RF-CRM-004 - reused_from_gamilit: false - - - name: AvanceObraViewer - path: apps/portal-cliente/src/components/AvanceObraViewer.tsx - description: Visualizaci贸n de avance de obra - rf: RF-CRM-004 - reused_from_gamilit: false - note: "Fotos, hitos, % avance desde MOB-005" - - - name: EstadoCuentaCliente - path: apps/portal-cliente/src/components/EstadoCuentaCliente.tsx - description: Estado de cuenta del cliente - rf: RF-CRM-004 - reused_from_gamilit: false - - - name: SolicitudServicioForm - path: apps/portal-cliente/src/components/SolicitudServicioForm.tsx - description: Formulario de solicitud de servicio - rf: RF-CRM-004 - reused_from_gamilit: false - - - name: NotificacionesPortal - path: apps/portal-cliente/src/components/NotificacionesPortal.tsx - description: Centro de notificaciones del portal - rf: RF-CRM-004 - reused_from_gamilit: false - - stores: - - name: prospectosStore - path: apps/frontend/src/stores/prospectosStore.ts - description: Store de prospectos - rf: RF-CRM-001 - reused_from_gamilit: false - - - name: interaccionesStore - path: apps/frontend/src/stores/interaccionesStore.ts - description: Store de interacciones - rf: RF-CRM-002 - reused_from_gamilit: false - - - name: cotizacionesStore - path: apps/frontend/src/stores/cotizacionesStore.ts - description: Store de cotizaciones - rf: RF-CRM-003 - reused_from_gamilit: false - - - name: portalClienteStore - path: apps/portal-cliente/src/stores/portalClienteStore.ts - description: Store del portal del cliente - rf: RF-CRM-004 - reused_from_gamilit: false - - pages: - - name: ProspectosPage - path: apps/frontend/src/pages/crm/prospectos/index.tsx - rf: RF-CRM-001 - - - name: PipelinePage - path: apps/frontend/src/pages/crm/pipeline/index.tsx - rf: RF-CRM-002 - - - name: KPIsPage - path: apps/frontend/src/pages/crm/kpis/index.tsx - rf: RF-CRM-002 - - - name: CotizacionesPage - path: apps/frontend/src/pages/crm/cotizaciones/index.tsx - rf: RF-CRM-003 - - - name: NuevaCotizacionPage - path: apps/frontend/src/pages/crm/cotizaciones/nueva.tsx - rf: RF-CRM-003 - - - name: PortalHomePage - path: apps/portal-cliente/src/pages/index.tsx - rf: RF-CRM-004 - - - name: PortalAvanceObraPage - path: apps/portal-cliente/src/pages/avance-obra.tsx - rf: RF-CRM-004 - -# ============================================================================ -# TESTING -# ============================================================================ - - testing: - unit_tests: - - module: ProspectosService - file: apps/backend/src/modules/crm/services/prospectos.service.spec.ts - coverage_target: 85% - reused_from_gamilit: false - - - module: ScoringService - file: apps/backend/src/modules/crm/services/scoring.service.spec.ts - coverage_target: 90% - reused_from_gamilit: false - - - module: CotizacionesService - file: apps/backend/src/modules/crm/services/cotizaciones.service.spec.ts - coverage_target: 85% - reused_from_gamilit: false - - - module: FinanciamientoService - file: apps/backend/src/modules/crm/services/financiamiento.service.spec.ts - coverage_target: 95% - reused_from_gamilit: false - note: "Cr铆tico - C谩lculos financieros" - - - module: PortalClienteService - file: apps/backend/src/modules/crm-portal/services/portal-cliente.service.spec.ts - coverage_target: 85% - reused_from_gamilit: false - - - module: IntegracionMOB005Service - file: apps/backend/src/modules/crm-portal/services/integracion-mob005.service.spec.ts - coverage_target: 80% - reused_from_gamilit: false - - e2e_tests: - - name: CRM Prospectos E2E - file: apps/backend/test/crm/prospectos.e2e-spec.ts - scenarios: - - Crear prospecto y calcular scoring - - Asignar prospecto a vendedor - - Cambiar estado de prospecto - - Buscar prospectos con filtros - reused_from_gamilit: false - - - name: CRM Cotizaciones E2E - file: apps/backend/test/crm/cotizaciones.e2e-spec.ts - scenarios: - - Crear cotizaci贸n completa - - Calcular financiamiento - - Generar PDF - - Enviar cotizaci贸n a cliente - - Aceptar/rechazar cotizaci贸n - - Crear nueva versi贸n - reused_from_gamilit: false - - - name: Portal Cliente E2E - file: apps/backend/test/portal/portal-cliente.e2e-spec.ts - scenarios: - - Registro de cliente - - Activaci贸n de cuenta - - Login al portal - - Consultar avance de obra - - Solicitar servicio post-venta - reused_from_gamilit: false - - integration_tests: - - name: Integraci贸n MOB-005 - file: apps/backend/test/integration/mob005-integration.spec.ts - scenarios: - - Obtener datos de derechohabiente - - Consultar avance de obra desde MOB-005 - - Sincronizar hitos de construcci贸n - reused_from_gamilit: false - note: "Requiere MOB-005 implementado o mock" - - - name: PDF Generation Integration - file: apps/backend/test/integration/pdf-generation.spec.ts - scenarios: - - Generar PDF de cotizaci贸n - - Validar formato y contenido - - Enviar PDF por email - reused_from_gamilit: false - -# ============================================================================ -# INTEGRATIONS -# ============================================================================ - -integrations: - internal_modules: - - module: MOB-005 - name: Derechohabiente - description: M贸dulo m贸vil de seguimiento de derechohabiente - integration_type: REST API - direction: consume - rf: RF-CRM-004 - endpoints: - - GET /api/mob/derechohabiente/:id - - GET /api/mob/derechohabiente/:id/avance-obra - - GET /api/mob/derechohabiente/:id/fotos-obra - - GET /api/mob/derechohabiente/:id/hitos - note: "Portal del cliente consume datos de avance de obra desde MOB-005" - - - module: MAI-002 - name: Proyectos y Estructura - description: Informaci贸n de proyectos y prototipos - integration_type: Database FK - direction: consume - rf: [RF-CRM-001, RF-CRM-003] - tables: - - proyectos.proyectos - - proyectos.prototipos - - proyectos.lotes - - - module: MAI-003 - name: Contratos y Ventas - description: Conversi贸n de cotizaci贸n a contrato - integration_type: REST API + Event - direction: provide - rf: RF-CRM-003 - events: - - cotizacion.aceptada - - prospecto.convertido - note: "MAI-003 consume cotizaciones aceptadas para generar contratos" - - external_services: - - name: Email Service (SendGrid/AWS SES) - description: Env铆o de emails a clientes - rf: RF-CRM-004 - note: "Notificaciones del portal" - - - name: SMS Service (Twilio) - description: Env铆o de SMS - rf: RF-CRM-004 - note: "Notificaciones urgentes a clientes" - - - name: PDF Generator (Puppeteer) - description: Generaci贸n de PDFs - rf: RF-CRM-003 - note: "Cotizaciones en PDF" - - - name: WhatsApp Business API - description: Integraci贸n con WhatsApp - rf: RF-CRM-002 - note: "Opcional: Registro de interacciones por WhatsApp" - -# ============================================================================ -# M脡TRICAS -# ============================================================================ - -metrics: - story_points: - planned: 120 - completed: 0 - variance: 0% - - budget: - planned: 85000 - actual: 0 - variance: 0% - - reuse_from_gamilit: - infrastructure: 20% - database: 0% - backend: 30% - frontend: 25% - overall: 19% - - complexity: - database: high - backend: high - frontend: medium - integrations: high - - time_saved_weeks: 0.5 - note: "Baja reutilizaci贸n - M贸dulo espec铆fico de CRM" - -# ============================================================================ -# ROADMAP -# ============================================================================ - -roadmap: - sprint_8: - weeks: [8] - goal: "RF-CRM-001 - Gesti贸n de Prospectos" - tasks: - - Dise帽ar schema crm.prospectos - - Implementar ProspectosService y API - - Desarrollar motor de scoring - - UI de captura y lista de prospectos - - Tests unitarios - story_points: 26 - deliverables: - - "Sistema de captura de prospectos funcional" - - "Motor de scoring autom谩tico" - - "Asignaci贸n de prospectos a vendedores" - - sprint_9: - weeks: [9] - goal: "RF-CRM-002 - Seguimiento Comercial" - tasks: - - Dise帽ar schema crm_comercial - - Implementar InteraccionesService y PipelineService - - Desarrollar tablero de pipeline - - Dashboard de KPIs comerciales - - Calendario de actividades - - Tests E2E - story_points: 29 - deliverables: - - "Pipeline de ventas con Kanban" - - "Dashboard de KPIs por vendedor" - - "Sistema de registro de interacciones" - - sprint_10: - weeks: [10] - goal: "RF-CRM-003 - Sistema de Cotizaciones" - tasks: - - Dise帽ar schema cotizaciones - - Implementar motor de financiamiento - - Desarrollar wizard de cotizaci贸n - - Integrar generaci贸n de PDF - - Versionamiento de cotizaciones - - Tests de c谩lculos financieros - story_points: 39 - deliverables: - - "Wizard completo de cotizaci贸n" - - "Motor de c谩lculo de financiamiento certificado" - - "Generaci贸n de PDF profesional" - - "Sistema de versiones" - - sprint_11: - weeks: [11] - goal: "RF-CRM-004 - Portal del Cliente" - tasks: - - Dise帽ar schema crm_portal - - Implementar autenticaci贸n de portal - - Desarrollar frontend de portal-cliente app - - Integrar con MOB-005 - - Sistema de notificaciones - - Solicitudes de servicio post-venta - - Tests E2E del portal - story_points: 26 - deliverables: - - "Portal del cliente funcional" - - "Integraci贸n con MOB-005 para avance de obra" - - "Sistema de notificaciones email/SMS" - - "M贸dulo de solicitudes post-venta" - -# ============================================================================ -# RISKS & CHALLENGES -# ============================================================================ - -risks: - - risk: "Complejidad del motor de financiamiento" - impact: high - probability: medium - mitigation: "Validar f贸rmulas con experto financiero, tests exhaustivos" - - - risk: "Integraci贸n con MOB-005 requiere que est茅 implementado" - impact: high - probability: medium - mitigation: "Crear mock de API de MOB-005 para desarrollo en paralelo" - - - risk: "Generaci贸n de PDF puede ser lenta" - impact: medium - probability: high - mitigation: "Usar cola de jobs (Bull/BullMQ) para generaci贸n as铆ncrona" - - - risk: "Seguridad del portal del cliente" - impact: high - probability: low - mitigation: "Autenticaci贸n separada, rate limiting, auditor铆a de accesos" - - - risk: "C谩lculos de financiamiento incorrectos" - impact: critical - probability: low - mitigation: "Coverage 95%+ en tests de FinanciamientoService, validaci贸n manual" - -# ============================================================================ -# DEPENDENCIES -# ============================================================================ - -dependencies: - prerequisites: - - MAI-001: "Fundamentos (autenticaci贸n, multi-tenancy)" - - MAI-002: "Proyectos y Estructura (cat谩logo de prototipos)" - - parallel: - - MAI-003: "Contratos y Ventas (integraci贸n bidireccional)" - - MOB-005: "Derechohabiente (para portal del cliente)" - - blocking: - - "MOB-005 debe exponerse API de avance de obra para RF-CRM-004" - -# ============================================================================ -# NOTAS -# ============================================================================ - -notes: - - "M贸dulo 100% nuevo - Sin reutilizaci贸n de GAMILIT" - - "Motor de financiamiento cr铆tico - Requiere validaci贸n contable" - - "Portal del cliente es app separada (Next.js standalone)" - - "Integraci贸n con MOB-005 es feature diferenciadora clave" - - "Considerar WhatsApp Business API para seguimiento comercial" - - "PDF templates deben ser configurables por constructora" - - "Scoring de prospectos debe ser ajustable por reglas de negocio" - - "Pipeline debe soportar m煤ltiples embudos por tipo de proyecto" - - "Sistema de notificaciones debe ser escalable (miles de clientes)" - - "Considerar integraci贸n futura con Facebook Leads Ads" diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/README.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/README.md deleted file mode 100644 index dae8bf880..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# MAE-014: Finanzas y Controlling - -**M贸dulo:** Gesti贸n Financiera y Control de Gesti贸n -**Story Points:** 55 | **Prioridad:** Alta | **Fase:** 2 (Enterprise) - -## Descripci贸n General - -Sistema integral para gesti贸n financiera de proyectos de construcci贸n, incluyendo flujo de efectivo, cuentas por cobrar/pagar, conciliaci贸n bancaria, y control de gesti贸n con KPIs financieros. - -## Alcance Funcional - -### 1. Flujo de Efectivo (Cash Flow) -- Proyecci贸n de ingresos y egresos -- Flujo real vs proyectado -- Alertas de liquidez -- Escenarios what-if - -### 2. Cuentas por Cobrar (CxC) -- Facturaci贸n a clientes -- Antig眉edad de saldos -- Gesti贸n de cobranza -- Conciliaci贸n de pagos - -### 3. Cuentas por Pagar (CxP) -- Registro de facturas de proveedores -- Programaci贸n de pagos -- Antig眉edad de saldos -- Control de vencimientos - -### 4. Conciliaci贸n Bancaria -- Importaci贸n de estados de cuenta -- Match autom谩tico de movimientos -- Partidas en conciliaci贸n -- Reportes de conciliaci贸n - -### 5. Control de Gesti贸n -- Dashboard financiero ejecutivo -- KPIs por proyecto y empresa -- An谩lisis de rentabilidad -- Presupuesto vs real - -## Componentes T茅cnicos - -### Backend (NestJS + TypeORM) -```typescript -@Module({ - imports: [TypeOrmModule.forFeature([ - CashFlow, Invoice, Payment, BankStatement, - Receivable, Payable, BankReconciliation - ])], - providers: [ - CashFlowService, InvoiceService, PaymentService, - BankReconciliationService, ControllingService - ], - controllers: [FinanceController, ControllingController] -}) -export class FinanceModule {} -``` - -### Base de Datos (PostgreSQL) -```sql -CREATE SCHEMA finance; - -CREATE TYPE finance.transaction_type AS ENUM ('income', 'expense'); -CREATE TYPE finance.payment_status AS ENUM ('pending', 'partial', 'paid', 'overdue'); -CREATE TYPE finance.reconciliation_status AS ENUM ('pending', 'matched', 'exception'); -``` - -## Integraciones - -- **MAI-008 (Estimaciones):** Generaci贸n autom谩tica de CxC desde estimaciones -- **MAI-004 (Compras):** Registro autom谩tico de CxP desde 贸rdenes de compra -- **MAI-012 (Contratos):** Montos contratados para proyecciones - -## M茅tricas Clave - -- **Liquidez:** D铆as de cobertura de efectivo -- **DSO:** Days Sales Outstanding (d铆as de cobranza) -- **DPO:** Days Payable Outstanding (d铆as de pago) -- **Rentabilidad:** Margen por proyecto - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/_MAP.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/_MAP.md deleted file mode 100644 index 44241bd29..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/_MAP.md +++ /dev/null @@ -1,698 +0,0 @@ -# _MAP: MAE-014 - Finanzas y Controlling de Obra - -**脡pica:** MAE-014 -**Nombre:** Finanzas y Controlling de Obra -**Fase:** 2 - Enterprise B谩sico -**Presupuesto:** $45,000 MXN -**Story Points:** 80 SP -**Estado:** 馃摑 A crear -**Sprint:** Sprint 7-8 (Semanas 13-16) -**脷ltima actualizaci贸n:** 2025-11-17 -**Prioridad:** P1 - ---- - -## 馃搵 Prop贸sito - -M贸dulo enterprise de gesti贸n financiera integrada a nivel proyecto, elevando el sistema a competidor directo de ERPs como SAP S/4HANA Construction: -- Libro mayor integrado con proyectos y centros de costo -- Cuentas por pagar/cobrar ligadas a compras y estimaciones -- Flujo de efectivo proyectado vs real por obra -- Integraci贸n con sistemas contables externos (SAP, CONTPAQi) -- Conciliaci贸n bancaria por proyecto -- Reportes financieros (balance, PyG, cash flow) - -**Integraci贸n clave:** Se vincula con todos los m贸dulos operativos: Estimaciones (MAI-008), Compras (MAI-004), Contratos (MAI-012), Proyectos (MAI-002). - -**Diferenciador:** Sistema financiero nativo vs integraciones de terceros (como Procore). - ---- - -## 馃搧 Contenido - -### Requerimientos Funcionales (Estimados: 6) - -| ID | T铆tulo | Estado | -|----|--------|--------| -| RF-FIN-001 | Libro mayor y cat谩logo de cuentas contables | 馃摑 A crear | -| RF-FIN-002 | Cuentas por pagar ligadas a compras y contratos | 馃摑 A crear | -| RF-FIN-003 | Cuentas por cobrar ligadas a estimaciones | 馃摑 A crear | -| RF-FIN-004 | Flujo de efectivo proyectado vs real por obra | 馃摑 A crear | -| RF-FIN-005 | Conciliaci贸n bancaria por proyecto | 馃摑 A crear | -| RF-FIN-006 | Integraci贸n con ERP contable externo | 馃摑 A crear | - -### Especificaciones T茅cnicas (Estimadas: 6) - -| ID | T铆tulo | RF | Estado | -|----|--------|----|--------| -| ET-FIN-001 | Modelo de datos de contabilidad por proyecto | RF-FIN-001 | 馃摑 A crear | -| ET-FIN-002 | Sistema de cuentas por pagar y aging | RF-FIN-002 | 馃摑 A crear | -| ET-FIN-003 | Sistema de cuentas por cobrar y cobranza | RF-FIN-003 | 馃摑 A crear | -| ET-FIN-004 | Motor de proyecci贸n de cash flow | RF-FIN-004 | 馃摑 A crear | -| ET-FIN-005 | Conciliaci贸n autom谩tica de movimientos bancarios | RF-FIN-005 | 馃摑 A crear | -| ET-FIN-006 | API de integraci贸n con SAP/CONTPAQi | RF-FIN-006 | 馃摑 A crear | - -### Historias de Usuario (Estimadas: 16) - -| ID | T铆tulo | SP | Estado | -|----|--------|----|--------| -| US-FIN-001 | Configurar cat谩logo de cuentas contables | 5 | 馃摑 A crear | -| US-FIN-002 | Generar p贸liza contable desde compra | 5 | 馃摑 A crear | -| US-FIN-003 | Generar p贸liza contable desde estimaci贸n | 5 | 馃摑 A crear | -| US-FIN-004 | Registrar cuenta por pagar a proveedor | 5 | 馃摑 A crear | -| US-FIN-005 | Registrar pago a proveedor | 5 | 馃摑 A crear | -| US-FIN-006 | Consultar aging de cuentas por pagar | 5 | 馃摑 A crear | -| US-FIN-007 | Registrar cuenta por cobrar a cliente | 5 | 馃摑 A crear | -| US-FIN-008 | Registrar cobro de cliente | 5 | 馃摑 A crear | -| US-FIN-009 | Consultar aging de cuentas por cobrar | 5 | 馃摑 A crear | -| US-FIN-010 | Proyectar flujo de efectivo por obra | 5 | 馃摑 A crear | -| US-FIN-011 | Comparar cash flow proyectado vs real | 5 | 馃摑 A crear | -| US-FIN-012 | Conciliar movimientos bancarios | 5 | 馃摑 A crear | -| US-FIN-013 | Generar reporte de balance por proyecto | 5 | 馃摑 A crear | -| US-FIN-014 | Generar reporte de PyG por proyecto | 5 | 馃摑 A crear | -| US-FIN-015 | Exportar p贸lizas a SAP/CONTPAQi | 5 | 馃摑 A crear | -| US-FIN-016 | Dashboard financiero ejecutivo | 5 | 馃摑 A crear | - -**Total Story Points:** 80 SP - -### Implementaci贸n - -馃搳 **Inventarios de trazabilidad:** -- [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) - Matriz completa de trazabilidad -- [DATABASE.yml](./implementacion/DATABASE.yml) - Objetos de base de datos -- [BACKEND.yml](./implementacion/BACKEND.yml) - M贸dulos backend -- [FRONTEND.yml](./implementacion/FRONTEND.yml) - Componentes frontend - -### Pruebas - -馃搵 Documentaci贸n de testing: -- [TEST-PLAN.md](./pruebas/TEST-PLAN.md) - Plan de pruebas -- [TEST-CASES.md](./pruebas/TEST-CASES.md) - Casos de prueba - ---- - -## 馃敆 Referencias - -- **README:** [README.md](./README.md) - Descripci贸n detallada de la 茅pica -- **Fase 2:** [../README.md](../README.md) - Informaci贸n de la fase completa -- **M贸dulo relacionado MVP:** M贸dulo 14 - Finanzas y Controlling (MVP-APP.md) - ---- - -## 馃搳 M茅tricas - -| M茅trica | Valor | -|---------|-------| -| **Presupuesto estimado** | $45,000 MXN | -| **Story Points estimados** | 80 SP | -| **Duraci贸n estimada** | 16 d铆as | -| **Reutilizaci贸n GAMILIT** | 5% (funcionalidad enterprise nueva) | -| **RF a implementar** | 6/6 | -| **ET a implementar** | 6/6 | -| **US a completar** | 16/16 | - ---- - -## 馃幆 M贸dulos Afectados - -### Base de Datos -- **Schema:** `finance` -- **Tablas principales:** - * `chart_of_accounts` - Cat谩logo de cuentas contables - * `accounting_entries` - P贸lizas contables - * `accounting_entry_lines` - Detalle de p贸lizas (debe/haber) - * `accounts_payable` - Cuentas por pagar - * `ap_payments` - Pagos a proveedores - * `accounts_receivable` - Cuentas por cobrar - * `ar_payments` - Cobros de clientes - * `cash_flow_projections` - Proyecciones de flujo - * `bank_accounts` - Cuentas bancarias - * `bank_movements` - Movimientos bancarios - * `bank_reconciliation` - Conciliaci贸n bancaria - * `cost_centers` - Centros de costo (hereda de admin) -- **ENUMs:** - * `account_type` (asset, liability, equity, income, expense) - * `account_nature` (debit, credit) - * `entry_type` (purchase, sale, payment, collection, adjustment) - * `payment_status` (pending, paid, partial, overdue, cancelled) - * `payment_method` (cash, check, transfer, card) - -### Backend -- **M贸dulo:** `finance` -- **Path:** `apps/backend/src/modules/finance/` -- **Services:** - * AccountingService - * APService (Accounts Payable) - * ARService (Accounts Receivable) - * CashFlowService - * BankReconciliationService - * FinancialReportsService - * ERPIntegrationService -- **Controllers:** - * AccountingController - * APController - * ARController - * CashFlowController - * ReportsController -- **Middlewares:** FinancialAccessGuard, AccountingPeriodGuard - -### Frontend -- **Features:** `finance`, `accounting`, `cash-flow`, `reports` -- **Path:** `apps/frontend/src/features/finance/` -- **Componentes:** - * ChartOfAccountsManager - * AccountingEntryForm - * AccountingEntryList - * APDashboard - * APPaymentForm - * APAgingReport - * ARDashboard - * ARCollectionForm - * ARAgingReport - * CashFlowProjection - * CashFlowComparison - * BankReconciliationTool - * FinancialDashboard - * BalanceSheetReport - * PnLReport - * CostCenterAnalysis -- **Stores:** financeStore, accountingStore, cashFlowStore - ---- - -## 馃挵 Cat谩logo de Cuentas Contables - -### Estructura de Cuenta Contable - -```yaml -account: - code: "5101-001-01" # C贸digo jer谩rquico - name: "Materiales de construcci贸n - Cemento" - type: "expense" # Tipo: activo, pasivo, capital, ingreso, gasto - nature: "debit" # Naturaleza: debe o haber - level: 3 # Nivel jer谩rquico (1=mayor, 2=submay or, 3=detalle) - parent_code: "5101-001" # Cuenta padre - cost_center_required: true # Requiere imputaci贸n a centro de costo - project_required: true # Requiere asignaci贸n a proyecto - status: "active" - sap_code: "410010001" # C贸digo equivalente en SAP (si aplica) - contpaqi_code: "510-001-001" # C贸digo en CONTPAQi -``` - -### Cat谩logo T铆pico para Construcci贸n - -``` -ACTIVOS (1000) -鈹溾攢鈹 ACTIVO CIRCULANTE (1100) -鈹 鈹溾攢鈹 Bancos (1101) -鈹 鈹 鈹溾攢鈹 Banco BBVA Cuenta General (1101-001) -鈹 鈹 鈹溾攢鈹 Banco BBVA Obra A (1101-002) -鈹 鈹 鈹斺攢鈹 Banco Santander N贸mina (1101-003) -鈹 鈹溾攢鈹 Cuentas por Cobrar (1102) -鈹 鈹 鈹溾攢鈹 Clientes (1102-001) -鈹 鈹 鈹溾攢鈹 Estimaciones por Cobrar (1102-002) -鈹 鈹 鈹斺攢鈹 Anticipos Otorgados (1102-003) -鈹 鈹斺攢鈹 Inventarios (1103) -鈹 鈹溾攢鈹 Materiales en Almac茅n (1103-001) -鈹 鈹斺攢鈹 Obra en Proceso (1103-002) -鈹溾攢鈹 ACTIVO FIJO (1200) -鈹 鈹溾攢鈹 Maquinaria y Equipo (1201) -鈹 鈹溾攢鈹 Veh铆culos (1202) -鈹 鈹斺攢鈹 Equipo de Oficina (1203) - -PASIVOS (2000) -鈹溾攢鈹 PASIVO A CORTO PLAZO (2100) -鈹 鈹溾攢鈹 Proveedores (2101) -鈹 鈹溾攢鈹 Acreedores Diversos (2102) -鈹 鈹溾攢鈹 Retenciones por Pagar (2103) -鈹 鈹 鈹溾攢鈹 Retenciones IMSS (2103-001) -鈹 鈹 鈹溾攢鈹 Retenciones INFONAVIT (2103-002) -鈹 鈹 鈹斺攢鈹 Retenciones ISR (2103-003) -鈹 鈹斺攢鈹 Estimaciones por Pagar (2104) - -CAPITAL (3000) -鈹溾攢鈹 Capital Social (3101) -鈹溾攢鈹 Utilidades Retenidas (3102) -鈹斺攢鈹 Utilidad del Ejercicio (3103) - -INGRESOS (4000) -鈹溾攢鈹 Ingresos por Obra (4101) -鈹 鈹溾攢鈹 Venta de Vivienda (4101-001) -鈹 鈹溾攢鈹 Obra por Administraci贸n (4101-002) -鈹 鈹斺攢鈹 Obra Extraordinaria (4101-003) - -GASTOS (5000) -鈹溾攢鈹 COSTO DE VENTAS (5100) -鈹 鈹溾攢鈹 Materiales (5101) -鈹 鈹 鈹溾攢鈹 Cemento (5101-001) -鈹 鈹 鈹溾攢鈹 Acero (5101-002) -鈹 鈹 鈹溾攢鈹 Block (5101-003) -鈹 鈹 鈹斺攢鈹 Arena y Grava (5101-004) -鈹 鈹溾攢鈹 Mano de Obra Directa (5102) -鈹 鈹溾攢鈹 Subcontratos (5103) -鈹 鈹斺攢鈹 Maquinaria y Equipo (5104) -鈹溾攢鈹 GASTOS DE OPERACI脫N (5200) -鈹 鈹溾攢鈹 Sueldos Administrativos (5201) -鈹 鈹溾攢鈹 Renta de Oficinas (5202) -鈹 鈹斺攢鈹 Servicios (5203) -鈹斺攢鈹 GASTOS FINANCIEROS (5300) - 鈹斺攢鈹 Intereses Bancarios (5301) -``` - ---- - -## 馃搾 P贸lizas Contables - -### Tipos de P贸lizas - -| Tipo | Fuente | Generaci贸n | Ejemplo | -|------|--------|------------|---------| -| **Ingresos** | Estimaciones cobradas | Autom谩tica | Cobro de estimaci贸n a INFONAVIT | -| **Egresos** | Pagos a proveedores | Autom谩tica | Pago de factura de cemento | -| **Diario** | Ajustes, depreciaci贸n | Manual | Depreciaci贸n mensual de maquinaria | -| **Traspaso** | Reclasificaciones | Manual | Reclasificaci贸n de anticipos | - ---- - -### Ejemplo de P贸liza: Compra de Materiales - -**Transacci贸n:** Compra de 100 toneladas de cemento por $150,000 + IVA - -```yaml -accounting_entry: - id: "POL-2025-001234" - type: "purchase" - date: "2025-11-17" - description: "Compra cemento Portland - Proveedor Cementos Mexicanos" - reference: "Factura A-12345" - source_module: "purchases" - source_id: "PO-2025-456" # Orden de compra - project_id: "PROJ-001" - cost_center: "Obra A - Etapa 1" - created_by: "Sistema" - approved_by: "Dir. Finanzas" - status: "posted" - - lines: - - line_number: 1 - account_code: "5101-001" # Materiales - Cemento - description: "100 ton cemento Portland" - debit: 150000.00 - credit: 0.00 - cost_center: "Obra A - Etapa 1" - project_id: "PROJ-001" - - - line_number: 2 - account_code: "1105-001" # IVA Acreditable - description: "IVA 16%" - debit: 24000.00 - credit: 0.00 - cost_center: null - project_id: null - - - line_number: 3 - account_code: "2101-001" # Proveedores - description: "Cementos Mexicanos SA" - debit: 0.00 - credit: 174000.00 - cost_center: null - project_id: null - - totals: - total_debit: 174000.00 - total_credit: 174000.00 - balanced: true -``` - ---- - -### Ejemplo de P贸liza: Estimaci贸n Cobrada - -**Transacci贸n:** Cobro de estimaci贸n #3 a INFONAVIT por $5,000,000 - -```yaml -accounting_entry: - id: "POL-2025-001235" - type: "collection" - date: "2025-11-17" - description: "Cobro estimaci贸n #3 - INFONAVIT Fraccionamiento Los Pinos" - reference: "Transferencia 789456" - source_module: "estimations" - source_id: "EST-2025-003" - project_id: "PROJ-001" - status: "posted" - - lines: - - line_number: 1 - account_code: "1101-002" # Banco BBVA Obra A - description: "Transferencia INFONAVIT" - debit: 5000000.00 - credit: 0.00 - - - line_number: 2 - account_code: "1102-002" # Estimaciones por Cobrar - description: "Aplicaci贸n estimaci贸n #3" - debit: 0.00 - credit: 5000000.00 - project_id: "PROJ-001" - - totals: - total_debit: 5000000.00 - total_credit: 5000000.00 - balanced: true -``` - ---- - -## 馃摜 Cuentas por Pagar (AP) - -### Aging de Cuentas por Pagar - -| Proveedor | Factura | Fecha | Monto | Vencimiento | D铆as | Estado | Proyecto | -|-----------|---------|-------|-------|-------------|------|--------|----------| -| Cementos MX | A-12345 | 2025-10-15 | $174,000 | 2025-11-14 | **3 d铆as vencida** | 馃敶 Vencida | Obra A | -| Aceros del Norte | B-567 | 2025-11-01 | $350,000 | 2025-12-01 | 14 d铆as | 馃煝 Vigente | Obra A | -| Instalaciones SA | C-890 | 2025-11-10 | $125,000 | 2025-12-10 | 23 d铆as | 馃煝 Vigente | Obra B | -| Subcontratista XYZ | S-123 | 2025-10-01 | $500,000 | 2025-10-31 | **17 d铆as vencida** | 馃敶 Vencida | Obra A | - -**Resumen:** -- Total por pagar: $1,149,000 -- Vencidas: $674,000 (59%) -- Por vencer 0-30 d铆as: $475,000 (41%) - ---- - -### Flujo de Cuenta por Pagar - -1. **Origen:** Orden de compra aprobada -2. **Recepci贸n:** Entrada de mercanc铆a al almac茅n -3. **Factura:** Proveedor env铆a factura -4. **Validaci贸n:** Se valida factura vs OC y recepci贸n (3-way match) -5. **Registro:** Se crea cuenta por pagar -6. **Aprobaci贸n:** Finanzas aprueba para pago -7. **Programaci贸n:** Se programa pago seg煤n fecha de vencimiento -8. **Pago:** Se ejecuta pago (transferencia/cheque) -9. **Conciliaci贸n:** Se concilia pago con estado de cuenta bancario - ---- - -## 馃摛 Cuentas por Cobrar (AR) - -### Aging de Cuentas por Cobrar - -| Cliente | Estimaci贸n | Fecha | Monto | Vencimiento | D铆as | Estado | Proyecto | -|---------|------------|-------|-------|-------------|------|--------|----------| -| INFONAVIT | EST-003 | 2025-10-01 | $5,000,000 | 2025-10-31 | **17 d铆as vencida** | 馃敶 Vencida | Obra A | -| Fideicomiso XYZ | EST-002 | 2025-11-10 | $2,500,000 | 2025-12-10 | 23 d铆as | 馃煝 Vigente | Obra B | -| Desarrollador ABC | EST-001 | 2025-11-15 | $1,000,000 | 2025-12-15 | 28 d铆as | 馃煝 Vigente | Obra C | - -**Resumen:** -- Total por cobrar: $8,500,000 -- Vencidas: $5,000,000 (59%) -- Por vencer 0-30 d铆as: $3,500,000 (41%) - -**Acciones:** -- 馃敶 Seguimiento urgente con INFONAVIT (17 d铆as vencida) -- 馃摓 Llamada a contacto de cobranza -- 馃摟 Email de recordatorio formal - ---- - -## 馃挼 Flujo de Efectivo (Cash Flow) - -### Proyecci贸n de Cash Flow - -**Proyecto: Fraccionamiento Los Pinos** -**Periodo: Noviembre 2025** - -#### Ingresos Proyectados - -| Concepto | Semana 1 | Semana 2 | Semana 3 | Semana 4 | Total mes | -|----------|----------|----------|----------|----------|-----------| -| Cobro estimaci贸n #3 | $5,000,000 | - | - | - | $5,000,000 | -| Cobro estimaci贸n #4 | - | - | $4,500,000 | - | $4,500,000 | -| Venta de viviendas | $500,000 | $750,000 | $500,000 | $1,000,000 | $2,750,000 | -| **Total ingresos** | **$5,500,000** | **$750,000** | **$5,000,000** | **$1,000,000** | **$12,250,000** | - -#### Egresos Proyectados - -| Concepto | Semana 1 | Semana 2 | Semana 3 | Semana 4 | Total mes | -|----------|----------|----------|----------|----------|-----------| -| Pago a proveedores | $1,500,000 | $1,200,000 | $1,500,000 | $1,000,000 | $5,200,000 | -| Pago a subcontratistas | $800,000 | $600,000 | $900,000 | $500,000 | $2,800,000 | -| N贸mina | - | $400,000 | - | $400,000 | $800,000 | -| IMSS/INFONAVIT | $150,000 | - | - | $150,000 | $300,000 | -| Gastos operativos | $100,000 | $100,000 | $100,000 | $100,000 | $400,000 | -| **Total egresos** | **$2,550,000** | **$2,300,000** | **$2,500,000** | **$2,150,000** | **$9,500,000** | - -#### Saldo de Efectivo - -| Concepto | Semana 1 | Semana 2 | Semana 3 | Semana 4 | -|----------|----------|----------|----------|----------| -| Saldo inicial | $2,000,000 | $4,950,000 | $3,400,000 | $5,900,000 | -| (+) Ingresos | $5,500,000 | $750,000 | $5,000,000 | $1,000,000 | -| (-) Egresos | ($2,550,000) | ($2,300,000) | ($2,500,000) | ($2,150,000) | -| **Saldo final** | **$4,950,000** | **$3,400,000** | **$5,900,000** | **$4,750,000** | - -**An谩lisis:** -- 鉁 Liquidez positiva durante todo el mes -- 鈿狅笍 Semana 2 con menor saldo ($3.4M), monitorear -- 鉁 Capacidad para cubrir compromisos - ---- - -### Comparaci贸n Proyectado vs Real - -**An谩lisis de Varianza - Octubre 2025** - -| Concepto | Proyectado | Real | Varianza | % Var | -|----------|------------|------|----------|-------| -| **Ingresos totales** | $10,000,000 | $9,500,000 | -$500,000 | -5% | -| - Estimaciones | $8,000,000 | $7,500,000 | -$500,000 | -6.25% | -| - Ventas | $2,000,000 | $2,000,000 | $0 | 0% | -| **Egresos totales** | $8,500,000 | $9,000,000 | $500,000 | 5.88% | -| - Materiales | $4,000,000 | $4,500,000 | $500,000 | 12.5% | -| - Mano de obra | $2,500,000 | $2,400,000 | -$100,000 | -4% | -| - Subcontratos | $2,000,000 | $2,100,000 | $100,000 | 5% | -| **Flujo neto** | **$1,500,000** | **$500,000** | **-$1,000,000** | **-66.7%** | - -**Causas de varianza:** -- 馃敶 Estimaci贸n #3 retrasada por INFONAVIT (impacto -$500K) -- 馃敶 Sobrecosto en materiales por incremento de precios (+12.5%) -- 馃煝 Ahorro en mano de obra directa (-4%) - -**Acciones:** -- Seguimiento urgente de estimaci贸n #3 -- Revisi贸n de precios con proveedores -- Ajuste de proyecci贸n para noviembre - ---- - -## 馃彟 Conciliaci贸n Bancaria - -### Proceso de Conciliaci贸n - -1. **Importaci贸n:** Descarga de estado de cuenta bancario (archivo .xlsx o API) -2. **Matching autom谩tico:** Sistema vincula movimientos con registros contables -3. **Partidas en conciliaci贸n:** Se identifican diferencias -4. **Ajustes:** Se registran ajustes necesarios (comisiones, intereses, errores) -5. **Validaci贸n:** Se confirma saldo conciliado = saldo en bancos - ---- - -### Ejemplo de Conciliaci贸n - -**Banco:** BBVA Obra A -**Periodo:** Octubre 2025 -**Fecha de corte:** 2025-10-31 - -| Concepto | Monto | -|----------|-------| -| **Saldo seg煤n bancos** | $4,850,000 | -| (-) Cheques en tr谩nsito | -$50,000 | -| (+) Dep贸sitos en tr谩nsito | +$200,000 | -| (-) Comisiones bancarias no registradas | -$5,000 | -| (+) Intereses ganados no registrados | +$1,000 | -| **Saldo seg煤n libros** | **$4,996,000** | - -**Estado:** 鉁 Conciliado - -**Ajustes a registrar:** -- Comisi贸n bancaria: $5,000 (cuenta 5301 - Gastos Financieros) -- Intereses ganados: $1,000 (cuenta 4201 - Productos Financieros) - ---- - -## 馃搳 Reportes Financieros - -### 1. Balance General por Proyecto - -**Proyecto:** Fraccionamiento Los Pinos -**Fecha:** 30 de Noviembre 2025 - -``` -ACTIVO - Circulante - Bancos $4,750,000 - Estimaciones por cobrar $3,500,000 - Inventarios $2,000,000 - Total Activo Circulante $10,250,000 - - Fijo - Maquinaria y equipo (asignado) $5,000,000 - Depreciaci贸n acumulada -$1,000,000 - Total Activo Fijo $4,000,000 - -TOTAL ACTIVO $14,250,000 - -PASIVO - Corto Plazo - Proveedores $2,500,000 - Estimaciones por pagar $1,500,000 - Retenciones $300,000 - Total Pasivo Corto Plazo $4,300,000 - -TOTAL PASIVO $4,300,000 - -CAPITAL - Inversi贸n del proyecto $8,000,000 - Utilidad acumulada $1,950,000 -TOTAL CAPITAL $9,950,000 - -TOTAL PASIVO + CAPITAL $14,250,000 -``` - ---- - -### 2. Estado de Resultados por Proyecto - -**Proyecto:** Fraccionamiento Los Pinos -**Periodo:** Enero - Noviembre 2025 - -``` -INGRESOS - Venta de viviendas $50,000,000 - Obra por administraci贸n $5,000,000 -TOTAL INGRESOS $55,000,000 - -COSTO DE VENTAS - Materiales $20,000,000 - Mano de obra directa $12,000,000 - Subcontratos $8,000,000 - Maquinaria y equipo $2,000,000 -TOTAL COSTO DE VENTAS $42,000,000 - -UTILIDAD BRUTA $13,000,000 -Margen bruto 23.6% - -GASTOS DE OPERACI脫N - Sueldos administrativos $3,000,000 - Gastos generales $1,500,000 -TOTAL GASTOS DE OPERACI脫N $4,500,000 - -UTILIDAD DE OPERACI脫N $8,500,000 - -GASTOS FINANCIEROS - Intereses bancarios $500,000 - -UTILIDAD ANTES DE IMPUESTOS $8,000,000 - -IMPUESTOS (30%) $2,400,000 - -UTILIDAD NETA $5,600,000 -Margen neto 10.2% -``` - ---- - -### 3. Dashboard Financiero Ejecutivo - -**KPIs Principales:** - -| M茅trica | Valor | Meta | Estado | -|---------|-------|------|--------| -| **Margen bruto** | 23.6% | 鈮25% | 馃煛 Cerca de meta | -| **Margen neto** | 10.2% | 鈮12% | 馃煛 Cerca de meta | -| **Liquidez** | $4.75M | 鈮$3M | 馃煝 Saludable | -| **Cuentas por cobrar vencidas** | 59% | 鈮20% | 馃敶 Alto riesgo | -| **Cuentas por pagar vencidas** | 59% | 鈮10% | 馃敶 Alto riesgo | -| **Cash flow proyectado (30 d铆as)** | +$2.75M | Positivo | 馃煝 OK | - -**Alertas:** -- 馃敶 Seguimiento urgente de cobranza (59% vencidas) -- 馃敶 Negociar pr贸rroga con proveedores vencidos -- 馃煛 Mejorar margen bruto (optimizar compras) - ---- - -## 馃攲 Integraci贸n con ERP Externo - -### Tipos de Integraci贸n - -| Sistema | M茅todo | Direcci贸n | Datos | -|---------|--------|-----------|-------| -| **SAP S/4HANA** | API REST | Bidireccional | P贸lizas, cuentas, saldos | -| **CONTPAQi** | XML/TXT | Exportaci贸n | P贸lizas, cat谩logo | -| **ASPEL COI** | Archivo | Exportaci贸n | P贸lizas | -| **QuickBooks** | API | Bidireccional | Facturas, pagos | - ---- - -### Exportaci贸n de P贸lizas a CONTPAQi - -**Formato:** XML - -```xml - - - Eg - 1234 - 2025-11-17 - Compra cemento Portland - - - 5101-001 - 100 ton cemento - 150000.00 - 0.00 - Obra A - - - 1105-001 - IVA Acreditable - 24000.00 - 0.00 - - - 2101-001 - Cementos Mexicanos - 0.00 - 174000.00 - - - -``` - ---- - -## 馃毃 Puntos Cr铆ticos - -1. **Balances cuadrados:** Toda p贸liza debe estar balanceada (debe = haber) -2. **Centros de costo:** Imputaci贸n obligatoria a proyecto/obra -3. **Conciliaci贸n bancaria mensual:** No acumular meses sin conciliar -4. **Aging de cuentas:** Monitoreo semanal para evitar atrasos -5. **Cash flow:** Proyecci贸n actualizada semanalmente -6. **Integraci贸n ERP:** Evitar doble captura, sincronizaci贸n diaria -7. **Cierre mensual:** Proceso formal de cierre contable - ---- - -## 馃幆 Siguiente Paso - -Crear documentaci贸n de requerimientos y especificaciones t茅cnicas del m贸dulo. - ---- - -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @frontend-team @finance-team -**Estado:** 馃摑 A crear diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-001-modelo de datos financiero.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-001-modelo de datos financiero.md deleted file mode 100644 index a20106cd4..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-001-modelo de datos financiero.md +++ /dev/null @@ -1,51 +0,0 @@ -# ET-FIN-001: Modelo de Datos Financiero - -**ID:** ET-FIN-001 | **M贸dulo:** MAE-014 - -## Schema -```sql -CREATE SCHEMA finance; - -CREATE TYPE finance.transaction_type AS ENUM ('income', 'expense'); -CREATE TYPE finance.payment_status AS ENUM ('pending', 'partial', 'paid', 'overdue'); - -CREATE TABLE finance.cash_flows ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - project_id UUID REFERENCES projects.projects(id), - type finance.transaction_type NOT NULL, - category VARCHAR(100) NOT NULL, - amount BIGINT NOT NULL, - projected_date DATE NOT NULL, - actual_date DATE, - actual_amount BIGINT, - description TEXT, - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE finance.receivables ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - project_id UUID REFERENCES projects.projects(id), - client_id UUID REFERENCES clients.clients(id), - invoice_number VARCHAR(50) UNIQUE, - amount BIGINT NOT NULL, - paid_amount BIGINT DEFAULT 0, - due_date DATE NOT NULL, - status finance.payment_status DEFAULT 'pending', - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE finance.payables ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - project_id UUID REFERENCES projects.projects(id), - vendor_id UUID REFERENCES preconstruction.vendors(id), - invoice_number VARCHAR(50), - amount BIGINT NOT NULL, - paid_amount BIGINT DEFAULT 0, - due_date DATE NOT NULL, - status finance.payment_status DEFAULT 'pending', - created_at TIMESTAMPTZ DEFAULT NOW() -); -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-002-servicio de flujo de efectivo.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-002-servicio de flujo de efectivo.md deleted file mode 100644 index 0f32470d6..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-002-servicio de flujo de efectivo.md +++ /dev/null @@ -1,41 +0,0 @@ -# ET-FIN-002: Servicio de Flujo de Efectivo - -**ID:** ET-FIN-002 | **M贸dulo:** MAE-014 - -## Cash Flow Service -```typescript -@Injectable() -export class CashFlowService { - async getProjection(projectId: string, months: number): Promise { - const incomes = await this.getProjectedIncomes(projectId, months); - const expenses = await this.getProjectedExpenses(projectId, months); - - return this.buildProjection(incomes, expenses, months); - } - - async compareRealVsProjected(projectId: string, month: Date): Promise { - const projected = await this.getMonthProjection(projectId, month); - const actual = await this.getMonthActual(projectId, month); - - return { - projected, actual, - variance: actual.total - projected.total, - variancePercent: ((actual.total - projected.total) / projected.total) * 100 - }; - } - - async checkLiquidityAlert(projectId: string): Promise { - const balance = await this.getCurrentBalance(projectId); - const avgDailyExpense = await this.getAvgDailyExpense(projectId); - const coverageDays = balance / avgDailyExpense; - - if (coverageDays < 15) { - return { level: 'critical', coverageDays, message: 'Liquidez cr铆tica' }; - } - return null; - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-003-servicio de facturaci贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-003-servicio de facturaci贸n.md deleted file mode 100644 index 68f54164a..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-003-servicio de facturaci贸n.md +++ /dev/null @@ -1,34 +0,0 @@ -# ET-FIN-003: Servicio de Facturaci贸n - -**ID:** ET-FIN-003 | **M贸dulo:** MAE-014 - -## Invoice Service -```typescript -@Injectable() -export class InvoiceService { - async createFromEstimation(estimationId: string): Promise { - const estimation = await this.estimationService.findOne(estimationId); - - const invoice = await this.invoiceRepo.save({ - projectId: estimation.projectId, - clientId: estimation.clientId, - amount: estimation.montoNeto, - concept: `Estimaci贸n ${estimation.numero}`, - dueDate: this.calculateDueDate(estimation) - }); - - await this.cfdiService.stamp(invoice); // Timbrado SAT - return invoice; - } - - async applyPayment(invoiceId: string, amount: number): Promise { - const invoice = await this.findOne(invoiceId); - invoice.paidAmount += amount; - invoice.status = invoice.paidAmount >= invoice.amount ? 'paid' : 'partial'; - await this.invoiceRepo.save(invoice); - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-004-conciliaci贸n bancaria.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-004-conciliaci贸n bancaria.md deleted file mode 100644 index 6d615b1a7..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-004-conciliaci贸n bancaria.md +++ /dev/null @@ -1,39 +0,0 @@ -# ET-FIN-004: Conciliaci贸n Bancaria - -**ID:** ET-FIN-004 | **M贸dulo:** MAE-014 - -## Bank Reconciliation Service -```typescript -@Injectable() -export class BankReconciliationService { - async importStatement(file: Buffer, format: 'csv' | 'ofx'): Promise { - const parser = format === 'csv' ? new CSVParser() : new OFXParser(); - const transactions = await parser.parse(file); - - return this.statementRepo.save(transactions.map(t => ({ - date: t.date, - reference: t.reference, - amount: t.amount, - description: t.description, - status: 'pending' - }))); - } - - async autoMatch(statementId: string): Promise { - const statement = await this.statementRepo.findOne(statementId); - - // Buscar match por monto y referencia - const candidates = await this.findCandidates(statement); - - if (candidates.length === 1 && this.isConfidentMatch(candidates[0], statement)) { - await this.createMatch(statement.id, candidates[0].id); - return { matched: true, transaction: candidates[0] }; - } - - return { matched: false, candidates }; - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-005-dashboard de control de gesti贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-005-dashboard de control de gesti贸n.md deleted file mode 100644 index 813a2bec6..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/especificaciones/ET-FIN-005-dashboard de control de gesti贸n.md +++ /dev/null @@ -1,35 +0,0 @@ -# ET-FIN-005: Dashboard de Control de Gesti贸n - -**ID:** ET-FIN-005 | **M贸dulo:** MAE-014 - -## Controlling Dashboard Service -```typescript -@Injectable() -export class ControllingService { - async getDashboard(projectId?: string): Promise { - const scope = projectId ? { projectId } : {}; - - return { - kpis: await this.calculateKPIs(scope), - cashFlow: await this.cashFlowService.getSummary(scope), - budgetVsActual: await this.getBudgetComparison(scope), - alerts: await this.getActiveAlerts(scope) - }; - } - - private async calculateKPIs(scope: Scope): Promise { - const receivables = await this.receivableRepo.find(scope); - const payables = await this.payableRepo.find(scope); - - return { - dso: this.calculateDSO(receivables), // Days Sales Outstanding - dpo: this.calculateDPO(payables), // Days Payable Outstanding - liquidityRatio: await this.getLiquidityRatio(scope), - profitMargin: await this.getProfitMargin(scope) - }; - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-001-proyectar flujo de efectivo.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-001-proyectar flujo de efectivo.md deleted file mode 100644 index 88348eb9d..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-001-proyectar flujo de efectivo.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-001: Proyectar Flujo de Efectivo - -**ID:** US-FIN-001 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Director/Gerente Financiero -**Quiero** Define horizonte 鈫 Captura ingresos esperados 鈫 Captura egresos 鈫 Ve proyecci贸n -**Para** anticipar necesidades de efectivo - -## Criterios -1. Define horizonte 鈫 Captura ingresos esperados 鈫 Captura egresos 鈫 Ve proyecci贸n 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-002-registrar movimientos reales.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-002-registrar movimientos reales.md deleted file mode 100644 index 7d46cb04b..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-002-registrar movimientos reales.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-002: Registrar Movimientos Reales - -**ID:** US-FIN-002 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Contador -**Quiero** Registra ingreso/egreso 鈫 Vincula a partida proyectada 鈫 Actualiza saldo -**Para** comparar real vs proyectado - -## Criterios -1. Registra ingreso/egreso 鈫 Vincula a partida proyectada 鈫 Actualiza saldo 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-003-crear factura desde estimaci贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-003-crear factura desde estimaci贸n.md deleted file mode 100644 index 9be31be2a..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-003-crear factura desde estimaci贸n.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-003: Crear Factura desde Estimaci贸n - -**ID:** US-FIN-003 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Contador -**Quiero** Selecciona estimaci贸n 鈫 Genera factura 鈫 Timbra SAT 鈫 Env铆a cliente -**Para** formalizar cobranza - -## Criterios -1. Selecciona estimaci贸n 鈫 Genera factura 鈫 Timbra SAT 鈫 Env铆a cliente 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-004-gestionar cobranza.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-004-gestionar cobranza.md deleted file mode 100644 index 252ba17be..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-004-gestionar cobranza.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-004: Gestionar Cobranza - -**ID:** US-FIN-004 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** 脕rea de Cobranza -**Quiero** Ve antig眉edad saldos 鈫 Filtra vencidas 鈫 Registra gestiones 鈫 Programa llamadas -**Para** reducir d铆as de cobranza - -## Criterios -1. Ve antig眉edad saldos 鈫 Filtra vencidas 鈫 Registra gestiones 鈫 Programa llamadas 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-005-aplicar pagos de clientes.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-005-aplicar pagos de clientes.md deleted file mode 100644 index 50df1377c..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-005-aplicar pagos de clientes.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-005: Aplicar Pagos de Clientes - -**ID:** US-FIN-005 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Contador -**Quiero** Busca factura 鈫 Registra pago 鈫 Concilia con banco 鈫 Actualiza saldo -**Para** mantener CxC actualizada - -## Criterios -1. Busca factura 鈫 Registra pago 鈫 Concilia con banco 鈫 Actualiza saldo 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-006-registrar factura de proveedor.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-006-registrar factura de proveedor.md deleted file mode 100644 index 12d24d932..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-006-registrar factura de proveedor.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-006: Registrar Factura de Proveedor - -**ID:** US-FIN-006 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Contador -**Quiero** Sube XML 鈫 Valida ante SAT 鈫 Vincula OC 鈫 Programa pago -**Para** controlar CxP - -## Criterios -1. Sube XML 鈫 Valida ante SAT 鈫 Vincula OC 鈫 Programa pago 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-007-programar pagos.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-007-programar pagos.md deleted file mode 100644 index 3e6b30513..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-007-programar pagos.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-007: Programar Pagos - -**ID:** US-FIN-007 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Tesorer铆a -**Quiero** Ve facturas pendientes 鈫 Selecciona a pagar 鈫 Genera programaci贸n 鈫 Autoriza -**Para** optimizar flujo de efectivo - -## Criterios -1. Ve facturas pendientes 鈫 Selecciona a pagar 鈫 Genera programaci贸n 鈫 Autoriza 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-008-importar estado de cuenta.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-008-importar estado de cuenta.md deleted file mode 100644 index dee61cdc8..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-008-importar estado de cuenta.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-008: Importar Estado de Cuenta - -**ID:** US-FIN-008 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Contador -**Quiero** Sube archivo CSV/OFX 鈫 Sistema parsea 鈫 Muestra movimientos 鈫 Confirma -**Para** facilitar conciliaci贸n - -## Criterios -1. Sube archivo CSV/OFX 鈫 Sistema parsea 鈫 Muestra movimientos 鈫 Confirma 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-009-conciliar movimientos bancarios.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-009-conciliar movimientos bancarios.md deleted file mode 100644 index 6dbcf6706..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-009-conciliar movimientos bancarios.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-009: Conciliar Movimientos Bancarios - -**ID:** US-FIN-009 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Contador -**Quiero** Ve movimientos pendientes 鈫 Match autom谩tico 鈫 Ajusta manuales 鈫 Cierra mes -**Para** cuadrar saldos bancarios - -## Criterios -1. Ve movimientos pendientes 鈫 Match autom谩tico 鈫 Ajusta manuales 鈫 Cierra mes 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-010-dashboard financiero.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-010-dashboard financiero.md deleted file mode 100644 index bfa6636f8..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-010-dashboard financiero.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-010: Dashboard Financiero - -**ID:** US-FIN-010 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Director -**Quiero** Ve KPIs 鈫 Flujo efectivo 鈫 Budget vs Real 鈫 Alertas -**Para** tomar decisiones financieras - -## Criterios -1. Ve KPIs 鈫 Flujo efectivo 鈫 Budget vs Real 鈫 Alertas 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-011-an谩lisis de rentabilidad.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-011-an谩lisis de rentabilidad.md deleted file mode 100644 index 66558c14d..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/historias-usuario/US-FIN-011-an谩lisis de rentabilidad.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-FIN-011: An谩lisis de Rentabilidad - -**ID:** US-FIN-011 | **M贸dulo:** MAE-014 | **SP:** 5 - -## Historia -**Como** Director -**Quiero** Selecciona proyecto 鈫 Ve costos vs ingresos 鈫 Margen 鈫 Tendencia -**Para** evaluar desempe帽o financiero - -## Criterios -1. Selecciona proyecto 鈫 Ve costos vs ingresos 鈫 Margen 鈫 Tendencia 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-001-flujo de efectivo.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-001-flujo de efectivo.md deleted file mode 100644 index 286a59a1d..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-001-flujo de efectivo.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-FIN-001: Flujo de Efectivo - -**ID:** RF-FIN-001 | **M贸dulo:** MAE-014 | **Prioridad:** Alta | **SP:** 12 - -## Descripci贸n -Proyecci贸n, seguimiento real vs plan, alertas - -## Reglas de Negocio -1. Proyecci贸n semanal/mensual de ingresos y egresos -2. Registro de movimientos reales -3. Comparativo real vs proyectado -4. Alertas de liquidez (< 15 d铆as cobertura) -5. Escenarios what-if -6. Exportaci贸n a Excel - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-002-cuentas por cobrar.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-002-cuentas por cobrar.md deleted file mode 100644 index 963a9afb7..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-002-cuentas por cobrar.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-FIN-002: Cuentas por Cobrar - -**ID:** RF-FIN-002 | **M贸dulo:** MAE-014 | **Prioridad:** Alta | **SP:** 10 - -## Descripci贸n -Facturaci贸n, cobranza, antig眉edad - -## Reglas de Negocio -1. Generaci贸n de facturas desde estimaciones -2. CFDI 4.0 con timbrado SAT -3. Antig眉edad de saldos (30, 60, 90+ d铆as) -4. Gesti贸n de cobranza con seguimiento -5. Aplicaci贸n de pagos parciales -6. Notas de cr茅dito - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-003-cuentas por pagar.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-003-cuentas por pagar.md deleted file mode 100644 index 99b0e4e35..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-003-cuentas por pagar.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-FIN-003: Cuentas por Pagar - -**ID:** RF-FIN-003 | **M贸dulo:** MAE-014 | **Prioridad:** Alta | **SP:** 10 - -## Descripci贸n -Facturas proveedores, programaci贸n pagos - -## Reglas de Negocio -1. Registro de facturas de proveedores -2. Validaci贸n de XML ante SAT -3. Programaci贸n de pagos por fecha -4. Control de vencimientos -5. Antig眉edad de saldos -6. Pagos parciales y complementos - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-004-conciliaci贸n bancaria.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-004-conciliaci贸n bancaria.md deleted file mode 100644 index 0ef9ad0e0..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-004-conciliaci贸n bancaria.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-FIN-004: Conciliaci贸n Bancaria - -**ID:** RF-FIN-004 | **M贸dulo:** MAE-014 | **Prioridad:** Alta | **SP:** 12 - -## Descripci贸n -Importaci贸n estados, match autom谩tico - -## Reglas de Negocio -1. Importaci贸n de estados de cuenta (CSV, OFX) -2. Match autom谩tico por monto y referencia -3. Partidas en conciliaci贸n -4. Ajustes manuales con justificaci贸n -5. Cierre mensual -6. Reportes de conciliaci贸n - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-005-control de gesti贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-005-control de gesti贸n.md deleted file mode 100644 index 0da3a3f86..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-014-finanzas-controlling/requerimientos/RF-FIN-005-control de gesti贸n.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-FIN-005: Control de Gesti贸n - -**ID:** RF-FIN-005 | **M贸dulo:** MAE-014 | **Prioridad:** Alta | **SP:** 11 - -## Descripci贸n -Dashboard financiero, KPIs, an谩lisis - -## Reglas de Negocio -1. Dashboard ejecutivo en tiempo real -2. KPIs: DSO, DPO, liquidez, rentabilidad -3. Comparativo presupuesto vs real -4. An谩lisis de rentabilidad por proyecto -5. Alertas de desviaciones > 10% -6. Drill-down a detalle - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/README.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/README.md deleted file mode 100644 index 41d397fd2..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# MAE-015: Activos y Maquinaria - -**M贸dulo:** Gesti贸n de Activos Fijos y Maquinaria de Construcci贸n -**Story Points:** 40 | **Prioridad:** Media | **Fase:** 2 (Enterprise) - -## Descripci贸n General - -Sistema para gesti贸n y control de activos fijos, maquinaria, veh铆culos y herramientas utilizados en proyectos de construcci贸n. Incluye inventario, mantenimiento, depreciaci贸n, y asignaci贸n a proyectos. - -## Alcance Funcional - -### 1. Cat谩logo de Activos -- Registro de maquinaria y equipo -- Caracter铆sticas t茅cnicas -- Documentos (factura, p贸liza, manuales) -- Valor de adquisici贸n y depreciaci贸n -- Ubicaci贸n actual - -### 2. Asignaci贸n a Proyectos -- Transfer de activos entre proyectos -- Tracking de ubicaci贸n -- Costeo por uso (horas/d铆as) -- Historial de asignaciones - -### 3. Mantenimiento Preventivo -- Calendario de mantenimientos -- Checklist por tipo de activo -- Registro de mantenimientos realizados -- Alertas de pr贸ximos mantenimientos -- Bit谩cora de fallas - -### 4. Control de Herramientas -- Vale de salida/entrada -- Responsable por herramienta -- Inventario en resguardo -- Reportes de p茅rdidas/robos - -### 5. Depreciaci贸n Contable -- C谩lculo autom谩tico (l铆nea recta, acelerada) -- Depreciaci贸n mensual -- Valor en libros -- Reportes para contabilidad - -## Componentes T茅cnicos - -### Backend (NestJS + TypeORM) -```typescript -@Module({ - imports: [TypeOrmModule.forFeature([ - Asset, AssetAssignment, MaintenanceSchedule, - MaintenanceRecord, ToolCheckout, Depreciation - ])], - providers: [ - AssetService, MaintenanceService, - ToolService, DepreciationService - ], - controllers: [AssetController, MaintenanceController] -}) -export class AssetModule {} -``` - -### Base de Datos (PostgreSQL) -```sql -CREATE SCHEMA assets; - -CREATE TYPE assets.asset_type AS ENUM ('machinery', 'vehicle', 'tool', 'equipment'); -CREATE TYPE assets.asset_status AS ENUM ('available', 'in_use', 'maintenance', 'retired'); -CREATE TYPE assets.maintenance_type AS ENUM ('preventive', 'corrective', 'inspection'); -``` - -## Integraciones - -- **MAI-002 (Proyectos):** Asignaci贸n de activos a proyectos -- **MAI-003 (Presupuestos):** Costeo de uso de maquinaria -- **MAE-014 (Finanzas):** Depreciaci贸n para contabilidad - -## M茅tricas Clave - -- **Utilizaci贸n:** % de tiempo en uso vs disponible -- **Costo por hora:** Depreciaci贸n + mantenimiento / horas uso -- **Tiempo fuera de servicio:** D铆as en mantenimiento -- **ROI:** Retorno sobre inversi贸n por activo - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/_MAP.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/_MAP.md deleted file mode 100644 index 8dc2e1d21..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/_MAP.md +++ /dev/null @@ -1,457 +0,0 @@ -# _MAP: MAE-015 - Activos, Maquinaria y Mantenimiento - -**脡pica:** MAE-015 -**Nombre:** Activos, Maquinaria y Mantenimiento -**Fase:** 2 - Enterprise B谩sico -**Presupuesto:** $40,000 MXN -**Story Points:** 70 SP -**Estado:** 馃摑 A crear -**Sprint:** Sprint 8-9 (Semanas 15-18) -**脷ltima actualizaci贸n:** 2025-11-17 -**Prioridad:** P1 - ---- - -## 馃搵 Prop贸sito - -Gesti贸n completa de activos fijos, maquinaria pesada, equipo y veh铆culos con control de mantenimiento: -- Cat谩logo de activos (maquinaria pesada, equipo, veh铆culos) -- Control de ubicaci贸n y asignaci贸n por obra -- Mantenimiento preventivo y correctivo programado -- 脫rdenes de trabajo de mantenimiento -- Costeo TCO (Total Cost of Ownership) -- Localizaci贸n GPS en tiempo real (IoT opcional) - -**Integraci贸n clave:** Se vincula con Proyectos (MAI-002), Finanzas (MAE-014), Compras (MAI-004) y RRHH (MAI-007). - ---- - -## 馃搧 Contenido - -### Requerimientos Funcionales (Estimados: 6) - -| ID | T铆tulo | Estado | -|----|--------|--------| -| RF-AST-001 | Cat谩logo de activos y registro | 馃摑 A crear | -| RF-AST-002 | Control de ubicaci贸n y asignaci贸n a obras | 馃摑 A crear | -| RF-AST-003 | Mantenimiento preventivo programado | 馃摑 A crear | -| RF-AST-004 | 脫rdenes de trabajo de mantenimiento correctivo | 馃摑 A crear | -| RF-AST-005 | Costeo por hora y TCO | 馃摑 A crear | -| RF-AST-006 | Rastreo GPS y telemetr铆a (IoT) | 馃摑 A crear | - -### Especificaciones T茅cnicas (Estimadas: 6) - -| ID | T铆tulo | RF | Estado | -|----|--------|----|--------| -| ET-AST-001 | Modelo de datos de activos y deprecaci贸n | RF-AST-001 | 馃摑 A crear | -| ET-AST-002 | Sistema de transferencias y asignaciones | RF-AST-002 | 馃摑 A crear | -| ET-AST-003 | Motor de programaci贸n de mantenimientos | RF-AST-003 | 馃摑 A crear | -| ET-AST-004 | Sistema de 贸rdenes de trabajo | RF-AST-004 | 馃摑 A crear | -| ET-AST-005 | C谩lculo de TCO y costeo | RF-AST-005 | 馃摑 A crear | -| ET-AST-006 | Integraci贸n con dispositivos GPS/IoT | RF-AST-006 | 馃摑 A crear | - -### Historias de Usuario (Estimadas: 14) - -| ID | T铆tulo | SP | Estado | -|----|--------|----|--------| -| US-AST-001 | Registrar activo nuevo (maquinaria/veh铆culo) | 5 | 馃摑 A crear | -| US-AST-002 | Asignar activo a obra | 5 | 馃摑 A crear | -| US-AST-003 | Transferir activo entre obras | 5 | 馃摑 A crear | -| US-AST-004 | Configurar plan de mantenimiento preventivo | 5 | 馃摑 A crear | -| US-AST-005 | Generar orden de mantenimiento autom谩tica | 5 | 馃摑 A crear | -| US-AST-006 | Crear orden de trabajo correctivo | 5 | 馃摑 A crear | -| US-AST-007 | Ejecutar checklist de mantenimiento | 5 | 馃摑 A crear | -| US-AST-008 | Registrar costo de mantenimiento | 5 | 馃摑 A crear | -| US-AST-009 | Calcular costo por hora de uso | 5 | 馃摑 A crear | -| US-AST-010 | Calcular TCO de activo | 5 | 馃摑 A crear | -| US-AST-011 | Rastrear ubicaci贸n GPS de activo | 5 | 馃摑 A crear | -| US-AST-012 | Dashboard de activos y disponibilidad | 5 | 馃摑 A crear | -| US-AST-013 | Reporte de utilizaci贸n de activos | 5 | 馃摑 A crear | -| US-AST-014 | Alertas de mantenimiento vencido | 5 | 馃摑 A crear | - -**Total Story Points:** 70 SP - -### Implementaci贸n - -馃搳 **Inventarios de trazabilidad:** -- [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) - Matriz completa de trazabilidad -- [DATABASE.yml](./implementacion/DATABASE.yml) - Objetos de base de datos -- [BACKEND.yml](./implementacion/BACKEND.yml) - M贸dulos backend -- [FRONTEND.yml](./implementacion/FRONTEND.yml) - Componentes frontend - -### Pruebas - -馃搵 Documentaci贸n de testing: -- [TEST-PLAN.md](./pruebas/TEST-PLAN.md) - Plan de pruebas -- [TEST-CASES.md](./pruebas/TEST-CASES.md) - Casos de prueba - ---- - -## 馃敆 Referencias - -- **README:** [README.md](./README.md) - Descripci贸n detallada de la 茅pica -- **Fase 2:** [../README.md](../README.md) - Informaci贸n de la fase completa -- **M贸dulo relacionado MVP:** M贸dulo 15 - Activos y Maquinaria (MVP-APP.md) - ---- - -## 馃搳 M茅tricas - -| M茅trica | Valor | -|---------|-------| -| **Presupuesto estimado** | $40,000 MXN | -| **Story Points estimados** | 70 SP | -| **Duraci贸n estimada** | 14 d铆as | -| **Reutilizaci贸n GAMILIT** | 10% (funcionalidad nueva) | -| **RF a implementar** | 6/6 | -| **ET a implementar** | 6/6 | -| **US a completar** | 14/14 | - ---- - -## 馃幆 M贸dulos Afectados - -### Base de Datos -- **Schema:** `assets` -- **Tablas principales:** - * `assets` - Cat谩logo de activos - * `asset_assignments` - Asignaciones a obras - * `maintenance_plans` - Planes de mantenimiento - * `maintenance_schedules` - Programaci贸n de mantenimientos - * `work_orders` - 脫rdenes de trabajo - * `maintenance_history` - Historial de mantenimientos - * `asset_costs` - Costos de operaci贸n y mantenimiento - * `asset_locations` - Ubicaciones GPS (hist贸rico) -- **ENUMs:** - * `asset_type` (heavy_machinery, light_equipment, vehicle, tool) - * `asset_status` (active, in_maintenance, inactive, retired) - * `maintenance_type` (preventive, corrective, predictive) - * `work_order_status` (scheduled, in_progress, completed, cancelled) - -### Backend -- **M贸dulo:** `assets` -- **Path:** `apps/backend/src/modules/assets/` -- **Services:** AssetService, MaintenanceService, WorkOrderService, CostingService, GPSTrackingService -- **Controllers:** AssetController, MaintenanceController, WorkOrderController -- **Middlewares:** AssetAccessGuard, MaintenanceSchedulerJob - -### Frontend -- **Features:** `assets`, `maintenance` -- **Path:** `apps/frontend/src/features/assets/` -- **Componentes:** - * AssetCatalog - * AssetForm - * AssetDetail - * AssignmentManager - * MaintenancePlanConfig - * WorkOrderList - * WorkOrderForm - * MaintenanceChecklistExecutor - * TCOCalculator - * AssetLocationMap - * AssetDashboard - * UtilizationReport -- **Stores:** assetStore, maintenanceStore, workOrderStore - ---- - -## 馃殰 Tipos de Activos - -| Categor铆a | Ejemplos | Costo t铆pico | Vida 煤til | Mantenimiento | -|-----------|----------|--------------|-----------|---------------| -| **Maquinaria pesada** | Excavadoras, retroexcavadoras, gr煤as, revolvedoras | $500K-$2M | 10-15 a帽os | Intensivo | -| **Equipo ligero** | Vibradores, cortadoras, compactadoras, andamios | $10K-$100K | 5-8 a帽os | Moderado | -| **Veh铆culos** | Camiones, camionetas, pick-ups | $300K-$800K | 8-10 a帽os | Regular | -| **Herramienta especializada** | Equipo topogr谩fico, equipos el茅ctricos | $5K-$50K | 3-5 a帽os | Bajo | - ---- - -## 馃搵 Ficha de Activo - -```yaml -asset: - id: "AST-001" - code: "EXC-001" - name: "Excavadora Caterpillar 320D" - type: "heavy_machinery" - category: "Excavadoras" - status: "active" - - acquisition: - purchase_date: "2023-05-15" - purchase_price: 1500000.00 # $1.5M MXN - supplier: "Maquinaria del Baj铆o SA" - invoice: "FAC-2023-5678" - financing: "own" # own, leased, rented - - specifications: - brand: "Caterpillar" - model: "320D" - year: 2023 - serial_number: "CAT320D2023001234" - engine: "Diesel C6.6" - capacity: "20 toneladas" - hours_rated: 10000 # Horas 煤tiles estimadas - - accounting: - asset_account: "1201-001" # Cuenta contable - depreciation_method: "straight_line" # L铆nea recta - useful_life_years: 10 - salvage_value: 300000.00 # Valor de rescate - accumulated_depreciation: 225000.00 # $225K (18 meses) - book_value: 1275000.00 # $1.275M - - current_assignment: - project_id: "PROJ-001" - location: "Fraccionamiento Los Pinos - Etapa 1" - assigned_date: "2025-10-01" - assigned_to_operator: "OP-045" # Operador - - usage: - hours_worked: 1800 # Horas acumuladas - last_usage_date: "2025-11-17" - avg_hours_per_month: 100 - - maintenance: - last_preventive: "2025-10-15" - next_preventive: "2025-12-15" # Cada 200 horas - maintenance_plan_id: "PLAN-EXC-001" - hours_since_maintenance: 180 -``` - ---- - -## 馃敡 Mantenimiento Preventivo - -### Plan de Mantenimiento - -**Activo:** Excavadora CAT 320D -**C贸digo:** PLAN-EXC-001 - -| Actividad | Frecuencia | 脷ltima | Pr贸xima | Responsable | -|-----------|------------|--------|---------|-------------| -| **Cambio de aceite motor** | 250 hrs | 1750 hrs | 2000 hrs | Mec谩nico | -| **Cambio de filtros** | 250 hrs | 1750 hrs | 2000 hrs | Mec谩nico | -| **Revisi贸n de orugas** | 500 hrs | 1500 hrs | 2000 hrs | Mec谩nico | -| **Lubricaci贸n general** | 100 hrs | 1700 hrs | 1800 hrs | 馃敶 Vencido | Operador | -| **Inspecci贸n hidr谩ulica** | 1000 hrs | 1000 hrs | 2000 hrs | Mec谩nico | - -**Alertas:** -- 馃敶 Lubricaci贸n general vencida (1800 hrs alcanzadas) -- 馃煛 Cambio de aceite pr贸ximo (faltan 20 horas) - ---- - -### Checklist de Mantenimiento - -**Orden de trabajo:** WO-2025-123 -**Actividad:** Mantenimiento preventivo 2000 horas -**Activo:** EXC-001 - Excavadora CAT 320D -**Fecha:** 2025-11-20 - -```yaml -checklist: - - task: "Drenar aceite de motor" - completed: true - technician: "Juan P茅rez" - time: "09:00" - - - task: "Reemplazar filtro de aceite" - completed: true - part_used: "Filtro CAT 1R-0750" - quantity: 1 - - - task: "Llenar con aceite nuevo (25 litros)" - completed: true - part_used: "Aceite CAT 15W-40" - quantity: 25 - - - task: "Cambiar filtro de combustible" - completed: true - part_used: "Filtro CAT 1R-0749" - - - task: "Cambiar filtro de aire" - completed: true - part_used: "Filtro CAT 6I-2503" - - - task: "Lubricaci贸n de pivotes" - completed: true - notes: "Aplicados 10 puntos de grasa" - - - task: "Inspecci贸n de orugas (tensi贸n, desgaste)" - completed: true - notes: "Tensi贸n OK, desgaste 40%, reemplazar en 3000 hrs" - - - task: "Revisi贸n de mangueras hidr谩ulicas" - completed: true - notes: "Todas en buen estado" - - - task: "Prueba de funcionamiento" - completed: true - notes: "Operaci贸n normal, sin ruidos anormales" - -totals: - hours_worked: 3.5 - parts_cost: 4500.00 # MXN - labor_cost: 1200.00 # MXN - total_cost: 5700.00 # MXN -``` - ---- - -## 馃挼 Costeo de Activos - -### Costo por Hora de Uso - -**F贸rmula:** - -``` -Costo/hora = (Depreciaci贸n + Mantenimiento + Combustible + Operador + Seguro) / Horas trabajadas -``` - -**Ejemplo: Excavadora CAT 320D** - -| Concepto | Costo anual | Horas/a帽o | Costo/hora | -|----------|-------------|-----------|------------| -| **Depreciaci贸n** | $120,000 | 1,200 | $100.00 | -| **Mantenimiento** | $36,000 | 1,200 | $30.00 | -| **Combustible** | $60,000 | 1,200 | $50.00 | -| **Operador** | $180,000 | 1,200 | $150.00 | -| **Seguro** | $24,000 | 1,200 | $20.00 | -| **TOTAL** | **$420,000** | **1,200** | **$350.00/hr** | - -**Imputaci贸n a proyecto:** - -```yaml -usage_record: - asset_id: "AST-001" - project_id: "PROJ-001" - date: "2025-11-17" - hours_worked: 8 - cost_per_hour: 350.00 - total_cost: 2800.00 # 8 hrs 脳 $350 - charged_to_account: "5104-001" # Maquinaria y equipo - cost_center: "Obra A - Excavaci贸n" -``` - ---- - -### TCO (Total Cost of Ownership) - -**An谩lisis de TCO - 5 a帽os** - -**Activo:** Excavadora CAT 320D - -| A帽o | Depreciaci贸n | Mantenimiento | Combustible | Operador | Seguro | **Total** | -|-----|--------------|---------------|-------------|----------|--------|-----------| -| 1 | $120,000 | $24,000 | $60,000 | $180,000 | $24,000 | $408,000 | -| 2 | $120,000 | $30,000 | $60,000 | $180,000 | $24,000 | $414,000 | -| 3 | $120,000 | $36,000 | $60,000 | $180,000 | $24,000 | $420,000 | -| 4 | $120,000 | $45,000 | $60,000 | $180,000 | $24,000 | $429,000 | -| 5 | $120,000 | $60,000 | $60,000 | $180,000 | $24,000 | $444,000 | -| **Total 5 a帽os** | **$600,000** | **$195,000** | **$300,000** | **$900,000** | **$120,000** | **$2,115,000** | - -**An谩lisis:** -- Costo inicial: $1,500,000 -- TCO 5 a帽os: $2,115,000 -- **TCO total:** $3,615,000 (2.4脳 costo inicial) -- Horas trabajadas: 6,000 hrs -- **Costo promedio/hora:** $602.50 - -**Decisi贸n:** -- 驴Comprar vs rentar? Si renta = $400/hr 鈫 Costo 5 a帽os = $2.4M (m谩s econ贸mico) -- 驴Mantener vs vender? Si valor de reventa a帽o 5 = $600K 鈫 TCO neto = $3.015M - ---- - -## 馃搷 Rastreo GPS y Telemetr铆a - -### Datos de Localizaci贸n - -```yaml -gps_tracking: - asset_id: "AST-001" - timestamp: "2025-11-17T14:30:00Z" - location: - latitude: 20.588818 - longitude: -100.389880 - address: "Fraccionamiento Los Pinos, Quer茅taro" - project: "PROJ-001" - geofence_status: "inside" # inside, outside, near - - telemetry: - engine_status: "running" - rpm: 1800 - fuel_level: 75 # % - engine_hours: 1850 - oil_pressure: 45 # PSI - coolant_temp: 85 # 掳C - battery_voltage: 24.5 # V - - alerts: - - type: "geofence_exit" - timestamp: "2025-11-17T08:15:00Z" - message: "Activo sali贸 de geofence de proyecto" - acknowledged: true - - - type: "idle_time" - timestamp: "2025-11-17T12:00:00Z" - message: "Motor encendido sin uso por 30 minutos" - acknowledged: false -``` - ---- - -### Dashboard de Activos - -**Indicadores en tiempo real:** - -| Activo | Ubicaci贸n | Estado | Operador | Horas hoy | Combustible | Alertas | -|--------|-----------|--------|----------|-----------|-------------|---------| -| EXC-001 | Obra A | 馃煝 Operando | Juan P. | 6.5 hrs | 75% | - | -| RET-002 | Obra A | 馃煝 Operando | Pedro M. | 5.0 hrs | 60% | 鈿狅笍 Mtto pr贸ximo | -| CAM-003 | En tr谩nsito | 馃煛 Moviendo | Carlos R. | 3.0 hrs | 40% | - | -| VIB-004 | Obra B | 馃敶 Parado | - | 0 hrs | N/A | 馃敶 Mtto vencido | - ---- - -## 馃搳 Reportes de Utilizaci贸n - -### Reporte Mensual - Octubre 2025 - -| Activo | Tipo | Hrs disponibles | Hrs trabajadas | Utilizaci贸n | Costo/hora | Costo total | -|--------|------|-----------------|----------------|-------------|------------|-------------| -| EXC-001 | Excavadora | 200 | 180 | 90% | $350 | $63,000 | -| RET-002 | Retroexcavadora | 200 | 150 | 75% | $300 | $45,000 | -| GRU-003 | Gr煤a | 200 | 80 | 40% | $450 | $36,000 | -| CAM-004 | Cami贸n volteo | 200 | 160 | 80% | $200 | $32,000 | -| **TOTAL** | - | **800** | **570** | **71.3%** | - | **$176,000** | - -**An谩lisis:** -- 鉁 Excavadora: Alta utilizaci贸n (90%) -- 鈿狅笍 Gr煤a: Baja utilizaci贸n (40%) 鈫 Evaluar renta vs propiedad -- 鉁 Promedio general: 71% (meta: >70%) - ---- - -## 馃毃 Puntos Cr铆ticos - -1. **Mantenimiento preventivo:** No omitir para evitar fallas costosas -2. **Registro de horas:** Captura diaria para costeo preciso -3. **Asignaci贸n clara:** Siempre debe haber responsable del activo -4. **Alertas de mantenimiento:** Atender a tiempo para evitar tiempos muertos -5. **Rastreo GPS:** Prevenir robo y uso no autorizado -6. **An谩lisis TCO:** Decisiones de compra vs renta basadas en datos -7. **Depreciaci贸n correcta:** Impacto en estados financieros - ---- - -## 馃幆 Siguiente Paso - -Crear documentaci贸n de requerimientos y especificaciones t茅cnicas del m贸dulo. - ---- - -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @frontend-team @maintenance-team -**Estado:** 馃摑 A crear diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-001-modelo de datos de activos.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-001-modelo de datos de activos.md deleted file mode 100644 index afbfe8202..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-001-modelo de datos de activos.md +++ /dev/null @@ -1,41 +0,0 @@ -# ET-AST-001: Modelo de Datos de Activos - -**ID:** ET-AST-001 | **M贸dulo:** MAE-015 - -## Schema -```sql -CREATE SCHEMA assets; - -CREATE TYPE assets.asset_type AS ENUM ('machinery', 'vehicle', 'tool', 'equipment'); -CREATE TYPE assets.asset_status AS ENUM ('available', 'in_use', 'maintenance', 'retired'); - -CREATE TABLE assets.assets ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(50) UNIQUE NOT NULL, - name VARCHAR(255) NOT NULL, - type assets.asset_type NOT NULL, - brand VARCHAR(100), - model VARCHAR(100), - year INT, - acquisition_cost BIGINT NOT NULL, - depreciation_method VARCHAR(20) DEFAULT 'straight_line', - useful_life_years INT DEFAULT 10, - current_book_value BIGINT, - status assets.asset_status DEFAULT 'available', - current_location UUID REFERENCES projects.projects(id), - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE assets.assignments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - asset_id UUID REFERENCES assets.assets(id), - project_id UUID REFERENCES projects.projects(id), - assigned_at TIMESTAMPTZ DEFAULT NOW(), - returned_at TIMESTAMPTZ, - hourly_rate BIGINT, - hours_used DECIMAL(10,2) -); -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-002-servicio de gesti贸n de activos.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-002-servicio de gesti贸n de activos.md deleted file mode 100644 index 75d665d84..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-002-servicio de gesti贸n de activos.md +++ /dev/null @@ -1,38 +0,0 @@ -# ET-AST-002: Servicio de Gesti贸n de Activos - -**ID:** ET-AST-002 | **M贸dulo:** MAE-015 - -## Asset Service -```typescript -@Injectable() -export class AssetService { - async assignToProject(assetId: string, projectId: string): Promise { - const asset = await this.findOne(assetId); - if (asset.status !== 'available') { - throw new BadRequestException('Asset not available'); - } - - asset.status = 'in_use'; - asset.currentLocation = projectId; - await this.assetRepo.save(asset); - - return this.assignmentRepo.save({ - assetId, projectId, assignedAt: new Date() - }); - } - - async returnFromProject(assignmentId: string, hoursUsed: number): Promise { - const assignment = await this.assignmentRepo.findOne(assignmentId); - assignment.returnedAt = new Date(); - assignment.hoursUsed = hoursUsed; - await this.assignmentRepo.save(assignment); - - const asset = await this.findOne(assignment.assetId); - asset.status = 'available'; - await this.assetRepo.save(asset); - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-003-motor de mantenimiento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-003-motor de mantenimiento.md deleted file mode 100644 index 2df4bcc5f..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-003-motor de mantenimiento.md +++ /dev/null @@ -1,42 +0,0 @@ -# ET-AST-003: Motor de Mantenimiento - -**ID:** ET-AST-003 | **M贸dulo:** MAE-015 - -## Maintenance Service -```typescript -@Injectable() -export class MaintenanceService { - async schedulePreventive(assetId: string): Promise { - const asset = await this.assetService.findOne(assetId); - const template = await this.getMaintenanceTemplate(asset.type); - - const schedules = template.intervals.map(interval => ({ - assetId, - type: 'preventive', - scheduledDate: this.calculateNextDate(interval), - checklist: template.checklist - })); - - return this.scheduleRepo.save(schedules); - } - - @Cron('0 8 * * *') // Daily at 8am - async sendMaintenanceAlerts(): Promise { - const upcoming = await this.scheduleRepo.findUpcoming(30); - - for (const schedule of upcoming) { - const daysUntil = this.getDaysUntil(schedule.scheduledDate); - if ([30, 15, 7].includes(daysUntil)) { - await this.notificationService.send({ - to: schedule.asset.responsibleEmail, - subject: `Mantenimiento pr贸ximo: ${schedule.asset.name}`, - template: 'maintenance_alert' - }); - } - } - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-004-control de herramientas.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-004-control de herramientas.md deleted file mode 100644 index f1fad13bd..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-004-control de herramientas.md +++ /dev/null @@ -1,42 +0,0 @@ -# ET-AST-004: Control de Herramientas - -**ID:** ET-AST-004 | **M贸dulo:** MAE-015 - -## Tool Checkout Service -```typescript -@Injectable() -export class ToolService { - async checkout(toolId: string, employeeId: string): Promise { - const tool = await this.assetRepo.findOne({ id: toolId, type: 'tool' }); - if (tool.status !== 'available') { - throw new BadRequestException('Tool not available'); - } - - const checkout = await this.checkoutRepo.save({ - toolId, - employeeId, - checkedOutAt: new Date(), - expectedReturnAt: this.addDays(new Date(), 7) - }); - - tool.status = 'in_use'; - await this.assetRepo.save(tool); - - return checkout; - } - - async checkin(checkoutId: string, condition: string): Promise { - const checkout = await this.checkoutRepo.findOne(checkoutId); - checkout.checkedInAt = new Date(); - checkout.returnCondition = condition; - await this.checkoutRepo.save(checkout); - - const tool = await this.assetRepo.findOne(checkout.toolId); - tool.status = condition === 'good' ? 'available' : 'maintenance'; - await this.assetRepo.save(tool); - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-005-c谩lculo de depreciaci贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-005-c谩lculo de depreciaci贸n.md deleted file mode 100644 index e5ea6e774..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/especificaciones/ET-AST-005-c谩lculo de depreciaci贸n.md +++ /dev/null @@ -1,44 +0,0 @@ -# ET-AST-005: C谩lculo de Depreciaci贸n - -**ID:** ET-AST-005 | **M贸dulo:** MAE-015 - -## Depreciation Service -```typescript -@Injectable() -export class DepreciationService { - @Cron('0 2 1 * *') // 1st day of month at 2am - async calculateMonthlyDepreciation(): Promise { - const assets = await this.assetRepo.findActive(); - - for (const asset of assets) { - const monthlyDep = this.calculateMonthly(asset); - - await this.depreciationRepo.save({ - assetId: asset.id, - month: new Date(), - amount: monthlyDep, - method: asset.depreciationMethod - }); - - asset.currentBookValue -= monthlyDep; - await this.assetRepo.save(asset); - } - } - - private calculateMonthly(asset: Asset): number { - switch (asset.depreciationMethod) { - case 'straight_line': - return asset.acquisitionCost / (asset.usefulLifeYears * 12); - case 'accelerated': - return this.calculateAccelerated(asset); - case 'by_use': - return this.calculateByUse(asset); - default: - return 0; - } - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-001-registrar activo.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-001-registrar activo.md deleted file mode 100644 index 72324f6ca..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-001-registrar activo.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-AST-001: Registrar Activo - -**ID:** US-AST-001 | **M贸dulo:** MAE-015 | **SP:** 5 - -## Historia -**Como** 脕rea de Activos -**Quiero** Captura datos 鈫 Adjunta documentos 鈫 Sube fotos 鈫 Genera c贸digo -**Para** mantener inventario actualizado - -## Criterios -1. Captura datos 鈫 Adjunta documentos 鈫 Sube fotos 鈫 Genera c贸digo 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-002-asignar activo a proyecto.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-002-asignar activo a proyecto.md deleted file mode 100644 index 21e6b4467..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-002-asignar activo a proyecto.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-AST-002: Asignar Activo a Proyecto - -**ID:** US-AST-002 | **M贸dulo:** MAE-015 | **SP:** 5 - -## Historia -**Como** Coordinador de Proyecto -**Quiero** Selecciona activo 鈫 Solicita asignaci贸n 鈫 Aprueba 鈫 Transfer -**Para** usar activo en construcci贸n - -## Criterios -1. Selecciona activo 鈫 Solicita asignaci贸n 鈫 Aprueba 鈫 Transfer 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-003-programar mantenimiento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-003-programar mantenimiento.md deleted file mode 100644 index b26ddc2bb..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-003-programar mantenimiento.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-AST-003: Programar Mantenimiento - -**ID:** US-AST-003 | **M贸dulo:** MAE-015 | **SP:** 5 - -## Historia -**Como** Encargado de Mantenimiento -**Quiero** Selecciona activo 鈫 Define calendario 鈫 Crea checklist 鈫 Activa alertas -**Para** prevenir fallas - -## Criterios -1. Selecciona activo 鈫 Define calendario 鈫 Crea checklist 鈫 Activa alertas 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-004-registrar mantenimiento realizado.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-004-registrar mantenimiento realizado.md deleted file mode 100644 index d78511da2..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-004-registrar mantenimiento realizado.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-AST-004: Registrar Mantenimiento Realizado - -**ID:** US-AST-004 | **M贸dulo:** MAE-015 | **SP:** 5 - -## Historia -**Como** T茅cnico -**Quiero** Completa checklist 鈫 Registra hallazgos 鈫 Sube evidencias 鈫 Cierra orden -**Para** documentar mantenimiento - -## Criterios -1. Completa checklist 鈫 Registra hallazgos 鈫 Sube evidencias 鈫 Cierra orden 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-005-vale de herramienta.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-005-vale de herramienta.md deleted file mode 100644 index 00aa33238..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-005-vale de herramienta.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-AST-005: Vale de Herramienta - -**ID:** US-AST-005 | **M贸dulo:** MAE-015 | **SP:** 5 - -## Historia -**Como** Residente de Obra -**Quiero** Solicita herramienta 鈫 Empleado firma 鈫 Valida estado 鈫 Entrega -**Para** controlar pr茅stamo de herramientas - -## Criterios -1. Solicita herramienta 鈫 Empleado firma 鈫 Valida estado 鈫 Entrega 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-006-devoluci贸n de herramienta.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-006-devoluci贸n de herramienta.md deleted file mode 100644 index b0919280d..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-006-devoluci贸n de herramienta.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-AST-006: Devoluci贸n de Herramienta - -**ID:** US-AST-006 | **M贸dulo:** MAE-015 | **SP:** 5 - -## Historia -**Como** Almacenista -**Quiero** Recibe herramienta 鈫 Verifica estado 鈫 Registra devoluci贸n 鈫 Actualiza inventario -**Para** cerrar vale de pr茅stamo - -## Criterios -1. Recibe herramienta 鈫 Verifica estado 鈫 Registra devoluci贸n 鈫 Actualiza inventario 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-007-dashboard de activos.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-007-dashboard de activos.md deleted file mode 100644 index aeb7a91ec..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-007-dashboard de activos.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-AST-007: Dashboard de Activos - -**ID:** US-AST-007 | **M贸dulo:** MAE-015 | **SP:** 5 - -## Historia -**Como** Gerente de Activos -**Quiero** Ve inventario 鈫 Utilizaci贸n 鈫 Mantenimientos pendientes 鈫 Depreciaci贸n -**Para** controlar activos de la empresa - -## Criterios -1. Ve inventario 鈫 Utilizaci贸n 鈫 Mantenimientos pendientes 鈫 Depreciaci贸n 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-008-reporte de depreciaci贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-008-reporte de depreciaci贸n.md deleted file mode 100644 index 58e1ed77f..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/historias-usuario/US-AST-008-reporte de depreciaci贸n.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-AST-008: Reporte de Depreciaci贸n - -**ID:** US-AST-008 | **M贸dulo:** MAE-015 | **SP:** 5 - -## Historia -**Como** Contador -**Quiero** Selecciona periodo 鈫 Ve c谩lculo 鈫 Valida 鈫 Exporta -**Para** registrar depreciaci贸n contable - -## Criterios -1. Selecciona periodo 鈫 Ve c谩lculo 鈫 Valida 鈫 Exporta 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-001-cat谩logo de activos.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-001-cat谩logo de activos.md deleted file mode 100644 index 1ba225a7e..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-001-cat谩logo de activos.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-AST-001: Cat谩logo de Activos - -**ID:** RF-AST-001 | **M贸dulo:** MAE-015 | **Prioridad:** Media | **SP:** 8 - -## Descripci贸n -Registro, caracter铆sticas, documentos, depreciaci贸n - -## Reglas de Negocio -1. Tipos: Maquinaria, Veh铆culos, Herramientas, Equipos -2. Caracter铆sticas t茅cnicas (marca, modelo, a帽o, capacidad) -3. Documentos digitales adjuntos -4. Valor de adquisici贸n y m茅todo de depreciaci贸n -5. Ubicaci贸n y responsable actual -6. Fotos del activo - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-002-asignaci贸n a proyectos.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-002-asignaci贸n a proyectos.md deleted file mode 100644 index fe78ed818..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-002-asignaci贸n a proyectos.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-AST-002: Asignaci贸n a Proyectos - -**ID:** RF-AST-002 | **M贸dulo:** MAE-015 | **Prioridad:** Media | **SP:** 8 - -## Descripci贸n -Transfer, tracking, costeo por uso - -## Reglas de Negocio -1. Transfer entre proyectos con aprobaci贸n -2. Tracking GPS para veh铆culos/maquinaria mayor -3. Costeo por hora o d铆a de uso -4. Historial completo de asignaciones -5. Disponibilidad en tiempo real -6. Alertas de activos subutilizados - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-003-mantenimiento preventivo.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-003-mantenimiento preventivo.md deleted file mode 100644 index 9860988a0..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-003-mantenimiento preventivo.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-AST-003: Mantenimiento Preventivo - -**ID:** RF-AST-003 | **M贸dulo:** MAE-015 | **Prioridad:** Media | **SP:** 8 - -## Descripci贸n -Calendario, checklist, alertas, bit谩cora - -## Reglas de Negocio -1. Calendario de mantenimientos por tipo de activo -2. Checklist din谩mico seg煤n fabricante -3. Registro de mantenimientos realizados -4. Alertas 7/15/30 d铆as antes -5. Bit谩cora de fallas y reparaciones -6. Costo de mantenimientos - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-004-control de herramientas.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-004-control de herramientas.md deleted file mode 100644 index cb2748b64..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-004-control de herramientas.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-AST-004: Control de Herramientas - -**ID:** RF-AST-004 | **M贸dulo:** MAE-015 | **Prioridad:** Media | **SP:** 8 - -## Descripci贸n -Vale entrada/salida, responsables, inventarios - -## Reglas de Negocio -1. Vale de salida con firma electr贸nica -2. Vale de entrada con verificaci贸n de estado -3. Responsable individual por herramienta -4. Inventario en resguardo -5. Reportes de p茅rdidas con responsable -6. Costos de reposici贸n - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-005-depreciaci贸n contable.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-005-depreciaci贸n contable.md deleted file mode 100644 index 5bb45eea8..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos-maquinaria/requerimientos/RF-AST-005-depreciaci贸n contable.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-AST-005: Depreciaci贸n Contable - -**ID:** RF-AST-005 | **M贸dulo:** MAE-015 | **Prioridad:** Media | **SP:** 8 - -## Descripci贸n -C谩lculo autom谩tico, reportes contables - -## Reglas de Negocio -1. M茅todos: L铆nea recta, Acelerada, Por uso -2. C谩lculo mensual autom谩tico -3. Valor en libros actualizado -4. Vida 煤til configurable por tipo -5. Reportes para contabilidad -6. Exportaci贸n a sistema contable - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos/implementacion/TRACEABILITY.yml deleted file mode 100644 index 48017b286..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-015-activos/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,2000 +0,0 @@ -# ============================================================================ -# MATRIZ DE TRAZABILIDAD - MAE-015 ACTIVOS FIJOS -# ============================================================================ -# Proyecto: ERP Suite - Vertical Construcci贸n -# M贸dulo: MAE-015 - Gesti贸n de Activos Fijos -# Prop贸sito: Control y administraci贸n de maquinaria pesada de construcci贸n -# Fecha: 2025-12-06 -# ============================================================================ - -metadata: - module_id: MAE-015 - module_name: Activos Fijos - vertical: construccion - version: 1.0.0 - status: planificado - owner: Equipo Verticales Construcci贸n - description: > - Sistema de gesti贸n de activos fijos orientado a maquinaria pesada de - construcci贸n (excavadoras, gr煤as, retroexcavadoras, etc.) que incluye - control de depreciaci贸n, mantenimiento preventivo y baja de activos. - -# ============================================================================ -# REQUERIMIENTOS FUNCIONALES -# ============================================================================ - -functional_requirements: - - # -------------------------------------------------------------------------- - # RF-001: ALTA DE ACTIVOS FIJOS - # -------------------------------------------------------------------------- - - id: RF-MAE015-001 - name: Alta de Activos Fijos - priority: alta - status: pendiente - description: > - Registro y catalogaci贸n de nuevos activos fijos de maquinaria pesada - de construcci贸n en el sistema, incluyendo datos t茅cnicos, financieros - y de ubicaci贸n. - - acceptance_criteria: - - criterion: Registro completo de datos del activo - description: > - El sistema debe permitir capturar: tipo de maquinaria, marca, modelo, - n煤mero de serie, a帽o de fabricaci贸n, capacidad, especificaciones t茅cnicas, - fecha de adquisici贸n, costo de adquisici贸n, proveedor, ubicaci贸n, centro - de costos, m茅todo de depreciaci贸n, vida 煤til estimada, valor residual. - validation: > - Formulario de alta con todos los campos requeridos y validaciones. - - - criterion: Generaci贸n autom谩tica de c贸digo de activo - description: > - El sistema debe asignar autom谩ticamente un c贸digo 煤nico al activo - basado en nomenclatura: [TIPO]-[A脩O]-[SECUENCIAL]. - validation: > - C贸digo generado autom谩ticamente y sin duplicados. - - - criterion: Carga de documentaci贸n soporte - description: > - Permitir adjuntar documentos: factura de compra, p贸liza de seguro, - manual de operaci贸n, certificados, garant铆as. - validation: > - Repositorio de documentos asociados al activo. - - - criterion: Registro fotogr谩fico del activo - description: > - Captura y almacenamiento de fotograf铆as del activo en diferentes - 谩ngulos y detalle de componentes principales. - validation: > - Galer铆a de im谩genes asociada al activo. - - - criterion: Asignaci贸n de responsable - description: > - Designar responsable del activo (operador, supervisor, gerente de obra). - validation: > - Campo de responsable con b煤squeda de usuarios activos. - - business_rules: - - rule: El costo de adquisici贸n debe ser mayor a cero - validation: Validaci贸n en formulario - - - rule: La vida 煤til debe estar entre 1 y 50 a帽os - validation: Rango permitido seg煤n normativa contable - - - rule: El valor residual no puede ser mayor al 30% del costo de adquisici贸n - validation: Validaci贸n cruzada de campos - - - rule: La fecha de adquisici贸n no puede ser futura - validation: Validaci贸n de fecha - - - rule: Maquinaria pesada debe tener p贸liza de seguro obligatoria - validation: Campo de p贸liza requerido para categor铆a espec铆fica - - user_stories: - - US-MAE015-001: Como gerente de activos, quiero registrar nueva maquinaria para tener control del inventario - - US-MAE015-002: Como contador, quiero capturar datos de adquisici贸n para calcular depreciaci贸n - - US-MAE015-003: Como administrador de obra, quiero asignar responsables para el control de uso - - technical_requirements: - - type: database - description: Tabla assets con campos completos - implementation: Schema con 铆ndices en c贸digo y tipo - - - type: storage - description: Almacenamiento de documentos y fotograf铆as - implementation: Integraci贸n con servicio de storage (S3/Azure Blob) - - - type: validation - description: Validaciones de negocio en frontend y backend - implementation: Schemas de validaci贸n con Zod/Joi - - - type: audit - description: Registro de creaci贸n y modificaciones - implementation: Triggers de auditor铆a autom谩tica - - dependencies: - - module: MGN-001 - requirement: Gesti贸n de usuarios para asignar responsables - - - module: MGN-005 - requirement: Cat谩logos de tipos de maquinaria y marcas - - - module: MGN-007 - requirement: Auditor铆a de operaciones - - test_scenarios: - - scenario: Alta exitosa de excavadora - steps: - - Ingresar datos completos de excavadora CAT 320D - - Adjuntar factura y p贸liza de seguro - - Cargar 4 fotograf铆as - - Asignar responsable - - Guardar registro - expected: Activo creado con c贸digo generado y confirmaci贸n - - - scenario: Validaci贸n de valor residual excesivo - steps: - - Ingresar costo de adquisici贸n $500,000 - - Ingresar valor residual $200,000 (40%) - expected: Error de validaci贸n, valor residual m谩ximo 30% - - - scenario: Alta sin p贸liza de seguro en maquinaria pesada - steps: - - Seleccionar tipo "Gr煤a Torre" - - No ingresar datos de p贸liza - - Intentar guardar - expected: Error, p贸liza de seguro obligatoria - - implementation: - estimated_effort: 40 horas - complexity: media - components: - - AssetRegistrationForm - - AssetDocumentManager - - AssetPhotoGallery - - AssetCodeGenerator - endpoints: - - POST /api/v1/construction/assets - - POST /api/v1/construction/assets/{id}/documents - - POST /api/v1/construction/assets/{id}/photos - - # -------------------------------------------------------------------------- - # RF-002: DEPRECIACI脫N DE ACTIVOS - # -------------------------------------------------------------------------- - - id: RF-MAE015-002 - name: Depreciaci贸n de Activos - priority: alta - status: pendiente - description: > - C谩lculo autom谩tico y registro de la depreciaci贸n de activos fijos - utilizando diferentes m茅todos contables, generaci贸n de reportes - y asientos contables correspondientes. - - acceptance_criteria: - - criterion: M茅todos de depreciaci贸n soportados - description: > - El sistema debe calcular depreciaci贸n por: l铆nea recta, saldos - decrecientes, suma de d铆gitos, y unidades de producci贸n (horas m谩quina). - validation: > - Configuraci贸n de m茅todo por activo y c谩lculo correcto. - - - criterion: C谩lculo autom谩tico mensual - description: > - Proceso batch que calcula depreciaci贸n mensual de todos los activos - activos al cierre de mes. - validation: > - Job programado que ejecuta c谩lculo y genera registros. - - - criterion: Generaci贸n de asientos contables - description: > - Crear autom谩ticamente asientos de depreciaci贸n con cuentas de - gasto depreciaci贸n y depreciaci贸n acumulada. - validation: > - Asientos generados seg煤n plan de cuentas configurado. - - - criterion: Historial de depreciaci贸n - description: > - Mantener registro mensual de depreciaci贸n acumulada y valor en libros. - validation: > - Tabla de movimientos con hist贸rico completo. - - - criterion: Reportes de depreciaci贸n - description: > - Generar reporte de depreciaci贸n mensual, anual, y proyecci贸n de - vida 煤til restante. - validation: > - Reportes con totales por tipo, centro de costos, y estado. - - business_rules: - - rule: La depreciaci贸n inicia el mes siguiente a la fecha de adquisici贸n - validation: L贸gica de c谩lculo de per铆odos - - - rule: No se deprecia el valor residual del activo - validation: C谩lculo considera valor residual - - - rule: Activos dados de baja no se deprecian - validation: Filtro de activos activos - - - rule: M茅todo de unidades de producci贸n requiere registro de horas trabajadas - validation: Validaci贸n de datos requeridos - - - rule: Cambio de m茅todo de depreciaci贸n requiere autorizaci贸n - validation: Workflow de aprobaci贸n - - user_stories: - - US-MAE015-004: Como contador, quiero calcular depreciaci贸n autom谩tica para cierre mensual - - US-MAE015-005: Como auditor, quiero consultar historial de depreciaci贸n para verificar c谩lculos - - US-MAE015-006: Como CFO, quiero reportes de depreciaci贸n para an谩lisis financiero - - technical_requirements: - - type: batch - description: Proceso programado de c谩lculo mensual - implementation: Job scheduler (cron/agenda) para cierre de mes - - - type: calculation - description: Algoritmos de depreciaci贸n - implementation: Service layer con m茅todos de c谩lculo - - - type: integration - description: Integraci贸n con m贸dulo contable - implementation: API calls para generaci贸n de asientos - - - type: reporting - description: Generaci贸n de reportes - implementation: Query builders y exportaci贸n a Excel/PDF - - dependencies: - - module: FIN-001 - requirement: Integraci贸n con contabilidad para asientos - - - module: MGN-009 - requirement: Motor de reportes - - - module: MGN-006 - requirement: Configuraci贸n de plan de cuentas - - test_scenarios: - - scenario: Depreciaci贸n l铆nea recta - steps: - - Crear activo con costo $120,000, vida 煤til 10 a帽os, valor residual $12,000 - - Ejecutar c谩lculo mensual - expected: Depreciaci贸n mensual de $900 (($120,000-$12,000)/120 meses) - - - scenario: Depreciaci贸n por unidades de producci贸n - steps: - - Activo con 10,000 horas de vida 煤til estimada - - Registrar 150 horas trabajadas en el mes - - Ejecutar c谩lculo - expected: Depreciaci贸n proporcional a horas trabajadas - - - scenario: No depreciar activo dado de baja - steps: - - Dar de baja activo el d铆a 15 - - Ejecutar c谩lculo mensual - expected: No generar depreciaci贸n para ese mes - - implementation: - estimated_effort: 60 horas - complexity: alta - components: - - DepreciationCalculator - - DepreciationBatchJob - - DepreciationReportGenerator - - AccountingIntegrationService - endpoints: - - POST /api/v1/construction/assets/depreciation/calculate - - GET /api/v1/construction/assets/{id}/depreciation/history - - GET /api/v1/construction/assets/depreciation/report - - # -------------------------------------------------------------------------- - # RF-003: MANTENIMIENTO PREVENTIVO - # -------------------------------------------------------------------------- - - id: RF-MAE015-003 - name: Mantenimiento Preventivo - priority: alta - status: pendiente - description: > - Sistema de programaci贸n, seguimiento y control de mantenimientos - preventivos para maquinaria pesada basado en calendario, horas de - operaci贸n o kil贸metros recorridos, con alertas y registro de actividades. - - acceptance_criteria: - - criterion: Planes de mantenimiento configurables - description: > - Definir planes de mantenimiento preventivo por tipo de maquinaria - con periodicidad (d铆as, horas m谩quina, km) y actividades a realizar. - validation: > - Cat谩logo de planes con actividades y frecuencias configurables. - - - criterion: Programaci贸n autom谩tica de mantenimientos - description: > - Generar autom谩ticamente 贸rdenes de mantenimiento seg煤n plan - y fecha/horas programadas. - validation: > - 脫rdenes creadas autom谩ticamente seg煤n calendario. - - - criterion: Alertas y notificaciones - description: > - Enviar alertas cuando se aproxima mantenimiento programado (7 d铆as, - 50 horas antes) y alertas de mantenimientos vencidos. - validation: > - Notificaciones por email y en sistema a responsables. - - - criterion: Registro de ejecuci贸n de mantenimiento - description: > - Capturar datos de mantenimiento realizado: fecha, t茅cnico, actividades - ejecutadas, repuestos utilizados, horas trabajadas, observaciones, - evidencias fotogr谩ficas. - validation: > - Formulario de cierre de orden con todos los datos. - - - criterion: Control de costos de mantenimiento - description: > - Registrar costos de mano de obra, repuestos y servicios externos - asociados a cada mantenimiento. - validation: > - Detalle de costos por orden y totales por activo. - - - criterion: Historial de mantenimientos - description: > - Consultar historial completo de mantenimientos preventivos y correctivos - realizados a cada activo. - validation: > - Vista de timeline con todos los mantenimientos. - - business_rules: - - rule: Maquinaria con mantenimiento vencido no puede ser asignada a obras - validation: Validaci贸n en asignaci贸n de recursos - - - rule: Mantenimientos cr铆ticos (>100 hrs vencidos) requieren justificaci贸n - validation: Campo obligatorio de justificaci贸n - - - rule: T茅cnico debe estar certificado para el tipo de maquinaria - validation: Verificaci贸n de certificaciones - - - rule: Repuestos utilizados deben descontarse de inventario - validation: Integraci贸n con m贸dulo de inventarios - - - rule: Mantenimiento mayor (>500 hrs) requiere aprobaci贸n gerencial - validation: Workflow de aprobaci贸n - - user_stories: - - US-MAE015-007: Como jefe de mantenimiento, quiero programar mantenimientos para evitar fallas - - US-MAE015-008: Como operador, quiero recibir alertas de mantenimiento para no usar equipo vencido - - US-MAE015-009: Como gerente, quiero consultar costos de mantenimiento para optimizar presupuesto - - technical_requirements: - - type: scheduling - description: Motor de programaci贸n de mantenimientos - implementation: Scheduler con reglas de periodicidad - - - type: notifications - description: Sistema de alertas y notificaciones - implementation: Integraci贸n con m贸dulo de notificaciones - - - type: workflow - description: Flujo de trabajo de 贸rdenes - implementation: State machine para estados de orden - - - type: integration - description: Integraci贸n con inventarios y contabilidad - implementation: APIs para movimientos de stock y costos - - dependencies: - - module: MGN-008 - requirement: Sistema de notificaciones para alertas - - - module: INV-001 - requirement: Control de inventario de repuestos - - - module: MGN-007 - requirement: Auditor铆a de operaciones - - - module: MGN-001 - requirement: Gesti贸n de usuarios y certificaciones - - test_scenarios: - - scenario: Generaci贸n autom谩tica de orden por horas - steps: - - Configurar plan: mantenimiento cada 250 horas - - Registrar 240 horas en activo - - Registrar 15 horas m谩s (total 255) - expected: Orden de mantenimiento generada autom谩ticamente - - - scenario: Alerta de mantenimiento pr贸ximo - steps: - - Mantenimiento programado para dentro de 5 d铆as - - Ejecutar job de alertas - expected: Notificaci贸n enviada a responsable y jefe de mantenimiento - - - scenario: Cierre de orden con consumo de repuestos - steps: - - Ejecutar mantenimiento - - Registrar 2 filtros y 20 litros de aceite - - Cerrar orden - expected: Repuestos descontados de inventario y costo registrado - - - scenario: Bloqueo de asignaci贸n por mantenimiento vencido - steps: - - Activo con mantenimiento vencido hace 15 d铆as - - Intentar asignar a obra - expected: Error, mantenimiento vencido debe completarse primero - - implementation: - estimated_effort: 80 horas - complexity: alta - components: - - MaintenancePlanManager - - MaintenanceScheduler - - MaintenanceOrderWorkflow - - MaintenanceAlertService - - MaintenanceCostTracker - endpoints: - - POST /api/v1/construction/assets/maintenance/plans - - POST /api/v1/construction/assets/maintenance/orders - - PATCH /api/v1/construction/assets/maintenance/orders/{id}/complete - - GET /api/v1/construction/assets/{id}/maintenance/history - - GET /api/v1/construction/assets/maintenance/alerts - - # -------------------------------------------------------------------------- - # RF-004: BAJA DE ACTIVOS - # -------------------------------------------------------------------------- - - id: RF-MAE015-004 - name: Baja de Activos Fijos - priority: media - status: pendiente - description: > - Proceso de baja de activos fijos por venta, donaci贸n, robo, obsolescencia - o deterioro total, incluyendo c谩lculo de utilidad/p茅rdida, generaci贸n - de asientos contables y actualizaci贸n de inventario. - - acceptance_criteria: - - criterion: Tipos de baja soportados - description: > - El sistema debe permitir dar de baja por: venta, donaci贸n, robo, - siniestro, obsolescencia, chatarra, permuta. - validation: > - Cat谩logo de motivos de baja con flujos espec铆ficos. - - - criterion: C谩lculo de utilidad o p茅rdida - description: > - Calcular autom谩ticamente utilidad o p茅rdida en la disposici贸n - considerando: valor en libros, precio de venta, gastos de venta, - valor recuperado de seguros. - validation: > - C谩lculo correcto y presentaci贸n de detalle. - - - criterion: Generaci贸n de asientos contables - description: > - Crear asientos de baja que afecten: depreciaci贸n acumulada, - costo del activo, utilidad/p茅rdida en venta, efectivo/cuentas por cobrar. - validation: > - Asientos seg煤n tipo de baja y normativa contable. - - - criterion: Documentaci贸n de baja - description: > - Adjuntar documentos soporte: contrato de venta, acta de donaci贸n, - denuncia de robo, dictamen de perito, autorizaci贸n gerencial. - validation: > - Repositorio de documentos seg煤n tipo de baja. - - - criterion: Workflow de aprobaci贸n - description: > - Solicitud de baja requiere aprobaci贸n de jefe de activos y gerencia - financiera antes de ejecutarse. - validation: > - Flujo de aprobaci贸n multinivel implementado. - - - criterion: Actualizaci贸n de inventario - description: > - Marcar activo como "Dado de baja" en inventario y excluir de reportes - de activos activos, pero mantener en hist贸rico. - validation: > - Estado actualizado y filtros en consultas. - - business_rules: - - rule: No se puede dar de baja activo con mantenimientos pendientes sin autorizaci贸n - validation: Verificaci贸n de 贸rdenes abiertas - - - rule: Baja por venta requiere precio m铆nimo del 50% del valor en libros - validation: Validaci贸n de precio vs valor contable - - - rule: Activo con garant铆a vigente requiere notificaci贸n a proveedor - validation: Alerta en proceso de baja - - - rule: Baja por robo requiere denuncia policial adjunta - validation: Validaci贸n de documento obligatorio - - - rule: Activos mayores a $100,000 requieren aprobaci贸n de direcci贸n general - validation: Nivel de aprobaci贸n seg煤n monto - - user_stories: - - US-MAE015-010: Como gerente de activos, quiero dar de baja maquinaria obsoleta para actualizar inventario - - US-MAE015-011: Como contador, quiero calcular utilidad/p茅rdida en venta para estados financieros - - US-MAE015-012: Como auditor, quiero verificar documentaci贸n de bajas para cumplimiento normativo - - technical_requirements: - - type: workflow - description: Sistema de aprobaciones multinivel - implementation: Workflow engine con roles y permisos - - - type: calculation - description: C谩lculo de utilidad/p茅rdida - implementation: Service con f贸rmulas contables - - - type: integration - description: Integraci贸n con contabilidad - implementation: Generaci贸n de asientos contables - - - type: audit - description: Auditor铆a completa de bajas - implementation: Log detallado de todas las operaciones - - dependencies: - - module: FIN-001 - requirement: M贸dulo contable para asientos - - - module: MGN-001 - requirement: Usuarios y roles para aprobaciones - - - module: MGN-007 - requirement: Auditor铆a de operaciones cr铆ticas - - - module: MGN-008 - requirement: Notificaciones de aprobaciones - - test_scenarios: - - scenario: Baja por venta con utilidad - steps: - - Activo con costo $100,000, depreciaci贸n acumulada $60,000, valor en libros $40,000 - - Solicitar baja por venta en $50,000 - - Aprobar solicitud - - Confirmar baja - expected: Utilidad en venta $10,000, asientos generados, activo marcado como dado de baja - - - scenario: Baja por robo sin denuncia - steps: - - Seleccionar motivo "Robo" - - No adjuntar denuncia policial - - Intentar enviar solicitud - expected: Error, denuncia policial obligatoria - - - scenario: Baja rechazada por precio bajo - steps: - - Activo con valor en libros $80,000 - - Solicitar baja por venta en $30,000 (37.5%) - expected: Alerta, precio menor al 50% del valor en libros - - - scenario: Workflow de aprobaci贸n exitoso - steps: - - Solicitar baja de excavadora ($150,000) - - Jefe de activos aprueba - - Gerente financiero aprueba - - Director general aprueba (>$100,000) - - Sistema ejecuta baja - expected: Baja procesada, asientos contables generados, notificaciones enviadas - - implementation: - estimated_effort: 50 horas - complexity: media-alta - components: - - AssetDisposalWorkflow - - DisposalCalculator - - DisposalApprovalManager - - DisposalAccountingService - endpoints: - - POST /api/v1/construction/assets/{id}/disposal/request - - PATCH /api/v1/construction/assets/disposal/{id}/approve - - PATCH /api/v1/construction/assets/disposal/{id}/reject - - POST /api/v1/construction/assets/disposal/{id}/execute - - GET /api/v1/construction/assets/disposal/pending-approval - -# ============================================================================ -# REQUERIMIENTOS NO FUNCIONALES -# ============================================================================ - -non_functional_requirements: - - - id: NFR-MAE015-001 - category: performance - description: C谩lculo de depreciaci贸n de 10,000 activos en menos de 5 minutos - measurement: Tiempo de ejecuci贸n del batch job - priority: alta - - - id: NFR-MAE015-002 - category: performance - description: Consulta de historial de mantenimiento en menos de 2 segundos - measurement: Response time de endpoint - priority: media - - - id: NFR-MAE015-003 - category: security - description: Encriptaci贸n de documentos sensibles (facturas, contratos) - measurement: Documentos almacenados con encriptaci贸n AES-256 - priority: alta - - - id: NFR-MAE015-004 - category: availability - description: Disponibilidad del sistema 99.5% (excluyendo mantenimientos programados) - measurement: Uptime monitoring - priority: media - - - id: NFR-MAE015-005 - category: auditability - description: Registro completo de todas las operaciones cr铆ticas (alta, baja, cambios) - measurement: 100% de operaciones auditadas - priority: alta - - - id: NFR-MAE015-006 - category: usability - description: Interfaz mobile-responsive para registro de mantenimientos en campo - measurement: UI funcional en tablets y smartphones - priority: media - - - id: NFR-MAE015-007 - category: scalability - description: Soportar hasta 50,000 activos sin degradaci贸n de performance - measurement: Load testing con 50K registros - priority: media - - - id: NFR-MAE015-008 - category: integration - description: APIs REST documentadas con OpenAPI/Swagger para integraciones - measurement: Documentaci贸n completa y actualizada - priority: alta - -# ============================================================================ -# REGLAS DE NEGOCIO TRANSVERSALES -# ============================================================================ - -business_rules: - - - id: BR-MAE015-001 - name: Segregaci贸n de funciones - description: > - Usuario que da de alta un activo no puede aprobarlo para uso. - Usuario que solicita baja no puede aprobarla. - impact: Todos los m贸dulos - validation: Permisos y workflow - - - id: BR-MAE015-002 - name: Inmutabilidad de registros contables - description: > - Una vez generados los asientos contables de depreciaci贸n o baja, - no pueden modificarse, solo revertirse con asientos de ajuste. - impact: Depreciaci贸n y Baja - validation: Permisos y l贸gica de negocio - - - id: BR-MAE015-003 - name: Unicidad de c贸digos de activo - description: > - Cada activo debe tener un c贸digo 煤nico que no se reutiliza aun - despu茅s de darlo de baja. - impact: Alta de activos - validation: Constraint en base de datos - - - id: BR-MAE015-004 - name: Trazabilidad de costos - description: > - Todo costo asociado a un activo (adquisici贸n, mantenimiento, mejoras) - debe ser trazable y auditable. - impact: Todos los m贸dulos - validation: Registro de movimientos - - - id: BR-MAE015-005 - name: Alertas tempranas - description: > - Sistema debe alertar proactivamente sobre: mantenimientos pr贸ximos, - fin de garant铆a, fin de vida 煤til, seguros por vencer. - impact: Todo el m贸dulo - validation: Jobs programados de alertas - -# ============================================================================ -# HISTORIAS DE USUARIO -# ============================================================================ - -user_stories: - - - id: US-MAE015-001 - title: Registro de nueva excavadora - as_a: Gerente de activos - i_want: Registrar nueva maquinaria pesada en el sistema - so_that: Pueda tener control del inventario y comenzar su depreciaci贸n - acceptance_criteria: - - Formulario con todos los datos t茅cnicos y financieros - - Generaci贸n autom谩tica de c贸digo de activo - - Carga de documentaci贸n soporte - - Asignaci贸n de responsable - priority: alta - story_points: 8 - sprint: 1 - - - id: US-MAE015-002 - title: Captura de datos de adquisici贸n - as_a: Contador - i_want: Capturar todos los datos financieros de la adquisici贸n - so_that: Pueda calcular correctamente la depreciaci贸n - acceptance_criteria: - - Campos de costo, fecha, m茅todo de depreciaci贸n, vida 煤til - - Validaciones de rangos y valores - - Selecci贸n de centro de costos - priority: alta - story_points: 5 - sprint: 1 - - - id: US-MAE015-003 - title: Asignaci贸n de responsables - as_a: Administrador de obra - i_want: Asignar responsables a cada activo - so_that: Pueda tener control de qui茅n usa cada maquinaria - acceptance_criteria: - - B煤squeda de usuarios - - Historial de asignaciones - - Notificaci贸n al responsable - priority: media - story_points: 3 - sprint: 1 - - - id: US-MAE015-004 - title: C谩lculo autom谩tico de depreciaci贸n - as_a: Contador - i_want: Que el sistema calcule autom谩ticamente la depreciaci贸n mensual - so_that: No tenga que hacerlo manualmente cada mes - acceptance_criteria: - - Job programado de c谩lculo mensual - - Soporte de m煤ltiples m茅todos de depreciaci贸n - - Generaci贸n de asientos contables - priority: alta - story_points: 13 - sprint: 2 - - - id: US-MAE015-005 - title: Consulta de historial de depreciaci贸n - as_a: Auditor - i_want: Consultar el historial completo de depreciaci贸n de un activo - so_that: Pueda verificar la correctitud de los c谩lculos - acceptance_criteria: - - Vista de timeline con todos los per铆odos - - Detalle de c谩lculo por per铆odo - - Exportaci贸n a Excel - priority: media - story_points: 5 - sprint: 2 - - - id: US-MAE015-006 - title: Reportes de depreciaci贸n - as_a: CFO - i_want: Generar reportes de depreciaci贸n por diferentes criterios - so_that: Pueda analizar el impacto financiero - acceptance_criteria: - - Reporte mensual y anual - - Filtros por tipo, centro de costos, estado - - Exportaci贸n a PDF y Excel - priority: media - story_points: 8 - sprint: 2 - - - id: US-MAE015-007 - title: Programaci贸n de mantenimientos preventivos - as_a: Jefe de mantenimiento - i_want: Programar mantenimientos preventivos para cada maquinaria - so_that: Pueda evitar fallas y prolongar vida 煤til - acceptance_criteria: - - Planes de mantenimiento por tipo de maquinaria - - Generaci贸n autom谩tica de 贸rdenes - - Alertas de mantenimientos pr贸ximos - priority: alta - story_points: 13 - sprint: 3 - - - id: US-MAE015-008 - title: Alertas de mantenimiento - as_a: Operador de maquinaria - i_want: Recibir alertas cuando se aproxima un mantenimiento - so_that: No use equipo con mantenimiento vencido - acceptance_criteria: - - Notificaciones por email y en sistema - - Alertas con 7 d铆as y 50 horas de anticipaci贸n - - Bloqueo de uso si est谩 vencido - priority: alta - story_points: 8 - sprint: 3 - - - id: US-MAE015-009 - title: Consulta de costos de mantenimiento - as_a: Gerente de operaciones - i_want: Consultar los costos acumulados de mantenimiento por activo - so_that: Pueda optimizar presupuesto y tomar decisiones - acceptance_criteria: - - Vista de costos por activo y per铆odo - - Desglose por tipo de costo - - Gr谩ficas de tendencia - priority: media - story_points: 5 - sprint: 3 - - - id: US-MAE015-010 - title: Baja de maquinaria obsoleta - as_a: Gerente de activos - i_want: Dar de baja maquinaria que ya no se usa - so_that: Pueda mantener actualizado el inventario - acceptance_criteria: - - Selecci贸n de motivo de baja - - Workflow de aprobaci贸n - - Documentaci贸n soporte - priority: media - story_points: 8 - sprint: 4 - - - id: US-MAE015-011 - title: C谩lculo de utilidad/p茅rdida en venta - as_a: Contador - i_want: Que el sistema calcule autom谩ticamente utilidad o p茅rdida en venta - so_that: Pueda registrarlo correctamente en estados financieros - acceptance_criteria: - - C谩lculo autom谩tico considerando valor en libros - - Generaci贸n de asientos contables - - Detalle de c谩lculo - priority: alta - story_points: 8 - sprint: 4 - - - id: US-MAE015-012 - title: Verificaci贸n de documentaci贸n de bajas - as_a: Auditor - i_want: Verificar que todas las bajas tengan documentaci贸n completa - so_that: Cumpla con requisitos normativos - acceptance_criteria: - - Repositorio de documentos por baja - - Checklist de documentos obligatorios - - Reporte de bajas sin documentaci贸n completa - priority: media - story_points: 5 - sprint: 4 - -# ============================================================================ -# CASOS DE USO -# ============================================================================ - -use_cases: - - - id: UC-MAE015-001 - name: Registrar nuevo activo de maquinaria pesada - actor: Gerente de activos - preconditions: - - Usuario autenticado con permiso de alta de activos - - Cat谩logos de tipos y marcas configurados - postconditions: - - Activo registrado en sistema - - C贸digo 煤nico asignado - - Auditor铆a registrada - main_flow: - - Actor selecciona opci贸n "Nuevo Activo" - - Sistema presenta formulario de registro - - Actor ingresa datos t茅cnicos y financieros - - Actor adjunta documentos soporte - - Actor carga fotograf铆as - - Actor asigna responsable - - Sistema valida datos - - Sistema genera c贸digo 煤nico - - Sistema guarda registro - - Sistema env铆a notificaci贸n a responsable - - Sistema muestra confirmaci贸n - alternative_flows: - - "3a. Datos inv谩lidos: Sistema muestra errores, actor corrige" - - "7a. C贸digo duplicado: Sistema regenera c贸digo" - related_requirements: - - RF-MAE015-001 - - - id: UC-MAE015-002 - name: Ejecutar c谩lculo mensual de depreciaci贸n - actor: Sistema (Job programado) - preconditions: - - Es cierre de mes - - Existen activos activos - postconditions: - - Depreciaci贸n calculada para todos los activos - - Asientos contables generados - - Notificaciones enviadas - main_flow: - - Job se activa al cierre de mes - - Sistema obtiene lista de activos activos - - Para cada activo, sistema calcula depreciaci贸n seg煤n m茅todo - - Sistema registra movimiento de depreciaci贸n - - Sistema genera asiento contable - - Sistema actualiza valor en libros - - Sistema env铆a reporte a contabilidad - - Sistema registra log de ejecuci贸n - alternative_flows: - - "3a. Error en c谩lculo: Sistema registra error y contin煤a con siguiente" - - "5a. Error en asiento: Sistema marca para revisi贸n manual" - related_requirements: - - RF-MAE015-002 - - - id: UC-MAE015-003 - name: Registrar ejecuci贸n de mantenimiento preventivo - actor: T茅cnico de mantenimiento - preconditions: - - Orden de mantenimiento generada - - T茅cnico con certificaci贸n para el tipo de maquinaria - postconditions: - - Mantenimiento registrado - - Repuestos descontados de inventario - - Costos registrados - - Pr贸ximo mantenimiento programado - main_flow: - - Actor abre orden de mantenimiento - - Actor ejecuta actividades programadas - - Actor ingresa a sistema - - Actor marca actividades completadas - - Actor registra repuestos utilizados - - Actor registra horas de mano de obra - - Actor adjunta evidencias fotogr谩ficas - - Actor ingresa observaciones - - Sistema valida disponibilidad de repuestos - - Sistema descuenta repuestos de inventario - - Sistema calcula costo total - - Sistema cierra orden - - Sistema programa pr贸ximo mantenimiento - - Sistema env铆a notificaci贸n a jefe de mantenimiento - alternative_flows: - - "9a. Repuestos no disponibles: Sistema alerta, actor gestiona pedido" - - "12a. Problemas detectados: Sistema genera orden correctiva" - related_requirements: - - RF-MAE015-003 - - - id: UC-MAE015-004 - name: Procesar baja de activo por venta - actor: Gerente de activos - preconditions: - - Activo existe y est谩 activo - - Usuario con permiso de solicitar bajas - postconditions: - - Baja aprobada y ejecutada - - Utilidad/p茅rdida calculada - - Asientos contables generados - - Activo marcado como dado de baja - main_flow: - - Actor selecciona activo - - Actor selecciona "Solicitar baja" - - Actor selecciona motivo "Venta" - - Sistema muestra valor en libros actual - - Actor ingresa precio de venta y comprador - - Sistema calcula utilidad o p茅rdida - - Actor adjunta contrato de venta - - Actor env铆a solicitud - - Sistema notifica a jefe de activos - - Jefe de activos aprueba - - Sistema notifica a gerente financiero - - Gerente financiero aprueba - - Sistema ejecuta baja - - Sistema genera asientos contables - - Sistema actualiza estado del activo - - Sistema env铆a confirmaci贸n - alternative_flows: - - "6a. Precio < 50% valor en libros: Sistema alerta, requiere justificaci贸n" - - "10a. Jefe rechaza: Sistema notifica actor, fin del caso de uso" - - "12a. Gerente rechaza: Sistema notifica actor, fin del caso de uso" - related_requirements: - - RF-MAE015-004 - -# ============================================================================ -# MODELO DE DATOS -# ============================================================================ - -data_model: - - entities: - - - name: Asset - description: Activo fijo de maquinaria pesada - attributes: - - name: id - type: uuid - description: Identificador 煤nico - constraints: [primary_key] - - - name: code - type: string(20) - description: C贸digo del activo (EXC-2025-001) - constraints: [unique, not_null] - - - name: asset_type_id - type: uuid - description: Tipo de maquinaria - constraints: [foreign_key, not_null] - - - name: brand - type: string(100) - description: Marca - constraints: [not_null] - - - name: model - type: string(100) - description: Modelo - constraints: [not_null] - - - name: serial_number - type: string(100) - description: N煤mero de serie - constraints: [unique] - - - name: manufacturing_year - type: integer - description: A帽o de fabricaci贸n - - - name: acquisition_date - type: date - description: Fecha de adquisici贸n - constraints: [not_null] - - - name: acquisition_cost - type: decimal(15,2) - description: Costo de adquisici贸n - constraints: [not_null, positive] - - - name: supplier_id - type: uuid - description: Proveedor - constraints: [foreign_key] - - - name: depreciation_method - type: enum - description: M茅todo de depreciaci贸n - values: [straight_line, declining_balance, sum_of_digits, units_of_production] - constraints: [not_null] - - - name: useful_life_years - type: integer - description: Vida 煤til en a帽os - constraints: [not_null, between_1_and_50] - - - name: residual_value - type: decimal(15,2) - description: Valor residual - constraints: [not_null, max_30_percent_of_cost] - - - name: current_book_value - type: decimal(15,2) - description: Valor en libros actual - constraints: [not_null] - - - name: accumulated_depreciation - type: decimal(15,2) - description: Depreciaci贸n acumulada - constraints: [not_null, default_0] - - - name: location - type: string(200) - description: Ubicaci贸n f铆sica - - - name: cost_center_id - type: uuid - description: Centro de costos - constraints: [foreign_key] - - - name: responsible_user_id - type: uuid - description: Usuario responsable - constraints: [foreign_key] - - - name: status - type: enum - description: Estado del activo - values: [active, in_maintenance, inactive, disposed] - constraints: [not_null, default_active] - - - name: insurance_policy_number - type: string(100) - description: N煤mero de p贸liza de seguro - - - name: insurance_expiry_date - type: date - description: Fecha de vencimiento del seguro - - - name: warranty_expiry_date - type: date - description: Fecha de vencimiento de garant铆a - - - name: technical_specs - type: jsonb - description: Especificaciones t茅cnicas en JSON - - - name: notes - type: text - description: Observaciones - - - name: tenant_id - type: uuid - description: Tenant (multitenancy) - constraints: [foreign_key, not_null] - - - name: created_at - type: timestamp - constraints: [not_null, default_now] - - - name: updated_at - type: timestamp - constraints: [not_null, default_now] - - - name: created_by - type: uuid - constraints: [foreign_key, not_null] - - - name: updated_by - type: uuid - constraints: [foreign_key] - - indexes: - - fields: [code] - unique: true - - fields: [asset_type_id] - - fields: [status] - - fields: [tenant_id, status] - - fields: [responsible_user_id] - - - name: AssetDocument - description: Documentos asociados a activos - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: asset_id - type: uuid - constraints: [foreign_key, not_null] - - name: document_type - type: enum - values: [invoice, insurance_policy, manual, certificate, warranty, contract, other] - constraints: [not_null] - - name: file_name - type: string(255) - constraints: [not_null] - - name: file_path - type: string(500) - constraints: [not_null] - - name: file_size - type: bigint - - name: mime_type - type: string(100) - - name: uploaded_at - type: timestamp - constraints: [not_null, default_now] - - name: uploaded_by - type: uuid - constraints: [foreign_key, not_null] - - - name: AssetPhoto - description: Fotograf铆as de activos - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: asset_id - type: uuid - constraints: [foreign_key, not_null] - - name: photo_url - type: string(500) - constraints: [not_null] - - name: description - type: string(200) - - name: is_primary - type: boolean - constraints: [default_false] - - name: uploaded_at - type: timestamp - constraints: [not_null, default_now] - - - name: DepreciationMovement - description: Movimientos de depreciaci贸n mensuales - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: asset_id - type: uuid - constraints: [foreign_key, not_null] - - name: period - type: date - description: Per铆odo (primer d铆a del mes) - constraints: [not_null] - - name: depreciation_amount - type: decimal(15,2) - description: Monto depreciado en el per铆odo - constraints: [not_null] - - name: accumulated_depreciation - type: decimal(15,2) - description: Depreciaci贸n acumulada hasta el per铆odo - constraints: [not_null] - - name: book_value - type: decimal(15,2) - description: Valor en libros al final del per铆odo - constraints: [not_null] - - name: accounting_entry_id - type: uuid - description: Referencia al asiento contable - constraints: [foreign_key] - - name: calculation_method - type: string(50) - description: M茅todo usado para el c谩lculo - - name: calculation_details - type: jsonb - description: Detalles del c谩lculo en JSON - - name: created_at - type: timestamp - constraints: [not_null, default_now] - - indexes: - - fields: [asset_id, period] - unique: true - - fields: [period] - - - name: MaintenancePlan - description: Planes de mantenimiento preventivo - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: name - type: string(200) - constraints: [not_null] - - name: asset_type_id - type: uuid - description: Tipo de maquinaria al que aplica - constraints: [foreign_key] - - name: frequency_type - type: enum - values: [days, hours, kilometers] - constraints: [not_null] - - name: frequency_value - type: integer - description: Cada cu谩ntos d铆as/horas/km - constraints: [not_null, positive] - - name: activities - type: jsonb - description: Lista de actividades a realizar - constraints: [not_null] - - name: estimated_duration_hours - type: decimal(5,2) - - name: is_active - type: boolean - constraints: [default_true] - - name: tenant_id - type: uuid - constraints: [foreign_key, not_null] - - name: created_at - type: timestamp - constraints: [not_null, default_now] - - - name: MaintenanceOrder - description: 脫rdenes de mantenimiento - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: order_number - type: string(20) - constraints: [unique, not_null] - - name: asset_id - type: uuid - constraints: [foreign_key, not_null] - - name: maintenance_plan_id - type: uuid - constraints: [foreign_key] - - name: order_type - type: enum - values: [preventive, corrective] - constraints: [not_null] - - name: scheduled_date - type: date - constraints: [not_null] - - name: scheduled_hours - type: integer - description: Horas de m谩quina al momento de programar - - name: status - type: enum - values: [scheduled, in_progress, completed, cancelled] - constraints: [not_null, default_scheduled] - - name: assigned_technician_id - type: uuid - constraints: [foreign_key] - - name: activities_planned - type: jsonb - description: Actividades programadas - - name: activities_completed - type: jsonb - description: Actividades completadas - - name: completion_date - type: timestamp - - name: labor_hours - type: decimal(5,2) - description: Horas de mano de obra - - name: labor_cost - type: decimal(10,2) - - name: parts_cost - type: decimal(10,2) - - name: external_services_cost - type: decimal(10,2) - - name: total_cost - type: decimal(10,2) - - name: observations - type: text - - name: tenant_id - type: uuid - constraints: [foreign_key, not_null] - - name: created_at - type: timestamp - constraints: [not_null, default_now] - - name: completed_by - type: uuid - constraints: [foreign_key] - - indexes: - - fields: [asset_id, status] - - fields: [scheduled_date] - - fields: [status] - - - name: MaintenanceOrderPart - description: Repuestos utilizados en mantenimientos - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: maintenance_order_id - type: uuid - constraints: [foreign_key, not_null] - - name: part_id - type: uuid - description: Referencia a cat谩logo de repuestos - constraints: [foreign_key, not_null] - - name: quantity - type: decimal(10,2) - constraints: [not_null, positive] - - name: unit_cost - type: decimal(10,2) - constraints: [not_null] - - name: total_cost - type: decimal(10,2) - constraints: [not_null] - - - name: MaintenancePhoto - description: Evidencias fotogr谩ficas de mantenimientos - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: maintenance_order_id - type: uuid - constraints: [foreign_key, not_null] - - name: photo_url - type: string(500) - constraints: [not_null] - - name: description - type: string(200) - - name: uploaded_at - type: timestamp - constraints: [not_null, default_now] - - - name: AssetDisposal - description: Bajas de activos - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: disposal_number - type: string(20) - constraints: [unique, not_null] - - name: asset_id - type: uuid - constraints: [foreign_key, not_null] - - name: disposal_type - type: enum - values: [sale, donation, theft, accident, obsolescence, scrap, exchange] - constraints: [not_null] - - name: disposal_date - type: date - constraints: [not_null] - - name: book_value_at_disposal - type: decimal(15,2) - constraints: [not_null] - - name: sale_price - type: decimal(15,2) - description: Precio de venta (si aplica) - - name: sale_expenses - type: decimal(15,2) - description: Gastos de venta - - name: insurance_recovery - type: decimal(15,2) - description: Recuperaci贸n de seguro - - name: gain_loss_amount - type: decimal(15,2) - description: Utilidad o p茅rdida (negativo = p茅rdida) - - name: buyer_supplier - type: string(200) - description: Comprador o receptor - - name: status - type: enum - values: [requested, pending_approval, approved, rejected, executed] - constraints: [not_null, default_requested] - - name: requested_by - type: uuid - constraints: [foreign_key, not_null] - - name: requested_at - type: timestamp - constraints: [not_null, default_now] - - name: approved_by - type: uuid - constraints: [foreign_key] - - name: approved_at - type: timestamp - - name: rejection_reason - type: text - - name: accounting_entry_id - type: uuid - constraints: [foreign_key] - - name: justification - type: text - - name: notes - type: text - - name: tenant_id - type: uuid - constraints: [foreign_key, not_null] - - indexes: - - fields: [asset_id] - - fields: [status] - - fields: [disposal_date] - - - name: DisposalDocument - description: Documentos de baja - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: disposal_id - type: uuid - constraints: [foreign_key, not_null] - - name: document_type - type: enum - values: [sale_contract, donation_deed, police_report, appraisal, authorization, other] - constraints: [not_null] - - name: file_name - type: string(255) - constraints: [not_null] - - name: file_path - type: string(500) - constraints: [not_null] - - name: uploaded_at - type: timestamp - constraints: [not_null, default_now] - - name: uploaded_by - type: uuid - constraints: [foreign_key, not_null] - - - name: DisposalApproval - description: Aprobaciones de bajas - attributes: - - name: id - type: uuid - constraints: [primary_key] - - name: disposal_id - type: uuid - constraints: [foreign_key, not_null] - - name: approval_level - type: integer - description: Nivel de aprobaci贸n (1, 2, 3) - constraints: [not_null] - - name: approver_role - type: string(100) - description: Rol del aprobador - constraints: [not_null] - - name: approver_user_id - type: uuid - constraints: [foreign_key] - - name: status - type: enum - values: [pending, approved, rejected] - constraints: [not_null, default_pending] - - name: decision_date - type: timestamp - - name: comments - type: text - -# ============================================================================ -# INTEGRACIONES -# ============================================================================ - -integrations: - - - module: MGN-001 - name: Gesti贸n de Usuarios - type: interna - purpose: Autenticaci贸n, roles, permisos, asignaci贸n de responsables - endpoints: - - GET /api/v1/users - - GET /api/v1/users/{id} - - GET /api/v1/users/by-role/{role} - data_exchanged: - - Usuario (id, nombre, email, rol) - - Certificaciones de t茅cnicos - - - module: MGN-005 - name: Cat谩logos - type: interna - purpose: Tipos de maquinaria, marcas, modelos, tipos de repuestos - endpoints: - - GET /api/v1/catalogs/asset-types - - GET /api/v1/catalogs/brands - - GET /api/v1/catalogs/parts - data_exchanged: - - Tipos de activos - - Marcas y modelos - - Repuestos - - - module: MGN-006 - name: Configuraci贸n - type: interna - purpose: Plan de cuentas, centros de costos, par谩metros del sistema - endpoints: - - GET /api/v1/settings/chart-of-accounts - - GET /api/v1/settings/cost-centers - - GET /api/v1/settings/parameters - data_exchanged: - - Cuentas contables - - Centros de costos - - Par谩metros de depreciaci贸n - - - module: MGN-007 - name: Auditor铆a - type: interna - purpose: Registro de todas las operaciones cr铆ticas - endpoints: - - POST /api/v1/audit/log - data_exchanged: - - Eventos de auditor铆a (alta, modificaci贸n, baja) - - - module: MGN-008 - name: Notificaciones - type: interna - purpose: Env铆o de alertas y notificaciones - endpoints: - - POST /api/v1/notifications/send - - POST /api/v1/notifications/send-bulk - data_exchanged: - - Notificaciones de mantenimiento - - Alertas de vencimientos - - Notificaciones de aprobaciones - - - module: MGN-009 - name: Reportes - type: interna - purpose: Generaci贸n de reportes del m贸dulo - endpoints: - - POST /api/v1/reports/generate - - GET /api/v1/reports/{id}/download - data_exchanged: - - Par谩metros de reporte - - Archivos PDF/Excel generados - - - module: FIN-001 - name: Contabilidad - type: interna - purpose: Generaci贸n de asientos contables de depreciaci贸n y baja - endpoints: - - POST /api/v1/accounting/entries - - GET /api/v1/accounting/entries/{id} - data_exchanged: - - Asientos contables - - Estado de asientos - - - module: INV-001 - name: Inventarios - type: interna - purpose: Control de repuestos utilizados en mantenimientos - endpoints: - - GET /api/v1/inventory/parts/{id} - - POST /api/v1/inventory/movements - data_exchanged: - - Disponibilidad de repuestos - - Movimientos de salida - - Costos de repuestos - -# ============================================================================ -# APIS EXTERNAS -# ============================================================================ - -external_apis: - - - name: Servicio de Storage - provider: AWS S3 / Azure Blob Storage - purpose: Almacenamiento de documentos y fotograf铆as - authentication: API Key / OAuth - endpoints: - - PUT /storage/upload - - GET /storage/download/{key} - - DELETE /storage/{key} - - - name: Servicio de Email - provider: SendGrid / AWS SES - purpose: Env铆o de notificaciones por correo - authentication: API Key - endpoints: - - POST /email/send - - - name: Servicio de geolocalizaci贸n (opcional) - provider: Google Maps API - purpose: Geolocalizaci贸n de activos en obras - authentication: API Key - endpoints: - - GET /geocoding/address - -# ============================================================================ -# COMPONENTES T脡CNICOS -# ============================================================================ - -technical_components: - - backend: - - component: AssetService - responsibility: L贸gica de negocio de activos - dependencies: [AssetRepository, UserService, CatalogService] - - - component: DepreciationService - responsibility: C谩lculo de depreciaci贸n - dependencies: [AssetRepository, DepreciationRepository, AccountingService] - - - component: DepreciationBatchJob - responsibility: Job programado de depreciaci贸n mensual - dependencies: [DepreciationService, NotificationService] - - - component: MaintenanceService - responsibility: Gesti贸n de mantenimientos - dependencies: [MaintenanceRepository, AssetRepository, InventoryService] - - - component: MaintenanceScheduler - responsibility: Programaci贸n autom谩tica de mantenimientos - dependencies: [MaintenanceService, MaintenancePlanRepository] - - - component: MaintenanceAlertService - responsibility: Generaci贸n de alertas de mantenimiento - dependencies: [MaintenanceRepository, NotificationService] - - - component: DisposalService - responsibility: Gesti贸n de bajas - dependencies: [DisposalRepository, AssetRepository, AccountingService] - - - component: DisposalWorkflow - responsibility: Workflow de aprobaciones - dependencies: [DisposalRepository, UserService, NotificationService] - - - component: AssetReportService - responsibility: Generaci贸n de reportes - dependencies: [AssetRepository, DepreciationRepository, ReportService] - - frontend: - - component: AssetRegistrationForm - responsibility: Formulario de alta de activos - technology: React/Vue component - - - component: AssetDetailView - responsibility: Vista de detalle de activo - technology: React/Vue component - - - component: AssetDocumentManager - responsibility: Gesti贸n de documentos - technology: React/Vue component - - - component: DepreciationDashboard - responsibility: Dashboard de depreciaci贸n - technology: React/Vue component - - - component: MaintenanceCalendar - responsibility: Calendario de mantenimientos - technology: React/Vue component with FullCalendar - - - component: MaintenanceOrderForm - responsibility: Formulario de orden de mantenimiento - technology: React/Vue component - - - component: DisposalRequestForm - responsibility: Formulario de solicitud de baja - technology: React/Vue component - - - component: DisposalApprovalPanel - responsibility: Panel de aprobaciones - technology: React/Vue component - - database: - - component: AssetRepository - responsibility: Acceso a datos de activos - technology: TypeORM/Prisma repository - - - component: DepreciationRepository - responsibility: Acceso a datos de depreciaci贸n - technology: TypeORM/Prisma repository - - - component: MaintenanceRepository - responsibility: Acceso a datos de mantenimientos - technology: TypeORM/Prisma repository - - - component: DisposalRepository - responsibility: Acceso a datos de bajas - technology: TypeORM/Prisma repository - -# ============================================================================ -# PLAN DE IMPLEMENTACI脫N -# ============================================================================ - -implementation_plan: - - sprints: - - - sprint: 1 - name: Alta de Activos - duration: 2 semanas - objectives: - - Implementar registro de activos - - Gesti贸n de documentos y fotograf铆as - - Asignaci贸n de responsables - deliverables: - - RF-MAE015-001 completo - - US-MAE015-001, US-MAE015-002, US-MAE015-003 - stories: - - US-MAE015-001 - - US-MAE015-002 - - US-MAE015-003 - estimated_effort: 80 horas - - - sprint: 2 - name: Depreciaci贸n - duration: 2 semanas - objectives: - - Implementar c谩lculo de depreciaci贸n - - Job batch mensual - - Integraci贸n con contabilidad - - Reportes de depreciaci贸n - deliverables: - - RF-MAE015-002 completo - - US-MAE015-004, US-MAE015-005, US-MAE015-006 - stories: - - US-MAE015-004 - - US-MAE015-005 - - US-MAE015-006 - estimated_effort: 100 horas - - - sprint: 3 - name: Mantenimiento Preventivo - duration: 3 semanas - objectives: - - Implementar planes de mantenimiento - - Programaci贸n autom谩tica - - Sistema de alertas - - Registro de ejecuci贸n - - Control de costos - deliverables: - - RF-MAE015-003 completo - - US-MAE015-007, US-MAE015-008, US-MAE015-009 - stories: - - US-MAE015-007 - - US-MAE015-008 - - US-MAE015-009 - estimated_effort: 120 horas - - - sprint: 4 - name: Baja de Activos - duration: 2 semanas - objectives: - - Implementar proceso de baja - - Workflow de aprobaciones - - C谩lculo de utilidad/p茅rdida - - Integraci贸n contable - deliverables: - - RF-MAE015-004 completo - - US-MAE015-010, US-MAE015-011, US-MAE015-012 - stories: - - US-MAE015-010 - - US-MAE015-011 - - US-MAE015-012 - estimated_effort: 90 horas - - - sprint: 5 - name: Testing y Refinamiento - duration: 2 semanas - objectives: - - Testing integral - - Correcci贸n de bugs - - Optimizaci贸n de performance - - Documentaci贸n - deliverables: - - M贸dulo completo y testeado - - Documentaci贸n t茅cnica - - Manual de usuario - estimated_effort: 80 horas - - total_estimated_effort: 470 horas - total_duration: 11 semanas - - milestones: - - milestone: M1 - Alta de Activos - date: Fin Sprint 1 - deliverable: Registro de activos funcional - - - milestone: M2 - Depreciaci贸n - date: Fin Sprint 2 - deliverable: C谩lculo autom谩tico de depreciaci贸n - - - milestone: M3 - Mantenimiento - date: Fin Sprint 3 - deliverable: Sistema de mantenimiento preventivo - - - milestone: M4 - Baja de Activos - date: Fin Sprint 4 - deliverable: Proceso de baja completo - - - milestone: M5 - Go Live - date: Fin Sprint 5 - deliverable: M贸dulo en producci贸n - -# ============================================================================ -# CRITERIOS DE ACEPTACI脫N DEL M脫DULO -# ============================================================================ - -acceptance_criteria: - - functional: - - criterion: Registro completo de activos - validation: Crear 10 activos de diferentes tipos con documentaci贸n completa - status: pendiente - - - criterion: Depreciaci贸n mensual autom谩tica - validation: Ejecutar c谩lculo de 100 activos con diferentes m茅todos - status: pendiente - - - criterion: Mantenimientos programados - validation: Configurar 5 planes y verificar generaci贸n autom谩tica de 贸rdenes - status: pendiente - - - criterion: Alertas de mantenimiento - validation: Verificar env铆o de notificaciones 7 d铆as antes - status: pendiente - - - criterion: Proceso de baja completo - validation: Ejecutar baja por venta con workflow de aprobaci贸n - status: pendiente - - non_functional: - - criterion: Performance de c谩lculo - validation: Depreciar 10,000 activos en menos de 5 minutos - status: pendiente - - - criterion: Seguridad de documentos - validation: Verificar encriptaci贸n de archivos sensibles - status: pendiente - - - criterion: Auditor铆a completa - validation: Verificar registro de todas las operaciones cr铆ticas - status: pendiente - - - criterion: Responsive design - validation: Verificar funcionalidad en tablets y smartphones - status: pendiente - -# ============================================================================ -# RIESGOS Y MITIGACIONES -# ============================================================================ - -risks: - - - risk: Complejidad de c谩lculos de depreciaci贸n - probability: media - impact: alto - mitigation: > - Validar f贸rmulas con equipo contable, crear suite completa de tests - unitarios, verificar con casos reales. - owner: Tech Lead - - - risk: Integraci贸n con m贸dulo contable - probability: media - impact: alto - mitigation: > - Definir contrato de integraci贸n tempranamente, desarrollar en paralelo, - crear mocks para testing independiente. - owner: Arquitecto de Software - - - risk: Performance en c谩lculo masivo - probability: baja - impact: medio - mitigation: > - Implementar procesamiento en lotes, optimizar queries, considerar - procesamiento as铆ncrono si es necesario. - owner: Tech Lead - - - risk: Complejidad de workflow de aprobaciones - probability: media - impact: medio - mitigation: > - Usar librer铆a probada de workflow (state machine), dise帽ar flujos - simples y extensibles. - owner: Desarrollador Backend - - - risk: Disponibilidad de datos de cat谩logos - probability: alta - impact: bajo - mitigation: > - Coordinar con equipo de MGN-005, tener cat谩logos base precargados, - permitir creaci贸n on-the-fly con permisos. - owner: Product Owner - -# ============================================================================ -# M脡TRICAS DE 脡XITO -# ============================================================================ - -success_metrics: - - - metric: Adopci贸n del m贸dulo - target: 90% de activos registrados en sistema en 3 meses - measurement: Comparar inventario f铆sico vs sistema - - - metric: Precisi贸n de depreciaci贸n - target: 100% de coincidencia con c谩lculos contables - measurement: Auditor铆a mensual de c谩lculos - - - metric: Cumplimiento de mantenimientos - target: 95% de mantenimientos ejecutados en fecha programada - measurement: Reporte de cumplimiento mensual - - - metric: Reducci贸n de tiempo de cierre - target: Reducir 80% tiempo de c谩lculo de depreciaci贸n mensual - measurement: Comparar tiempo antes vs despu茅s de automatizaci贸n - - - metric: Trazabilidad de costos - target: 100% de costos de mantenimiento registrados y trazables - measurement: Auditor铆a de registros de costos - - - metric: Satisfacci贸n de usuarios - target: Calificaci贸n promedio de 4.5/5 en encuesta de usuarios - measurement: Encuesta trimestral a usuarios del m贸dulo - -# ============================================================================ -# DOCUMENTACI脫N REQUERIDA -# ============================================================================ - -documentation: - - technical: - - Manual de instalaci贸n y configuraci贸n - - Documentaci贸n de APIs (OpenAPI/Swagger) - - Diagrama de arquitectura - - Modelo de datos (ERD) - - Gu铆a de desarrollo y est谩ndares de c贸digo - - Manual de pruebas y casos de test - - user: - - Manual de usuario general - - Gu铆as r谩pidas por rol (gerente, contador, t茅cnico) - - Videos tutoriales - - FAQ - - business: - - Especificaci贸n de requerimientos - - Reglas de negocio - - Procesos de negocio (BPMN) - - Matriz de trazabilidad (este documento) - -# ============================================================================ -# HISTORIAL DE CAMBIOS -# ============================================================================ - -change_history: - - version: 1.0.0 - date: 2025-12-06 - author: Equipo Verticales Construcci贸n - changes: - - Creaci贸n inicial del documento - - Definici贸n de 4 RF principales - - Modelo de datos completo - - Plan de implementaci贸n en 5 sprints - approved_by: Product Owner - -# ============================================================================ -# FIN DEL DOCUMENTO -# ============================================================================ diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/README.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/README.md deleted file mode 100644 index ccaa76aa8..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# MAE-016: Gesti贸n Documental - -**M贸dulo:** Sistema de Gesti贸n Documental Corporativa -**Story Points:** 35 | **Prioridad:** Media | **Fase:** 2 (Enterprise) - -## Descripci贸n General - -Sistema centralizado para gesti贸n, almacenamiento y control de documentos de proyectos de construcci贸n. Incluye versionamiento, control de acceso, firma electr贸nica, y repositorio con clasificaci贸n por tipo y proyecto. - -## Alcance Funcional - -### 1. Repositorio Documental -- Almacenamiento en la nube (AWS S3) -- Clasificaci贸n por proyecto y tipo -- Metadata y etiquetado -- B煤squeda avanzada -- Versionamiento autom谩tico - -### 2. Control de Versiones -- Historial completo de versiones -- Comparaci贸n entre versiones -- Restauraci贸n de versiones anteriores -- Trazabilidad de cambios - -### 3. Firma Electr贸nica -- Firma simple y avanzada -- Validaci贸n de identidad -- Certificado digital -- Trazabilidad de firmas - -### 4. Control de Acceso -- Permisos por rol y proyecto -- Documentos confidenciales -- Registro de accesos -- Compartir temporal - -### 5. Workflow de Aprobaci贸n -- Flujo de revisi贸n de documentos -- Aprobaci贸n multinivel -- Comentarios y observaciones -- Notificaciones autom谩ticas - -## Componentes T茅cnicos - -### Backend (NestJS + TypeORM) -```typescript -@Module({ - imports: [TypeOrmModule.forFeature([ - Document, DocumentVersion, DocumentSignature, - DocumentAccess, DocumentApproval - ])], - providers: [ - DocumentService, VersionService, SignatureService, - StorageService, ApprovalWorkflowService - ], - controllers: [DocumentController, SignatureController] -}) -export class DocumentModule {} -``` - -### Base de Datos (PostgreSQL) -```sql -CREATE SCHEMA documents; - -CREATE TYPE documents.document_type AS ENUM ('contract', 'blueprint', 'permit', 'invoice', 'report', 'other'); -CREATE TYPE documents.signature_type AS ENUM ('simple', 'advanced'); -CREATE TYPE documents.approval_status AS ENUM ('pending', 'approved', 'rejected', 'revision'); -``` - -### Storage (AWS S3) -- Bucket: documents-inmobiliaria -- Estructura: {constructoraId}/{projectId}/{type}/{year}/{month} -- Encriptaci贸n: AES-256 -- Lifecycle: Archive despu茅s de 2 a帽os - -## Integraciones - -- **MAI-001 (Proyectos):** Documentos por proyecto -- **MAI-012 (Contratos):** Firma de contratos -- **MAI-013 (Seguridad):** Control de acceso por rol - -## M茅tricas Clave - -- **Volumen:** GB almacenados por proyecto -- **Accesos:** Documentos m谩s consultados -- **Tiempo de aprobaci贸n:** D铆as promedio -- **Firmas:** Documentos firmados vs pendientes - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/_MAP.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/_MAP.md deleted file mode 100644 index 72a45c476..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/_MAP.md +++ /dev/null @@ -1,568 +0,0 @@ -# _MAP: MAE-016 - Gesti贸n Documental y Planos (DMS) - -**脡pica:** MAE-016 -**Nombre:** Gesti贸n Documental y Planos (DMS) -**Fase:** 2 - Enterprise B谩sico -**Presupuesto:** $35,000 MXN -**Story Points:** 60 SP -**Estado:** 馃摑 A crear -**Sprint:** Sprint 9-10 (Semanas 17-20) -**脷ltima actualizaci贸n:** 2025-11-17 -**Prioridad:** P2 - ---- - -## 馃搵 Prop贸sito - -Sistema de gesti贸n documental enterprise con versionado, control de acceso y flujos de aprobaci贸n, similar a Procore Docs o Autodesk Docs: -- Repositorio centralizado de documentos y planos -- Versionado de planos (rev. A, B, C, etc.) -- Control de acceso granular por documento -- Flujos de aprobaci贸n (borrador 鈫 revisado 鈫 aprobado) -- Acceso desde app m贸vil con anotaciones -- Comparaci贸n visual entre versiones de planos -- B煤squeda avanzada con OCR - -**Integraci贸n clave:** Se vincula con Proyectos (MAI-002), Calidad (MAI-009), INFONAVIT (MAI-011) y Contratos (MAI-012). - ---- - -## 馃搧 Contenido - -### Requerimientos Funcionales (Estimados: 6) - -| ID | T铆tulo | Estado | -|----|--------|--------| -| RF-DMS-001 | Repositorio centralizado y clasificaci贸n de documentos | 馃摑 A crear | -| RF-DMS-002 | Versionado y control de cambios | 馃摑 A crear | -| RF-DMS-003 | Control de acceso granular y permisos | 馃摑 A crear | -| RF-DMS-004 | Flujos de aprobaci贸n de documentos | 馃摑 A crear | -| RF-DMS-005 | Visualizaci贸n de planos con anotaciones | 馃摑 A crear | -| RF-DMS-006 | B煤squeda avanzada y metadata | 馃摑 A crear | - -### Especificaciones T茅cnicas (Estimadas: 6) - -| ID | T铆tulo | RF | Estado | -|----|--------|----|--------| -| ET-DMS-001 | Modelo de datos de documentos y versiones | RF-DMS-001, RF-DMS-002 | 馃摑 A crear | -| ET-DMS-002 | Sistema de control de acceso (ACL) | RF-DMS-003 | 馃摑 A crear | -| ET-DMS-003 | Motor de workflows de aprobaci贸n | RF-DMS-004 | 馃摑 A crear | -| ET-DMS-004 | Visualizador de planos con canvas | RF-DMS-005 | 馃摑 A crear | -| ET-DMS-005 | Motor de b煤squeda con indexaci贸n | RF-DMS-006 | 馃摑 A crear | -| ET-DMS-006 | Storage y CDN para archivos | RF-DMS-001 | 馃摑 A crear | - -### Historias de Usuario (Estimadas: 12) - -| ID | T铆tulo | SP | Estado | -|----|--------|----|--------| -| US-DMS-001 | Subir documento y clasificar | 5 | 馃摑 A crear | -| US-DMS-002 | Crear nueva versi贸n de plano | 5 | 馃摑 A crear | -| US-DMS-003 | Comparar versiones de plano | 5 | 馃摑 A crear | -| US-DMS-004 | Configurar permisos por documento/carpeta | 5 | 馃摑 A crear | -| US-DMS-005 | Iniciar workflow de aprobaci贸n | 5 | 馃摑 A crear | -| US-DMS-006 | Aprobar/rechazar documento | 5 | 馃摑 A crear | -| US-DMS-007 | Visualizar plano con anotaciones | 5 | 馃摑 A crear | -| US-DMS-008 | Hacer anotaciones sobre plano desde m贸vil | 5 | 馃摑 A crear | -| US-DMS-009 | Buscar documentos por metadata | 5 | 馃摑 A crear | -| US-DMS-010 | Marcar plano como obsoleto | 5 | 馃摑 A crear | -| US-DMS-011 | Dashboard de documentos del proyecto | 5 | 馃摑 A crear | -| US-DMS-012 | Exportar paquete de documentos | 5 | 馃摑 A crear | - -**Total Story Points:** 60 SP - -### Implementaci贸n - -馃搳 **Inventarios de trazabilidad:** -- [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) - Matriz completa de trazabilidad -- [DATABASE.yml](./implementacion/DATABASE.yml) - Objetos de base de datos -- [BACKEND.yml](./implementacion/BACKEND.yml) - M贸dulos backend -- [FRONTEND.yml](./implementacion/FRONTEND.yml) - Componentes frontend - -### Pruebas - -馃搵 Documentaci贸n de testing: -- [TEST-PLAN.md](./pruebas/TEST-PLAN.md) - Plan de pruebas -- [TEST-CASES.md](./pruebas/TEST-CASES.md) - Casos de prueba - ---- - -## 馃敆 Referencias - -- **README:** [README.md](./README.md) - Descripci贸n detallada de la 茅pica -- **Fase 2:** [../README.md](../README.md) - Informaci贸n de la fase completa -- **M贸dulo relacionado MVP:** M贸dulo 16 - Gesti贸n Documental (MVP-APP.md) - ---- - -## 馃搳 M茅tricas - -| M茅trica | Valor | -|---------|-------| -| **Presupuesto estimado** | $35,000 MXN | -| **Story Points estimados** | 60 SP | -| **Duraci贸n estimada** | 12 d铆as | -| **Reutilizaci贸n GAMILIT** | 20% (gesti贸n de archivos b谩sica) | -| **RF a implementar** | 6/6 | -| **ET a implementar** | 6/6 | -| **US a completar** | 12/12 | - ---- - -## 馃幆 M贸dulos Afectados - -### Base de Datos -- **Schema:** `documents` -- **Tablas principales:** - * `document_categories` - Categor铆as de documentos - * `documents` - Documentos maestros - * `document_versions` - Versiones de documentos - * `document_permissions` - Control de acceso - * `approval_workflows` - Workflows de aprobaci贸n - * `approval_steps` - Pasos del workflow - * `annotations` - Anotaciones sobre planos - * `document_metadata` - Metadata para b煤squeda -- **ENUMs:** - * `document_type` (drawing, contract, specification, report, photo, rfi, submittal) - * `document_status` (draft, under_review, approved, rejected, obsolete) - * `permission_level` (no_access, view, comment, edit, approve, admin) - * `approval_action` (pending, approved, rejected, requested_changes) - -### Backend -- **M贸dulo:** `documents` -- **Path:** `apps/backend/src/modules/documents/` -- **Services:** - * DocumentService - * VersionService - * PermissionService - * WorkflowService - * AnnotationService - * SearchService - * StorageService (S3/Azure Blob) -- **Controllers:** DocumentController, VersionController, WorkflowController -- **Middlewares:** DocumentAccessGuard, VersionControl Middleware - -### Frontend -- **Features:** `documents`, `document-viewer` -- **Path:** `apps/frontend/src/features/documents/` -- **Componentes:** - * DocumentLibrary (tree view) - * DocumentUploader - * DocumentDetail - * VersionHistory - * VersionComparator - * PermissionManager - * WorkflowInitiator - * ApprovalDashboard - * PlanViewer (canvas-based) - * AnnotationTool - * DocumentSearch -- **Stores:** documentStore, versionStore, workflowStore - -### App M贸vil -- **Features:** `document-viewer-mobile` -- **Componentes:** - * PlanViewerMobile (touch-optimized) - * AnnotationToolMobile (draw on screen) - * OfflineDocumentCache - ---- - -## 馃搨 Estructura de Repositorio - -### Taxonom铆a de Documentos - -``` -Proyecto: Fraccionamiento Los Pinos -鈹溾攢鈹 01-Planos -鈹 鈹溾攢鈹 Arquitect贸nicos -鈹 鈹 鈹溾攢鈹 Conjunto -鈹 鈹 鈹 鈹溾攢鈹 PC-01-Planta de Conjunto.dwg (Rev. C) -鈹 鈹 鈹 鈹斺攢鈹 PC-02-Distribuci贸n de Prototipos.dwg (Rev. B) -鈹 鈹 鈹溾攢鈹 Prototipos -鈹 鈹 鈹 鈹溾攢鈹 PA-01-Tipo A Plantas.dwg (Rev. D) -鈹 鈹 鈹 鈹溾攢鈹 PA-02-Tipo A Fachadas.dwg (Rev. C) -鈹 鈹 鈹 鈹溾攢鈹 PB-01-Tipo B Plantas.dwg (Rev. B) -鈹 鈹 鈹 鈹斺攢鈹 PB-02-Tipo B Fachadas.dwg (Rev. B) -鈹 鈹 鈹斺攢鈹 Detalles -鈹 鈹溾攢鈹 Estructurales -鈹 鈹 鈹溾攢鈹 PE-01-Cimentaci贸n Tipo A.dwg (Rev. A) -鈹 鈹 鈹溾攢鈹 PE-02-Estructura Tipo A.dwg (Rev. B) -鈹 鈹 鈹斺攢鈹 PE-03-Detalles Estructurales.dwg (Rev. A) -鈹 鈹溾攢鈹 Instalaciones -鈹 鈹 鈹溾攢鈹 Hidr谩ulicas -鈹 鈹 鈹 鈹斺攢鈹 PH-01-Inst. Hidr谩ulica Tipo A.dwg (Rev. B) -鈹 鈹 鈹溾攢鈹 Sanitarias -鈹 鈹 鈹 鈹斺攢鈹 PS-01-Inst. Sanitaria Tipo A.dwg (Rev. B) -鈹 鈹 鈹溾攢鈹 El茅ctricas -鈹 鈹 鈹 鈹斺攢鈹 PE-01-Inst. El茅ctrica Tipo A.dwg (Rev. C) -鈹 鈹 鈹斺攢鈹 Gas -鈹 鈹 鈹斺攢鈹 PG-01-Inst. Gas Tipo A.dwg (Rev. A) -鈹 鈹斺攢鈹 Urbanizaci贸n -鈹 鈹溾攢鈹 PU-01-Lotificaci贸n.dwg (Rev. B) -鈹 鈹溾攢鈹 PU-02-Pavimentos.dwg (Rev. A) -鈹 鈹斺攢鈹 PU-03-Redes de Servicios.dwg (Rev. B) -鈹 -鈹溾攢鈹 02-Contratos -鈹 鈹溾攢鈹 Contrato Principal INFONAVIT.pdf -鈹 鈹溾攢鈹 Subcontratos -鈹 鈹 鈹溾攢鈹 SC-001-Instalaciones El茅ctricas.pdf -鈹 鈹 鈹溾攢鈹 SC-002-Plomer铆a.pdf -鈹 鈹 鈹斺攢鈹 SC-003-Herrer铆a.pdf -鈹 鈹斺攢鈹 脫rdenes de Cambio -鈹 鈹溾攢鈹 OC-001-Cambio de acabados.pdf -鈹 鈹斺攢鈹 OC-002-Ampliaci贸n caseta.pdf -鈹 -鈹溾攢鈹 03-Especificaciones -鈹 鈹溾攢鈹 Especificaciones T茅cnicas Generales.pdf -鈹 鈹溾攢鈹 Especificaciones de Materiales.xlsx -鈹 鈹斺攢鈹 Normas Aplicables.pdf -鈹 -鈹溾攢鈹 04-Permisos y Licencias -鈹 鈹溾攢鈹 Licencia de Construcci贸n.pdf -鈹 鈹溾攢鈹 Uso de Suelo.pdf -鈹 鈹溾攢鈹 Manifesto IMSS.pdf -鈹 鈹斺攢鈹 Impacto Ambiental.pdf -鈹 -鈹溾攢鈹 05-RFIs (Request for Information) -鈹 鈹溾攢鈹 RFI-001-Aclaraci贸n estructura.pdf -鈹 鈹溾攢鈹 RFI-002-Detalle ventaner铆a.pdf -鈹 鈹斺攢鈹 RFI-003-Ubicaci贸n registros.pdf -鈹 -鈹溾攢鈹 06-Submittals -鈹 鈹溾攢鈹 SUB-001-Muestras de piso.pdf -鈹 鈹溾攢鈹 SUB-002-Ficha t茅cnica pintura.pdf -鈹 鈹斺攢鈹 SUB-003-Certificado de canceler铆a.pdf -鈹 -鈹溾攢鈹 07-Reportes -鈹 鈹溾攢鈹 Semanales -鈹 鈹 鈹溾攢鈹 Reporte Semana 01.pdf -鈹 鈹 鈹斺攢鈹 ... -鈹 鈹溾攢鈹 Mensuales -鈹 鈹 鈹溾攢鈹 Reporte Mes 01.pdf -鈹 鈹 鈹斺攢鈹 ... -鈹 鈹斺攢鈹 Fotograf铆as -鈹 鈹溾攢鈹 2025-11-01-Avance Cimentaci贸n -鈹 鈹斺攢鈹 2025-11-15-Avance Estructura -鈹 -鈹溾攢鈹 08-Certificados -鈹 鈹溾攢鈹 Certificado de Calidad INFONAVIT.pdf -鈹 鈹溾攢鈹 Certificado de Sustentabilidad.pdf -鈹 鈹斺攢鈹 Garant铆as -鈹 鈹溾攢鈹 Garant铆a Impermeabilizaci贸n.pdf -鈹 鈹斺攢鈹 Garant铆a Canceler铆a.pdf -鈹 -鈹斺攢鈹 09-Manuales - 鈹溾攢鈹 Manual de Operaci贸n y Mantenimiento.pdf - 鈹斺攢鈹 Manual del Propietario.pdf -``` - ---- - -## 馃搫 Versionado de Planos - -### Nomenclatura de Revisiones - -| Revisi贸n | Descripci贸n | Uso | -|----------|-------------|-----| -| **Rev. 0** | Borrador inicial | Uso interno, no para construcci贸n | -| **Rev. A** | Primera emisi贸n formal | Para construcci贸n | -| **Rev. B** | Primera revisi贸n | Incorpora cambios menores | -| **Rev. C** | Segunda revisi贸n | Incorpora RFIs u 贸rdenes de cambio | -| **Rev. D+** | Revisiones subsecuentes | Cambios adicionales | - ---- - -### Ejemplo de Versi贸n de Plano - -```yaml -document: - id: "DOC-001" - code: "PA-01" - title: "Plano Arquitect贸nico Tipo A - Plantas" - type: "drawing" - category: "Planos > Arquitect贸nicos > Prototipos" - project_id: "PROJ-001" - - current_version: - version_number: 4 - revision: "D" - status: "approved" - approved_date: "2025-11-15" - approved_by: "Ing. Mar铆a L贸pez" - - versions: - - version: 1 - revision: "0" - date: "2025-01-15" - description: "Borrador inicial" - author: "Arq. Juan P茅rez" - file: "PA-01-Rev0.dwg" - status: "obsolete" - - - version: 2 - revision: "A" - date: "2025-02-01" - description: "Primera emisi贸n para construcci贸n" - author: "Arq. Juan P茅rez" - file: "PA-01-RevA.dwg" - status: "obsolete" - changes: "Ajustes de dimensiones seg煤n normativa" - - - version: 3 - revision: "B" - date: "2025-05-15" - description: "Revisi贸n por cambio de ventaner铆a" - author: "Arq. Juan P茅rez" - file: "PA-01-RevB.dwg" - status: "obsolete" - changes: "Cambio de ventanas de aluminio a PVC seg煤n RFI-002" - references: ["RFI-002"] - - - version: 4 - revision: "C" - date: "2025-08-20" - description: "Revisi贸n por adecuaciones INFONAVIT" - author: "Arq. Juan P茅rez" - file: "PA-01-RevC.dwg" - status: "obsolete" - changes: "Incremento de superficie de rec谩mara 2 de 7.0m虏 a 7.5m虏" - references: ["OC-001", "Observaci贸n INFONAVIT"] - - - version: 5 - revision: "D" - date: "2025-11-15" - description: "Revisi贸n por cambio de acabados" - author: "Arq. Juan P茅rez" - file: "PA-01-RevD.dwg" - status: "approved" - changes: "Cambio de piso cer谩mico a porcelanato en sala-comedor" - references: ["OC-002"] - watermark: "PARA CONSTRUCCI脫N - REV D" -``` - ---- - -## 馃敀 Control de Acceso - -### Niveles de Permiso - -| Nivel | Descripci贸n | Acciones permitidas | -|-------|-------------|---------------------| -| **Sin acceso** | No puede ver el documento | - | -| **Vista** | Solo lectura | Ver, descargar | -| **Comentarios** | Puede agregar anotaciones | Ver, descargar, comentar | -| **Edici贸n** | Puede subir nuevas versiones | Ver, descargar, comentar, subir versi贸n | -| **Aprobaci贸n** | Puede aprobar documentos | Ver, descargar, comentar, aprobar | -| **Admin** | Control total | Todas las anteriores + eliminar, cambiar permisos | - ---- - -### Matriz de Permisos por Rol - -| Carpeta / Documento | Director | Ingeniero | Residente | Subcontratista | Cliente | -|---------------------|----------|-----------|-----------|----------------|---------| -| **Planos Arquitect贸nicos** | Admin | Edici贸n | Vista | Vista | Sin acceso | -| **Planos Instalaciones** | Admin | Edici贸n | Vista | Edici贸n* | Sin acceso | -| **Contratos** | Admin | Vista | Sin acceso | Vista* | Sin acceso | -| **RFIs** | Vista | Edici贸n | Edici贸n | Vista | Sin acceso | -| **Reportes** | Admin | Vista | Edici贸n | Sin acceso | Sin acceso | -| **Planos As-Built** | Admin | Edici贸n | Comentarios | Sin acceso | Vista | - -(*) Solo para su especialidad - ---- - -## 鉁 Flujos de Aprobaci贸n - -### Workflow T铆pico: Aprobaci贸n de Plano - -```mermaid -graph LR - A[Borrador] --> B{Revisi贸n T茅cnica} - B -->|Rechazado| A - B -->|Aprobado| C{Revisi贸n Cliente} - C -->|Cambios requeridos| A - C -->|Aprobado| D[Aprobado para Construcci贸n] - D --> E[Vigente] -``` - ---- - -### Ejemplo de Workflow - -```yaml -workflow: - id: "WF-2025-045" - document_id: "DOC-001" - document_version: 5 - title: "Aprobaci贸n Plano PA-01 Rev. D" - initiated_by: "Arq. Juan P茅rez" - initiated_date: "2025-11-10" - status: "completed" - - steps: - - step: 1 - role: "Ingeniero Estructural" - assignee: "Ing. Carlos Ram铆rez" - action_required: "approval" - status: "approved" - completed_date: "2025-11-11" - notes: "Sin conflictos estructurales" - - - step: 2 - role: "Residente de Obra" - assignee: "Ing. Pedro Mart铆nez" - action_required: "approval" - status: "approved" - completed_date: "2025-11-12" - notes: "Verificado en sitio, viable" - - - step: 3 - role: "Director T茅cnico" - assignee: "Ing. Mar铆a L贸pez" - action_required: "approval" - status: "approved" - completed_date: "2025-11-15" - notes: "Aprobado para construcci贸n" - final_approval: true - - final_status: "approved" - approved_date: "2025-11-15" - watermark_added: "PARA CONSTRUCCI脫N - REV D - APROBADO 2025-11-15" -``` - ---- - -## 馃柤锔 Visualizador de Planos - -### Funcionalidades - -1. **Navegaci贸n:** - - Zoom in/out - - Pan (arrastrar) - - Ajustar a pantalla - - Rotaci贸n - -2. **Anotaciones:** - - Dibujo libre (l谩piz) - - Formas (c铆rculo, rect谩ngulo, flecha) - - Texto - - Mediciones (distancia, 谩rea) - - Marcadores de ubicaci贸n - -3. **Comparaci贸n:** - - Vista lado a lado de 2 versiones - - Overlay con opacidad ajustable - - Resaltado de diferencias autom谩tico - -4. **Colaboraci贸n:** - - Anotaciones multi-usuario - - Comentarios vinculados a punto en plano - - Notificaciones en tiempo real - ---- - -### Ejemplo de Anotaci贸n - -```yaml -annotation: - id: "ANN-123" - document_id: "DOC-001" - document_version: 5 - created_by: "Residente Pedro Mart铆nez" - created_date: "2025-11-16T10:30:00Z" - type: "comment" - position: - x: 1250 # p铆xeles - y: 800 - page: 1 - shape: - type: "circle" - radius: 20 - color: "#FF0000" - content: - text: "Verificar medida de ventana en sitio. Parece tener 10cm menos." - priority: "high" - status: "open" - responses: - - user: "Arq. Juan P茅rez" - date: "2025-11-16T14:00:00Z" - text: "Verificado en sitio, es correcto. Ventana instalada seg煤n especificaci贸n." - resolved_by: "Arq. Juan P茅rez" - resolved_date: "2025-11-16T14:00:00Z" -``` - ---- - -## 馃攳 B煤squeda Avanzada - -### Criterios de B煤squeda - -| Campo | Tipo | Ejemplo | -|-------|------|---------| -| **Texto completo** | Full-text | "instalaci贸n el茅ctrica" | -| **C贸digo** | Exacto | "PA-01" | -| **Categor铆a** | Jer谩rquico | "Planos > Arquitect贸nicos" | -| **Tipo** | Enum | "Plano", "Contrato", "RFI" | -| **Estado** | Enum | "Aprobado", "Vigente" | -| **Revisi贸n** | Texto | "Rev. D" | -| **Autor** | Usuario | "Arq. Juan P茅rez" | -| **Fecha** | Rango | "2025-01-01" a "2025-12-31" | -| **Proyecto** | Relaci贸n | "Fraccionamiento Los Pinos" | -| **Tags** | M煤ltiple | "tipo-a", "instalaciones", "aprobado-infonavit" | - ---- - -### OCR para B煤squeda - -**Proceso:** -1. Al subir PDF escaneado o imagen -2. Sistema ejecuta OCR (Tesseract o Cloud Vision API) -3. Texto extra铆do se indexa para b煤squeda -4. Usuario puede buscar contenido dentro de escaneos - -**Ejemplo:** -- Documento: Licencia de construcci贸n (PDF escaneado) -- OCR detecta: "Licencia de construcci贸n No. 2025-456..." -- Usuario busca: "licencia 2025" -- Resultado: Se encuentra el documento - ---- - -## 馃搳 Dashboard de Documentos - -### Indicadores - -| M茅trica | Valor | Meta | -|---------|-------|------| -| **Total documentos** | 1,250 | - | -| **Planos vigentes** | 85 | 100% al d铆a | -| **Planos obsoletos** | 120 | Archivados | -| **Documentos en revisi贸n** | 12 | <20 | -| **RFIs pendientes** | 3 | <5 | -| **Aprobaciones pendientes** | 5 | <10 | -| **Espacio usado** | 45 GB | <100 GB | - ---- - -## 馃毃 Puntos Cr铆ticos - -1. **Versionado estricto:** Nunca sobrescribir, siempre nueva versi贸n -2. **Planos obsoletos:** Marcar claramente para evitar uso incorrecto -3. **Control de acceso:** Subcontratistas solo ven su especialidad -4. **Aprobaciones formales:** Workflow obligatorio para planos de construcci贸n -5. **Backup:** Repositorio cr铆tico, backup diario -6. **Sincronizaci贸n m贸vil:** Cache offline para uso en obra -7. **Watermarks:** Planos para construcci贸n deben estar marcados - ---- - -## 馃幆 Siguiente Paso - -Crear documentaci贸n de requerimientos y especificaciones t茅cnicas del m贸dulo. - ---- - -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @frontend-team @document-team -**Estado:** 馃摑 A crear diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-001-modelo de datos documental.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-001-modelo de datos documental.md deleted file mode 100644 index 2357e4ca2..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-001-modelo de datos documental.md +++ /dev/null @@ -1,51 +0,0 @@ -# ET-DOC-001: Modelo de Datos Documental - -**ID:** ET-DOC-001 | **M贸dulo:** MAE-016 - -## Schema -```sql -CREATE SCHEMA documents; - -CREATE TYPE documents.document_type AS ENUM ('contract', 'blueprint', 'permit', 'invoice', 'report', 'other'); -CREATE TYPE documents.signature_type AS ENUM ('simple', 'advanced'); - -CREATE TABLE documents.documents ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - project_id UUID REFERENCES projects.projects(id), - type documents.document_type NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - current_version INT DEFAULT 1, - s3_path VARCHAR(500) NOT NULL, - file_size BIGINT, - mime_type VARCHAR(100), - is_confidential BOOLEAN DEFAULT false, - created_by UUID REFERENCES auth.users(id), - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE TABLE documents.versions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - document_id UUID REFERENCES documents.documents(id), - version INT NOT NULL, - s3_path VARCHAR(500) NOT NULL, - file_size BIGINT, - change_notes TEXT, - created_by UUID REFERENCES auth.users(id), - created_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(document_id, version) -); - -CREATE TABLE documents.signatures ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - document_id UUID REFERENCES documents.documents(id), - signer_id UUID REFERENCES auth.users(id), - type documents.signature_type NOT NULL, - signature_data TEXT, - certificate TEXT, - signed_at TIMESTAMPTZ DEFAULT NOW() -); -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-002-servicio de almacenamiento s3.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-002-servicio de almacenamiento s3.md deleted file mode 100644 index 122b1c954..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-002-servicio de almacenamiento s3.md +++ /dev/null @@ -1,50 +0,0 @@ -# ET-DOC-002: Servicio de Almacenamiento S3 - -**ID:** ET-DOC-002 | **M贸dulo:** MAE-016 - -## Storage Service -```typescript -@Injectable() -export class StorageService { - private s3: S3; - - async upload(file: Express.Multer.File, metadata: FileMetadata): Promise { - const key = this.generateKey(metadata); - - await this.s3.upload({ - Bucket: process.env.S3_BUCKET, - Key: key, - Body: file.buffer, - ContentType: file.mimetype, - ServerSideEncryption: 'AES256', - Metadata: { - projectId: metadata.projectId, - uploadedBy: metadata.uploadedBy, - type: metadata.type - } - }).promise(); - - return key; - } - - async download(key: string): Promise { - const result = await this.s3.getObject({ - Bucket: process.env.S3_BUCKET, - Key: key - }).promise(); - - return result.Body as Buffer; - } - - private generateKey(metadata: FileMetadata): string { - const { constructoraId, projectId, type } = metadata; - const date = new Date(); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - return `${constructoraId}/${projectId}/${type}/${year}/${month}/${uuidv4()}`; - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-003-servicio de versionamiento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-003-servicio de versionamiento.md deleted file mode 100644 index 35523ec6a..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-003-servicio de versionamiento.md +++ /dev/null @@ -1,53 +0,0 @@ -# ET-DOC-003: Servicio de Versionamiento - -**ID:** ET-DOC-003 | **M贸dulo:** MAE-016 - -## Version Service -```typescript -@Injectable() -export class VersionService { - async createVersion( - documentId: string, - file: Express.Multer.File, - changeNotes: string, - userId: string - ): Promise { - const document = await this.documentRepo.findOne(documentId); - const newVersion = document.currentVersion + 1; - - const s3Path = await this.storageService.upload(file, { - projectId: document.projectId, - type: document.type, - uploadedBy: userId - }); - - const version = await this.versionRepo.save({ - documentId, - version: newVersion, - s3Path, - fileSize: file.size, - changeNotes, - createdBy: userId - }); - - document.currentVersion = newVersion; - document.s3Path = s3Path; - await this.documentRepo.save(document); - - return version; - } - - async compareVersions(v1Id: string, v2Id: string): Promise { - const v1 = await this.versionRepo.findOne(v1Id); - const v2 = await this.versionRepo.findOne(v2Id); - - // For PDFs, generate visual diff - const diff = await this.pdfDiffService.compare(v1.s3Path, v2.s3Path); - - return { v1, v2, diff }; - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-004-servicio de firma electr贸nica.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-004-servicio de firma electr贸nica.md deleted file mode 100644 index e3b7a2b61..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-004-servicio de firma electr贸nica.md +++ /dev/null @@ -1,51 +0,0 @@ -# ET-DOC-004: Servicio de Firma Electr贸nica - -**ID:** ET-DOC-004 | **M贸dulo:** MAE-016 - -## Signature Service -```typescript -@Injectable() -export class SignatureService { - async requestSignature( - documentId: string, - signerId: string, - type: 'simple' | 'advanced' - ): Promise { - const document = await this.documentRepo.findOne(documentId); - const signer = await this.userRepo.findOne(signerId); - - if (type === 'simple') { - const otp = this.generateOTP(); - await this.sendOTPEmail(signer.email, otp); - - return { requestId: uuidv4(), otp, expiresAt: this.addMinutes(new Date(), 15) }; - } else { - // FIEL (SAT) signature - return this.initializeFIELSignature(documentId, signerId); - } - } - - async sign( - documentId: string, - signerId: string, - signatureData: string, - certificate?: string - ): Promise { - const signature = await this.signatureRepo.save({ - documentId, signerId, - type: certificate ? 'advanced' : 'simple', - signatureData, - certificate, - signedAt: new Date() - }); - - // Embed signature in PDF - await this.embedSignatureInPDF(documentId, signature); - - return signature; - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-005-workflow de aprobaci贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-005-workflow de aprobaci贸n.md deleted file mode 100644 index 34332e543..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/especificaciones/ET-DOC-005-workflow de aprobaci贸n.md +++ /dev/null @@ -1,46 +0,0 @@ -# ET-DOC-005: Workflow de Aprobaci贸n - -**ID:** ET-DOC-005 | **M贸dulo:** MAE-016 - -## Approval Workflow Service -```typescript -@Injectable() -export class ApprovalWorkflowService { - async initiateApproval(documentId: string, workflowId: string): Promise { - const workflow = await this.workflowRepo.findOne(workflowId); - const document = await this.documentRepo.findOne(documentId); - - for (const step of workflow.steps) { - await this.approvalRepo.save({ - documentId, - approverRole: step.role, - order: step.order, - status: 'pending', - deadline: this.addDays(new Date(), step.deadlineDays) - }); - } - - await this.notifyNextApprover(documentId); - } - - async approve(approvalId: string, userId: string, comments?: string): Promise { - const approval = await this.approvalRepo.findOne(approvalId); - approval.status = 'approved'; - approval.approvedBy = userId; - approval.approvedAt = new Date(); - approval.comments = comments; - await this.approvalRepo.save(approval); - - const hasNext = await this.hasNextApprover(approval.documentId); - - if (hasNext) { - await this.notifyNextApprover(approval.documentId); - } else { - await this.finalizeApproval(approval.documentId); - } - } -} -``` - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-001-subir documento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-001-subir documento.md deleted file mode 100644 index 5029ee6e6..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-001-subir documento.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-DOC-001: Subir Documento - -**ID:** US-DOC-001 | **M贸dulo:** MAE-016 | **SP:** 5 - -## Historia -**Como** Usuario -**Quiero** Arrastra archivo 鈫 Clasifica tipo 鈫 Agrega metadata 鈫 Sube a S3 -**Para** almacenar documento en repositorio - -## Criterios -1. Arrastra archivo 鈫 Clasifica tipo 鈫 Agrega metadata 鈫 Sube a S3 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-002-buscar y descargar.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-002-buscar y descargar.md deleted file mode 100644 index 92c2cfee7..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-002-buscar y descargar.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-DOC-002: Buscar y Descargar - -**ID:** US-DOC-002 | **M贸dulo:** MAE-016 | **SP:** 5 - -## Historia -**Como** Usuario -**Quiero** Busca por nombre/tipo/proyecto 鈫 Ve resultados 鈫 Previsualiza 鈫 Descarga -**Para** encontrar documentos r谩pidamente - -## Criterios -1. Busca por nombre/tipo/proyecto 鈫 Ve resultados 鈫 Previsualiza 鈫 Descarga 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-003-crear nueva versi贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-003-crear nueva versi贸n.md deleted file mode 100644 index 8fae8afb8..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-003-crear nueva versi贸n.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-DOC-003: Crear Nueva Versi贸n - -**ID:** US-DOC-003 | **M贸dulo:** MAE-016 | **SP:** 5 - -## Historia -**Como** Usuario -**Quiero** Selecciona documento 鈫 Sube nueva versi贸n 鈫 Agrega notas 鈫 Guarda -**Para** mantener historial de cambios - -## Criterios -1. Selecciona documento 鈫 Sube nueva versi贸n 鈫 Agrega notas 鈫 Guarda 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-004-firmar documento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-004-firmar documento.md deleted file mode 100644 index 6fe25178f..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-004-firmar documento.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-DOC-004: Firmar Documento - -**ID:** US-DOC-004 | **M贸dulo:** MAE-016 | **SP:** 5 - -## Historia -**Como** Firmante -**Quiero** Abre documento 鈫 Solicita firma 鈫 Ingresa OTP 鈫 Firma -**Para** autorizar documento legalmente - -## Criterios -1. Abre documento 鈫 Solicita firma 鈫 Ingresa OTP 鈫 Firma 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-005-compartir documento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-005-compartir documento.md deleted file mode 100644 index 3b6baf153..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-005-compartir documento.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-DOC-005: Compartir Documento - -**ID:** US-DOC-005 | **M贸dulo:** MAE-016 | **SP:** 5 - -## Historia -**Como** Due帽o del Documento -**Quiero** Selecciona documento 鈫 Define permisos 鈫 Genera link 鈫 Establece expiraci贸n -**Para** compartir temporalmente con externos - -## Criterios -1. Selecciona documento 鈫 Define permisos 鈫 Genera link 鈫 Establece expiraci贸n 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-006-aprobar documento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-006-aprobar documento.md deleted file mode 100644 index a4f4fff74..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-006-aprobar documento.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-DOC-006: Aprobar Documento - -**ID:** US-DOC-006 | **M贸dulo:** MAE-016 | **SP:** 5 - -## Historia -**Como** Aprobador -**Quiero** Ve documento pendiente 鈫 Revisa 鈫 Agrega comentarios 鈫 Aprueba/Rechaza -**Para** completar workflow de aprobaci贸n - -## Criterios -1. Ve documento pendiente 鈫 Revisa 鈫 Agrega comentarios 鈫 Aprueba/Rechaza 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-007-dashboard documental.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-007-dashboard documental.md deleted file mode 100644 index 93245d71f..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/historias-usuario/US-DOC-007-dashboard documental.md +++ /dev/null @@ -1,16 +0,0 @@ -# US-DOC-007: Dashboard Documental - -**ID:** US-DOC-007 | **M贸dulo:** MAE-016 | **SP:** 5 - -## Historia -**Como** Gerente de Proyecto -**Quiero** Ve documentos por proyecto 鈫 Firmas pendientes 鈫 Aprobaciones 鈫 Alertas -**Para** supervisar gesti贸n documental - -## Criterios -1. Ve documentos por proyecto 鈫 Firmas pendientes 鈫 Aprobaciones 鈫 Alertas 鉁 -2. Validaciones OK 鉁 -3. Permisos correctos 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-001-repositorio y almacenamiento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-001-repositorio y almacenamiento.md deleted file mode 100644 index 916acffda..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-001-repositorio y almacenamiento.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-DOC-001: Repositorio y Almacenamiento - -**ID:** RF-DOC-001 | **M贸dulo:** MAE-016 | **Prioridad:** Media | **SP:** 7 - -## Descripci贸n -Upload, clasificaci贸n, b煤squeda, S3 - -## Reglas de Negocio -1. Upload drag & drop con validaci贸n de tipo -2. Almacenamiento en AWS S3 con encriptaci贸n -3. Clasificaci贸n por proyecto, tipo, a帽o -4. Metadata autom谩tica (tama帽o, fecha, autor) -5. B煤squeda por metadata y contenido (OCR) -6. Preview en navegador (PDF, im谩genes) - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-002-versionamiento.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-002-versionamiento.md deleted file mode 100644 index 6513f97b4..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-002-versionamiento.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-DOC-002: Versionamiento - -**ID:** RF-DOC-002 | **M贸dulo:** MAE-016 | **Prioridad:** Media | **SP:** 7 - -## Descripci贸n -Historial, comparaci贸n, restauraci贸n - -## Reglas de Negocio -1. Nueva versi贸n al modificar documento -2. Historial completo de versiones -3. Comparaci贸n visual entre versiones -4. Restauraci贸n de versi贸n anterior -5. Trazabilidad de cambios (qui茅n, cu谩ndo) -6. Descarga de cualquier versi贸n - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-003-firma electr贸nica.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-003-firma electr贸nica.md deleted file mode 100644 index 40fcd68d9..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-003-firma electr贸nica.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-DOC-003: Firma Electr贸nica - -**ID:** RF-DOC-003 | **M贸dulo:** MAE-016 | **Prioridad:** Media | **SP:** 7 - -## Descripci贸n -Firma simple/avanzada, certificado, validaci贸n - -## Reglas de Negocio -1. Firma simple con OTP por email -2. Firma avanzada con FIEL (SAT) -3. Certificado digital incorporado -4. Validaci贸n de identidad -5. Trazabilidad completa de firmas -6. Descarga de documento firmado con certificado - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-004-control de acceso.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-004-control de acceso.md deleted file mode 100644 index 7d68b2e49..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-004-control de acceso.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-DOC-004: Control de Acceso - -**ID:** RF-DOC-004 | **M贸dulo:** MAE-016 | **Prioridad:** Media | **SP:** 7 - -## Descripci贸n -Permisos, confidencialidad, registro accesos - -## Reglas de Negocio -1. Permisos por rol y proyecto -2. Documentos confidenciales (nivel adicional) -3. Compartir temporal con expiraci贸n -4. Registro de todos los accesos -5. Descarga con watermark personalizado -6. Revocaci贸n de acceso inmediata - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-005-workflow de aprobaci贸n.md b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-005-workflow de aprobaci贸n.md deleted file mode 100644 index 78b356843..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-gestion-documental/requerimientos/RF-DOC-005-workflow de aprobaci贸n.md +++ /dev/null @@ -1,22 +0,0 @@ -# RF-DOC-005: Workflow de Aprobaci贸n - -**ID:** RF-DOC-005 | **M贸dulo:** MAE-016 | **Prioridad:** Media | **SP:** 7 - -## Descripci贸n -Revisi贸n, aprobaci贸n, comentarios, notificaciones - -## Reglas de Negocio -1. Flujo configurable por tipo de documento -2. Aprobaci贸n multinivel con autorizaci贸n -3. Comentarios y observaciones inline -4. Notificaciones autom谩ticas por nivel -5. Deadline de aprobaci贸n con escalaci贸n -6. Dashboard de documentos pendientes - -## Criterios de Aceptaci贸n -1. Funcionalidad implementada 鉁 -2. Validaciones correctas 鉁 -3. Integraciones OK 鉁 - ---- -**Generado:** 2025-11-21 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-reportes/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-reportes/implementacion/TRACEABILITY.yml deleted file mode 100644 index 8785e11d8..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAE-016-reportes/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,816 +0,0 @@ -# TRACEABILITY.yml - MAE-016: Reportes y BI -# Matriz de trazabilidad: Documentacion -> Codigo -# Ubicacion: docs/02-definicion-modulos/MAE-016-reportes/implementacion/ - -epic_code: MAE-016 -epic_name: Reportes y BI -phase: 2 -phase_name: Modulos Avanzados -story_points: 28 -status: rf_documented -reused_from_core: 75% -core_module: MGN-009 - -# ============================================================================= -# DOCUMENTACION -# ============================================================================= - -documentation: - - requirements: - - id: RF-BI-001 - title: Dashboards Ejecutivos - file: ../requerimientos/RF-BI-001.md - priority: P0 - story_points: 8 - status: documented - reused_from: MGN-009/RF-REPORT-002 - reuse_percentage: 70% - adaptations: - - "Dashboards especificos para construccion" - - "KPIs de obras: avance fisico, financiero, rentabilidad" - - "Widgets de alertas de desviaciones presupuestarias" - - "Graficos de flujo de caja por proyecto" - - "Indicadores de productividad de cuadrillas" - traces_to: - tables: [dashboards, dashboard_widgets, construction_kpis] - services: [DashboardsService, WidgetService, ConstructionKPIService] - endpoints: [GET /api/v1/bi/dashboards, POST /api/v1/bi/dashboards, GET /api/v1/bi/dashboards/:id/data] - - - id: RF-BI-002 - title: Reportes Operativos - file: ../requerimientos/RF-BI-002.md - priority: P0 - story_points: 8 - status: documented - reused_from: MGN-009/RF-REPORT-001 - reuse_percentage: 80% - adaptations: - - "Reportes de avance fisico y financiero" - - "Reportes de consumo de recursos por obra" - - "Reportes de productividad de personal" - - "Reportes de estimaciones y facturacion" - - "Reportes de incidencias y bitacora" - - "Reportes de compras e inventarios" - traces_to: - tables: [report_definitions, report_executions, construction_report_templates] - services: [ReportsService, ExportService, ConstructionReportService] - endpoints: [GET /api/v1/bi/reports, POST /api/v1/bi/reports/:id/execute, GET /api/v1/bi/reports/:id/export/:format] - - - id: RF-BI-003 - title: Exportacion de Datos - file: ../requerimientos/RF-BI-003.md - priority: P1 - story_points: 6 - status: documented - reused_from: MGN-009/RF-REPORT-001 - reuse_percentage: 90% - adaptations: - - "Exportacion a Excel con formato especifico de construccion" - - "Exportacion de estimaciones en formato CFE/SECODAM" - - "Exportacion de bitacora en PDF firmado" - - "Exportacion masiva de fotografias de obra" - traces_to: - tables: [export_templates, export_history] - services: [ExportService, PdfGeneratorService, ExcelGeneratorService] - endpoints: [POST /api/v1/bi/export, GET /api/v1/bi/export/:id/download, GET /api/v1/bi/export/templates] - - - id: RF-BI-004 - title: Indicadores KPI - file: ../requerimientos/RF-BI-004.md - priority: P0 - story_points: 6 - status: documented - reused_from: null - reuse_percentage: 0% - adaptations: - - "KPIs especificos de construccion (nuevo desarrollo)" - - "Indice de Desempeno de Costos (CPI)" - - "Indice de Desempeno del Cronograma (SPI)" - - "Rentabilidad por proyecto" - - "Productividad de mano de obra" - - "Tasa de consumo de materiales vs presupuestado" - - "Indice de calidad (NC/conformes)" - traces_to: - tables: [construction_kpis, kpi_definitions, kpi_history] - services: [KPIService, ConstructionMetricsService, EVMService] - endpoints: [GET /api/v1/bi/kpis, GET /api/v1/bi/kpis/:projectId, POST /api/v1/bi/kpis/calculate] - - specifications: [] - # Pendiente de documentacion - - user_stories: [] - # Pendiente de documentacion - -# ============================================================================= -# IMPLEMENTACION -# ============================================================================= - -implementation: - - database: - schema: construction_bi - path: apps/database/ddl/schemas/construction_bi/ - status: pending - - tables: - # Tablas reutilizadas del core (MGN-009) - - name: dashboards - file: apps/database/ddl/schemas/core_reports/tables/dashboards.sql - status: reused - requirement: RF-BI-001 - reused_from: MGN-009 - adaptations: - - "Agregar campo construction_context JSONB" - - "Agregar campo project_filter UUID[]" - columns: - - {name: id, type: UUID, pk: true} - - {name: tenant_id, type: UUID, fk: tenants} - - {name: user_id, type: UUID, fk: users} - - {name: name, type: VARCHAR(255)} - - {name: description, type: TEXT} - - {name: layout, type: JSONB} - - {name: construction_context, type: JSONB, note: "Nuevo campo"} - - {name: project_filter, type: UUID[], note: "Nuevo campo"} - - {name: refresh_interval, type: INTEGER} - - {name: is_default, type: BOOLEAN, default: false} - - {name: is_shared, type: BOOLEAN, default: false} - - {name: shared_with, type: JSONB} - - {name: created_at, type: TIMESTAMPTZ} - - {name: updated_at, type: TIMESTAMPTZ} - - - name: dashboard_widgets - file: apps/database/ddl/schemas/core_reports/tables/dashboard_widgets.sql - status: reused - requirement: RF-BI-001 - reused_from: MGN-009 - adaptations: - - "Agregar widget_types especificos de construccion" - - - name: report_definitions - file: apps/database/ddl/schemas/core_reports/tables/report_definitions.sql - status: reused - requirement: RF-BI-002 - reused_from: MGN-009 - adaptations: - - "Agregar templates especificos de construccion" - - - name: report_executions - file: apps/database/ddl/schemas/core_reports/tables/report_executions.sql - status: reused - requirement: RF-BI-002 - reused_from: MGN-009 - adaptations: [] - - # Tablas nuevas especificas de construccion - - name: construction_kpis - file: apps/database/ddl/schemas/construction_bi/tables/construction_kpis.sql - status: pending - requirement: RF-BI-004 - columns: - - {name: id, type: UUID, pk: true} - - {name: tenant_id, type: UUID, fk: tenants} - - {name: project_id, type: UUID, fk: projects} - - {name: kpi_code, type: VARCHAR(50), note: "CPI, SPI, etc"} - - {name: kpi_name, type: VARCHAR(255)} - - {name: kpi_value, type: DECIMAL(10,4)} - - {name: target_value, type: DECIMAL(10,4)} - - {name: threshold_warning, type: DECIMAL(10,4)} - - {name: threshold_critical, type: DECIMAL(10,4)} - - {name: status, type: VARCHAR(20), note: "ok, warning, critical"} - - {name: calculation_data, type: JSONB} - - {name: period_start, type: DATE} - - {name: period_end, type: DATE} - - {name: calculated_at, type: TIMESTAMPTZ} - - {name: calculated_by, type: UUID, fk: users} - - - name: kpi_definitions - file: apps/database/ddl/schemas/construction_bi/tables/kpi_definitions.sql - status: pending - requirement: RF-BI-004 - columns: - - {name: id, type: UUID, pk: true} - - {name: code, type: VARCHAR(50), unique: true} - - {name: name, type: VARCHAR(255)} - - {name: description, type: TEXT} - - {name: formula, type: TEXT} - - {name: data_sources, type: JSONB} - - {name: calculation_frequency, type: VARCHAR(20)} - - {name: target_value, type: DECIMAL(10,4)} - - {name: threshold_warning, type: DECIMAL(10,4)} - - {name: threshold_critical, type: DECIMAL(10,4)} - - {name: unit, type: VARCHAR(50)} - - {name: is_active, type: BOOLEAN, default: true} - - {name: created_at, type: TIMESTAMPTZ} - - {name: updated_at, type: TIMESTAMPTZ} - - - name: kpi_history - file: apps/database/ddl/schemas/construction_bi/tables/kpi_history.sql - status: pending - requirement: RF-BI-004 - columns: - - {name: id, type: UUID, pk: true} - - {name: kpi_id, type: UUID, fk: construction_kpis} - - {name: project_id, type: UUID, fk: projects} - - {name: kpi_value, type: DECIMAL(10,4)} - - {name: status, type: VARCHAR(20)} - - {name: recorded_at, type: TIMESTAMPTZ} - - - name: construction_report_templates - file: apps/database/ddl/schemas/construction_bi/tables/construction_report_templates.sql - status: pending - requirement: RF-BI-002 - columns: - - {name: id, type: UUID, pk: true} - - {name: tenant_id, type: UUID, fk: tenants} - - {name: code, type: VARCHAR(100), unique: true} - - {name: name, type: VARCHAR(255)} - - {name: template_type, type: VARCHAR(50), note: "estimacion, bitacora, avance"} - - {name: template_file, type: TEXT, note: "Path o contenido del template"} - - {name: format, type: VARCHAR(20), note: "PDF, XLSX, DOCX"} - - {name: parameters, type: JSONB} - - {name: header_config, type: JSONB} - - {name: footer_config, type: JSONB} - - {name: is_system, type: BOOLEAN, default: false} - - {name: is_active, type: BOOLEAN, default: true} - - {name: created_at, type: TIMESTAMPTZ} - - {name: updated_at, type: TIMESTAMPTZ} - - - name: export_templates - file: apps/database/ddl/schemas/construction_bi/tables/export_templates.sql - status: pending - requirement: RF-BI-003 - columns: - - {name: id, type: UUID, pk: true} - - {name: tenant_id, type: UUID, fk: tenants} - - {name: name, type: VARCHAR(255)} - - {name: export_type, type: VARCHAR(50), note: "CFE, SECODAM, custom"} - - {name: format, type: VARCHAR(20), note: "XLSX, PDF"} - - {name: template_config, type: JSONB} - - {name: columns_mapping, type: JSONB} - - {name: is_active, type: BOOLEAN, default: true} - - {name: created_at, type: TIMESTAMPTZ} - - - name: export_history - file: apps/database/ddl/schemas/construction_bi/tables/export_history.sql - status: pending - requirement: RF-BI-003 - columns: - - {name: id, type: UUID, pk: true} - - {name: tenant_id, type: UUID, fk: tenants} - - {name: template_id, type: UUID, fk: export_templates, nullable: true} - - {name: user_id, type: UUID, fk: users} - - {name: entity_type, type: VARCHAR(50), note: "estimacion, bitacora, etc"} - - {name: entity_id, type: UUID} - - {name: file_name, type: VARCHAR(255)} - - {name: file_url, type: TEXT} - - {name: file_format, type: VARCHAR(20)} - - {name: file_size, type: BIGINT} - - {name: status, type: VARCHAR(20)} - - {name: error, type: TEXT, nullable: true} - - {name: created_at, type: TIMESTAMPTZ} - - backend: - module: construction-bi - path: apps/backend/src/modules/construction-bi/ - framework: NestJS - status: pending - - entities: - - name: ConstructionKPI - file: apps/backend/src/modules/construction-bi/entities/construction-kpi.entity.ts - status: pending - requirement: RF-BI-004 - - - name: KPIDefinition - file: apps/backend/src/modules/construction-bi/entities/kpi-definition.entity.ts - status: pending - requirement: RF-BI-004 - - - name: KPIHistory - file: apps/backend/src/modules/construction-bi/entities/kpi-history.entity.ts - status: pending - requirement: RF-BI-004 - - - name: ConstructionReportTemplate - file: apps/backend/src/modules/construction-bi/entities/construction-report-template.entity.ts - status: pending - requirement: RF-BI-002 - - - name: ExportTemplate - file: apps/backend/src/modules/construction-bi/entities/export-template.entity.ts - status: pending - requirement: RF-BI-003 - - services: - # Servicios reutilizados del core (adaptados) - - name: DashboardsService - file: apps/backend/src/modules/construction-bi/dashboards.service.ts - status: pending - requirement: RF-BI-001 - reused_from: MGN-009/DashboardsService - reuse_percentage: 70% - adaptations: - - "Agregar metodos para dashboards de construccion" - - "Integracion con KPIs de obra" - methods: - - {name: create, description: Crear dashboard} - - {name: update, description: Actualizar dashboard} - - {name: getData, description: Obtener datos de widgets} - - {name: getConstructionDashboard, description: Dashboard especifico de construccion} - - {name: getProjectKPIs, description: Obtener KPIs de proyecto} - - - name: ReportsService - file: apps/backend/src/modules/construction-bi/reports.service.ts - status: pending - requirement: RF-BI-002 - reused_from: MGN-009/ReportsService - reuse_percentage: 80% - adaptations: - - "Agregar reportes especificos de construccion" - methods: - - {name: execute, description: Ejecutar reporte} - - {name: getAvailable, description: Listar reportes disponibles} - - {name: getExecution, description: Obtener resultado de ejecucion} - - {name: executeConstructionReport, description: Ejecutar reporte de construccion} - - {name: getEstimacionReport, description: Reporte de estimacion} - - {name: getBitacoraReport, description: Reporte de bitacora} - - {name: getAvanceFisicoReport, description: Reporte de avance fisico} - - - name: ExportService - file: apps/backend/src/modules/construction-bi/export.service.ts - status: pending - requirement: RF-BI-003 - reused_from: MGN-009/ExportService - reuse_percentage: 85% - adaptations: - - "Formatos especificos de construccion" - methods: - - {name: toPdf, description: Exportar a PDF} - - {name: toExcel, description: Exportar a Excel} - - {name: toCsv, description: Exportar a CSV} - - {name: exportEstimacionCFE, description: Exportar estimacion formato CFE} - - {name: exportBitacoraFirmada, description: Exportar bitacora con firma digital} - - {name: exportFotosObra, description: Exportacion masiva de fotos} - - # Servicios nuevos especificos - - name: KPIService - file: apps/backend/src/modules/construction-bi/kpi.service.ts - status: pending - requirement: RF-BI-004 - methods: - - {name: calculateKPI, description: Calcular KPI especifico} - - {name: calculateAllProjectKPIs, description: Calcular todos los KPIs de un proyecto} - - {name: getKPIHistory, description: Obtener historial de KPI} - - {name: getKPIDefinitions, description: Listar definiciones de KPIs} - - {name: createKPIDefinition, description: Crear definicion de KPI} - - - name: ConstructionMetricsService - file: apps/backend/src/modules/construction-bi/construction-metrics.service.ts - status: pending - requirement: RF-BI-004 - methods: - - {name: getProjectMetrics, description: Obtener metricas de proyecto} - - {name: getResourceProductivity, description: Productividad de recursos} - - {name: getMaterialConsumption, description: Consumo de materiales} - - {name: getLaborProductivity, description: Productividad de mano de obra} - - - name: EVMService - file: apps/backend/src/modules/construction-bi/evm.service.ts - status: pending - requirement: RF-BI-004 - note: "Earned Value Management - Nuevo servicio" - methods: - - {name: calculateCPI, description: Calcular Cost Performance Index} - - {name: calculateSPI, description: Calcular Schedule Performance Index} - - {name: calculateEAC, description: Calcular Estimate at Completion} - - {name: calculateETC, description: Calcular Estimate to Complete} - - {name: getEVMReport, description: Reporte completo de EVM} - - - name: ConstructionReportService - file: apps/backend/src/modules/construction-bi/construction-report.service.ts - status: pending - requirement: RF-BI-002 - methods: - - {name: getReportTemplates, description: Listar templates de reportes} - - {name: generateFromTemplate, description: Generar reporte desde template} - - {name: createCustomTemplate, description: Crear template personalizado} - - - name: PdfGeneratorService - file: apps/backend/src/modules/construction-bi/pdf-generator.service.ts - status: pending - requirement: RF-BI-003 - methods: - - {name: generatePdf, description: Generar PDF generico} - - {name: generateEstimacionPdf, description: Generar PDF de estimacion} - - {name: generateBitacoraPdf, description: Generar PDF de bitacora} - - {name: addDigitalSignature, description: Agregar firma digital al PDF} - - - name: ExcelGeneratorService - file: apps/backend/src/modules/construction-bi/excel-generator.service.ts - status: pending - requirement: RF-BI-003 - methods: - - {name: generateExcel, description: Generar Excel generico} - - {name: generateEstimacionExcel, description: Generar Excel de estimacion} - - {name: applyTemplate, description: Aplicar template de Excel} - - controllers: - - name: ConstructionBIController - file: apps/backend/src/modules/construction-bi/construction-bi.controller.ts - status: pending - endpoints: - # Dashboards - - method: GET - path: /api/v1/bi/dashboards - description: Listar dashboards - requirement: RF-BI-001 - - - method: POST - path: /api/v1/bi/dashboards - description: Crear dashboard - requirement: RF-BI-001 - - - method: GET - path: /api/v1/bi/dashboards/:id/data - description: Obtener datos de dashboard - requirement: RF-BI-001 - - - method: GET - path: /api/v1/bi/dashboards/construction/:projectId - description: Dashboard de construccion por proyecto - requirement: RF-BI-001 - - # Reportes - - method: GET - path: /api/v1/bi/reports - description: Listar reportes disponibles - requirement: RF-BI-002 - - - method: POST - path: /api/v1/bi/reports/:id/execute - description: Ejecutar reporte - requirement: RF-BI-002 - - - method: GET - path: /api/v1/bi/reports/:id/export/:format - description: Exportar reporte - requirement: RF-BI-002 - - - method: GET - path: /api/v1/bi/reports/estimacion/:estimacionId - description: Reporte de estimacion - requirement: RF-BI-002 - - - method: GET - path: /api/v1/bi/reports/bitacora/:obraId - description: Reporte de bitacora - requirement: RF-BI-002 - - - method: GET - path: /api/v1/bi/reports/avance-fisico/:obraId - description: Reporte de avance fisico - requirement: RF-BI-002 - - # Exportacion - - method: POST - path: /api/v1/bi/export - description: Crear exportacion - requirement: RF-BI-003 - - - method: GET - path: /api/v1/bi/export/:id/download - description: Descargar archivo exportado - requirement: RF-BI-003 - - - method: GET - path: /api/v1/bi/export/templates - description: Listar templates de exportacion - requirement: RF-BI-003 - - - method: POST - path: /api/v1/bi/export/estimacion-cfe - description: Exportar estimacion formato CFE - requirement: RF-BI-003 - - - method: POST - path: /api/v1/bi/export/bitacora-firmada - description: Exportar bitacora con firma digital - requirement: RF-BI-003 - - # KPIs - - method: GET - path: /api/v1/bi/kpis - description: Listar definiciones de KPIs - requirement: RF-BI-004 - - - method: GET - path: /api/v1/bi/kpis/:projectId - description: Obtener KPIs de proyecto - requirement: RF-BI-004 - - - method: POST - path: /api/v1/bi/kpis/calculate - description: Calcular KPIs de proyecto - requirement: RF-BI-004 - - - method: GET - path: /api/v1/bi/kpis/:projectId/history - description: Historial de KPIs - requirement: RF-BI-004 - - - method: GET - path: /api/v1/bi/kpis/:projectId/evm - description: Earned Value Management - requirement: RF-BI-004 - - frontend: - module: construction-bi - path: apps/frontend/src/features/construction-bi/ - framework: React - status: pending - - components: - # Dashboards - - name: DashboardBuilder - file: apps/frontend/src/features/construction-bi/components/DashboardBuilder.tsx - requirement: RF-BI-001 - reused_from: MGN-009 - reuse_percentage: 60% - - - name: ConstructionDashboard - file: apps/frontend/src/features/construction-bi/components/ConstructionDashboard.tsx - requirement: RF-BI-001 - note: "Nuevo componente especifico" - - - name: KPIWidget - file: apps/frontend/src/features/construction-bi/components/KPIWidget.tsx - requirement: RF-BI-001 - note: "Widget para mostrar KPIs" - - - name: ProjectMetricsWidget - file: apps/frontend/src/features/construction-bi/components/ProjectMetricsWidget.tsx - requirement: RF-BI-001 - - - name: CashFlowWidget - file: apps/frontend/src/features/construction-bi/components/CashFlowWidget.tsx - requirement: RF-BI-001 - - # Reportes - - name: ReportGenerator - file: apps/frontend/src/features/construction-bi/components/ReportGenerator.tsx - requirement: RF-BI-002 - reused_from: MGN-009 - reuse_percentage: 75% - - - name: ReportTemplateSelector - file: apps/frontend/src/features/construction-bi/components/ReportTemplateSelector.tsx - requirement: RF-BI-002 - - - name: EstimacionReportViewer - file: apps/frontend/src/features/construction-bi/components/EstimacionReportViewer.tsx - requirement: RF-BI-002 - - - name: BitacoraReportViewer - file: apps/frontend/src/features/construction-bi/components/BitacoraReportViewer.tsx - requirement: RF-BI-002 - - # Exportacion - - name: ExportDialog - file: apps/frontend/src/features/construction-bi/components/ExportDialog.tsx - requirement: RF-BI-003 - reused_from: MGN-009 - reuse_percentage: 80% - - - name: ExportTemplateManager - file: apps/frontend/src/features/construction-bi/components/ExportTemplateManager.tsx - requirement: RF-BI-003 - - - name: ExportHistory - file: apps/frontend/src/features/construction-bi/components/ExportHistory.tsx - requirement: RF-BI-003 - - # KPIs - - name: KPIDashboard - file: apps/frontend/src/features/construction-bi/components/KPIDashboard.tsx - requirement: RF-BI-004 - - - name: KPIChart - file: apps/frontend/src/features/construction-bi/components/KPIChart.tsx - requirement: RF-BI-004 - - - name: EVMChart - file: apps/frontend/src/features/construction-bi/components/EVMChart.tsx - requirement: RF-BI-004 - - - name: KPIHistoryChart - file: apps/frontend/src/features/construction-bi/components/KPIHistoryChart.tsx - requirement: RF-BI-004 - - - name: KPIDefinitionManager - file: apps/frontend/src/features/construction-bi/components/KPIDefinitionManager.tsx - requirement: RF-BI-004 - -# ============================================================================= -# DEPENDENCIAS -# ============================================================================= - -dependencies: - depends_on: - # Dependencias del core - - module: MGN-001 - type: hard - reason: Autenticacion requerida - - module: MGN-004 - type: hard - reason: Aislamiento por tenant - - module: MGN-009 - type: hard - reason: Reutilizacion de infraestructura de reportes (75%) - - # Dependencias de verticales construccion - - module: MAI-001 - type: hard - reason: Datos de proyectos de construccion - - module: MAI-002 - type: hard - reason: Datos de planificacion y presupuestos - - module: MAI-004 - type: hard - reason: Datos de compras e inventarios - - module: MAI-005 - type: hard - reason: Datos de avances fisicos y financieros - - required_by: [] - -# ============================================================================= -# INTEGRACIONES -# ============================================================================= - -integrations: - core_modules: - - code: MGN-009 - name: Reports - reuse_percentage: 75% - components_reused: - - DashboardsService - - ReportsService - - ExportService - - Dashboard UI components - adaptations: - - "Dashboards especificos de construccion" - - "Reportes especificos de obra" - - "Templates de exportacion CFE/SECODAM" - - construction_modules: - - code: MAI-001 - name: Fundamentos - integration: Datos de proyectos y usuarios - - code: MAI-002 - name: Planificacion de Obras - integration: Presupuestos, cronogramas, WBS - - code: MAI-004 - name: Compras e Inventarios - integration: Datos de consumo de materiales - - code: MAI-005 - name: Control de Obra - integration: Avances fisicos, financieros, bitacora - -# ============================================================================= -# METRICAS -# ============================================================================= - -metrics: - story_points: - estimated: 28 - actual: null - saved_by_reuse: 12 - note: "75% reutilizado de MGN-009 ahorra ~12 SP" - - reuse_metrics: - from_core: 75% - core_module: MGN-009 - components_reused: 8 - components_new: 7 - components_adapted: 5 - lines_reused: ~2500 - lines_new: ~1200 - - documentation: - requirements: 4 - specifications: 0 - user_stories: 0 - - files: - database: 8 - backend: 15 - frontend: 15 - total: 38 - -# ============================================================================= -# REUTILIZACION DE MGN-009 -# ============================================================================= - -reuse_details: - database: - reused_tables: - - name: dashboards - percentage: 85% - adaptations: ["Agregar campos de construccion"] - - name: dashboard_widgets - percentage: 90% - adaptations: ["Widgets especificos"] - - name: report_definitions - percentage: 80% - adaptations: ["Templates de construccion"] - - name: report_executions - percentage: 100% - adaptations: [] - - new_tables: - - construction_kpis - - kpi_definitions - - kpi_history - - construction_report_templates - - export_templates - - export_history - - backend: - reused_services: - - name: DashboardsService - percentage: 70% - adaptations: ["Metodos especificos de construccion"] - - name: ReportsService - percentage: 80% - adaptations: ["Reportes de obra"] - - name: ExportService - percentage: 85% - adaptations: ["Formatos CFE/SECODAM"] - - new_services: - - KPIService - - ConstructionMetricsService - - EVMService - - ConstructionReportService - - PdfGeneratorService - - ExcelGeneratorService - - frontend: - reused_components: - - name: DashboardBuilder - percentage: 60% - - name: ReportGenerator - percentage: 75% - - name: ExportDialog - percentage: 80% - - new_components: - - ConstructionDashboard - - KPIDashboard - - EVMChart - - EstimacionReportViewer - - BitacoraReportViewer - - KPIWidget - - ProjectMetricsWidget - - CashFlowWidget - -# ============================================================================= -# HISTORIAL -# ============================================================================= - -history: - - date: "2025-12-06" - action: "Creacion de estructura MAE-016" - author: Requirements-Analyst - changes: - - "Creacion de TRACEABILITY.yml" - - "Definicion de estructura base" - - "Documentacion de reutilizacion de MGN-009 (75%)" - - - date: "2025-12-06" - action: "Documentacion de RF" - author: Requirements-Analyst - changes: - - "RF-BI-001: Dashboards Ejecutivos" - - "RF-BI-002: Reportes Operativos" - - "RF-BI-003: Exportacion de Datos" - - "RF-BI-004: Indicadores KPI" - - "Actualizacion de trazabilidad RF -> implementacion" - - "Documentacion de adaptaciones vs MGN-009" - -# ============================================================================= -# NOTAS DE IMPLEMENTACION -# ============================================================================= - -notes: - - "75% de reutilizacion de MGN-009 reduce significativamente el esfuerzo" - - "KPIs especificos de construccion (CPI, SPI, EVM) requieren desarrollo nuevo" - - "Formatos de exportacion CFE/SECODAM son requerimientos especificos de Mexico" - - "Firma digital en bitacora requiere integracion con certificados digitales" - - "Dashboard ejecutivo debe mostrar metricas en tiempo real" - - "Considerar integracion con Power BI / Tableau para analisis avanzado" - - "Implementar cache de KPIs para mejorar rendimiento" - - "Sistema de alertas automaticas cuando KPIs caen fuera de umbrales" - - "Reportes deben soportar multiples idiomas (ES/EN)" - - "Exportacion de fotos debe incluir metadata EXIF (ubicacion, fecha)" diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/README.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/README.md deleted file mode 100644 index 318d49cf0..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/README.md +++ /dev/null @@ -1,662 +0,0 @@ -# MAI-001: Fundamentos - -## Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | MAI-001 | -| **Nombre** | Fundamentos | -| **Vertical** | Construccion | -| **Fase** | 1 - Alcance Inicial | -| **Prioridad** | P0 (Critico) | -| **Story Points** | 50 SP | -| **Presupuesto** | $25,000 MXN | -| **Estado** | Planificado | -| **Sprint** | Sprint 0-2 (Semanas 1-2) | -| **Duracion estimada** | 10 dias | -| **Dependencias** | Ninguna (modulo base) | - ---- - -## Descripcion - -**MAI-001 Fundamentos** es el modulo base del Sistema de Administracion de Obra que establece la infraestructura tecnica y funcional para todo el sistema vertical de construccion. Este modulo reutiliza el 95% de los componentes del core ERP (MGN-001, MGN-002, MGN-003) adaptandolos a las necesidades especificas de la industria de la construccion. - -### Componentes principales: - -- **Autenticacion y Autorizacion:** Sistema de login basado en JWT con OAuth social (Google, Microsoft) -- **Sistema de Roles de Construccion:** 7 roles especializados (Director, Ingeniero, Residente, Compras, Finanzas, RRHH, Postventa) -- **Multi-tenancy:** Aislamiento por constructora con Row-Level Security (RLS) -- **Infraestructura Base:** Backend (NestJS), Frontend (React/Vite), Database (PostgreSQL) -- **Dashboards por Rol:** 7 variantes de dashboard adaptadas a cada perfil de usuario -- **Gestion de Usuarios:** CRUD completo con perfiles extendidos y preferencias - ---- - -## Alcance Funcional - -### Incluido en MAI-001 - -- Sistema de autenticacion JWT (access + refresh tokens) -- Login/logout con email y password -- OAuth con Google y Microsoft -- Recuperacion de password via email -- Registro de usuarios con validacion -- Gestion de perfiles de usuario -- Sistema de roles RBAC con 7 roles de construccion -- Asignacion de permisos granulares -- Multi-tenancy por constructora -- Dashboard principal adaptado por rol -- Sistema de sesiones multiples por usuario -- Proteccion contra brute force -- Auditoria de acciones de usuarios -- API RESTful base con versionado -- Sistema de navegacion y routing -- UI/UX base con componentes reutilizables -- Preferencias de usuario (tema, idioma, notificaciones) - -### Excluido de MAI-001 - -- Modulos funcionales de construccion (proyectos, presupuestos, etc.) -- Gestion de catalogos de construccion -- Reportes especializados de obra -- 2FA/MFA (enhancement futuro) -- Notificaciones push (fase posterior) -- Sistema de archivos/documentos (fase posterior) - ---- - -## Reutilizacion del Core ERP - -Este modulo reutiliza el **95%** de los componentes base del ERP Core, especificamente de los modulos: - -### MGN-001: Autenticacion (95% de reutilizacion) - -**Componentes heredados:** -- Sistema de autenticacion JWT completo -- Manejo de access tokens (15 min) y refresh tokens (7 dias) -- OAuth con Google y Microsoft -- Recuperacion de password via email -- Proteccion contra brute force (rate limiting) -- Bloqueo temporal por intentos fallidos -- Registro de intentos de login -- Manejo de sesiones multiples - -**Adaptaciones requeridas:** -- Integracion con 7 roles de construccion (vs 3 roles genericos) -- Validaciones especificas de constructora -- Estados de cuenta extendidos para construccion - -**Referencias:** -- [MGN-001 README](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-001-auth/README.md) -- [MGN-001 Requerimientos](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-001-auth/requerimientos/) - -### MGN-002: Usuarios (95% de reutilizacion) - -**Componentes heredados:** -- CRUD completo de usuarios -- Gestion de perfiles extendidos -- Sistema de preferencias (tema, idioma, notificaciones) -- Busqueda y filtrado de usuarios -- Soft delete con trazabilidad -- Activacion/desactivacion de cuentas - -**Adaptaciones requeridas:** -- Campos adicionales en perfil para construccion -- Vinculacion obligatoria con constructora -- Validaciones de datos especificos de construccion - -**Referencias:** -- [MGN-002 README](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-002-users/README.md) -- [MGN-002 Requerimientos](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-002-users/requerimientos/) - -### MGN-003: Roles y Permisos (95% de reutilizacion) - -**Componentes heredados:** -- Sistema RBAC (Role-Based Access Control) -- Modelo de permisos granulares (module:action:resource) -- Guards y decoradores (@Roles, @RequirePermission) -- Asignacion multiple de roles a usuarios -- Verificacion de permisos en tiempo de ejecucion - -**Adaptaciones requeridas:** -- Definicion de 7 roles especificos de construccion -- Matriz de permisos adaptada a modulos de obra -- Validacion de permisos por constructora - -**Referencias:** -- [MGN-003 README](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-003-roles/README.md) -- [MGN-003 Requerimientos](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-003-roles/requerimientos/) - ---- - -## Requerimientos Funcionales - -| ID | Titulo | Prioridad | Estado | Documentacion | -|----|--------|-----------|--------|---------------| -| RF-AUTH-001 | Sistema de Roles de Construccion | P0 | Planificado | [Ver RF](./requerimientos/RF-AUTH-001-roles-construccion.md) | -| RF-AUTH-002 | Estados de Cuenta de Usuario | P0 | Planificado | [Ver RF](./requerimientos/RF-AUTH-002-estados-cuenta.md) | -| RF-AUTH-003 | Multi-tenancy por Constructora | P0 | Planificado | [Ver RF](./requerimientos/RF-AUTH-003-multi-tenancy.md) | - -**Total:** 3 Requerimientos Funcionales - ---- - -## Dependencias con Otros Modulos - -### Este modulo NO depende de otros modulos - -MAI-001 es el modulo base del sistema de construccion y no tiene dependencias externas. Hereda componentes del Core ERP pero no depende de otros modulos verticales. - -### Modulos que dependen de MAI-001 - -Todos los modulos funcionales del sistema de construccion dependen de MAI-001: - -| Modulo | Dependencia | Razon | -|--------|-------------|-------| -| **MAI-002 Proyectos** | MAI-001 | Requiere autenticacion y roles | -| **MAI-003 Presupuestos** | MAI-001 | Valida permisos de ingeniero/director | -| **MAI-004 Programacion** | MAI-001 | Auditoria de cambios en cronograma | -| **MAI-005 Avances** | MAI-001 | Rol de residente para registro | -| **MAI-006 Compras** | MAI-001 | Rol de compras y autorizaciones | -| **MAI-007 Almacen** | MAI-001 | Control de acceso a inventarios | -| **MAI-008 Finanzas** | MAI-001 | Rol de finanzas y auditoria | -| **MAI-009 RRHH** | MAI-001 | Rol de RRHH y permisos | -| **MAI-010 Postventa** | MAI-001 | Rol de postventa y seguimiento | -| **Todos los demas** | MAI-001 | Autenticacion y autorizacion base | - ---- - -## Diagrama de Arquitectura - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CAPA DE PRESENTACION 鈹 -鈹 Frontend (React 18 + Vite) 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Login 鈹 鈹 Dashboard 鈹 鈹 Profile 鈹 鈹 -鈹 鈹 Register 鈹 鈹 (7 roles) 鈹 鈹 Settings 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Guards & Routing 鈹 鈹 -鈹 鈹 - AuthGuard - RoleGuard - ConstructoraGuard 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 State Management (Zustand) 鈹 鈹 -鈹 鈹 - authStore - userStore - constructoraStore 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹 HTTPS / REST API - 鈹 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CAPA DE NEGOCIO 鈹 -鈹 Backend (NestJS + TypeScript) 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 AuthModule 鈹 鈹 UsersModule 鈹 鈹 RolesModule 鈹 鈹 -鈹 鈹 鈹 鈹 鈹 鈹 鈹 鈹 -鈹 鈹 - AuthSvc 鈹 鈹 - UsersSvc 鈹 鈹 - RolesSvc 鈹 鈹 -鈹 鈹 - JwtSvc 鈹 鈹 - ProfileSvc 鈹 鈹 - PermSvc 鈹 鈹 -鈹 鈹 - OAuthSvc 鈹 鈹 - PrefSvc 鈹 鈹 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Guards & Middlewares 鈹 鈹 -鈹 鈹 - JwtAuthGuard - RolesGuard - PermissionsGuard 鈹 鈹 -鈹 鈹 - ValidationMiddleware - ErrorMiddleware 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Shared Services 鈹 鈹 -鈹 鈹 - LoggingService - AuditService - CacheService 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹 SQL / TypeORM - 鈹 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CAPA DE DATOS 鈹 -鈹 PostgreSQL 15+ 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Schema: auth 鈹 鈹 Schema: public 鈹 鈹 Schema: audit 鈹 鈹 -鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 - users_auth 鈹 鈹 - users 鈹 鈹 - audit_logs 鈹 鈹 -鈹 鈹 - sessions 鈹 鈹 - user_profiles 鈹 鈹 - login_attempts 鈹 鈹 -鈹 鈹 - refresh_tokens 鈹 鈹 - preferences 鈹 鈹 鈹 鈹 -鈹 鈹 - password_reset 鈹 鈹 - roles 鈹 鈹 鈹 鈹 -鈹 鈹 - oauth_accounts 鈹 鈹 - permissions 鈹 鈹 鈹 鈹 -鈹 鈹 鈹 鈹 - user_roles 鈹 鈹 鈹 鈹 -鈹 鈹 鈹 鈹 - constructoras 鈹 鈹 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 Row Level Security (RLS) 鈹 鈹 -鈹 鈹 - Politicas por constructora 鈹 鈹 -鈹 鈹 - Validacion de permisos por rol 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 ENUMS 鈹 鈹 -鈹 鈹 - construction_role (7 roles) 鈹 鈹 -鈹 鈹 - account_status (active, suspended, etc.) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 SERVICIOS EXTERNOS 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 - Google OAuth 2.0 鈹 -鈹 - Microsoft Azure AD 鈹 -鈹 - SMTP (Email de recuperacion) 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -### Flujo de Autenticacion - -``` -Usuario 鈫 Login Form 鈫 POST /api/v1/auth/login - 鈫 - [Validate Credentials] - 鈫 - [Check Account Status] - 鈫 - [Check Constructora] - 鈫 - [Generate JWT Tokens] - 鈫 - Access Token (15 min) + Refresh Token (7 dias) - 鈫 - [Store in State] - 鈫 - Redirect to Dashboard (por rol) -``` - ---- - -## Sistema de Roles de Construccion - -A diferencia del Core ERP que tiene roles genericos, MAI-001 implementa **7 roles especializados** para la industria de la construccion: - -| Rol | Codigo | Descripcion | Permisos Clave | Dashboard | -|-----|--------|-------------|----------------|-----------| -| **Director** | `director` | Director General/Proyectos | - Vision global de proyectos
- Margenes y rentabilidad
- Riesgos y alertas
- Aprobaciones criticas | Tablero ejecutivo con KPIs | -| **Ingeniero** | `engineer` | Ingenieria/Planeacion | - Presupuestos de obra
- Programacion de actividades
- Control de avances
- Explosiones de insumos | Tablero de control tecnico | -| **Residente** | `resident` | Residente/Supervisor | - Registro de avances
- Incidencias de obra
- Checklists de calidad
- Solicitudes de material | Tablero operativo de campo | -| **Compras** | `purchases` | Compras/Almacen | - Ordenes de compra
- Cotizaciones
- Inventarios
- Proveedores | Tablero de abastecimiento | -| **Finanzas** | `finance` | Administracion/Finanzas | - Presupuestos financieros
- Flujo de caja
- Pagos y cobranza
- Conciliaciones | Tablero financiero | -| **RRHH** | `hr` | Recursos Humanos | - Asistencias
- Costeo mano de obra
- Nominas
- Capacitacion | Tablero de personal | -| **Postventa** | `post_sales` | Postventa/Garantias | - Incidencias post-entrega
- Seguimiento clientes
- Garantias
- Satisfaccion | Tablero de servicio | - -### Matriz de Permisos Base - -| Permiso | Director | Ingeniero | Residente | Compras | Finanzas | RRHH | Postventa | -|---------|----------|-----------|-----------|---------|----------|------|-----------| -| **users:read:all** | 鉁 | 鉁 | - | - | - | 鉁 | - | -| **users:write:all** | 鉁 | - | - | - | - | - | - | -| **projects:read:all** | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | - | 鉁 | -| **projects:write:all** | 鉁 | 鉁 | - | - | - | - | - | -| **budgets:read:all** | 鉁 | 鉁 | - | - | 鉁 | - | - | -| **budgets:write:all** | 鉁 | 鉁 | - | - | - | - | - | -| **reports:export:all** | 鉁 | 鉁 | - | - | 鉁 | - | - | - -**Nota:** La matriz completa se define en [ET-AUTH-001-rbac.md](./especificaciones/ET-AUTH-001-rbac.md) - ---- - -## Stack Tecnologico - -### Frontend - -| Tecnologia | Version | Proposito | -|------------|---------|-----------| -| **React** | 18.3.x | Framework UI | -| **TypeScript** | 5.6.x | Lenguaje type-safe | -| **Vite** | 6.x | Build tool & dev server | -| **Zustand** | 5.x | State management | -| **React Router** | 7.x | Routing & navegacion | -| **React Hook Form** | 7.x | Manejo de formularios | -| **Zod** | 3.x | Validacion de schemas | -| **Axios** | 1.x | Cliente HTTP | -| **TailwindCSS** | 4.x | Estilos utility-first | -| **Shadcn/ui** | Latest | Componentes UI | -| **Lucide React** | Latest | Iconografia | - -### Backend - -| Tecnologia | Version | Proposito | -|------------|---------|-----------| -| **NestJS** | 10.x | Framework backend | -| **TypeScript** | 5.6.x | Lenguaje type-safe | -| **TypeORM** | 0.3.x | ORM para PostgreSQL | -| **PostgreSQL** | 15+ | Base de datos | -| **JWT** | 9.x | Autenticacion | -| **Passport** | 0.7.x | Estrategias de auth | -| **Bcrypt** | 5.x | Hashing de passwords | -| **Class Validator** | 0.14.x | Validacion DTOs | -| **Class Transformer** | 0.5.x | Transformacion datos | -| **Winston** | 3.x | Logging estructurado | - -### Base de Datos - -| Tecnologia | Version | Proposito | -|------------|---------|-----------| -| **PostgreSQL** | 15+ | Base de datos principal | -| **pgcrypto** | Extension | Funciones de encriptacion | -| **uuid-ossp** | Extension | Generacion de UUIDs | - -### DevOps & Testing - -| Tecnologia | Version | Proposito | -|------------|---------|-----------| -| **Docker** | Latest | Contenedorizacion | -| **Docker Compose** | Latest | Orquestacion local | -| **Jest** | 29.x | Testing framework | -| **Vitest** | 2.x | Testing para Vite | -| **Supertest** | 6.x | Testing API | -| **ESLint** | 9.x | Linting | -| **Prettier** | 3.x | Code formatting | - ---- - -## Estructura de Directorios - -``` -MAI-001-fundamentos/ -鈹溾攢鈹 README.md # Este archivo -鈹溾攢鈹 _MAP.md # Mapa del modulo -鈹 -鈹溾攢鈹 requerimientos/ # Requerimientos Funcionales -鈹 鈹溾攢鈹 RF-AUTH-001-roles-construccion.md -鈹 鈹溾攢鈹 RF-AUTH-002-estados-cuenta.md -鈹 鈹斺攢鈹 RF-AUTH-003-multi-tenancy.md -鈹 -鈹溾攢鈹 especificaciones/ # Especificaciones Tecnicas -鈹 鈹溾攢鈹 ET-AUTH-001-rbac.md -鈹 鈹溾攢鈹 ET-AUTH-002-estados-cuenta.md -鈹 鈹斺攢鈹 ET-AUTH-003-multi-tenancy.md -鈹 -鈹溾攢鈹 historias-usuario/ # User Stories -鈹 鈹溾攢鈹 US-FUND-001-autenticacion-basica-jwt.md -鈹 鈹溾攢鈹 US-FUND-002-perfiles-usuario-construccion.md -鈹 鈹溾攢鈹 US-FUND-003-dashboard-por-rol.md -鈹 鈹溾攢鈹 US-FUND-004-infraestructura-base.md -鈹 鈹溾攢鈹 US-FUND-005-sistema-sesiones.md -鈹 鈹溾攢鈹 US-FUND-006-api-restful-base.md -鈹 鈹溾攢鈹 US-FUND-007-navegacion-routing.md -鈹 鈹斺攢鈹 US-FUND-008-ui-ux-base.md -鈹 -鈹溾攢鈹 implementacion/ # Inventarios de implementacion -鈹 鈹溾攢鈹 TRACEABILITY.yml # Matriz de trazabilidad -鈹 鈹溾攢鈹 DATABASE.yml # Objetos de base de datos -鈹 鈹溾攢鈹 BACKEND.yml # Modulos backend -鈹 鈹斺攢鈹 FRONTEND.yml # Componentes frontend -鈹 -鈹斺攢鈹 pruebas/ # Documentacion de testing - 鈹溾攢鈹 TEST-PLAN.md # Plan de pruebas - 鈹斺攢鈹 TEST-CASES.md # Casos de prueba -``` - ---- - -## Enlaces a Documentacion - -### Documentacion del Modulo - -| Tipo | Documento | Descripcion | -|------|-----------|-------------| -| **Mapa** | [_MAP.md](./_MAP.md) | Indice completo del modulo con metricas | -| **Trazabilidad** | [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) | Matriz completa de trazabilidad RF鈫扙T鈫扷S鈫扗B鈫払E鈫扚E | -| **Base de Datos** | [DATABASE.yml](./implementacion/DATABASE.yml) | Inventario de schemas, tablas, funciones | -| **Backend** | [BACKEND.yml](./implementacion/BACKEND.yml) | Inventario de modulos, servicios, guards | -| **Frontend** | [FRONTEND.yml](./implementacion/FRONTEND.yml) | Inventario de componentes, hooks, stores | - -### Requerimientos Funcionales - -| ID | Documento | Titulo | -|----|-----------|--------| -| RF-AUTH-001 | [RF-AUTH-001-roles-construccion.md](./requerimientos/RF-AUTH-001-roles-construccion.md) | Sistema de Roles de Construccion | -| RF-AUTH-002 | [RF-AUTH-002-estados-cuenta.md](./requerimientos/RF-AUTH-002-estados-cuenta.md) | Estados de Cuenta de Usuario | -| RF-AUTH-003 | [RF-AUTH-003-multi-tenancy.md](./requerimientos/RF-AUTH-003-multi-tenancy.md) | Multi-tenancy por Constructora | - -### Especificaciones Tecnicas - -| ID | Documento | Titulo | -|----|-----------|--------| -| ET-AUTH-001 | [ET-AUTH-001-rbac.md](./especificaciones/ET-AUTH-001-rbac.md) | Implementacion RBAC | -| ET-AUTH-002 | [ET-AUTH-002-estados-cuenta.md](./especificaciones/ET-AUTH-002-estados-cuenta.md) | Estados de Cuenta | -| ET-AUTH-003 | [ET-AUTH-003-multi-tenancy.md](./especificaciones/ET-AUTH-003-multi-tenancy.md) | Multi-tenancy Implementation | - -### Historias de Usuario - -| ID | Documento | Titulo | SP | -|----|-----------|--------|----| -| US-FUND-001 | [US-FUND-001-autenticacion-basica-jwt.md](./historias-usuario/US-FUND-001-autenticacion-basica-jwt.md) | Autenticacion Basica JWT | 8 | -| US-FUND-002 | [US-FUND-002-perfiles-usuario-construccion.md](./historias-usuario/US-FUND-002-perfiles-usuario-construccion.md) | Perfiles de Usuario de Construccion | 5 | -| US-FUND-003 | [US-FUND-003-dashboard-por-rol.md](./historias-usuario/US-FUND-003-dashboard-por-rol.md) | Dashboard Principal por Rol | 8 | -| US-FUND-004 | [US-FUND-004-infraestructura-base.md](./historias-usuario/US-FUND-004-infraestructura-base.md) | Infraestructura Tecnica Base | 12 | -| US-FUND-005 | [US-FUND-005-sistema-sesiones.md](./historias-usuario/US-FUND-005-sistema-sesiones.md) | Sistema de Sesiones y Estado | 6 | -| US-FUND-006 | [US-FUND-006-api-restful-base.md](./historias-usuario/US-FUND-006-api-restful-base.md) | API RESTful Basica | 8 | -| US-FUND-007 | [US-FUND-007-navegacion-routing.md](./historias-usuario/US-FUND-007-navegacion-routing.md) | Navegacion y Routing | 5 | -| US-FUND-008 | [US-FUND-008-ui-ux-base.md](./historias-usuario/US-FUND-008-ui-ux-base.md) | UI/UX Base (migrada de GAMILIT) | 3 | - -### Pruebas - -| Documento | Descripcion | -|-----------|-------------| -| [TEST-PLAN.md](./pruebas/TEST-PLAN.md) | Plan de pruebas del modulo | -| [TEST-CASES.md](./pruebas/TEST-CASES.md) | Casos de prueba detallados | - -### Referencias al Core ERP - -| Modulo | Documento | Descripcion | -|--------|-----------|-------------| -| MGN-001 | [README.md](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-001-auth/README.md) | Autenticacion Core | -| MGN-002 | [README.md](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-002-users/README.md) | Usuarios Core | -| MGN-003 | [README.md](/home/isem/workspace/projects/erp-suite/apps/erp-core/docs/01-fase-foundation/MGN-003-roles/README.md) | Roles Core | - ---- - -## Metricas del Modulo - -### Metricas de Planificacion - -| Metrica | Valor | Notas | -|---------|-------|-------| -| **Presupuesto estimado** | $25,000 MXN | Incluye desarrollo + pruebas | -| **Presupuesto target** | $25,000 MXN 卤5% | Rango: $23,750 - $26,250 | -| **Story Points estimados** | 50 SP | Reducidos vs 60 SP de GAMILIT | -| **Duracion estimada** | 10 dias | vs 11 dias GAMILIT (90% reutilizacion) | -| **Reutilizacion Core** | 95% | De MGN-001, MGN-002, MGN-003 | -| **Ahorro estimado** | ~2.5 semanas | vs desarrollo desde cero | - -### Metricas de Alcance - -| Metrica | Cantidad | -|---------|----------| -| **Requerimientos Funcionales** | 3 | -| **Especificaciones Tecnicas** | 3 | -| **User Stories** | 8 | -| **Schemas de BD** | 4 (auth, auth_management, audit, public) | -| **Tablas estimadas** | ~18 | -| **Endpoints API** | ~25 | -| **Componentes Frontend** | ~30 | -| **Roles de sistema** | 7 | - -### Metricas de Calidad (Targets) - -| Metrica | Target | Estrategia | -|---------|--------|------------| -| **Code Coverage** | >80% | Jest + Vitest | -| **API Response Time** | <200ms | P95 | -| **Frontend Lighthouse** | >90 | Performance score | -| **Security Audit** | 0 vulnerabilities | npm audit | -| **TypeScript Strict** | 100% | strict mode enabled | -| **ESLint Issues** | 0 | Pre-commit hooks | - ---- - -## Seguridad y Cumplimiento - -### Medidas de Seguridad - -| Area | Implementacion | -|------|----------------| -| **Passwords** | Bcrypt con cost factor 12 | -| **Tokens JWT** | Access (15 min) + Refresh (7 dias) | -| **Rate Limiting** | 5 intentos/minuto por IP | -| **Bloqueo Temporal** | 15 minutos tras 5 intentos fallidos | -| **Reset Password** | Tokens expiran en 1 hora | -| **HTTPS** | Obligatorio en produccion | -| **CORS** | Whitelist de dominios | -| **Helmet.js** | Headers de seguridad | -| **SQL Injection** | TypeORM parametrizado | -| **XSS** | Sanitizacion de inputs | -| **CSRF** | Tokens CSRF en forms | - -### Row-Level Security (RLS) - -Todas las tablas implementan RLS para aislamiento por constructora: - -```sql --- Ejemplo de politica RLS -CREATE POLICY "users_select_policy" - ON public.users - FOR SELECT - USING ( - constructora_id = current_setting('app.current_constructora_id')::uuid - OR - EXISTS ( - SELECT 1 FROM public.user_roles ur - JOIN public.roles r ON ur.role_id = r.id - WHERE ur.user_id = auth.uid() - AND r.name = 'super_admin' - ) - ); -``` - -### Auditoria - -Todas las operaciones criticas se auditan: - -- Login/Logout -- Cambios de password -- Asignacion de roles -- Modificacion de permisos -- Acceso a datos sensibles -- Operaciones CRUD de usuarios - ---- - -## Roadmap de Implementacion - -### Sprint 0: Migracion (2 dias) - -- [x] Migrar componentes de GAMILIT -- [x] Adaptar estructura de carpetas -- [x] Configurar base de datos -- [x] Adaptar variables de entorno - -### Sprint 1: Autenticacion (4 dias) - -- [ ] Implementar login/logout JWT -- [ ] OAuth con Google/Microsoft -- [ ] Recuperacion de password -- [ ] Sistema de sesiones -- [ ] Rate limiting y bloqueo - -### Sprint 2: Usuarios y Roles (4 dias) - -- [ ] CRUD de usuarios -- [ ] Perfiles extendidos -- [ ] Sistema RBAC con 7 roles -- [ ] Asignacion de permisos -- [ ] Multi-tenancy por constructora - -### Sprint 3: Dashboard y UI (3 dias) - -- [ ] Dashboard por rol (7 variantes) -- [ ] Navegacion y routing -- [ ] Componentes UI base -- [ ] Sistema de preferencias -- [ ] Responsive design - -### Sprint 4: Testing y QA (2 dias) - -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests de integracion API -- [ ] Tests E2E criticos -- [ ] Security audit -- [ ] Performance testing - -**Total: 15 dias (10 dias de desarrollo + 5 dias buffer)** - ---- - -## Lecciones Aprendidas (de GAMILIT) - -### Best Practices - -1. **RLS desde dia 1:** Implementar Row Level Security desde el inicio evita refactoring posterior -2. **Tests rigurosos:** Coverage >80% = deployment mas seguro y tranquilo -3. **Modularizacion temprana:** Facilita desarrollo paralelo de equipos -4. **Documentacion previa:** Especificar antes de implementar reduce cambios y retrabajo -5. **Type Safety:** TypeScript strict mode evita bugs en produccion -6. **API Versionado:** `/api/v1` desde el inicio facilita evolucionar sin breaking changes - -### Errores a Evitar - -1. **NO** implementar autenticacion custom sin JWT -2. **NO** guardar passwords en texto plano (obvio pero critico) -3. **NO** usar ORMs sin prepared statements -4. **NO** olvidar rate limiting en endpoints publicos -5. **NO** exponer stack traces en produccion -6. **NO** hacer commits sin tests pasando - ---- - -## Contacto y Soporte - -### Equipo - -| Rol | Responsable | Contacto | -|-----|-------------|----------| -| **Tech Lead** | @tech-lead | tech-lead@example.com | -| **Backend Team** | @backend-team | backend@example.com | -| **Frontend Team** | @frontend-team | frontend@example.com | -| **Database Team** | @database-team | database@example.com | -| **QA Team** | @qa-team | qa@example.com | - -### Recursos - -- **Wiki del Proyecto:** [Confluence/Wiki URL] -- **Board de Tareas:** [Jira/Linear URL] -- **Repositorio:** [GitHub/GitLab URL] -- **CI/CD:** [Jenkins/GitHub Actions URL] -- **Monitoring:** [Datadog/Grafana URL] - ---- - -## Historial de Cambios - -| Fecha | Version | Cambio | Autor | -|-------|---------|--------|-------| -| 2025-12-06 | 1.0 | Creacion del README del modulo MAI-001 | Requirements-Analyst | -| 2025-11-17 | 0.1 | Creacion inicial del modulo con _MAP.md | Requirements-Analyst | - ---- - -**Generado por:** Requirements-Analyst -**Fecha:** 2025-12-06 -**Estado:** Planificado -**Proxima revision:** Sprint 1 (inicio de implementacion) diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/_MAP.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/_MAP.md deleted file mode 100644 index f9b8f2560..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/_MAP.md +++ /dev/null @@ -1,195 +0,0 @@ -# _MAP: MAI-001 - Fundamentos - -**脡pica:** MAI-001 -**Nombre:** Fundamentos -**Fase:** 1 - Alcance Inicial -**Presupuesto:** $25,000 MXN -**Story Points:** 50 SP -**Estado:** 馃毀 Planificado -**Sprint:** Sprint 0-2 (Semanas 1-2) -**脷ltima actualizaci贸n:** 2025-11-17 - ---- - -## 馃搵 Prop贸sito - -Establecer las bases t茅cnicas y funcionales del Sistema de Administraci贸n de Obra: -- Autenticaci贸n y autorizaci贸n (JWT, RBAC para construcci贸n) -- Infraestructura base (DB, API, frontend) migrada de GAMILIT -- Perfiles de usuario con 7 roles espec铆ficos -- Dashboard principal por rol -- Multi-tenancy por constructora - -**Reutilizaci贸n GAMILIT:** 90% de componentes de infraestructura - ---- - -## 馃搧 Contenido - -### Requerimientos Funcionales (3) - -| ID | Archivo | T铆tulo | Estado | -|----|---------|--------|--------| -| RF-AUTH-001 | [RF-AUTH-001-roles-construccion.md](./requerimientos/RF-AUTH-001-roles-construccion.md) | Sistema de Roles de Construcci贸n | 馃毀 Planificado | -| RF-AUTH-002 | [RF-AUTH-002-estados-cuenta.md](./requerimientos/RF-AUTH-002-estados-cuenta.md) | Estados de Cuenta de Usuario | 馃毀 Planificado | -| RF-AUTH-003 | [RF-AUTH-003-multi-tenancy.md](./requerimientos/RF-AUTH-003-multi-tenancy.md) | Multi-tenancy por Constructora | 馃毀 Planificado | - -### Especificaciones T茅cnicas (3) - -| ID | Archivo | T铆tulo | RF | Estado | -|----|---------|--------|-------|--------| -| ET-AUTH-001 | [ET-AUTH-001-rbac.md](./especificaciones/ET-AUTH-001-rbac.md) | RBAC Implementation | RF-AUTH-001 | 馃毀 Planificado | -| ET-AUTH-002 | [ET-AUTH-002-estados-cuenta.md](./especificaciones/ET-AUTH-002-estados-cuenta.md) | Estados de Cuenta | RF-AUTH-002 | 馃毀 Planificado | -| ET-AUTH-003 | [ET-AUTH-003-multi-tenancy.md](./especificaciones/ET-AUTH-003-multi-tenancy.md) | Multi-tenancy Implementation | RF-AUTH-003 | 馃毀 Planificado | - -### Historias de Usuario (8) - -| ID | Archivo | T铆tulo | SP | Estado | -|----|---------|--------|----|--------| -| US-FUND-001 | [US-FUND-001-autenticacion-basica-jwt.md](./historias-usuario/US-FUND-001-autenticacion-basica-jwt.md) | Autenticaci贸n B谩sica JWT | 8 | 馃毀 Planificado | -| US-FUND-002 | [US-FUND-002-perfiles-usuario-construccion.md](./historias-usuario/US-FUND-002-perfiles-usuario-construccion.md) | Perfiles de Usuario de Construcci贸n | 5 | 馃毀 Planificado | -| US-FUND-003 | [US-FUND-003-dashboard-por-rol.md](./historias-usuario/US-FUND-003-dashboard-por-rol.md) | Dashboard Principal por Rol | 8 | 馃毀 Planificado | -| US-FUND-004 | [US-FUND-004-infraestructura-base.md](./historias-usuario/US-FUND-004-infraestructura-base.md) | Infraestructura T茅cnica Base | 12 | 馃毀 Planificado | -| US-FUND-005 | [US-FUND-005-sistema-sesiones.md](./historias-usuario/US-FUND-005-sistema-sesiones.md) | Sistema de Sesiones y Estado | 6 | 馃毀 Planificado | -| US-FUND-006 | [US-FUND-006-api-restful-base.md](./historias-usuario/US-FUND-006-api-restful-base.md) | API RESTful B谩sica | 8 | 馃毀 Planificado | -| US-FUND-007 | [US-FUND-007-navegacion-routing.md](./historias-usuario/US-FUND-007-navegacion-routing.md) | Navegaci贸n y Routing | 5 | 馃毀 Planificado | -| US-FUND-008 | [US-FUND-008-ui-ux-base.md](./historias-usuario/US-FUND-008-ui-ux-base.md) | UI/UX Base (migrada de GAMILIT) | 3 | 馃毀 Planificado | - -**Total Story Points:** 50 SP (reducidos vs 60 SP de GAMILIT por reutilizaci贸n) - -### Implementaci贸n - -馃搳 **Inventarios de trazabilidad:** -- [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) - Matriz completa de trazabilidad -- [DATABASE.yml](./implementacion/DATABASE.yml) - Objetos de base de datos -- [BACKEND.yml](./implementacion/BACKEND.yml) - M贸dulos backend -- [FRONTEND.yml](./implementacion/FRONTEND.yml) - Componentes frontend - -### Pruebas - -馃搵 Documentaci贸n de testing: -- [TEST-PLAN.md](./pruebas/TEST-PLAN.md) - Plan de pruebas -- [TEST-CASES.md](./pruebas/TEST-CASES.md) - Casos de prueba - ---- - -## 馃敆 Referencias - -- **README:** [README.md](./README.md) - Descripci贸n detallada de la 茅pica -- **Fase 1:** [../README.md](../README.md) - Informaci贸n de la fase completa -- **Cat谩logo Auth:** `core/catalog/auth/` *(componentes reutilizables de autenticaci贸n)* - ---- - -## 馃搳 M茅tricas - -| M茅trica | Valor | -|---------|-------| -| **Presupuesto estimado** | $25,000 MXN | -| **Presupuesto target** | $25,000 MXN 卤5% | -| **Story Points estimados** | 50 SP | -| **Duraci贸n estimada** | 10 d铆as (vs 11 d铆as GAMILIT) | -| **Reutilizaci贸n GAMILIT** | 90% | -| **Ahorro estimado** | ~2.5 semanas vs desarrollo desde cero | -| **RF a implementar** | 3/3 | -| **ET a implementar** | 3/3 | -| **US a completar** | 8/8 | - ---- - -## 馃幆 M贸dulos Afectados - -### Base de Datos -- **Schemas:** `auth`, `auth_management`, `audit_logging`, `public` -- **Tablas:** ~18 tablas (auth, profiles, constructoras, audit_logs, etc.) -- **Funciones:** Funciones de RBAC, verificaci贸n de permisos por constructora -- **ENUMs:** - - `construction_role` (director, engineer, resident, purchases, finance, hr, post_sales) - - `account_status` (active, suspended, banned, pending_verification) - -### Backend -- **M贸dulo:** `auth` -- **Path:** `apps/backend/src/modules/auth/` -- **Services:** AuthService, JwtService, ConstructoraService -- **Guards:** JwtAuthGuard, RolesGuard, ConstructoraGuard -- **Middlewares:** ValidationMiddleware, ErrorMiddleware, LoggingMiddleware - -### Frontend -- **Features:** `auth`, `dashboard` -- **Path:** `apps/frontend/src/features/` -- **Componentes:** Login, Register, Dashboard (7 variantes por rol), Profile -- **Guards:** AuthGuard, RoleGuard, ConstructoraGuard -- **Stores:** authStore, constructoraStore - ---- - -## 馃殌 Roles Espec铆ficos de Construcci贸n - -A diferencia de GAMILIT (student, admin_teacher, super_admin), este sistema tiene 7 roles: - -| Rol | C贸digo | Descripci贸n | Permisos Clave | -|-----|--------|-------------|----------------| -| **Director** | `director` | Director general/proyectos | Visi贸n global, m谩rgenes, riesgos | -| **Ingeniero** | `engineer` | Ingenier铆a/Planeaci贸n | Presupuestos, programaci贸n, control | -| **Residente** | `resident` | Residente de obra/Supervisor | Avances, incidencias, checklists | -| **Compras** | `purchases` | Compras/Almac茅n | 脫rdenes de compra, inventarios | -| **Finanzas** | `finance` | Administraci贸n/Finanzas | Presupuestos, pagos, flujo | -| **RRHH** | `hr` | Recursos Humanos | Asistencias, costeo mano de obra | -| **Postventa** | `post_sales` | Postventa/Garant铆as | Incidencias, seguimiento clientes | - ---- - -## 馃攧 Migraci贸n desde GAMILIT - -### Componentes a Migrar (Sprint 0) - -**Backend:** -- [x] Sistema de autenticaci贸n JWT -- [x] Middleware de autenticaci贸n y autorizaci贸n -- [x] Guards (JwtAuthGuard, RolesGuard) -- [x] Error handlers y validadores -- [x] Sistema de logging estructurado -- [x] Sistema de auditor铆a - -**Frontend:** -- [x] Componentes UI base (Buttons, Inputs, Modales) -- [x] Layouts principales -- [x] Sistema de formularios (React Hook Form + Zod) -- [x] Hooks personalizados (useAuth, useApi) -- [x] Sistema de notificaciones - -**Database:** -- [x] Schemas modulares -- [x] Funciones comunes (now_mexico, get_current_user_id) -- [x] Triggers de auditor铆a -- [x] Pol铆ticas RLS base - -### Adaptaciones Requeridas - -| Componente | Adaptaci贸n | Esfuerzo | -|------------|------------|----------| -| **Roles ENUM** | Cambiar 3 roles 鈫 7 roles construcci贸n | Bajo | -| **RLS Policies** | Adaptar filtros por constructora | Medio | -| **Dashboard** | Crear 7 variantes por rol | Medio | -| **Permisos** | Matriz de permisos por m贸dulo | Alto | - ---- - -## 馃挕 Lessons Learned (de GAMILIT) - -1. **RLS desde d铆a 1:** Implementar Row Level Security desde el inicio evita refactoring -2. **Tests rigurosos:** Coverage >80% = deployment tranquilo -3. **Modularizaci贸n temprana:** Facilita desarrollo paralelo -4. **Documentaci贸n previa:** Especificar antes de implementar reduce cambios - ---- - -## 馃幆 Siguiente Paso - -Completar Sprint 0 con migraci贸n de componentes GAMILIT, luego iniciar implementaci贸n de MAI-001. - ---- - -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @frontend-team @database-team -**Estado:** 馃毀 Planificado diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-001-rbac.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-001-rbac.md deleted file mode 100644 index cef8c9123..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-001-rbac.md +++ /dev/null @@ -1,1295 +0,0 @@ -# ET-AUTH-001: RBAC (Role-Based Access Control) para Construcci贸n - -## 馃搵 Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | ET-AUTH-001 | -| **脡pica** | MAI-001 - Fundamentos | -| **M贸dulo** | Autenticaci贸n y Autorizaci贸n | -| **Tipo** | Especificaci贸n T茅cnica | -| **Estado** | 馃毀 Planificado | -| **Versi贸n** | 1.0 | -| **Fecha creaci贸n** | 2025-11-17 | -| **脷ltima actualizaci贸n** | 2025-11-17 | -| **Esfuerzo estimado** | 20h (vs 25h GAMILIT - 20% ahorro por reutilizaci贸n de infraestructura) | - -## 馃敆 Referencias - -### Requerimiento Funcional -馃搫 [RF-AUTH-001: Sistema de Roles de Construcci贸n](../requerimientos/RF-AUTH-001-roles-construccion.md) - -### Origen (GAMILIT) -鈾伙笍 **Reutilizaci贸n:** 80% -- **Cat谩logo de referencia:** `core/catalog/auth/` *(Patr贸n RBAC reutilizado)* -- **Componentes reutilizables:** - - Arquitectura general de guards y decorators - - RLS infrastructure - - Frontend role-based components - - Testing patterns -- **Adaptaciones:** - - 3 roles 鈫 7 roles de construcci贸n - - Agregar contexto multi-tenancy (constructora_id) - - Permisos espec铆ficos de construcci贸n - - RLS policies adaptadas al dominio - -### Implementaci贸n DDL - -馃梽锔 **ENUM Principal:** -```sql --- apps/database/ddl/00-prerequisites.sql -DO $$ BEGIN - CREATE TYPE auth_management.construction_role AS ENUM ( - 'director', -- Director general/proyectos - Acceso total - 'engineer', -- Ingeniero/Planeaci贸n - Presupuestos, programaci贸n - 'resident', -- Residente de obra - Supervisi贸n en campo - 'purchases', -- Compras/Almac茅n - 脫rdenes de compra - 'finance', -- Administraci贸n/Finanzas - Presupuestos, flujo - 'hr', -- Recursos Humanos - Asistencias, n贸mina - 'post_sales' -- Postventa/Garant铆as - Atenci贸n a clientes - ); -EXCEPTION WHEN duplicate_object THEN null; END $$; - -COMMENT ON TYPE auth_management.construction_role IS - 'Roles de usuario en el sistema de gesti贸n de obra: director, engineer, resident, purchases, finance, hr, post_sales'; -``` - -馃梽锔 **Tablas que usan el ENUM:** -- `auth_management.user_constructoras.role` - Rol del usuario en cada constructora -- `projects.project_team_assignments.role` - Rol del usuario en cada proyecto espec铆fico - -馃梽锔 **Funciones de Contexto:** -```sql --- Obtener rol del usuario en constructora actual -CREATE OR REPLACE FUNCTION auth_management.get_current_user_role() -RETURNS auth_management.construction_role -LANGUAGE sql STABLE SECURITY DEFINER -AS $$ - SELECT role - FROM auth_management.user_constructoras - WHERE user_id = auth_management.get_current_user_id() - AND constructora_id = auth_management.get_current_constructora_id() - AND status = 'active' - LIMIT 1; -$$; - --- Verificar si usuario tiene uno de los roles requeridos -CREATE OR REPLACE FUNCTION auth_management.user_has_any_role( - p_roles auth_management.construction_role[] -) RETURNS BOOLEAN -LANGUAGE sql STABLE SECURITY DEFINER -AS $$ - SELECT EXISTS ( - SELECT 1 - FROM auth_management.user_constructoras - WHERE user_id = auth_management.get_current_user_id() - AND constructora_id = auth_management.get_current_constructora_id() - AND role = ANY(p_roles) - AND status = 'active' - ); -$$; - --- Verificar si usuario es admin (director) -CREATE OR REPLACE FUNCTION auth_management.is_director() -RETURNS BOOLEAN -LANGUAGE sql STABLE SECURITY DEFINER -AS $$ - SELECT auth_management.get_current_user_role() = 'director'; -$$; -``` - -### Backend - -馃捇 **Archivos de Implementaci贸n:** -- **Enum:** `apps/backend/src/modules/auth/enums/construction-role.enum.ts` -- **Guards:** `apps/backend/src/modules/auth/guards/roles.guard.ts` -- **Decorators:** `apps/backend/src/modules/auth/decorators/roles.decorator.ts` -- **Constructora Guard:** `apps/backend/src/modules/auth/guards/constructora.guard.ts` -- **Utilities:** `apps/backend/src/modules/auth/utils/role-level.util.ts` - -### Frontend - -馃帹 **Componentes:** -- **Types:** `apps/frontend/src/types/auth.types.ts` -- **RoleBasedRoute:** `apps/frontend/src/components/auth/RoleBasedRoute.tsx` -- **RoleBadge:** `apps/frontend/src/components/ui/RoleBadge.tsx` -- **usePermissions:** `apps/frontend/src/hooks/usePermissions.ts` -- **PermissionGate:** `apps/frontend/src/components/auth/PermissionGate.tsx` - -### Trazabilidad -馃搳 [TRACEABILITY.yml](../implementacion/TRACEABILITY.yml#L79-L115) - ---- - -## 馃彈锔 Arquitectura de RBAC Multi-tenancy - -### Dise帽o General - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CAPA FRONTEND 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 RoleBadge 鈹 鈹 PermissionGate 鈹 鈹 RoleBasedRoute 鈹 鈹 -鈹 鈹 (director) 鈹 鈹 (can:view: 鈹 鈹 (allowedRoles: 鈹 鈹 -鈹 鈹 鈹 鈹 budgets) 鈹 鈹 [director,engineer])鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 HTTP + JWT (role + constructoraId claims) -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CAPA BACKEND 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 RolesGuard 鈹 鈹 @Roles() 鈹 鈹 ConstructoraGuard 鈹 鈹 -鈹 鈹 鈹 鈹 decorator 鈹 鈹 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 SetRlsContextInterceptor 鈹 鈹 -鈹 鈹 - set_config('app.current_user_id', userId) 鈹 鈹 -鈹 鈹 - set_config('app.current_constructora_id', constructoraId) 鈹 鈹 -鈹 鈹 - set_config('app.current_user_role', role) 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 SQL Queries con RLS -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈻尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CAPA DATABASE (PostgreSQL + RLS) 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 RLS POLICIES (Row Level Security) 鈹 鈹 -鈹 鈹 - directors_view_all_projects 鈹 鈹 -鈹 鈹 - engineers_view_budgets 鈹 鈹 -鈹 鈹 - residents_view_own_projects 鈹 鈹 -鈹 鈹 - hr_view_employees 鈹 鈹 -鈹 鈹 + ALWAYS: constructora_id isolation 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 ENUM: auth_management.construction_role 鈹 鈹 -鈹 鈹 VALUES: director | engineer | resident | purchases | 鈹 鈹 -鈹 鈹 finance | hr | post_sales 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 CONTEXT FUNCTIONS: 鈹 鈹 -鈹 鈹 - get_current_user_role() 鈫 construction_role 鈹 鈹 -鈹 鈹 - get_current_constructora_id() 鈫 UUID 鈹 鈹 -鈹 鈹 - user_has_any_role(roles[]) 鈫 BOOLEAN 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -**Flujo de Request:** -1. Frontend env铆a request con JWT que incluye `{ role: 'engineer', constructoraId: 'abc-123' }` -2. JwtAuthGuard extrae y valida JWT, inyecta `user` en request -3. ConstructoraGuard valida que usuario tenga acceso activo a esa constructora -4. SetRlsContextInterceptor configura variables de sesi贸n de PostgreSQL -5. RolesGuard valida que usuario tenga rol requerido en endpoint -6. Controller ejecuta l贸gica de negocio -7. TypeORM/Prisma ejecuta queries -8. PostgreSQL aplica RLS autom谩ticamente usando contexto configurado -9. Solo retorna datos de la constructora actual con permisos del rol - ---- - -## 馃搻 Matriz de Permisos Completa - -### Tabla Detallada de Permisos por M贸dulo - -| Recurso | Acci贸n | director | engineer | resident | purchases | finance | hr | post_sales | -|---------|--------|----------|----------|----------|-----------|---------|----|----------- -| -| **Perfil Propio** | -| Ver perfil | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | -| Editar perfil | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | -| Cambiar rol | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| **Proyectos/Obras** | -| Ver todos los proyectos | 鉁 | 鉁 | 鉂 (solo asignados) | 鉂 | 鉁 | 鉂 | 鉂 | -| Crear proyecto | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Editar proyecto | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Archivar proyecto | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Ver m谩rgenes de utilidad | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| **Presupuestos** | -| Ver presupuestos | 鉁 | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| Crear presupuesto | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Editar presupuesto | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Aprobar presupuesto | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Ver costos reales | 鉁 | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| **Avances de Obra** | -| Capturar avance f铆sico | 鉁 | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | -| Editar avance | 鉁 | 鉁 | 鉁 (solo hoy) | 鉂 | 鉂 | 鉂 | 鉂 | -| Ver historial avances | 鉁 | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | -| Aprobar avance | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| **Compras** | -| Ver 贸rdenes de compra | 鉁 | 鉁 | 鉂 | 鉁 | 鉁 | 鉂 | 鉂 | -| Crear orden de compra | 鉁 | 鉁 | 鉂 | 鉁 | 鉂 | 鉂 | 鉂 | -| Aprobar orden compra | 鉁 | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| Ver inventario | 鉁 | 鉁 | 鉁 (vista) | 鉁 | 鉂 | 鉂 | 鉂 | -| Gestionar inventario | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | 鉂 | -| **Finanzas** | -| Ver flujo de efectivo | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| Ver cuentas por pagar | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| Aprobar pagos | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| Generar reportes fin. | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| **Recursos Humanos** | -| Ver empleados | 鉁 | 鉁 (asignados) | 鉁 (asignados) | 鉂 | 鉂 | 鉁 | 鉂 | -| Crear empleado | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | -| Registrar asistencia | 鉁 | 鉂 | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | -| Ver n贸mina | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉁 | 鉂 | -| Exportar IMSS/INFONAVIT | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | -| **Postventa/Garant铆as** | -| Ver incidencias | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | -| Crear incidencia | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | -| Asignar incidencia | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | -| Cerrar incidencia | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | -| **Sistema** | -| Ver usuarios constructora | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Invitar usuarios | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Suspender usuarios | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Ver audit logs | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Config. constructora | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | - -**Leyenda:** -- 鉁 Acceso completo -- 鉁 (condici贸n) Acceso con restricci贸n -- 鉂 Sin acceso - ---- - -## 馃敡 Implementaci贸n T茅cnica Completa - -### 1. Backend - Enum TypeScript - -**Ubicaci贸n:** `apps/backend/src/modules/auth/enums/construction-role.enum.ts` - -```typescript -/** - * Roles de usuario en el Sistema de Gesti贸n de Obra - * - * IMPORTANTE: Debe estar sincronizado con: - * - Database ENUM: auth_management.construction_role - * - DDL: apps/database/ddl/00-prerequisites.sql - * - Frontend: apps/frontend/src/types/auth.types.ts - */ -export enum ConstructionRole { - /** Director general/proyectos - Acceso total, visi贸n estrat茅gica */ - DIRECTOR = 'director', - - /** Ingeniero/Planeaci贸n - Presupuestos, programaci贸n, control de obra */ - ENGINEER = 'engineer', - - /** Residente de obra - Supervisi贸n en campo, captura de avances */ - RESIDENT = 'resident', - - /** Compras/Almac茅n - 脫rdenes de compra, gesti贸n de inventario */ - PURCHASES = 'purchases', - - /** Administraci贸n/Finanzas - Presupuestos, flujo de efectivo, pagos */ - FINANCE = 'finance', - - /** Recursos Humanos - Asistencias, n贸mina, IMSS/INFONAVIT */ - HR = 'hr', - - /** Postventa/Garant铆as - Atenci贸n a clientes, seguimiento post-entrega */ - POST_SALES = 'post_sales', -} - -/** - * Nivel jer谩rquico de cada rol (para comparaciones) - * - * Nivel m谩s alto = m谩s permisos - */ -export const RoleLevel: Record = { - [ConstructionRole.DIRECTOR]: 7, // M谩ximo nivel - [ConstructionRole.ENGINEER]: 6, - [ConstructionRole.FINANCE]: 5, - [ConstructionRole.HR]: 4, - [ConstructionRole.PURCHASES]: 3, - [ConstructionRole.POST_SALES]: 2, - [ConstructionRole.RESIDENT]: 1, // M铆nimo nivel -}; - -/** - * Descripci贸n legible de cada rol - */ -export const RoleDisplayName: Record = { - [ConstructionRole.DIRECTOR]: 'Director', - [ConstructionRole.ENGINEER]: 'Ingeniero', - [ConstructionRole.RESIDENT]: 'Residente de Obra', - [ConstructionRole.PURCHASES]: 'Compras', - [ConstructionRole.FINANCE]: 'Finanzas', - [ConstructionRole.HR]: 'Recursos Humanos', - [ConstructionRole.POST_SALES]: 'Postventa', -}; - -/** - * Verifica si un rol tiene al menos el nivel requerido - */ -export function hasMinimumRole( - userRole: ConstructionRole, - requiredRole: ConstructionRole -): boolean { - return RoleLevel[userRole] >= RoleLevel[requiredRole]; -} - -/** - * Verifica si usuario tiene uno de los roles permitidos - */ -export function hasAnyRole( - userRole: ConstructionRole, - allowedRoles: ConstructionRole[] -): boolean { - return allowedRoles.includes(userRole); -} - -/** - * Roles con acceso a presupuestos - */ -export const BUDGET_ACCESS_ROLES: ConstructionRole[] = [ - ConstructionRole.DIRECTOR, - ConstructionRole.ENGINEER, - ConstructionRole.FINANCE, -]; - -/** - * Roles con acceso a 贸rdenes de compra - */ -export const PURCHASE_ORDER_ROLES: ConstructionRole[] = [ - ConstructionRole.DIRECTOR, - ConstructionRole.ENGINEER, - ConstructionRole.PURCHASES, - ConstructionRole.FINANCE, -]; - -/** - * Roles que pueden capturar avances de obra - */ -export const PROGRESS_CAPTURE_ROLES: ConstructionRole[] = [ - ConstructionRole.DIRECTOR, - ConstructionRole.ENGINEER, - ConstructionRole.RESIDENT, -]; - -/** - * Roles con acceso a RRHH - */ -export const HR_ACCESS_ROLES: ConstructionRole[] = [ - ConstructionRole.DIRECTOR, - ConstructionRole.HR, -]; -``` - ---- - -### 2. Backend - Guards - -#### RolesGuard (Validaci贸n de Roles) - -**Ubicaci贸n:** `apps/backend/src/modules/auth/guards/roles.guard.ts` - -```typescript -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { ConstructionRole } from '../enums/construction-role.enum'; - -/** - * Guard que valida roles de usuario en endpoints - * - * Uso: - * @Roles(ConstructionRole.DIRECTOR, ConstructionRole.ENGINEER) - * @Get('budgets') - * getBudgets() { ... } - */ -@Injectable() -export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} - - canActivate(context: ExecutionContext): boolean { - // Obtener roles permitidos del decorator @Roles() - const requiredRoles = this.reflector.getAllAndOverride('roles', [ - context.getHandler(), - context.getClass(), - ]); - - // Si no hay roles requeridos, permitir acceso - if (!requiredRoles || requiredRoles.length === 0) { - return true; - } - - // Obtener usuario del request (inyectado por JwtStrategy) - const request = context.switchToHttp().getRequest(); - const user = request.user; - - if (!user) { - throw new ForbiddenException('Usuario no autenticado'); - } - - // Validar que usuario tenga alguno de los roles permitidos - const hasRole = requiredRoles.some((role) => user.role === role); - - if (!hasRole) { - throw new ForbiddenException({ - statusCode: 403, - message: `Acceso denegado. Requiere uno de estos roles: ${requiredRoles.join(', ')}`, - errorCode: 'INSUFFICIENT_ROLE', - userRole: user.role, - requiredRoles, - }); - } - - return true; - } -} -``` - -#### ConstructoraGuard (Validaci贸n de Acceso a Constructora) - -**Ubicaci贸n:** `apps/backend/src/modules/auth/guards/constructora.guard.ts` - -```typescript -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { UserConstructora } from '../entities/user-constructora.entity'; -import { UserStatus } from '../enums/user-status.enum'; - -/** - * Guard que valida que usuario tenga acceso activo a la constructora - * - * Valida: - * 1. Usuario tiene relaci贸n con constructora - * 2. Status es 'active' - * 3. Constructora est谩 activa - */ -@Injectable() -export class ConstructoraGuard implements CanActivate { - constructor( - @InjectRepository(UserConstructora) - private readonly userConstructoraRepo: Repository, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user = request.user; - - if (!user) { - throw new ForbiddenException('Usuario no autenticado'); - } - - if (!user.constructoraId) { - throw new ForbiddenException({ - statusCode: 403, - message: 'No se ha seleccionado una constructora', - errorCode: 'NO_CONSTRUCTORA_SELECTED', - }); - } - - // Validar acceso a constructora - const access = await this.userConstructoraRepo.findOne({ - where: { - userId: user.id, - constructoraId: user.constructoraId, - }, - relations: ['constructora'], - }); - - if (!access) { - throw new ForbiddenException({ - statusCode: 403, - message: 'No tienes acceso a esta constructora', - errorCode: 'CONSTRUCTORA_ACCESS_DENIED', - constructoraId: user.constructoraId, - }); - } - - // Validar estado del usuario en constructora - if (access.status !== UserStatus.ACTIVE) { - throw new ForbiddenException({ - statusCode: 403, - message: `Tu acceso a esta constructora est谩 ${access.status}`, - errorCode: 'CONSTRUCTORA_ACCESS_INACTIVE', - status: access.status, - }); - } - - // Validar que constructora est茅 activa - if (!access.constructora.active) { - throw new ForbiddenException({ - statusCode: 403, - message: 'Esta constructora est谩 inactiva', - errorCode: 'CONSTRUCTORA_INACTIVE', - }); - } - - return true; - } -} -``` - ---- - -### 3. Backend - Decorators - -#### @Roles Decorator - -**Ubicaci贸n:** `apps/backend/src/modules/auth/decorators/roles.decorator.ts` - -```typescript -import { SetMetadata } from '@nestjs/common'; -import { ConstructionRole } from '../enums/construction-role.enum'; - -/** - * Decorator para especificar roles permitidos en un endpoint - * - * Ejemplos: - * @Roles(ConstructionRole.DIRECTOR) - * @Roles(ConstructionRole.DIRECTOR, ConstructionRole.ENGINEER) - * @Roles(...BUDGET_ACCESS_ROLES) - */ -export const Roles = (...roles: ConstructionRole[]) => SetMetadata('roles', roles); -``` - -#### @CurrentUser Decorator - -**Ubicaci贸n:** `apps/backend/src/modules/auth/decorators/current-user.decorator.ts` - -```typescript -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; - -/** - * Decorator para obtener usuario actual del request - * - * Uso: - * @Get('profile') - * getProfile(@CurrentUser() user: UserJwtPayload) { - * return user; - * } - */ -export const CurrentUser = createParamDecorator( - (data: string | undefined, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const user = request.user; - - return data ? user?.[data] : user; - }, -); - -/** - * Obtener solo el ID del usuario - * - * Uso: - * @Get('my-data') - * getMyData(@UserId() userId: string) { - * return this.service.findByUserId(userId); - * } - */ -export const UserId = createParamDecorator( - (data: unknown, ctx: ExecutionContext): string => { - const request = ctx.switchToHttp().getRequest(); - return request.user?.id; - }, -); - -/** - * Obtener constructora actual - */ -export const CurrentConstructora = createParamDecorator( - (data: unknown, ctx: ExecutionContext): string => { - const request = ctx.switchToHttp().getRequest(); - return request.user?.constructoraId; - }, -); -``` - ---- - -### 4. Backend - Interceptor para RLS Context - -**Ubicaci贸n:** `apps/backend/src/common/interceptors/set-rls-context.interceptor.ts` - -```typescript -import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; -import { Observable, from } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; -import { InjectDataSource } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; - -/** - * Interceptor que configura el contexto de RLS en PostgreSQL - * - * Configura variables de sesi贸n: - * - app.current_user_id - * - app.current_constructora_id - * - app.current_user_role - * - * Estas variables son usadas por RLS policies para filtrar datos - */ -@Injectable() -export class SetRlsContextInterceptor implements NestInterceptor { - constructor(@InjectDataSource() private readonly dataSource: DataSource) {} - - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest(); - const user = request.user; - - // Si no hay usuario autenticado, continuar sin configurar RLS - if (!user) { - return next.handle(); - } - - // Configurar variables de sesi贸n de PostgreSQL - return from( - this.dataSource.query(` - SELECT - set_config('app.current_user_id', $1, true), - set_config('app.current_constructora_id', $2, true), - set_config('app.current_user_role', $3, true) - `, [ - user.id || '', - user.constructoraId || '', - user.role || '', - ]) - ).pipe( - switchMap(() => next.handle()) - ); - } -} -``` - -**Aplicaci贸n global:** -```typescript -// apps/backend/src/main.ts -import { SetRlsContextInterceptor } from './common/interceptors/set-rls-context.interceptor'; - -async function bootstrap() { - const app = await NestFactory.create(AppModule); - - // Aplicar interceptor globalmente - app.useGlobalInterceptors(new SetRlsContextInterceptor(app.get(DataSource))); - - await app.listen(3000); -} -``` - ---- - -### 5. Backend - Ejemplo de Controller con Guards - -**Ubicaci贸n:** `apps/backend/src/modules/budgets/budgets.controller.ts` - -```typescript -import { Controller, Get, Post, Patch, Delete, Body, Param, UseGuards } from '@nestjs/common'; -import { JwtAuthGuard } from '@modules/auth/guards/jwt-auth.guard'; -import { RolesGuard } from '@modules/auth/guards/roles.guard'; -import { ConstructoraGuard } from '@modules/auth/guards/constructora.guard'; -import { Roles } from '@modules/auth/decorators/roles.decorator'; -import { CurrentUser, CurrentConstructora } from '@modules/auth/decorators/current-user.decorator'; -import { ConstructionRole, BUDGET_ACCESS_ROLES } from '@modules/auth/enums/construction-role.enum'; -import { BudgetsService } from './budgets.service'; - -/** - * Controller de Presupuestos - * - * Guards aplicados: - * 1. JwtAuthGuard: Validar que usuario est茅 autenticado - * 2. ConstructoraGuard: Validar acceso a constructora - * 3. RolesGuard: Validar rol requerido - */ -@Controller('budgets') -@UseGuards(JwtAuthGuard, ConstructoraGuard, RolesGuard) -export class BudgetsController { - constructor(private readonly budgetsService: BudgetsService) {} - - /** - * Listar presupuestos - * Acceso: director, engineer, finance - */ - @Roles(...BUDGET_ACCESS_ROLES) - @Get() - async findAll(@CurrentConstructora() constructoraId: string) { - return this.budgetsService.findAll(constructoraId); - } - - /** - * Crear presupuesto - * Acceso: director, engineer - */ - @Roles(ConstructionRole.DIRECTOR, ConstructionRole.ENGINEER) - @Post() - async create( - @Body() createDto: CreateBudgetDto, - @CurrentUser() user: UserJwtPayload, - @CurrentConstructora() constructoraId: string, - ) { - return this.budgetsService.create(createDto, user.id, constructoraId); - } - - /** - * Aprobar presupuesto - * Acceso: SOLO director - */ - @Roles(ConstructionRole.DIRECTOR) - @Patch(':id/approve') - async approve( - @Param('id') id: string, - @CurrentUser() user: UserJwtPayload, - ) { - return this.budgetsService.approve(id, user.id); - } - - /** - * Eliminar presupuesto - * Acceso: SOLO director - */ - @Roles(ConstructionRole.DIRECTOR) - @Delete(':id') - async remove(@Param('id') id: string) { - return this.budgetsService.remove(id); - } -} -``` - ---- - -## 馃敀 Row Level Security (RLS) Policies - -### Patr贸n Base para RLS con Multi-tenancy - -**Todas las policies DEBEN incluir filtrado por constructora_id:** - -```sql --- Patr贸n base -CREATE POLICY "policy_name" ON [schema].[table] - FOR [SELECT|INSERT|UPDATE|DELETE] - TO authenticated - USING ( - -- 1. Filtro por constructora (OBLIGATORIO) - constructora_id = auth_management.get_current_constructora_id() - - -- 2. Filtro por rol (seg煤n necesidad) - AND auth_management.user_has_any_role(ARRAY['director', 'engineer']) - - -- 3. Filtros adicionales (seg煤n l贸gica de negocio) - AND [condiciones espec铆ficas] - ); -``` - ---- - -### Ejemplo 1: Proyectos - Acceso Basado en Rol - -```sql --- apps/database/ddl/schemas/projects/tables/projects.sql - -ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY; - --- Policy 1: Director y Engineer ven todos los proyectos de su constructora -CREATE POLICY "directors_engineers_view_all_projects" - ON projects.projects - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'engineer']) - ); - --- Policy 2: Residente solo ve proyectos asignados -CREATE POLICY "residents_view_own_projects" - ON projects.projects - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.get_current_user_role() = 'resident' - AND id IN ( - SELECT project_id - FROM projects.project_team_assignments - WHERE user_id = auth_management.get_current_user_id() - AND role = 'resident' - AND active = TRUE - ) - ); - --- Policy 3: Finance ve todos los proyectos (para reportes) -CREATE POLICY "finance_view_all_projects" - ON projects.projects - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.get_current_user_role() = 'finance' - ); - --- Policy 4: Solo director y engineer pueden crear proyectos -CREATE POLICY "create_projects" - ON projects.projects - FOR INSERT - TO authenticated - WITH CHECK ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'engineer']) - ); - --- Policy 5: Solo director y engineer pueden editar proyectos -CREATE POLICY "update_projects" - ON projects.projects - FOR UPDATE - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'engineer']) - ); - --- Policy 6: Solo director puede archivar proyectos -CREATE POLICY "archive_projects" - ON projects.projects - FOR DELETE - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.get_current_user_role() = 'director' - ); -``` - ---- - -### Ejemplo 2: Presupuestos - Ocultar M谩rgenes seg煤n Rol - -```sql --- apps/database/ddl/schemas/budgets/tables/budgets.sql - -ALTER TABLE budgets.budgets ENABLE ROW LEVEL SECURITY; - --- Policy 1: Director y Finance ven presupuestos completos (incluye m谩rgenes) -CREATE POLICY "directors_finance_view_full_budgets" - ON budgets.budgets - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'finance']) - ); - --- Policy 2: Engineer ve presupuestos (pero m谩rgenes se ocultan en aplicaci贸n) -CREATE POLICY "engineers_view_budgets" - ON budgets.budgets - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.get_current_user_role() = 'engineer' - ); - --- Policy 3: Solo director y engineer pueden crear presupuestos -CREATE POLICY "create_budgets" - ON budgets.budgets - FOR INSERT - TO authenticated - WITH CHECK ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'engineer']) - ); - --- Policy 4: Solo director puede aprobar presupuestos -CREATE POLICY "approve_budgets" - ON budgets.budgets - FOR UPDATE - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.get_current_user_role() = 'director' - AND status = 'pending' -- Solo aprobar pendientes - ) - WITH CHECK ( - status = 'approved' -- Solo cambiar a aprobado - ); -``` - -**Nota:** Para ocultar columnas espec铆ficas seg煤n rol (ej: `margin_percentage`), se usa en el service: - -```typescript -// apps/backend/src/modules/budgets/budgets.service.ts -async findAll(constructoraId: string, userRole: ConstructionRole) { - const query = this.budgetRepo - .createQueryBuilder('budget') - .where('budget.constructoraId = :constructoraId', { constructoraId }); - - // Ocultar m谩rgenes si no es director o finance - if (![ConstructionRole.DIRECTOR, ConstructionRole.FINANCE].includes(userRole)) { - query.select([ - 'budget.id', - 'budget.projectId', - 'budget.totalCost', - 'budget.status', - // NO incluir: budget.marginPercentage, budget.profitAmount - ]); - } - - return query.getMany(); -} -``` - ---- - -### Ejemplo 3: Empleados (RRHH) - Acceso Selectivo - -```sql --- apps/database/ddl/schemas/hr/tables/employees.sql - -ALTER TABLE hr.employees ENABLE ROW LEVEL SECURITY; - --- Policy 1: Director y HR ven todos los empleados -CREATE POLICY "directors_hr_view_all_employees" - ON hr.employees - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'hr']) - ); - --- Policy 2: Engineer y Resident ven empleados asignados a sus proyectos -CREATE POLICY "engineers_residents_view_assigned_employees" - ON hr.employees - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['engineer', 'resident']) - AND id IN ( - SELECT employee_id - FROM hr.project_employee_assignments pea - INNER JOIN projects.project_team_assignments pta - ON pta.project_id = pea.project_id - WHERE pta.user_id = auth_management.get_current_user_id() - AND pea.active = TRUE - ) - ); - --- Policy 3: Solo HR puede crear empleados -CREATE POLICY "hr_create_employees" - ON hr.employees - FOR INSERT - TO authenticated - WITH CHECK ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'hr']) - ); -``` - ---- - -## 馃搳 Performance y Escalabilidad - -### 1. 脥ndices Requeridos - -```sql --- 脥ndices en columna role para filtrado r谩pido -CREATE INDEX idx_user_constructoras_role - ON auth_management.user_constructoras(user_id, constructora_id, role) - WHERE status = 'active'; - --- 脥ndice compuesto para RLS policies -CREATE INDEX idx_project_team_assignments_composite - ON projects.project_team_assignments(user_id, project_id, role, active); - --- 脥ndice para asignaciones de empleados -CREATE INDEX idx_project_employee_assignments_composite - ON hr.project_employee_assignments(project_id, employee_id, active); -``` - -### 2. Caching de Funciones - -```sql --- Funciones STABLE se cachean durante la transacci贸n -CREATE OR REPLACE FUNCTION auth_management.get_current_user_role() -RETURNS auth_management.construction_role -LANGUAGE sql STABLE SECURITY DEFINER -AS $$ - SELECT role - FROM auth_management.user_constructoras - WHERE user_id = auth_management.get_current_user_id() - AND constructora_id = auth_management.get_current_constructora_id() - AND status = 'active' - LIMIT 1; -$$; -``` - -### 3. Monitoreo de Queries Lentos - -```sql --- Query para detectar policies que causan slow queries -SELECT - schemaname, - tablename, - policyname, - qual -FROM pg_policies -WHERE schemaname NOT IN ('pg_catalog', 'information_schema') -ORDER BY tablename; - --- Habilitar logging de queries lentos --- postgresql.conf: --- log_min_duration_statement = 1000 (1 segundo) -``` - ---- - -## 馃И Testing - -### Unit Tests - RolesGuard - -```typescript -// apps/backend/src/modules/auth/guards/roles.guard.spec.ts -import { Test, TestingModule } from '@nestjs/testing'; -import { Reflector } from '@nestjs/core'; -import { RolesGuard } from './roles.guard'; -import { ConstructionRole } from '../enums/construction-role.enum'; -import { ExecutionContext, ForbiddenException } from '@nestjs/common'; - -describe('RolesGuard', () => { - let guard: RolesGuard; - let reflector: Reflector; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - RolesGuard, - { - provide: Reflector, - useValue: { - getAllAndOverride: jest.fn(), - }, - }, - ], - }).compile(); - - guard = module.get(RolesGuard); - reflector = module.get(Reflector); - }); - - const createMockContext = (user: any, requiredRoles: ConstructionRole[]): ExecutionContext => { - jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(requiredRoles); - - return { - switchToHttp: () => ({ - getRequest: () => ({ user }), - }), - getHandler: jest.fn(), - getClass: jest.fn(), - } as any; - }; - - it('should allow access if user has required role', () => { - const user = { id: '123', role: ConstructionRole.DIRECTOR }; - const requiredRoles = [ConstructionRole.DIRECTOR, ConstructionRole.ENGINEER]; - - const context = createMockContext(user, requiredRoles); - const result = guard.canActivate(context); - - expect(result).toBe(true); - }); - - it('should deny access if user lacks required role', () => { - const user = { id: '123', role: ConstructionRole.RESIDENT }; - const requiredRoles = [ConstructionRole.DIRECTOR]; - - const context = createMockContext(user, requiredRoles); - - expect(() => guard.canActivate(context)).toThrow(ForbiddenException); - }); - - it('should allow access if no roles required', () => { - const user = { id: '123', role: ConstructionRole.RESIDENT }; - const requiredRoles = []; - - const context = createMockContext(user, requiredRoles); - const result = guard.canActivate(context); - - expect(result).toBe(true); - }); - - it('should throw if user is not authenticated', () => { - const user = null; - const requiredRoles = [ConstructionRole.DIRECTOR]; - - const context = createMockContext(user, requiredRoles); - - expect(() => guard.canActivate(context)).toThrow(ForbiddenException); - }); -}); -``` - -### E2E Tests - RBAC Integration - -```typescript -// apps/backend/test/rbac/rbac.e2e-spec.ts -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication, HttpStatus } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from '@/app.module'; -import { ConstructionRole } from '@modules/auth/enums/construction-role.enum'; - -describe('RBAC E2E Tests', () => { - let app: INestApplication; - - beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - afterAll(async () => { - await app.close(); - }); - - describe('Budgets Endpoints', () => { - it('director should access budgets', async () => { - const director = await createUser({ role: ConstructionRole.DIRECTOR }); - const token = await getAuthToken(director); - - const response = await request(app.getHttpServer()) - .get('/budgets') - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.OK); - - expect(response.body.data).toBeDefined(); - }); - - it('engineer should access budgets', async () => { - const engineer = await createUser({ role: ConstructionRole.ENGINEER }); - const token = await getAuthToken(engineer); - - const response = await request(app.getHttpServer()) - .get('/budgets') - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.OK); - - expect(response.body.data).toBeDefined(); - }); - - it('resident should NOT access budgets', async () => { - const resident = await createUser({ role: ConstructionRole.RESIDENT }); - const token = await getAuthToken(resident); - - const response = await request(app.getHttpServer()) - .get('/budgets') - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.FORBIDDEN); - - expect(response.body.errorCode).toBe('INSUFFICIENT_ROLE'); - }); - - it('only director can approve budgets', async () => { - const engineer = await createUser({ role: ConstructionRole.ENGINEER }); - const budget = await createBudget({ status: 'pending' }); - const token = await getAuthToken(engineer); - - const response = await request(app.getHttpServer()) - .patch(`/budgets/${budget.id}/approve`) - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.FORBIDDEN); - - expect(response.body.errorCode).toBe('INSUFFICIENT_ROLE'); - }); - }); - - describe('RLS Data Isolation by Constructora', () => { - it('user should only see data from their constructora', async () => { - // Setup: 2 constructoras con 1 proyecto cada una - const constructoraA = await createConstructora({ nombre: 'Constructora A' }); - const constructoraB = await createConstructora({ nombre: 'Constructora B' }); - - const projectA = await createProject({ constructoraId: constructoraA.id, nombre: 'Proyecto A' }); - const projectB = await createProject({ constructoraId: constructoraB.id, nombre: 'Proyecto B' }); - - // Usuario con acceso SOLO a constructora A - const user = await createUser({ role: ConstructionRole.ENGINEER }); - await assignToConstructora(user.id, constructoraA.id); - const token = await getAuthToken(user, constructoraA.id); - - // Act: Solicitar todos los proyectos - const response = await request(app.getHttpServer()) - .get('/projects') - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.OK); - - // Assert: Solo debe ver proyecto de constructora A - expect(response.body.data).toHaveLength(1); - expect(response.body.data[0].id).toBe(projectA.id); - expect(response.body.data[0].nombre).toBe('Proyecto A'); - - // Proyecto B NO debe aparecer (RLS lo bloque贸) - const projectBInResponse = response.body.data.find(p => p.id === projectB.id); - expect(projectBInResponse).toBeUndefined(); - }); - }); - - describe('Role-based Field Visibility', () => { - it('director should see budget margins', async () => { - const director = await createUser({ role: ConstructionRole.DIRECTOR }); - const budget = await createBudget({ marginPercentage: 15.5 }); - const token = await getAuthToken(director); - - const response = await request(app.getHttpServer()) - .get(`/budgets/${budget.id}`) - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.OK); - - expect(response.body.marginPercentage).toBe(15.5); - expect(response.body.profitAmount).toBeDefined(); - }); - - it('engineer should NOT see budget margins', async () => { - const engineer = await createUser({ role: ConstructionRole.ENGINEER }); - const budget = await createBudget({ marginPercentage: 15.5 }); - const token = await getAuthToken(engineer); - - const response = await request(app.getHttpServer()) - .get(`/budgets/${budget.id}`) - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.OK); - - // M谩rgenes ocultos - expect(response.body.marginPercentage).toBeUndefined(); - expect(response.body.profitAmount).toBeUndefined(); - - // Pero ve otros campos - expect(response.body.totalCost).toBeDefined(); - expect(response.body.status).toBeDefined(); - }); - }); -}); -``` - ---- - -## 馃摎 Referencias Adicionales - -### Documentos Relacionados -- 馃搫 [RF-AUTH-001: Sistema de Roles de Construcci贸n](../requerimientos/RF-AUTH-001-roles-construccion.md) -- 馃搫 [RF-AUTH-002: Estados de Cuenta](../requerimientos/RF-AUTH-002-estados-cuenta.md) -- 馃搫 [RF-AUTH-003: Multi-tenancy](../requerimientos/RF-AUTH-003-multi-tenancy.md) -- 馃搫 [ET-AUTH-003: Multi-tenancy Implementation](./ET-AUTH-003-multi-tenancy.md) *(Pendiente)* - -### Est谩ndares y Best Practices -- [NIST RBAC](https://csrc.nist.gov/projects/role-based-access-control) - Est谩ndar de RBAC -- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) -- [OWASP Access Control](https://owasp.org/www-project-top-ten/2017/A5_2017-Broken_Access_Control) - -### Recursos T茅cnicos -- [NestJS Guards](https://docs.nestjs.com/guards) -- [NestJS Custom Decorators](https://docs.nestjs.com/custom-decorators) -- [TypeORM Indexes](https://typeorm.io/indices) - ---- - -## 馃搮 Historial de Cambios - -| Versi贸n | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-11-17 | Tech Team | Creaci贸n inicial adaptada de GAMILIT con 7 roles de construcci贸n y multi-tenancy | - ---- - -**Documento:** `MAI-001-fundamentos/especificaciones/ET-AUTH-001-rbac.md` -**Ruta absoluta:** `[RUTA-LEGACY-ELIMINADA]/docs/01-fase-alcance-inicial/MAI-001-fundamentos/especificaciones/ET-AUTH-001-rbac.md` -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @frontend-team @database-team diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-002-estados-cuenta.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-002-estados-cuenta.md deleted file mode 100644 index 1eb29000a..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-002-estados-cuenta.md +++ /dev/null @@ -1,1272 +0,0 @@ -# ET-AUTH-002: Gesti贸n de Estados de Cuenta - -## 馃搵 Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | ET-AUTH-002 | -| **脡pica** | MAI-001 - Fundamentos | -| **M贸dulo** | Autenticaci贸n y Autorizaci贸n | -| **Tipo** | Especificaci贸n T茅cnica | -| **Estado** | 馃毀 Planificado | -| **Versi贸n** | 1.0 | -| **Fecha creaci贸n** | 2025-11-17 | -| **脷ltima actualizaci贸n** | 2025-11-17 | -| **Esfuerzo estimado** | 16h (vs 20h GAMILIT - 20% ahorro por reutilizaci贸n) | - -## 馃敆 Referencias - -### Requerimiento Funcional -馃搫 [RF-AUTH-002: Estados de Cuenta de Usuario](../requerimientos/RF-AUTH-002-estados-cuenta.md) - -### Origen (GAMILIT) -鈾伙笍 **Reutilizaci贸n:** 75% -- **Cat谩logo de referencia:** `core/catalog/auth/` *(Patr贸n estados de cuenta reutilizado)* -- **Componentes reutilizables:** - - Funciones de gesti贸n de estado (suspend_user, ban_user, reactivate_user) - - Triggers de auditor铆a - - Middleware de validaci贸n -- **Adaptaciones:** - - Estados por constructora (tabla `user_constructoras`) - - Funciones multi-tenant - - Tabla `banned_emails` para bloqueo permanente - -### Implementaci贸n DDL - -馃梽锔 **ENUM Principal:** -```sql --- apps/database/ddl/00-prerequisites.sql -DO $$ BEGIN - CREATE TYPE auth_management.user_status AS ENUM ( - 'active', -- Usuario activo, puede acceder - 'inactive', -- Inactivo temporalmente (desactivaci贸n voluntaria) - 'suspended', -- Suspendido por admin (reversible) - 'banned', -- Baneado permanentemente (irreversible) - 'pending' -- Registro pendiente de verificaci贸n de email - ); -EXCEPTION WHEN duplicate_object THEN null; END $$; - -COMMENT ON TYPE auth_management.user_status IS - 'Estados de cuenta de usuario: pending 鈫 active 鈫 (inactive|suspended|banned)'; -``` - -馃梽锔 **Tablas Principales:** - -```sql --- 1. Perfil global (estado general) -CREATE TABLE auth_management.profiles ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - email VARCHAR(255) UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - full_name VARCHAR(255) NOT NULL, - - -- ESTADO GLOBAL DEL PERFIL - status auth_management.user_status NOT NULL DEFAULT 'pending', - - -- Metadata de estado global - status_changed_at TIMESTAMP WITH TIME ZONE, - status_changed_by UUID REFERENCES auth_management.profiles(id), - status_reason TEXT, -- Raz贸n de baneo global - - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- 2. Estado por constructora (multi-tenancy) -CREATE TABLE auth_management.user_constructoras ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID NOT NULL REFERENCES auth_management.profiles(id) ON DELETE CASCADE, - constructora_id UUID NOT NULL REFERENCES auth_management.constructoras(id) ON DELETE CASCADE, - role construction_role NOT NULL, - - -- ESTADO EN ESTA CONSTRUCTORA - status auth_management.user_status NOT NULL DEFAULT 'active', - - -- Metadata de estado en constructora - suspended_at TIMESTAMP WITH TIME ZONE, - suspended_by UUID REFERENCES auth_management.profiles(id), - suspended_reason TEXT, - suspended_until TIMESTAMP WITH TIME ZONE, -- Fecha de revisi贸n - - is_primary BOOLEAN DEFAULT FALSE, - invited_by UUID REFERENCES auth_management.profiles(id), - invited_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - joined_at TIMESTAMP WITH TIME ZONE, - - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - - UNIQUE(user_id, constructora_id) -); - --- 3. Emails bloqueados permanentemente -CREATE TABLE auth_management.banned_emails ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - email VARCHAR(255) UNIQUE NOT NULL, - reason TEXT NOT NULL, - banned_by UUID REFERENCES auth_management.profiles(id), - banned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - -CREATE INDEX idx_banned_emails_email ON auth_management.banned_emails(email); -``` - -馃梽锔 **Funciones:** - -```sql --- Verificar si usuario puede acceder -CREATE FUNCTION auth_management.verify_user_status( - p_user_id UUID, - p_constructora_id UUID -) RETURNS BOOLEAN; - --- Suspender usuario en constructora -CREATE FUNCTION auth_management.suspend_user_in_constructora( - p_user_id UUID, - p_constructora_id UUID, - p_reason TEXT, - p_duration_days INTEGER, - p_suspended_by UUID -) RETURNS VOID; - --- Banear usuario globalmente (PERMANENTE) -CREATE FUNCTION auth_management.ban_user_globally( - p_user_id UUID, - p_reason TEXT, - p_banned_by UUID -) RETURNS VOID; - --- Reactivar usuario -CREATE FUNCTION auth_management.reactivate_user( - p_user_id UUID, - p_constructora_id UUID, - p_reactivated_by UUID -) RETURNS VOID; - --- Levantar suspensi贸n -CREATE FUNCTION auth_management.lift_suspension( - p_user_id UUID, - p_constructora_id UUID, - p_lifted_by UUID -) RETURNS VOID; -``` - -馃梽锔 **Triggers:** - -```sql --- Auditar cambios de estado en profiles -CREATE TRIGGER trg_profiles_status_change - AFTER UPDATE OF status ON auth_management.profiles - FOR EACH ROW - WHEN (OLD.status IS DISTINCT FROM NEW.status) - EXECUTE FUNCTION audit_logging.log_status_change(); - --- Auditar cambios de estado en user_constructoras -CREATE TRIGGER trg_user_constructoras_status_change - AFTER UPDATE OF status ON auth_management.user_constructoras - FOR EACH ROW - WHEN (OLD.status IS DISTINCT FROM NEW.status) - EXECUTE FUNCTION audit_logging.log_status_change(); - --- Cambiar pending 鈫 active al verificar email -CREATE TRIGGER trg_verify_email_set_active - BEFORE UPDATE ON auth_management.profiles - FOR EACH ROW - WHEN (OLD.email_verified = FALSE AND NEW.email_verified = TRUE) - EXECUTE FUNCTION auth_management.set_status_active(); -``` - -### Backend - -馃捇 **Archivos de Implementaci贸n:** -- **Service:** `apps/backend/src/modules/auth/services/user-status.service.ts` -- **DTOs:** - - `apps/backend/src/modules/auth/dto/suspend-user.dto.ts` - - `apps/backend/src/modules/auth/dto/ban-user.dto.ts` - - `apps/backend/src/modules/auth/dto/reactivate-user.dto.ts` -- **Middleware:** `apps/backend/src/modules/auth/middleware/user-status.middleware.ts` -- **Controller:** `apps/backend/src/modules/admin/user-management.controller.ts` - -### Frontend - -馃帹 **Componentes:** -- **StatusBadge:** `apps/frontend/src/components/ui/UserStatusBadge.tsx` -- **SuspendModal:** `apps/frontend/src/features/admin/SuspendUserModal.tsx` -- **BanModal:** `apps/frontend/src/features/admin/BanUserModal.tsx` -- **ReactivateModal:** `apps/frontend/src/features/auth/ReactivateAccountModal.tsx` -- **AccountStatusPage:** `apps/frontend/src/features/auth/AccountStatusPage.tsx` - -### Trazabilidad -馃搳 [TRACEABILITY.yml](../implementacion/TRACEABILITY.yml#L45-L78) - ---- - -## 馃彈锔 Arquitectura de Estados Multi-tenant - -### Diagrama de Transiciones - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CICLO DE VIDA DE CUENTA (Multi-tenant) 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - - [INVITACI脫N A CONSTRUCTORA] - 鈹 - 鈻 - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 ESTADO GLOBAL: pending 鈹 - 鈹 ESTADO CONSTRUCTORA: pending 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - verify_email() 鈹 - 鈻 - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 ESTADO GLOBAL: active 鈹 - 鈹 ESTADO CONSTRUCTORA: active 鈹 - 鈹斺攢鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹 - 鈹 鈹 鈹 - user_deactivates() 鈹 admin_suspends_in_constructora() - 鈹 鈹 鈹 - 鈻 鈹 鈻 - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 GLOBAL: active 鈹 鈹 鈹 GLOBAL: active 鈹 - 鈹 CONST-A: inactive 鈹 鈹 CONST-A: suspended 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹 鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 鈹 鈹 - user_reactivates() 鈹 admin_lifts_suspension() - 鈹 鈹 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹尖攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - admin_bans_globally() - 鈹 - 鈻 - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 ESTADO GLOBAL: banned 鈹 - 鈹 TODAS CONSTRUCTORAS: banned 鈹 - 鈹 (IRREVERSIBLE - PERMANENTE) 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ESCENARIO MULTI-CONSTRUCTORA 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - - Usuario "Juan P茅rez" trabaja en 2 constructoras: - - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 CONSTRUCTORA A 鈹 - 鈹 - Estado: active 鈹 - 鈹 - Rol: engineer 鈹 - 鈹 - Puede acceder: 鉁 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 CONSTRUCTORA B 鈹 - 鈹 - Estado: suspended 鈹 - 鈹 - Rol: resident 鈹 - 鈹 - Raz贸n: "Registr贸 asistencias falsas" 鈹 - 鈹 - Suspendido hasta: 2025-12-01 鈹 - 鈹 - Puede acceder: 鉂 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - - Al hacer login: - - Ve solo CONSTRUCTORA A en selector - - NO ve CONSTRUCTORA B (suspendido ah铆) - - Puede trabajar normalmente en CONSTRUCTORA A - -LEYENDA: -鈹鈹鈹鈹鈻 Transici贸n autom谩tica/usuario -- - 鈫 Transici贸n admin only -鈺愨晲鈺愨柡 Transici贸n irreversible global -``` - ---- - -## 馃敡 Implementaci贸n T茅cnica Completa - -### 1. Funciones de Base de Datos - -#### verify_user_status() -**Prop贸sito:** Verificar si usuario puede acceder a una constructora - -```sql --- apps/database/ddl/schemas/auth_management/functions/verify-user-status.sql -CREATE OR REPLACE FUNCTION auth_management.verify_user_status( - p_user_id UUID, - p_constructora_id UUID DEFAULT NULL -) -RETURNS BOOLEAN -LANGUAGE plpgsql STABLE SECURITY DEFINER -AS $$ -DECLARE - v_global_status auth_management.user_status; - v_constructora_status auth_management.user_status; -BEGIN - -- 1. Verificar estado global del perfil - SELECT status INTO v_global_status - FROM auth_management.profiles - WHERE id = p_user_id; - - -- Usuario no existe - IF v_global_status IS NULL THEN - RETURN FALSE; - END IF; - - -- Baneado globalmente - IF v_global_status = 'banned' THEN - RETURN FALSE; - END IF; - - -- Email no verificado - IF v_global_status = 'pending' THEN - RETURN FALSE; - END IF; - - -- Si no se especifica constructora, solo validar estado global - IF p_constructora_id IS NULL THEN - RETURN v_global_status = 'active'; - END IF; - - -- 2. Verificar estado en constructora espec铆fica - SELECT status INTO v_constructora_status - FROM auth_management.user_constructoras - WHERE user_id = p_user_id - AND constructora_id = p_constructora_id; - - -- Usuario no est谩 asociado a esa constructora - IF v_constructora_status IS NULL THEN - RETURN FALSE; - END IF; - - -- Estado debe ser active en constructora - RETURN v_constructora_status = 'active'; -END; -$$; - -COMMENT ON FUNCTION auth_management.verify_user_status(UUID, UUID) IS - 'Verifica si usuario puede acceder (global: active, constructora: active)'; -``` - ---- - -#### suspend_user_in_constructora() -**Prop贸sito:** Suspender usuario en una constructora espec铆fica (no afecta otras) - -```sql --- apps/database/ddl/schemas/auth_management/functions/suspend-user-in-constructora.sql -CREATE OR REPLACE FUNCTION auth_management.suspend_user_in_constructora( - p_user_id UUID, - p_constructora_id UUID, - p_reason TEXT, - p_duration_days INTEGER DEFAULT 14, - p_suspended_by UUID DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql SECURITY DEFINER -AS $$ -DECLARE - v_current_status auth_management.user_status; - v_user_email TEXT; - v_user_name TEXT; - v_constructora_name TEXT; -BEGIN - -- 1. Validar que usuario est茅 activo en esta constructora - SELECT uc.status, p.email, p.full_name, c.nombre - INTO v_current_status, v_user_email, v_user_name, v_constructora_name - FROM auth_management.user_constructoras uc - INNER JOIN auth_management.profiles p ON p.id = uc.user_id - INNER JOIN auth_management.constructoras c ON c.id = uc.constructora_id - WHERE uc.user_id = p_user_id - AND uc.constructora_id = p_constructora_id; - - -- Usuario no encontrado en constructora - IF v_current_status IS NULL THEN - RAISE EXCEPTION 'User % not found in constructora %', p_user_id, p_constructora_id; - END IF; - - -- Solo se puede suspender si est谩 active - IF v_current_status != 'active' THEN - RAISE EXCEPTION 'User must be active to suspend. Current status: %', v_current_status; - END IF; - - -- 2. Validar raz贸n (m铆nimo 20 caracteres) - IF p_reason IS NULL OR LENGTH(TRIM(p_reason)) < 20 THEN - RAISE EXCEPTION 'Suspension reason must be at least 20 characters'; - END IF; - - -- 3. Validar duraci贸n - IF p_duration_days <= 0 OR p_duration_days > 90 THEN - RAISE EXCEPTION 'Suspension duration must be between 1 and 90 days'; - END IF; - - -- 4. Actualizar estado - UPDATE auth_management.user_constructoras - SET - status = 'suspended', - suspended_at = NOW(), - suspended_by = p_suspended_by, - suspended_reason = p_reason, - suspended_until = NOW() + (p_duration_days || ' days')::INTERVAL, - updated_at = NOW() - WHERE user_id = p_user_id - AND constructora_id = p_constructora_id; - - -- 5. Cerrar sesiones activas en esta constructora - DELETE FROM auth_management.user_sessions - WHERE user_id = p_user_id - AND constructora_id = p_constructora_id; - - -- 6. Trigger autom谩tico auditar谩 el cambio - - RAISE NOTICE 'User % (%) suspended in % for % days. Reason: %', - v_user_name, v_user_email, v_constructora_name, p_duration_days, p_reason; -END; -$$; - -COMMENT ON FUNCTION auth_management.suspend_user_in_constructora IS - 'Suspende usuario en una constructora espec铆fica (reversible, no afecta otras constructoras)'; -``` - ---- - -#### ban_user_globally() -**Prop贸sito:** Banear usuario en TODAS las constructoras (permanente, irreversible) - -```sql --- apps/database/ddl/schemas/auth_management/functions/ban-user-globally.sql -CREATE OR REPLACE FUNCTION auth_management.ban_user_globally( - p_user_id UUID, - p_reason TEXT, - p_banned_by UUID DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql SECURITY DEFINER -AS $$ -DECLARE - v_current_status auth_management.user_status; - v_email TEXT; - v_full_name TEXT; - v_affected_constructoras INTEGER; -BEGIN - -- 1. Obtener estado y email - SELECT status, email, full_name - INTO v_current_status, v_email, v_full_name - FROM auth_management.profiles - WHERE id = p_user_id; - - -- Usuario no existe - IF v_current_status IS NULL THEN - RAISE EXCEPTION 'User % not found', p_user_id; - END IF; - - -- Ya est谩 baneado - IF v_current_status = 'banned' THEN - RAISE NOTICE 'User % is already banned', p_user_id; - RETURN; - END IF; - - -- 2. Validar raz贸n (m铆nimo 50 caracteres para acci贸n permanente) - IF p_reason IS NULL OR LENGTH(TRIM(p_reason)) < 50 THEN - RAISE EXCEPTION 'Ban reason must be at least 50 characters (PERMANENT action requires detailed justification)'; - END IF; - - -- 3. Banear en perfil global (IRREVERSIBLE) - UPDATE auth_management.profiles - SET - status = 'banned', - status_changed_at = NOW(), - status_changed_by = p_banned_by, - status_reason = p_reason, - updated_at = NOW() - WHERE id = p_user_id; - - -- 4. Banear en TODAS las constructoras - UPDATE auth_management.user_constructoras - SET - status = 'banned', - suspended_at = NOW(), - suspended_by = p_banned_by, - suspended_reason = p_reason, - updated_at = NOW() - WHERE user_id = p_user_id - AND status != 'banned'; -- Solo actualizar las que no est茅n baneadas - - GET DIAGNOSTICS v_affected_constructoras = ROW_COUNT; - - -- 5. Bloquear email permanentemente - INSERT INTO auth_management.banned_emails ( - email, reason, banned_by, banned_at - ) VALUES ( - v_email, p_reason, p_banned_by, NOW() - ) ON CONFLICT (email) DO UPDATE - SET - reason = EXCLUDED.reason, - banned_by = EXCLUDED.banned_by, - banned_at = EXCLUDED.banned_at; - - -- 6. Cerrar TODAS las sesiones del usuario - DELETE FROM auth_management.user_sessions - WHERE user_id = p_user_id; - - -- 7. Trigger auditar谩 el cambio - - RAISE WARNING 'User % (%) BANNED GLOBALLY (PERMANENT). Affected % constructoras. Reason: %', - v_full_name, v_email, v_affected_constructoras, p_reason; -END; -$$; - -COMMENT ON FUNCTION auth_management.ban_user_globally IS - 'Banea usuario en TODAS las constructoras (PERMANENTE e IRREVERSIBLE)'; -``` - ---- - -#### lift_suspension() -**Prop贸sito:** Levantar suspensi贸n en constructora - -```sql --- apps/database/ddl/schemas/auth_management/functions/lift-suspension.sql -CREATE OR REPLACE FUNCTION auth_management.lift_suspension( - p_user_id UUID, - p_constructora_id UUID, - p_lifted_by UUID DEFAULT NULL -) -RETURNS VOID -LANGUAGE plpgsql SECURITY DEFINER -AS $$ -DECLARE - v_current_status auth_management.user_status; - v_user_name TEXT; -BEGIN - -- Obtener estado actual - SELECT uc.status, p.full_name - INTO v_current_status, v_user_name - FROM auth_management.user_constructoras uc - INNER JOIN auth_management.profiles p ON p.id = uc.user_id - WHERE uc.user_id = p_user_id - AND uc.constructora_id = p_constructora_id; - - -- Solo se puede levantar suspensi贸n si est谩 suspended - IF v_current_status != 'suspended' THEN - RAISE EXCEPTION 'User must be suspended to lift. Current status: %', v_current_status; - END IF; - - -- Reactivar - UPDATE auth_management.user_constructoras - SET - status = 'active', - suspended_at = NULL, - suspended_by = NULL, - suspended_reason = NULL, - suspended_until = NULL, - updated_at = NOW() - WHERE user_id = p_user_id - AND constructora_id = p_constructora_id; - - -- Trigger auditar谩 - - RAISE NOTICE 'Suspension lifted for user % in constructora %', v_user_name, p_constructora_id; -END; -$$; - -COMMENT ON FUNCTION auth_management.lift_suspension IS - 'Levanta suspensi贸n de usuario en constructora (suspended 鈫 active)'; -``` - ---- - -#### reactivate_user() -**Prop贸sito:** Usuario reactiva su propia cuenta (desde inactive) - -```sql --- apps/database/ddl/schemas/auth_management/functions/reactivate-user.sql -CREATE OR REPLACE FUNCTION auth_management.reactivate_user( - p_user_id UUID -) -RETURNS VOID -LANGUAGE plpgsql SECURITY DEFINER -AS $$ -DECLARE - v_current_status auth_management.user_status; - v_reactivations_today INTEGER; -BEGIN - -- Obtener estado global - SELECT status INTO v_current_status - FROM auth_management.profiles - WHERE id = p_user_id; - - -- Solo se puede reactivar desde inactive - IF v_current_status != 'inactive' THEN - RAISE EXCEPTION 'Can only reactivate inactive users. Current status: %', v_current_status; - END IF; - - -- Rate limiting: m谩ximo 3 reactivaciones por d铆a - SELECT COUNT(*) - INTO v_reactivations_today - FROM audit_logging.audit_logs - WHERE resource_id = p_user_id::TEXT - AND action = 'reactivate' - AND created_at >= CURRENT_DATE; - - IF v_reactivations_today >= 3 THEN - RAISE EXCEPTION 'Maximum reactivations per day reached (3). Try again tomorrow.'; - END IF; - - -- Reactivar - UPDATE auth_management.profiles - SET - status = 'active', - status_changed_at = NOW(), - status_changed_by = p_user_id, -- Usuario se reactiva a s铆 mismo - status_reason = NULL, - updated_at = NOW() - WHERE id = p_user_id; - - -- Trigger auditar谩 - - RAISE NOTICE 'User % reactivated (inactive 鈫 active)', p_user_id; -END; -$$; - -COMMENT ON FUNCTION auth_management.reactivate_user IS - 'Usuario reactiva su propia cuenta (inactive 鈫 active, m谩x 3/d铆a)'; -``` - ---- - -### 2. Triggers de Auditor铆a - -```sql --- apps/database/ddl/schemas/audit_logging/functions/log-status-change.sql -CREATE OR REPLACE FUNCTION audit_logging.log_status_change() -RETURNS TRIGGER -LANGUAGE plpgsql SECURITY DEFINER -AS $$ -DECLARE - v_current_user_id UUID; - v_priority TEXT; -BEGIN - -- Obtener usuario que ejecuta la acci贸n - v_current_user_id := NULLIF(current_setting('app.current_user_id', true), '')::UUID; - - -- Determinar prioridad seg煤n nuevo estado - v_priority := CASE NEW.status - WHEN 'banned' THEN 'critical' - WHEN 'suspended' THEN 'high' - WHEN 'inactive' THEN 'medium' - ELSE 'low' - END; - - -- Insertar en audit_logs - INSERT INTO audit_logging.audit_logs ( - action, - resource_type, - resource_id, - performed_by, - details, - priority, - created_at - ) VALUES ( - CASE NEW.status - WHEN 'suspended' THEN 'suspend' - WHEN 'banned' THEN 'ban' - WHEN 'active' THEN 'reactivate' - WHEN 'inactive' THEN 'deactivate' - ELSE 'update_status' - END, - 'user_status', - COALESCE(NEW.user_id::TEXT, NEW.id::TEXT), -- user_constructoras vs profiles - COALESCE(v_current_user_id, NEW.id), -- Si es NULL, asumir self-action - jsonb_build_object( - 'old_status', OLD.status, - 'new_status', NEW.status, - 'reason', COALESCE(NEW.suspended_reason, NEW.status_reason), - 'table', TG_TABLE_NAME, - 'constructora_id', CASE - WHEN TG_TABLE_NAME = 'user_constructoras' THEN NEW.constructora_id::TEXT - ELSE NULL - END, - 'timestamp', NOW() - ), - v_priority, - NOW() - ); - - RETURN NEW; -END; -$$; - --- Aplicar a ambas tablas -CREATE TRIGGER trg_profiles_status_change - AFTER UPDATE OF status ON auth_management.profiles - FOR EACH ROW - WHEN (OLD.status IS DISTINCT FROM NEW.status) - EXECUTE FUNCTION audit_logging.log_status_change(); - -CREATE TRIGGER trg_user_constructoras_status_change - AFTER UPDATE OF status ON auth_management.user_constructoras - FOR EACH ROW - WHEN (OLD.status IS DISTINCT FROM NEW.status) - EXECUTE FUNCTION audit_logging.log_status_change(); -``` - ---- - -### 3. Backend - Service de Gesti贸n de Estados - -**Ubicaci贸n:** `apps/backend/src/modules/auth/services/user-status.service.ts` - -```typescript -import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; -import { Profile } from '../entities/profile.entity'; -import { UserConstructora } from '../entities/user-constructora.entity'; -import { BannedEmail } from '../entities/banned-email.entity'; -import { UserStatus } from '../enums/user-status.enum'; -import { NotificationService } from '@modules/notifications/notification.service'; -import { EmailService } from '@modules/email/email.service'; - -@Injectable() -export class UserStatusService { - constructor( - @InjectRepository(Profile) - private readonly profileRepo: Repository, - - @InjectRepository(UserConstructora) - private readonly userConstructoraRepo: Repository, - - @InjectRepository(BannedEmail) - private readonly bannedEmailRepo: Repository, - - private readonly dataSource: DataSource, - private readonly notificationService: NotificationService, - private readonly emailService: EmailService, - ) {} - - /** - * Suspender usuario en una constructora espec铆fica - */ - async suspendUserInConstructora( - userId: string, - constructoraId: string, - reason: string, - durationDays: number, - suspendedBy: string, - ): Promise { - // Validaciones - if (!reason || reason.trim().length < 20) { - throw new BadRequestException('Raz贸n debe tener m铆nimo 20 caracteres'); - } - - if (durationDays < 1 || durationDays > 90) { - throw new BadRequestException('Duraci贸n debe estar entre 1 y 90 d铆as'); - } - - // Ejecutar funci贸n de base de datos - await this.dataSource.query(` - SELECT auth_management.suspend_user_in_constructora($1, $2, $3, $4, $5) - `, [userId, constructoraId, reason, durationDays, suspendedBy]); - - // Enviar notificaci贸n - await this.notifyStatusChange( - userId, - UserStatus.ACTIVE, - UserStatus.SUSPENDED, - reason, - suspendedBy, - constructoraId, - ); - } - - /** - * Banear usuario globalmente (todas las constructoras) - */ - async banUserGlobally( - userId: string, - reason: string, - bannedBy: string, - ): Promise { - // Validaci贸n estricta para acci贸n permanente - if (!reason || reason.trim().length < 50) { - throw new BadRequestException( - 'Raz贸n debe tener m铆nimo 50 caracteres (acci贸n PERMANENTE requiere justificaci贸n detallada)' - ); - } - - // Ejecutar funci贸n de base de datos - await this.dataSource.query(` - SELECT auth_management.ban_user_globally($1, $2, $3) - `, [userId, reason, bannedBy]); - - // Enviar notificaci贸n cr铆tica - await this.notifyStatusChange( - userId, - UserStatus.ACTIVE, - UserStatus.BANNED, - reason, - bannedBy, - null, // Global ban - ); - } - - /** - * Levantar suspensi贸n en constructora - */ - async liftSuspension( - userId: string, - constructoraId: string, - liftedBy: string, - ): Promise { - await this.dataSource.query(` - SELECT auth_management.lift_suspension($1, $2, $3) - `, [userId, constructoraId, liftedBy]); - - await this.notifyStatusChange( - userId, - UserStatus.SUSPENDED, - UserStatus.ACTIVE, - 'Suspensi贸n levantada por administrador', - liftedBy, - constructoraId, - ); - } - - /** - * Usuario reactiva su propia cuenta - */ - async reactivateAccount(userId: string): Promise { - try { - await this.dataSource.query(` - SELECT auth_management.reactivate_user($1) - `, [userId]); - - await this.notifyStatusChange( - userId, - UserStatus.INACTIVE, - UserStatus.ACTIVE, - 'Cuenta reactivada por el usuario', - userId, - null, - ); - } catch (error) { - if (error.message.includes('Maximum reactivations')) { - throw new BadRequestException( - 'Has alcanzado el l铆mite de reactivaciones por hoy (3 m谩ximo). Intenta ma帽ana.' - ); - } - throw error; - } - } - - /** - * Usuario desactiva su propia cuenta - */ - async deactivateAccount(userId: string, password: string): Promise { - // Validar contrase帽a primero - const user = await this.profileRepo.findOne({ where: { id: userId } }); - const isPasswordValid = await this.verifyPassword(password, user.passwordHash); - - if (!isPasswordValid) { - throw new BadRequestException('Contrase帽a incorrecta'); - } - - // Desactivar - await this.profileRepo.update( - { id: userId }, - { - status: UserStatus.INACTIVE, - statusChangedAt: new Date(), - statusChangedBy: userId, // Self-deactivation - } - ); - - // Enviar email de confirmaci贸n - await this.emailService.send({ - to: user.email, - subject: 'Cuenta desactivada temporalmente', - template: 'account-deactivated', - data: { - userName: user.fullName, - reactivationUrl: `${process.env.FRONTEND_URL}/auth/reactivate`, - }, - }); - } - - /** - * Verificar si email est谩 baneado - */ - async isEmailBanned(email: string): Promise { - const banned = await this.bannedEmailRepo.findOne({ where: { email } }); - return !!banned; - } - - /** - * Obtener raz贸n de baneo de email - */ - async getBannedEmailReason(email: string): Promise { - const banned = await this.bannedEmailRepo.findOne({ where: { email } }); - return banned?.reason || null; - } - - /** - * Notificar cambio de estado - */ - private async notifyStatusChange( - userId: string, - oldStatus: UserStatus, - newStatus: UserStatus, - reason: string, - changedBy: string, - constructoraId: string | null, - ): Promise { - // Solo notificar si el cambio NO fue iniciado por el propio usuario - if (changedBy !== userId) { - // Notificaci贸n push - await this.notificationService.send({ - userId, - type: 'system_announcement', - priority: newStatus === UserStatus.BANNED ? 'critical' : 'high', - title: this.getNotificationTitle(newStatus, constructoraId), - body: reason, - icon: this.getStatusIcon(newStatus), - }); - - // Email - const user = await this.profileRepo.findOne({ where: { id: userId } }); - await this.emailService.send({ - to: user.email, - subject: this.getEmailSubject(newStatus), - template: 'account-status-changed', - data: { - userName: user.fullName, - newStatus, - oldStatus, - reason, - constructoraId, - supportEmail: process.env.SUPPORT_EMAIL, - }, - }); - } - } - - private getNotificationTitle(status: UserStatus, constructoraId: string | null): string { - const scope = constructoraId ? 'en esta constructora' : 'globalmente'; - - switch (status) { - case UserStatus.SUSPENDED: - return `Tu cuenta ha sido suspendida ${scope}`; - case UserStatus.BANNED: - return 'Tu cuenta ha sido baneada permanentemente'; - case UserStatus.ACTIVE: - return 'Tu cuenta ha sido reactivada'; - case UserStatus.INACTIVE: - return 'Tu cuenta ha sido desactivada'; - default: - return 'Tu estado de cuenta ha cambiado'; - } - } - - private getStatusIcon(status: UserStatus): string { - const icons = { - [UserStatus.SUSPENDED]: '鈿狅笍', - [UserStatus.BANNED]: '馃毇', - [UserStatus.ACTIVE]: '鉁', - [UserStatus.INACTIVE]: '鈩癸笍', - [UserStatus.PENDING]: '鈴', - }; - return icons[status] || '馃敂'; - } - - private getEmailSubject(status: UserStatus): string { - switch (status) { - case UserStatus.SUSPENDED: - return 'Tu cuenta ha sido suspendida temporalmente'; - case UserStatus.BANNED: - return 'Tu cuenta ha sido baneada permanentemente'; - case UserStatus.ACTIVE: - return 'Tu cuenta ha sido reactivada'; - case UserStatus.INACTIVE: - return 'Cuenta desactivada temporalmente'; - default: - return 'Cambio en tu estado de cuenta'; - } - } - - private async verifyPassword(password: string, hash: string): Promise { - const bcrypt = require('bcrypt'); - return bcrypt.compare(password, hash); - } -} -``` - ---- - -### 4. Backend - Middleware de Validaci贸n de Estado - -**Ubicaci贸n:** `apps/backend/src/modules/auth/middleware/user-status.middleware.ts` - -```typescript -import { Injectable, NestMiddleware, ForbiddenException } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { UserStatus } from '../enums/user-status.enum'; - -/** - * Middleware que valida estado de usuario en CADA request autenticado - * - * Valida: - * 1. Estado global del perfil (no banned, no pending) - * 2. Estado en constructora actual (active) - */ -@Injectable() -export class UserStatusMiddleware implements NestMiddleware { - use(req: Request, res: Response, next: NextFunction) { - const user = req.user as any; - - // Si no hay usuario, continuar (otros guards manejar谩n autenticaci贸n) - if (!user) { - return next(); - } - - // Excepciones: endpoints que permiten ciertos estados - const allowedPaths = [ - '/auth/reactivate', // inactive puede reactivar - '/auth/status', // consultar estado - '/auth/switch-constructora', // cambiar constructora - '/auth/deactivate', // active puede desactivar - '/auth/logout', // cualquiera puede cerrar sesi贸n - ]; - - if (allowedPaths.some(path => req.path.startsWith(path))) { - return next(); - } - - // Validar estado global del perfil - if (user.profileStatus === UserStatus.BANNED) { - throw new ForbiddenException({ - statusCode: 403, - message: 'Tu cuenta ha sido baneada permanentemente.', - errorCode: 'ACCOUNT_BANNED', - contactSupport: true, - }); - } - - if (user.profileStatus === UserStatus.PENDING) { - throw new ForbiddenException({ - statusCode: 403, - message: 'Debes verificar tu email antes de acceder.', - errorCode: 'EMAIL_NOT_VERIFIED', - action: 'verify_email', - }); - } - - // Validar estado en constructora actual - if (user.constructoraId && user.constructoraStatus !== UserStatus.ACTIVE) { - throw new ForbiddenException({ - statusCode: 403, - message: `Tu acceso a esta constructora est谩 ${user.constructoraStatus}.`, - errorCode: 'CONSTRUCTORA_ACCESS_DENIED', - constructoraId: user.constructoraId, - status: user.constructoraStatus, - }); - } - - next(); - } -} -``` - -**Aplicaci贸n global:** -```typescript -// apps/backend/src/main.ts -import { UserStatusMiddleware } from './modules/auth/middleware/user-status.middleware'; - -async function bootstrap() { - const app = await NestFactory.create(AppModule); - - // Aplicar middleware globalmente - app.use(UserStatusMiddleware); - - await app.listen(3000); -} -``` - ---- - -## 馃И Testing - -### Test Suite 1: Funciones de Base de Datos - -```typescript -// apps/backend/test/database/user-status-functions.spec.ts -describe('User Status Database Functions', () => { - describe('suspend_user_in_constructora()', () => { - it('should suspend active user in constructora', async () => { - const user = await createUser({ status: UserStatus.ACTIVE }); - const constructora = await createConstructora(); - await assignToConstructora(user.id, constructora.id, 'engineer'); - - await db.query(` - SELECT auth_management.suspend_user_in_constructora($1, $2, $3, $4, $5) - `, [user.id, constructora.id, 'Test suspension', 14, adminUser.id]); - - const userConstructora = await getUserConstructora(user.id, constructora.id); - expect(userConstructora.status).toBe(UserStatus.SUSPENDED); - expect(userConstructora.suspendedReason).toBe('Test suspension'); - }); - - it('should reject suspension with short reason', async () => { - await expect( - db.query(` - SELECT auth_management.suspend_user_in_constructora($1, $2, $3, $4, $5) - `, [user.id, constructora.id, 'Short', 14, adminUser.id]) - ).rejects.toThrow('at least 20 characters'); - }); - }); - - describe('ban_user_globally()', () => { - it('should ban user in all constructoras', async () => { - const user = await createUser(); - const constructoraA = await createConstructora(); - const constructoraB = await createConstructora(); - - await assignToConstructora(user.id, constructoraA.id, 'engineer'); - await assignToConstructora(user.id, constructoraB.id, 'director'); - - const reason = 'Fraude financiero comprobado con evidencia documental y testimonio de testigos'; - await db.query(` - SELECT auth_management.ban_user_globally($1, $2, $3) - `, [user.id, reason, adminUser.id]); - - // Verificar perfil global - const profile = await getProfile(user.id); - expect(profile.status).toBe(UserStatus.BANNED); - - // Verificar ambas constructoras - const ucA = await getUserConstructora(user.id, constructoraA.id); - const ucB = await getUserConstructora(user.id, constructoraB.id); - expect(ucA.status).toBe(UserStatus.BANNED); - expect(ucB.status).toBe(UserStatus.BANNED); - - // Verificar email bloqueado - const bannedEmail = await getBannedEmail(user.email); - expect(bannedEmail).toBeDefined(); - expect(bannedEmail.reason).toBe(reason); - }); - - it('should reject ban with insufficient reason', async () => { - await expect( - db.query(` - SELECT auth_management.ban_user_globally($1, $2, $3) - `, [user.id, 'Too short', adminUser.id]) - ).rejects.toThrow('at least 50 characters'); - }); - }); - - describe('reactivate_user()', () => { - it('should enforce rate limiting (max 3/day)', async () => { - const user = await createUser({ status: UserStatus.INACTIVE }); - - // Primera reactivaci贸n: OK - await db.query(`SELECT auth_management.reactivate_user($1)`, [user.id]); - await db.query(`UPDATE auth_management.profiles SET status = 'inactive' WHERE id = $1`, [user.id]); - - // Segunda: OK - await db.query(`SELECT auth_management.reactivate_user($1)`, [user.id]); - await db.query(`UPDATE auth_management.profiles SET status = 'inactive' WHERE id = $1`, [user.id]); - - // Tercera: OK - await db.query(`SELECT auth_management.reactivate_user($1)`, [user.id]); - await db.query(`UPDATE auth_management.profiles SET status = 'inactive' WHERE id = $1`, [user.id]); - - // Cuarta: DEBE FALLAR - await expect( - db.query(`SELECT auth_management.reactivate_user($1)`, [user.id]) - ).rejects.toThrow('Maximum reactivations per day reached'); - }); - }); -}); -``` - -### Test Suite 2: Service Integration - -```typescript -// apps/backend/src/modules/auth/services/user-status.service.spec.ts -describe('UserStatusService', () => { - let service: UserStatusService; - - beforeEach(async () => { - const module = await Test.createTestingModule({ - providers: [UserStatusService, ...mockProviders], - }).compile(); - - service = module.get(UserStatusService); - }); - - describe('suspendUserInConstructora', () => { - it('should send notification to suspended user', async () => { - const user = await createUser(); - const constructora = await createConstructora(); - const notificationSpy = jest.spyOn(service['notificationService'], 'send'); - - await service.suspendUserInConstructora( - user.id, - constructora.id, - 'Comportamiento inapropiado en obra', - 14, - adminUser.id, - ); - - expect(notificationSpy).toHaveBeenCalledWith( - expect.objectContaining({ - userId: user.id, - type: 'system_announcement', - priority: 'high', - }) - ); - }); - }); - - describe('isEmailBanned', () => { - it('should return true for banned email', async () => { - await createBannedEmail('banned@test.com'); - - const isBanned = await service.isEmailBanned('banned@test.com'); - expect(isBanned).toBe(true); - }); - - it('should return false for non-banned email', async () => { - const isBanned = await service.isEmailBanned('clean@test.com'); - expect(isBanned).toBe(false); - }); - }); -}); -``` - ---- - -## 馃摎 Referencias Adicionales - -### Documentos Relacionados -- 馃搫 [RF-AUTH-002: Estados de Cuenta](../requerimientos/RF-AUTH-002-estados-cuenta.md) -- 馃搫 [RF-AUTH-003: Multi-tenancy](../requerimientos/RF-AUTH-003-multi-tenancy.md) -- 馃搫 [ET-AUTH-001: RBAC](./ET-AUTH-001-rbac.md) - -### Est谩ndares y Regulaciones -- [GDPR Article 17: Right to Erasure](https://gdpr-info.eu/art-17-gdpr/) -- [Ley Federal de Protecci贸n de Datos Personales (M茅xico)](https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf) - ---- - -## 馃搮 Historial de Cambios - -| Versi贸n | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-11-17 | Tech Team | Creaci贸n inicial adaptada de GAMILIT con multi-tenancy | - ---- - -**Documento:** `MAI-001-fundamentos/especificaciones/ET-AUTH-002-estados-cuenta.md` -**Ruta absoluta:** `[RUTA-LEGACY-ELIMINADA]/docs/01-fase-alcance-inicial/MAI-001-fundamentos/especificaciones/ET-AUTH-002-estados-cuenta.md` -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @database-team diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-003-multi-tenancy.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-003-multi-tenancy.md deleted file mode 100644 index a892f6e15..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/especificaciones/ET-AUTH-003-multi-tenancy.md +++ /dev/null @@ -1,1336 +0,0 @@ -# ET-AUTH-003: Multi-tenancy Implementation - -## 馃搵 Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | ET-AUTH-003 | -| **脡pica** | MAI-001 - Fundamentos | -| **M贸dulo** | Autenticaci贸n y Multi-tenancy | -| **Tipo** | Especificaci贸n T茅cnica | -| **Estado** | 馃毀 Planificado | -| **Versi贸n** | 1.0 | -| **Fecha creaci贸n** | 2025-11-17 | -| **脷ltima actualizaci贸n** | 2025-11-17 | -| **Esfuerzo estimado** | 22h | - -## 馃敆 Referencias - -### Requerimiento Funcional -馃搫 [RF-AUTH-003: Multi-tenancy por Constructora](../requerimientos/RF-AUTH-003-multi-tenancy.md) - -### Origen (GAMILIT) -鈾伙笍 **Reutilizaci贸n:** 0% - Funcionalidad completamente nueva -- **Justificaci贸n:** GAMILIT es single-tenant, no requiere multi-tenancy -- **Inspiraci贸n:** Patterns de RLS y contexto de GAMILIT adaptados a multi-tenant -- **Beneficio:** Permite profesionales trabajando en m煤ltiples constructoras - -### Documentos Relacionados -- 馃搫 [RF-AUTH-001: Sistema de Roles](../requerimientos/RF-AUTH-001-roles-construccion.md) -- 馃搫 [RF-AUTH-002: Estados de Cuenta](../requerimientos/RF-AUTH-002-estados-cuenta.md) -- 馃搫 [ET-AUTH-001: RBAC](./ET-AUTH-001-rbac.md) -- 馃搫 [ET-AUTH-002: Estados de Cuenta](./ET-AUTH-002-estados-cuenta.md) - -### Implementaci贸n - -馃梽锔 **Database:** Ver secci贸n "Implementaci贸n de Base de Datos" -馃捇 **Backend:** Ver secci贸n "Implementaci贸n Backend" -馃帹 **Frontend:** Ver secci贸n "Implementaci贸n Frontend" - -### Trazabilidad -馃搳 [TRACEABILITY.yml](../implementacion/TRACEABILITY.yml#L15-L44) - ---- - -## 馃彈锔 Arquitectura Multi-tenant - -### Modelo de Datos - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 ARQUITECTURA MULTI-TENANT 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CONSTRUCTORAS 鈹 Tenants (empresas) -鈹 (Tenants) 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 id (PK) 鈹 -鈹 nombre 鈹 -鈹 rfc (UNIQUE) 鈹 -鈹 logo_url 鈹 -鈹 settings (JSONB) 鈹 -鈹 active 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - 鈹 1:N - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 USER_CONSTRUCTORAS 鈹 Many-to-Many con metadata -鈹 (User-Tenant Relationship) 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 id (PK) 鈹 -鈹 user_id (FK) 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 constructora_id (FK) 鈹 鈹 -鈹 role 鈹 鈹 Rol DIFERENTE por constructora -鈹 status 鈹 鈹 Estado DIFERENTE por constructora -鈹 is_primary 鈹 鈹 -鈹 invited_by 鈹 鈹 -鈹 joined_at 鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 - 鈹 鈹 - 鈹 N:1 鈹 N:1 - 鈹 鈹 - 鈻 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CONSTRUCTORA 鈹 鈹 PROFILES 鈹 Usuario global -鈹 (Tenant Entity) 鈹 鈹 (Users) 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 id (PK) 鈹 - 鈹 email (UNIQUE) 鈹 - 鈹 password_hash 鈹 - 鈹 status (global) 鈹 Estado global - 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -TODOS LOS DATOS DE NEGOCIO TIENEN constructora_id: - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 PROJECTS 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 id 鈹 -鈹 constructora_id 鈹傗攢鈹鈹鈹鈹鈻 Aislamiento por tenant -鈹 nombre 鈹 -鈹 ... 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 BUDGETS 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 id 鈹 -鈹 constructora_id 鈹傗攢鈹鈹鈹鈹鈻 Aislamiento por tenant -鈹 project_id 鈹 -鈹 ... 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 EMPLOYEES 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 id 鈹 -鈹 constructora_id 鈹傗攢鈹鈹鈹鈹鈻 Aislamiento por tenant -鈹 nombre 鈹 -鈹 ... 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -RLS POLICIES garantizan que queries autom谩ticamente filtren por: - WHERE constructora_id = get_current_constructora_id() -``` - ---- - -## 馃敡 Implementaci贸n de Base de Datos - -### 1. Esquema Principal - -```sql --- apps/database/ddl/schemas/auth_management/tables/constructoras.sql - -CREATE TABLE auth_management.constructoras ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - - -- Informaci贸n b谩sica - nombre VARCHAR(255) NOT NULL, - razon_social VARCHAR(500) NOT NULL, - rfc VARCHAR(13) NOT NULL UNIQUE, - - -- Branding - logo_url VARCHAR(1000), - color_primary VARCHAR(7), -- Hex color #FF5733 - color_secondary VARCHAR(7), - - -- Configuraci贸n - settings JSONB DEFAULT '{}'::JSONB, - /* - settings: { - timezone: "America/Mexico_City", - currency: "MXN", - locale: "es-MX", - fiscalRegime: "601", - mainAddress: { - street: "...", - city: "...", - state: "...", - zipCode: "..." - }, - billingConfig: { - cfdiUse: "G03", - paymentMethod: "PUE", - paymentForm: "03" - }, - features: { - enableBiometric: true, - enableInventory: true, - maxProjects: 50 - } - } - */ - - -- Estado - active BOOLEAN DEFAULT TRUE, - - -- Metadata - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - created_by UUID REFERENCES auth_management.profiles(id) -); - --- 脥ndices -CREATE INDEX idx_constructoras_rfc ON auth_management.constructoras(rfc); -CREATE INDEX idx_constructoras_active ON auth_management.constructoras(active) WHERE active = TRUE; - --- Comentarios -COMMENT ON TABLE auth_management.constructoras IS - 'Constructoras (tenants) - Cada empresa constructora en el sistema'; - -COMMENT ON COLUMN auth_management.constructoras.settings IS - 'Configuraci贸n JSON espec铆fica de la constructora (timezone, moneda, features, etc.)'; -``` - ---- - -### 2. Relaci贸n User-Constructora - -```sql --- apps/database/ddl/schemas/auth_management/tables/user-constructoras.sql - -CREATE TABLE auth_management.user_constructoras ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - - -- Relaciones - user_id UUID NOT NULL REFERENCES auth_management.profiles(id) ON DELETE CASCADE, - constructora_id UUID NOT NULL REFERENCES auth_management.constructoras(id) ON DELETE CASCADE, - - -- Rol y estado EN ESTA CONSTRUCTORA - role construction_role NOT NULL, - status user_status NOT NULL DEFAULT 'active', - - -- Metadata de suspensi贸n (si aplica) - suspended_at TIMESTAMP WITH TIME ZONE, - suspended_by UUID REFERENCES auth_management.profiles(id), - suspended_reason TEXT, - suspended_until TIMESTAMP WITH TIME ZONE, - - -- Constructora principal - is_primary BOOLEAN DEFAULT FALSE, - - -- Metadata de invitaci贸n - invited_by UUID REFERENCES auth_management.profiles(id), - invited_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - joined_at TIMESTAMP WITH TIME ZONE, - - -- Timestamps - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - - -- Constraints - UNIQUE(user_id, constructora_id) -); - --- Constraint: Solo una constructora principal por usuario -CREATE UNIQUE INDEX idx_user_primary_constructora - ON auth_management.user_constructoras(user_id) - WHERE is_primary = TRUE; - --- 脥ndices para performance -CREATE INDEX idx_user_constructoras_user - ON auth_management.user_constructoras(user_id); - -CREATE INDEX idx_user_constructoras_constructora - ON auth_management.user_constructoras(constructora_id); - -CREATE INDEX idx_user_constructoras_active - ON auth_management.user_constructoras(user_id, constructora_id, status) - WHERE status = 'active'; - -CREATE INDEX idx_user_constructoras_role - ON auth_management.user_constructoras(constructora_id, role); - --- Comentarios -COMMENT ON TABLE auth_management.user_constructoras IS - 'Relaci贸n many-to-many usuarios-constructoras con rol y estado por constructora'; - -COMMENT ON COLUMN auth_management.user_constructoras.is_primary IS - 'Constructora principal del usuario (pre-seleccionada al login). Solo una puede ser true.'; -``` - ---- - -### 3. Funciones de Contexto - -#### get_current_constructora_id() -**Prop贸sito:** Obtener constructora activa en contexto actual - -```sql --- apps/database/ddl/schemas/auth_management/functions/get-current-constructora-id.sql - -CREATE OR REPLACE FUNCTION auth_management.get_current_constructora_id() -RETURNS UUID -LANGUAGE plpgsql STABLE SECURITY DEFINER -AS $$ -BEGIN - -- Obtener de variable de sesi贸n configurada por backend - RETURN NULLIF(current_setting('app.current_constructora_id', true), '')::UUID; -EXCEPTION - WHEN OTHERS THEN - RETURN NULL; -END; -$$; - -COMMENT ON FUNCTION auth_management.get_current_constructora_id IS - 'Retorna constructora activa del contexto actual (configurada por SetRlsContextInterceptor)'; -``` - ---- - -#### user_has_access_to_constructora() -**Prop贸sito:** Verificar si usuario tiene acceso activo a constructora - -```sql --- apps/database/ddl/schemas/auth_management/functions/user-has-access-to-constructora.sql - -CREATE OR REPLACE FUNCTION auth_management.user_has_access_to_constructora( - p_user_id UUID, - p_constructora_id UUID -) -RETURNS BOOLEAN -LANGUAGE plpgsql STABLE SECURITY DEFINER -AS $$ -BEGIN - RETURN EXISTS ( - SELECT 1 - FROM auth_management.user_constructoras uc - INNER JOIN auth_management.constructoras c ON c.id = uc.constructora_id - WHERE uc.user_id = p_user_id - AND uc.constructora_id = p_constructora_id - AND uc.status = 'active' - AND c.active = TRUE - ); -END; -$$; - -COMMENT ON FUNCTION auth_management.user_has_access_to_constructora IS - 'Verifica si usuario tiene acceso activo a una constructora espec铆fica'; -``` - ---- - -#### get_user_active_constructoras() -**Prop贸sito:** Obtener todas las constructoras activas del usuario - -```sql --- apps/database/ddl/schemas/auth_management/functions/get-user-active-constructoras.sql - -CREATE OR REPLACE FUNCTION auth_management.get_user_active_constructoras( - p_user_id UUID -) -RETURNS TABLE ( - constructora_id UUID, - nombre VARCHAR(255), - logo_url VARCHAR(1000), - role construction_role, - is_primary BOOLEAN -) -LANGUAGE plpgsql STABLE SECURITY DEFINER -AS $$ -BEGIN - RETURN QUERY - SELECT - c.id AS constructora_id, - c.nombre, - c.logo_url, - uc.role, - uc.is_primary - FROM auth_management.user_constructoras uc - INNER JOIN auth_management.constructoras c ON c.id = uc.constructora_id - WHERE uc.user_id = p_user_id - AND uc.status = 'active' - AND c.active = TRUE - ORDER BY uc.is_primary DESC, c.nombre ASC; -END; -$$; - -COMMENT ON FUNCTION auth_management.get_user_active_constructoras IS - 'Retorna todas las constructoras activas del usuario (para selector de constructora)'; -``` - ---- - -### 4. RLS Policies Multi-tenant - -**Patr贸n est谩ndar para TODAS las tablas de negocio:** - -```sql --- apps/database/ddl/schemas/projects/tables/projects.sql - --- Habilitar RLS -ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY; - --- Policy base: Constructora Isolation -CREATE POLICY "constructora_isolation_policy" - ON projects.projects - FOR ALL - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - ) - WITH CHECK ( - constructora_id = auth_management.get_current_constructora_id() - ); - --- Policy adicional: Role-based (si se necesita) -CREATE POLICY "directors_view_all" - ON projects.projects - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.get_current_user_role() = 'director' - ); -``` - -**Ejemplo m谩s complejo con proyectos:** - -```sql --- apps/database/ddl/schemas/projects/tables/projects.sql - -ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY; - --- SELECT: Depende del rol -CREATE POLICY "projects_select_by_role" - ON projects.projects - FOR SELECT - TO authenticated - USING ( - -- Siempre filtrar por constructora - constructora_id = auth_management.get_current_constructora_id() - AND ( - -- Director y Engineer ven todos los proyectos - auth_management.user_has_any_role(ARRAY['director', 'engineer', 'finance']) - OR - -- Residente solo ve proyectos asignados - ( - auth_management.get_current_user_role() = 'resident' - AND id IN ( - SELECT project_id - FROM projects.project_team_assignments - WHERE user_id = auth_management.get_current_user_id() - AND role = 'resident' - AND active = TRUE - ) - ) - ) - ); - --- INSERT: Solo director y engineer -CREATE POLICY "projects_insert" - ON projects.projects - FOR INSERT - TO authenticated - WITH CHECK ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'engineer']) - ); - --- UPDATE: Solo director y engineer -CREATE POLICY "projects_update" - ON projects.projects - FOR UPDATE - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_any_role(ARRAY['director', 'engineer']) - ); - --- DELETE: Solo director -CREATE POLICY "projects_delete" - ON projects.projects - FOR DELETE - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.get_current_user_role() = 'director' - ); -``` - ---- - -## 馃捇 Implementaci贸n Backend - -### 1. Entities de TypeORM - -#### Constructora Entity - -```typescript -// apps/backend/src/modules/auth/entities/constructora.entity.ts -import { Entity, Column, PrimaryGeneratedColumn, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm'; - -export interface ConstructoraSettings { - timezone?: string; - currency?: string; - locale?: string; - fiscalRegime?: string; - mainAddress?: { - street: string; - city: string; - state: string; - zipCode: string; - }; - billingConfig?: { - cfdiUse: string; - paymentMethod: string; - paymentForm: string; - }; - features?: { - enableBiometric?: boolean; - enableInventory?: boolean; - maxProjects?: number; - }; -} - -@Entity('constructoras', { schema: 'auth_management' }) -export class Constructora { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 255 }) - nombre: string; - - @Column({ type: 'varchar', length: 500, name: 'razon_social' }) - razonSocial: string; - - @Column({ type: 'varchar', length: 13, unique: true }) - rfc: string; - - @Column({ type: 'varchar', length: 1000, nullable: true, name: 'logo_url' }) - logoUrl: string; - - @Column({ type: 'varchar', length: 7, nullable: true, name: 'color_primary' }) - colorPrimary: string; - - @Column({ type: 'varchar', length: 7, nullable: true, name: 'color_secondary' }) - colorSecondary: string; - - @Column({ type: 'jsonb', default: {} }) - settings: ConstructoraSettings; - - @Column({ type: 'boolean', default: true }) - active: boolean; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; - - @Column({ type: 'uuid', nullable: true, name: 'created_by' }) - createdBy: string; - - @OneToMany(() => UserConstructora, (uc) => uc.constructora) - users: UserConstructora[]; -} -``` - ---- - -#### UserConstructora Entity - -```typescript -// apps/backend/src/modules/auth/entities/user-constructora.entity.ts -import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; -import { Profile } from './profile.entity'; -import { Constructora } from './constructora.entity'; -import { ConstructionRole } from '../enums/construction-role.enum'; -import { UserStatus } from '../enums/user-status.enum'; - -@Entity('user_constructoras', { schema: 'auth_management' }) -export class UserConstructora { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'uuid', name: 'user_id' }) - userId: string; - - @Column({ type: 'uuid', name: 'constructora_id' }) - constructoraId: string; - - @Column({ type: 'enum', enum: ConstructionRole }) - role: ConstructionRole; - - @Column({ type: 'enum', enum: UserStatus, default: UserStatus.ACTIVE }) - status: UserStatus; - - @Column({ type: 'timestamp with time zone', nullable: true, name: 'suspended_at' }) - suspendedAt: Date; - - @Column({ type: 'uuid', nullable: true, name: 'suspended_by' }) - suspendedBy: string; - - @Column({ type: 'text', nullable: true, name: 'suspended_reason' }) - suspendedReason: string; - - @Column({ type: 'timestamp with time zone', nullable: true, name: 'suspended_until' }) - suspendedUntil: Date; - - @Column({ type: 'boolean', default: false, name: 'is_primary' }) - isPrimary: boolean; - - @Column({ type: 'uuid', nullable: true, name: 'invited_by' }) - invitedBy: string; - - @Column({ type: 'timestamp with time zone', default: () => 'NOW()', name: 'invited_at' }) - invitedAt: Date; - - @Column({ type: 'timestamp with time zone', nullable: true, name: 'joined_at' }) - joinedAt: Date; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; - - @ManyToOne(() => Profile, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'user_id' }) - user: Profile; - - @ManyToOne(() => Constructora, (constructora) => constructora.users, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'constructora_id' }) - constructora: Constructora; -} -``` - ---- - -### 2. DTO's para Multi-tenancy - -```typescript -// apps/backend/src/modules/auth/dto/create-constructora.dto.ts -import { IsString, IsNotEmpty, Length, IsOptional, IsObject, Matches } from 'class-validator'; - -export class CreateConstructoraDto { - @IsString() - @IsNotEmpty() - @Length(3, 255) - nombre: string; - - @IsString() - @IsNotEmpty() - @Length(5, 500) - razonSocial: string; - - @IsString() - @IsNotEmpty() - @Length(13, 13) - @Matches(/^[A-Z&脩]{3,4}\d{6}[A-V1-9][A-Z0-9][0-9A]$/, { - message: 'RFC inv谩lido para persona moral mexicana', - }) - rfc: string; - - @IsString() - @IsOptional() - logoUrl?: string; - - @IsString() - @IsOptional() - @Matches(/^#[0-9A-Fa-f]{6}$/) - colorPrimary?: string; - - @IsString() - @IsOptional() - @Matches(/^#[0-9A-Fa-f]{6}$/) - colorSecondary?: string; - - @IsObject() - @IsOptional() - settings?: ConstructoraSettings; -} - -// apps/backend/src/modules/auth/dto/switch-constructora.dto.ts -import { IsUUID } from 'class-validator'; - -export class SwitchConstructoraDto { - @IsUUID() - constructoraId: string; -} - -// apps/backend/src/modules/auth/dto/invite-to-constructora.dto.ts -import { IsEmail, IsUUID, IsEnum } from 'class-validator'; -import { ConstructionRole } from '../enums/construction-role.enum'; - -export class InviteToConstructoraDto { - @IsEmail() - email: string; - - @IsUUID() - constructoraId: string; - - @IsEnum(ConstructionRole) - role: ConstructionRole; -} -``` - ---- - -### 3. Service: Constructora Service - -```typescript -// apps/backend/src/modules/auth/services/constructora.service.ts -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; -import { Constructora } from '../entities/constructora.entity'; -import { UserConstructora } from '../entities/user-constructora.entity'; -import { CreateConstructoraDto } from '../dto/create-constructora.dto'; -import { InviteToConstructoraDto } from '../dto/invite-to-constructora.dto'; -import { ConstructionRole } from '../enums/construction-role.enum'; -import { UserStatus } from '../enums/user-status.enum'; - -@Injectable() -export class ConstructoraService { - constructor( - @InjectRepository(Constructora) - private readonly constructoraRepo: Repository, - - @InjectRepository(UserConstructora) - private readonly userConstructoraRepo: Repository, - - private readonly dataSource: DataSource, - ) {} - - /** - * Crear nueva constructora - */ - async create(dto: CreateConstructoraDto, createdBy: string): Promise { - // Validar RFC 煤nico - const existing = await this.constructoraRepo.findOne({ where: { rfc: dto.rfc } }); - if (existing) { - throw new BadRequestException(`RFC ${dto.rfc} ya est谩 registrado`); - } - - // Crear constructora - const constructora = this.constructoraRepo.create({ - ...dto, - createdBy, - }); - - await this.constructoraRepo.save(constructora); - - // Asociar creador como director - await this.userConstructoraRepo.save({ - userId: createdBy, - constructoraId: constructora.id, - role: ConstructionRole.DIRECTOR, - status: UserStatus.ACTIVE, - isPrimary: false, // Usuario puede decidir despu茅s - joinedAt: new Date(), - }); - - return constructora; - } - - /** - * Obtener constructoras activas del usuario - */ - async getUserActiveConstructoras(userId: string): Promise { - const result = await this.dataSource.query(` - SELECT * FROM auth_management.get_user_active_constructoras($1) - `, [userId]); - - return result; - } - - /** - * Cambiar constructora primaria - */ - async setPrimaryConstructora(userId: string, constructoraId: string): Promise { - // Validar que usuario tenga acceso - const access = await this.userConstructoraRepo.findOne({ - where: { - userId, - constructoraId, - status: UserStatus.ACTIVE, - }, - }); - - if (!access) { - throw new NotFoundException('No tienes acceso a esta constructora'); - } - - // Transacci贸n: quitar primary de todas, asignar a nueva - await this.dataSource.transaction(async (manager) => { - // Quitar is_primary de todas las constructoras del usuario - await manager.update( - UserConstructora, - { userId }, - { isPrimary: false } - ); - - // Asignar is_primary a la nueva - await manager.update( - UserConstructora, - { userId, constructoraId }, - { isPrimary: true } - ); - }); - } - - /** - * Invitar usuario a constructora - */ - async inviteToConstructora(dto: InviteToConstructoraDto, invitedBy: string): Promise { - // Validar que invitador sea director en esa constructora - const inviterAccess = await this.userConstructoraRepo.findOne({ - where: { - userId: invitedBy, - constructoraId: dto.constructoraId, - role: ConstructionRole.DIRECTOR, - status: UserStatus.ACTIVE, - }, - }); - - if (!inviterAccess) { - throw new BadRequestException('Solo directores pueden invitar usuarios'); - } - - // Verificar si usuario ya existe - const existingUser = await this.profileRepo.findOne({ where: { email: dto.email } }); - - if (existingUser) { - // Usuario existente: asociar directamente a constructora - const existingAssociation = await this.userConstructoraRepo.findOne({ - where: { - userId: existingUser.id, - constructoraId: dto.constructoraId, - }, - }); - - if (existingAssociation) { - throw new BadRequestException('Usuario ya est谩 asociado a esta constructora'); - } - - // Crear asociaci贸n - await this.userConstructoraRepo.save({ - userId: existingUser.id, - constructoraId: dto.constructoraId, - role: dto.role, - status: UserStatus.ACTIVE, - invitedBy, - joinedAt: new Date(), - }); - - // Enviar notificaci贸n - await this.sendExistingUserInvitation(existingUser, dto.constructoraId, dto.role); - - return 'Usuario existente asociado a constructora'; - } else { - // Usuario nuevo: crear invitaci贸n - const invitation = await this.createInvitation(dto, invitedBy); - - // Enviar email de invitaci贸n - await this.sendNewUserInvitation(dto.email, invitation.token, dto.constructoraId, dto.role); - - return invitation.token; - } - } - - /** - * Verificar acceso a constructora - */ - async hasAccessToConstructora(userId: string, constructoraId: string): Promise { - const result = await this.dataSource.query(` - SELECT auth_management.user_has_access_to_constructora($1, $2) AS has_access - `, [userId, constructoraId]); - - return result[0]?.has_access || false; - } -} -``` - ---- - -### 4. Interceptor: SetRlsContextInterceptor - -**Ya documentado en ET-AUTH-001**, aqu铆 un recordatorio: - -```typescript -// apps/backend/src/common/interceptors/set-rls-context.interceptor.ts -@Injectable() -export class SetRlsContextInterceptor implements NestInterceptor { - constructor(@InjectDataSource() private readonly dataSource: DataSource) {} - - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest(); - const user = request.user; - - if (!user) { - return next.handle(); - } - - // Configurar variables de sesi贸n de PostgreSQL - return from( - this.dataSource.query(` - SELECT - set_config('app.current_user_id', $1, true), - set_config('app.current_constructora_id', $2, true), - set_config('app.current_user_role', $3, true) - `, [ - user.id || '', - user.constructoraId || '', - user.role || '', - ]) - ).pipe( - switchMap(() => next.handle()) - ); - } -} -``` - ---- - -## 馃帹 Implementaci贸n Frontend - -### 1. Types - -```typescript -// apps/frontend/src/types/constructora.types.ts -export interface Constructora { - id: string; - nombre: string; - razonSocial: string; - rfc: string; - logoUrl?: string; - colorPrimary?: string; - colorSecondary?: string; - settings: ConstructoraSettings; - active: boolean; -} - -export interface ConstructoraSettings { - timezone?: string; - currency?: string; - locale?: string; - fiscalRegime?: string; - mainAddress?: Address; - billingConfig?: BillingConfig; - features?: Features; -} - -export interface UserConstructoraAccess { - constructoraId: string; - nombre: string; - logoUrl?: string; - role: ConstructionRole; - isPrimary: boolean; -} -``` - ---- - -### 2. Zustand Store - -```typescript -// apps/frontend/src/stores/constructora-store.ts -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import { Constructora, UserConstructoraAccess } from '@/types/constructora.types'; -import api from '@/lib/api'; - -interface ConstructoraStore { - // State - currentConstructora: Constructora | null; - availableConstructoras: UserConstructoraAccess[]; - - // Actions - setCurrentConstructora: (constructora: Constructora) => void; - setAvailableConstructoras: (constructoras: UserConstructoraAccess[]) => void; - switchConstructora: (constructoraId: string) => Promise; - fetchAvailableConstructoras: () => Promise; - setPrimaryConstructora: (constructoraId: string) => Promise; -} - -export const useConstructoraStore = create()( - persist( - (set, get) => ({ - currentConstructora: null, - availableConstructoras: [], - - setCurrentConstructora: (constructora) => { - set({ currentConstructora: constructora }); - }, - - setAvailableConstructoras: (constructoras) => { - set({ availableConstructoras: constructoras }); - }, - - switchConstructora: async (constructoraId: string) => { - try { - // Request switch to backend - const response = await api.post('/auth/switch-constructora', { - constructoraId, - }); - - // Update token - const newToken = response.data.accessToken; - localStorage.setItem('accessToken', newToken); - - // Find constructora in available list - const constructora = get().availableConstructoras.find( - (c) => c.constructoraId === constructoraId - ); - - if (constructora) { - // Fetch full constructora details - const detailsResponse = await api.get(`/constructoras/${constructoraId}`); - set({ currentConstructora: detailsResponse.data }); - } - - // Reload page to apply new context - window.location.reload(); - } catch (error) { - console.error('Error switching constructora:', error); - throw error; - } - }, - - fetchAvailableConstructoras: async () => { - const response = await api.get('/auth/my-constructoras'); - set({ availableConstructoras: response.data }); - }, - - setPrimaryConstructora: async (constructoraId: string) => { - await api.patch('/user/set-primary-constructora', { - constructoraId, - }); - - // Update local state - const updated = get().availableConstructoras.map((c) => ({ - ...c, - isPrimary: c.constructoraId === constructoraId, - })); - set({ availableConstructoras: updated }); - }, - }), - { - name: 'constructora-storage', - partialize: (state) => ({ - currentConstructora: state.currentConstructora, - availableConstructoras: state.availableConstructoras, - }), - } - ) -); -``` - ---- - -### 3. Componente: Constructora Selector - -```tsx -// apps/frontend/src/components/auth/ConstructoraSelector.tsx -import React from 'react'; -import { useConstructoraStore } from '@/stores/constructora-store'; -import { UserConstructoraAccess } from '@/types/constructora.types'; -import { Building2, Star, ChevronRight } from 'lucide-react'; - -interface ConstructoraSelectorProps { - onSelect?: (constructoraId: string) => void; -} - -export const ConstructoraSelector: React.FC = ({ - onSelect, -}) => { - const { availableConstructoras, switchConstructora } = useConstructoraStore(); - - const handleSelect = async (constructoraId: string) => { - try { - await switchConstructora(constructoraId); - onSelect?.(constructoraId); - } catch (error) { - console.error('Error selecting constructora:', error); - // Show error toast - } - }; - - if (availableConstructoras.length === 0) { - return ( -
- -

No tienes acceso a ninguna constructora

-

- Contacta a un administrador para que te invite -

-
- ); - } - - return ( -
-

- Selecciona una constructora -

- -
- {availableConstructoras.map((constructora) => ( - - ))} -
-
- ); -}; -``` - ---- - -### 4. Componente: Constructora Switcher (Header) - -```tsx -// apps/frontend/src/components/layout/ConstructoraSwitcher.tsx -import React, { useState } from 'react'; -import { useConstructoraStore } from '@/stores/constructora-store'; -import { Building2, ChevronDown, Star, Check } from 'lucide-react'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator, -} from '@/components/ui/dropdown-menu'; - -export const ConstructoraSwitcher: React.FC = () => { - const { currentConstructora, availableConstructoras, switchConstructora, setPrimaryConstructora } = - useConstructoraStore(); - - const [isSwitching, setIsSwitching] = useState(false); - - const handleSwitch = async (constructoraId: string) => { - if (currentConstructora?.id === constructoraId) return; - - setIsSwitching(true); - try { - await switchConstructora(constructoraId); - } finally { - setIsSwitching(false); - } - }; - - const handleSetPrimary = async (e: React.MouseEvent, constructoraId: string) => { - e.stopPropagation(); - await setPrimaryConstructora(constructoraId); - }; - - if (!currentConstructora) return null; - - return ( - - - - - - -
- TUS CONSTRUCTORAS -
- - - {availableConstructoras.map((constructora) => { - const isCurrent = constructora.constructoraId === currentConstructora.id; - - return ( - handleSwitch(constructora.constructoraId)} - className="flex items-center gap-3 px-3 py-2" - > - {/* Logo */} - {constructora.logoUrl ? ( - {constructora.nombre} - ) : ( -
- -
- )} - - {/* Info */} -
-
-
{constructora.nombre}
- {isCurrent && } -
-
- {constructora.role.replace('_', ' ')} -
-
- - {/* Primary star */} - -
- ); - })} -
-
- ); -}; -``` - ---- - -## 馃И Testing - -### Database Functions - -```typescript -describe('Multi-tenancy Database Functions', () => { - it('get_user_active_constructoras should return only active constructoras', async () => { - const user = await createUser(); - const constructoraA = await createConstructora({ nombre: 'A', active: true }); - const constructoraB = await createConstructora({ nombre: 'B', active: false }); - - await assignToConstructora(user.id, constructoraA.id, 'engineer', 'active'); - await assignToConstructora(user.id, constructoraB.id, 'director', 'active'); - - const result = await db.query(` - SELECT * FROM auth_management.get_user_active_constructoras($1) - `, [user.id]); - - // Solo debe retornar constructora A (activa) - expect(result.rows).toHaveLength(1); - expect(result.rows[0].constructora_id).toBe(constructoraA.id); - }); - - it('user_has_access_to_constructora should validate status', async () => { - const user = await createUser(); - const constructora = await createConstructora(); - - // Sin acceso - let hasAccess = await db.query(` - SELECT auth_management.user_has_access_to_constructora($1, $2) AS result - `, [user.id, constructora.id]); - expect(hasAccess.rows[0].result).toBe(false); - - // Con acceso activo - await assignToConstructora(user.id, constructora.id, 'engineer', 'active'); - hasAccess = await db.query(` - SELECT auth_management.user_has_access_to_constructora($1, $2) AS result - `, [user.id, constructora.id]); - expect(hasAccess.rows[0].result).toBe(true); - - // Suspendido - await suspendInConstructora(user.id, constructora.id); - hasAccess = await db.query(` - SELECT auth_management.user_has_access_to_constructora($1, $2) AS result - `, [user.id, constructora.id]); - expect(hasAccess.rows[0].result).toBe(false); - }); -}); -``` - ---- - -## 馃摎 Referencias Adicionales - -### Documentos Relacionados -- 馃搫 [RF-AUTH-003: Multi-tenancy](../requerimientos/RF-AUTH-003-multi-tenancy.md) -- 馃搫 [ET-AUTH-001: RBAC](./ET-AUTH-001-rbac.md) -- 馃搫 [ET-AUTH-002: Estados de Cuenta](./ET-AUTH-002-estados-cuenta.md) - -### Recursos T茅cnicos -- [PostgreSQL Row Level Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) -- [Multi-tenancy Patterns (Microsoft)](https://docs.microsoft.com/en-us/azure/architecture/patterns/category/data-management) -- [Zustand State Management](https://zustand-demo.pmnd.rs/) - ---- - -## 馃搮 Historial de Cambios - -| Versi贸n | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-11-17 | Tech Team | Creaci贸n inicial - Implementaci贸n completa multi-tenancy | - ---- - -**Documento:** `MAI-001-fundamentos/especificaciones/ET-AUTH-003-multi-tenancy.md` -**Ruta absoluta:** `[RUTA-LEGACY-ELIMINADA]/docs/01-fase-alcance-inicial/MAI-001-fundamentos/especificaciones/ET-AUTH-003-multi-tenancy.md` -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @frontend-team @database-team diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-001-autenticacion-basica-jwt.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-001-autenticacion-basica-jwt.md deleted file mode 100644 index ce6b832d8..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-001-autenticacion-basica-jwt.md +++ /dev/null @@ -1,567 +0,0 @@ -# US-FUND-001: Autenticaci贸n b谩sica con JWT para Construcci贸n - -**脡pica:** MAI-001 - Fundamentos -**Sprint:** Sprint 1 (Semanas 2-3) -**Story Points:** 8 SP -**Presupuesto:** $2,900 MXN -**Prioridad:** P0 - Cr铆tica (Alcance Inicial) -**Estado:** 馃毀 Planificado -**Reutilizaci贸n GAMILIT:** 95% (adaptaci贸n m铆nima) - ---- - -## 馃摑 Descripci贸n - -Como **cualquier usuario del sistema (director, ingeniero, residente, etc.)**, quiero poder **registrarme, iniciar sesi贸n con selector de constructora, y recuperar mi contrase帽a** para **acceder de forma segura a la plataforma y mis obras asignadas**. - -**Contexto del Alcance Inicial:** -En el MVP se implement贸 un sistema de autenticaci贸n basado en GAMILIT con JWT que soporta 7 roles fijos espec铆ficos de construcci贸n. El sistema incluye: -- Multi-tenancy por constructora -- Selector de constructora al login -- Invitaci贸n de usuarios por constructora -- Rol por defecto: `resident` - -**Diferencias vs GAMILIT:** -- GAMILIT: Auto-registro abierto 鈫 Inmobiliario: Registro por invitaci贸n -- GAMILIT: 1 organizaci贸n 鈫 Inmobiliario: M煤ltiples constructoras -- GAMILIT: 3 roles 鈫 Inmobiliario: 7 roles - ---- - -## 鉁 Criterios de Aceptaci贸n - -- [ ] **CA-01:** El sistema permite registrar nuevos usuarios por invitaci贸n (email 煤nico) -- [ ] **CA-02:** Al registrarse por invitaci贸n, el usuario recibe rol especificado en la invitaci贸n (default: `resident`) -- [ ] **CA-03:** Las contrase帽as se almacenan hasheadas con bcrypt (min. 10 rounds) -- [ ] **CA-04:** El login incluye selector de constructora (si usuario pertenece a m煤ltiples) -- [ ] **CA-05:** El login genera un JWT token v谩lido por 24 horas -- [ ] **CA-06:** El JWT incluye: userId, role, constructoraId (activa), email -- [ ] **CA-07:** Existe endpoint de recuperaci贸n de contrase帽a que env铆a email con token temporal -- [ ] **CA-08:** El token de recuperaci贸n expira en 1 hora -- [ ] **CA-09:** El sistema permite cerrar sesi贸n (invalidaci贸n de token en frontend) -- [ ] **CA-10:** Las contrase帽as deben tener m铆nimo 8 caracteres (al menos 1 n煤mero) -- [ ] **CA-11:** Se retorna mensaje de error apropiado para credenciales inv谩lidas -- [ ] **CA-12:** Usuario puede cambiar de constructora activa sin volver a loggearse - ---- - -## 馃幆 Especificaciones T茅cnicas - -### Backend (Node.js + Express + TypeScript) - -**Endpoints:** -``` -POST /api/auth/register-by-invitation -- Body: { invitationToken, password, firstName, lastName } -- Response: { user, accessToken, constructora } -- Note: Valida token de invitaci贸n antes de crear usuario - -POST /api/auth/login -- Body: { email, password, constructoraId? } -- Response: { user, accessToken, constructoras[] } -- Note: Si usuario tiene m煤ltiples constructoras, retorna lista para selector - -POST /api/auth/switch-constructora -- Body: { constructoraId } -- Headers: Authorization: Bearer {token} -- Response: { accessToken } (nuevo token con constructora actualizada) - -POST /api/auth/forgot-password -- Body: { email } -- Response: { message: "Email sent" } - -POST /api/auth/reset-password -- Body: { token, newPassword } -- Response: { message: "Password updated" } - -GET /api/auth/me -- Headers: Authorization: Bearer {token} -- Response: { user, constructora, role } -``` - -**Servicios:** -- **AuthService:** L贸gica de autenticaci贸n (register, login, validateUser) -- **JwtService:** Generaci贸n y validaci贸n de tokens JWT -- **MailService:** Env铆o de emails de recuperaci贸n e invitaci贸n -- **ConstructoraService:** Gesti贸n de relaci贸n usuario-constructora - -**Entidades:** -```typescript -// apps/backend/src/modules/auth/entities/user.entity.ts -@Entity('users') -class User { - id: string (UUID) - email: string (unique) - password: string (hashed) - first_name: string - last_name: string - is_active: boolean - email_verified: boolean - createdAt: Date - updatedAt: Date -} - -// apps/backend/src/modules/auth/entities/user-constructora.entity.ts -@Entity('user_constructoras') -class UserConstructora { - id: string (UUID) - user_id: string (FK to users) - constructora_id: string (FK to constructoras) - role: ConstructionRole ('director' | 'engineer' | 'resident' | 'purchases' | 'finance' | 'hr' | 'post_sales') - is_primary: boolean (constructora por defecto) - active: boolean - created_at: Date -} - -// apps/backend/src/modules/auth/entities/invitation.entity.ts -@Entity('invitations') -class Invitation { - id: string (UUID) - constructora_id: string (FK) - email: string - role: ConstructionRole - token: string (unique) - expires_at: Date - used: boolean - invited_by: string (FK to users) - created_at: Date -} - -// apps/backend/src/modules/auth/entities/password-reset-token.entity.ts -@Entity('password_reset_tokens') -class PasswordResetToken { - id: string (UUID) - userId: string (FK to users) - token: string - expiresAt: Date - used: boolean -} -``` - -**Guards:** -- **JwtAuthGuard:** Protege rutas que requieren autenticaci贸n -- **RolesGuard:** Valida roles espec铆ficos (7 roles de construcci贸n) -- **ConstructoraGuard:** Valida que usuario tenga acceso a la constructora - -**JWT Payload:** -```typescript -interface JwtPayload { - sub: string; // userId - email: string; - role: ConstructionRole; - constructoraId: string; - iat: number; - exp: number; -} -``` - ---- - -### Frontend (React + Vite + TypeScript) - -**Componentes:** -- `LoginForm.tsx`: Formulario de inicio de sesi贸n con selector de constructora -- `ConstructoraSelector.tsx`: Selector de constructora (si usuario tiene m煤ltiples) -- `RegisterByInvitationForm.tsx`: Formulario de registro por invitaci贸n -- `ForgotPasswordForm.tsx`: Solicitud de recuperaci贸n -- `ResetPasswordForm.tsx`: Establecer nueva contrase帽a -- `SwitchConstructoraModal.tsx`: Modal para cambiar constructora activa - -**Estado (Zustand):** -```typescript -// apps/frontend/src/stores/authStore.ts -interface AuthStore { - user: User | null; - token: string | null; - constructora: Constructora | null; - constructoras: Constructora[]; - isAuthenticated: boolean; - - // Actions - login: (email, password, constructoraId?) => Promise; - registerByInvitation: (token, data) => Promise; - logout: () => void; - switchConstructora: (constructoraId) => Promise; - forgotPassword: (email) => Promise; - resetPassword: (token, newPassword) => Promise; - fetchMe: () => Promise; -} -``` - -**Rutas:** -```typescript -// apps/frontend/src/routes/auth.routes.tsx -const authRoutes = [ - { path: '/login', element: }, - { path: '/register/:invitationToken', element: }, - { path: '/forgot-password', element: }, - { path: '/reset-password/:token', element: }, -]; -``` - -**Almacenamiento:** -- Token JWT guardado en `localStorage` (key: 'auth_token') -- Constructora activa en `localStorage` (key: 'active_constructora') -- Auto-login si existe token v谩lido al cargar la app - ---- - -### Seguridad - -**Passwords:** -- Hasheadas con bcrypt (10 rounds) -- Validaci贸n: min 8 caracteres, 1 n煤mero, 1 may煤scula (recomendado) -- No se almacena ni loguea password en texto plano - -**JWT:** -- Firmado con secret key (desde .env: JWT_SECRET) -- Expiraci贸n: 24 horas -- Header: `Authorization: Bearer {token}` - -**Tokens de recuperaci贸n:** -- Generados con crypto.randomBytes(32) -- Un solo uso (flag `used`) -- Expiraci贸n: 1 hora -- Invalidados despu茅s de uso - -**Invitaciones:** -- Token 煤nico por invitaci贸n -- Expiraci贸n configurable (default: 7 d铆as) -- Solo 1 uso -- Vinculado a email espec铆fico - -**Rate Limiting:** -- Login: 5 intentos por minuto por IP -- Forgot password: 3 intentos por hora por IP -- Register: 10 por hora por IP - ---- - -## 馃搵 Dependencias - -**Antes:** -- Ninguna (primera historia del proyecto) -- Infraestructura base migrada de GAMILIT (Sprint 0) - -**Despu茅s:** -- US-FUND-002 (Perfiles de usuario - requiere autenticaci贸n) -- US-FUND-003 (Dashboard por rol - requiere autenticaci贸n) -- US-FUND-005 (Sistema de sesiones - extiende esta funcionalidad) - ---- - -## 馃搻 Definici贸n de Hecho (DoD) - -- [ ] C贸digo implementado y revisado (code review aprobado) -- [ ] Tests unitarios para AuthService (>80% coverage) -- [ ] Tests E2E para flujos de autenticaci贸n -- [ ] Validaci贸n de seguridad (password hashing, JWT signing) -- [ ] Documentaci贸n de API en Swagger/OpenAPI -- [ ] Probado en ambiente de desarrollo -- [ ] Sin warnings de seguridad en npm audit -- [ ] Logs de auditor铆a configurados - ---- - -## 馃帹 Mockups/Wireframes - -### Flujo de Login - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Login - Sistema de Obra 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 Email: 鈹 -鈹 [____________________________________] 鈹 -鈹 鈹 -鈹 Contrase帽a: 鈹 -鈹 [____________________________________] 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 [馃彚] Constructora (opcional) 鈹 鈹 -鈹 鈹 > ABC Constructora SA de CV 鈹 鈹 -鈹 鈹 XYZ Edificaciones 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 [ ] Recordarme 鈹 -鈹 鈹 -鈹 [ Iniciar Sesi贸n ] 鈹 -鈹 鈹 -鈹 驴Olvidaste tu contrase帽a? 鈹 -鈹 驴Tienes una invitaci贸n? Reg铆strate 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -### Flujo de Selector de Constructora (despu茅s de login) - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Selecciona tu Constructora 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 Tienes acceso a m煤ltiples constructoras. 鈹 -鈹 Selecciona con cu谩l deseas trabajar hoy: 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 馃彚 ABC Constructora SA de CV 鈹 鈹 -鈹 鈹 Rol: Ingeniero 鈹 鈹 -鈹 鈹 5 obras activas 鈹 鈹 -鈹 鈹 [Seleccionar] 鈹鈹鈹鈹尖攢鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 馃彚 XYZ Edificaciones 鈹 鈹 -鈹 鈹 Rol: Residente 鈹 鈹 -鈹 鈹 2 obras activas 鈹 鈹 -鈹 鈹 [Seleccionar] 鈹 鈹 -鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## 馃И Tareas de Implementaci贸n - -### Backend (Estimado: 16h, GAMILIT: 17h) - -**Total Backend:** ~15h (~3.75 SP) - Ahorro 2h por reutilizaci贸n - -- [ ] **Tarea B.1:** Migraci贸n de auth desde GAMILIT - Real: 2h - - [x] Copiar AuthService de GAMILIT - - [x] Copiar JwtStrategy - - [x] Adaptar DTOs para construcci贸n - - [x] Configurar JwtModule - -- [ ] **Tarea B.2:** Multi-tenancy y constructoras - Estimado: 4h - - [ ] Crear ConstructoraService - - [ ] Endpoints de gesti贸n de constructoras - - [ ] Relaci贸n user 鈫 user_constructoras 鈫 constructoras - - [ ] Selector de constructora en login - -- [ ] **Tarea B.3:** Sistema de invitaciones - Estimado: 3h - - [ ] Crear InvitationService - - [ ] Endpoint POST /invitations/create (solo director) - - [ ] Endpoint POST /auth/register-by-invitation - - [ ] Env铆o de email con link de invitaci贸n - -- [ ] **Tarea B.4:** Recuperaci贸n de contrase帽a - Estimado: 2h - - [x] Migrar MailService de GAMILIT - - [ ] POST /auth/forgot-password - - [ ] POST /auth/reset-password - - [ ] Templates de email - -- [ ] **Tarea B.5:** Endpoints adicionales - Estimado: 2h - - [ ] POST /auth/switch-constructora - - [ ] GET /auth/me (con constructora activa) - - [ ] Logs de auditor铆a para cambios de constructora - -- [ ] **Tarea B.6:** Documentaci贸n Swagger - Estimado: 2h - - [ ] Documentar todos los endpoints de auth - - [ ] Ejemplos de request/response - - [ ] Schemas de validaci贸n - ---- - -### Frontend (Estimado: 8h, GAMILIT: 9h) - -**Total Frontend:** ~7h (~1.75 SP) - Ahorro 2h - -- [ ] **Tarea F.1:** Migraci贸n de componentes auth - Real: 2h - - [x] Copiar LoginForm de GAMILIT - - [x] Copiar RegisterForm - - [x] Copiar ForgotPasswordForm - - [x] Copiar ResetPasswordForm - -- [ ] **Tarea F.2:** Selector de constructora - Estimado: 3h - - [ ] Componente ConstructoraSelector - - [ ] Modal de selecci贸n post-login - - [ ] Switcher en navbar (cambiar constructora activa) - - [ ] Persistir selecci贸n en localStorage - -- [ ] **Tarea F.3:** Registro por invitaci贸n - Estimado: 2h - - [ ] P谩gina /register/:invitationToken - - [ ] Validaci贸n de token - - [ ] Formulario de registro con datos de invitaci贸n - -- [ ] **Tarea F.4:** AuthStore con Zustand - Real: 0h (migrado) - - [x] Copiar authStore de GAMILIT - - [ ] Agregar: constructora, constructoras, switchConstructora - - [ ] Hook useAuth - ---- - -### Testing (Estimado: 6h, GAMILIT: 5.5h) - -**Total Testing:** ~6h (~1.5 SP) - Similar a GAMILIT - -- [ ] **Tarea T.1:** Tests unitarios backend - Estimado: 3h - - [ ] Tests de AuthService (login, register, JWT) - - [ ] Tests de ConstructoraService - - [ ] Tests de InvitationService - - [ ] Tests de guards (JwtAuthGuard, ConstructoraGuard) - -- [ ] **Tarea T.2:** Tests E2E - Estimado: 2h - - [ ] Login con constructora 煤nica - - [ ] Login con m煤ltiples constructoras - - [ ] Registro por invitaci贸n - - [ ] Recuperaci贸n de contrase帽a - - [ ] Cambio de constructora activa - -- [ ] **Tarea T.3:** Tests frontend - Estimado: 1h - - [ ] Tests de componentes de formularios - - [ ] Tests de AuthStore - ---- - -### Deployment (Estimado: 2h, GAMILIT: 2h) - -**Total Deployment:** ~2h (~0.5 SP) - Similar - -- [ ] **Tarea D.1:** Variables de entorno - Estimado: 1h - - [ ] JWT_SECRET configurado - - [ ] SMTP configurado (env铆o de emails) - - [ ] Frontend: API_URL configurado - -- [ ] **Tarea D.2:** Deploy y validaci贸n - Estimado: 1h - - [ ] Deploy a staging - - [ ] Smoke tests de autenticaci贸n - - [ ] Validaci贸n de seguridad (bcrypt, JWT) - ---- - -## 馃搳 Resumen de Horas - -| Categor铆a | Estimado | Real | Varianza | Ahorro vs GAMILIT | -|-----------|----------|------|----------|-------------------| -| Backend | 15h | TBD | - | -2h (13%) | -| Frontend | 7h | TBD | - | -2h (22%) | -| Testing | 6h | TBD | - | +0.5h (0%) | -| Deployment | 2h | TBD | - | 0h (0%) | -| **TOTAL** | **30h** | **TBD** | **-** | **-3.5h (~12%)** | - -**Validaci贸n:** 8 SP 脳 4h/SP = 32 horas estimadas (vs 30h optimizado) 鉁 - -**Ahorro total:** ~3.5 horas gracias a reutilizaci贸n de GAMILIT - ---- - -## 馃搮 Cronograma Real - -**Sprint:** Sprint 1 (Semanas 2-3) -**Fecha Inicio:** TBD -**Fecha Fin:** TBD -**Estado:** 馃毀 Planificado - -**Notas:** -- Multi-tenancy es la diferencia principal vs GAMILIT -- Selector de constructora requiere UX cuidadosa -- Invitaciones reemplazan auto-registro abierto - ---- - -## 馃И Testing - -### Tests Unitarios (Backend) -```typescript -describe('AuthService', () => { - it('should hash password on register by invitation', async () => { - const invitation = await createInvitation({ email: 'test@obra.com', role: 'resident' }); - const result = await authService.registerByInvitation(invitation.token, { - password: 'SecurePass123', - firstName: 'Juan', - lastName: 'P茅rez' - }); - - expect(result.user.password).not.toBe('SecurePass123'); - expect(await bcrypt.compare('SecurePass123', result.user.password)).toBe(true); - }); - - it('should generate JWT with constructora on login', async () => { - const user = await createUser({ email: 'test@obra.com' }); - const constructora = await assignUserToConstructora(user.id, { role: 'engineer' }); - - const result = await authService.login('test@obra.com', 'password', constructora.id); - const decoded = jwt.verify(result.accessToken, process.env.JWT_SECRET); - - expect(decoded.constructoraId).toBe(constructora.id); - expect(decoded.role).toBe('engineer'); - }); - - it('should allow switching constructora', async () => { - const user = await createUser(); - await assignUserToConstructora(user.id, { constructoraId: 'A', role: 'engineer' }); - await assignUserToConstructora(user.id, { constructoraId: 'B', role: 'resident' }); - - const newToken = await authService.switchConstructora(user.id, 'B'); - const decoded = jwt.verify(newToken, process.env.JWT_SECRET); - - expect(decoded.constructoraId).toBe('B'); - expect(decoded.role).toBe('resident'); - }); -}); -``` - -### Tests E2E -```typescript -describe('Auth API E2E', () => { - it('POST /auth/login - success with constructora selector', async () => { - const user = await createUser({ email: 'multi@obra.com' }); - await assignUserToConstructora(user.id, { constructoraId: 'A' }); - await assignUserToConstructora(user.id, { constructoraId: 'B' }); - - const response = await request(app) - .post('/api/auth/login') - .send({ email: 'multi@obra.com', password: 'password' }); - - expect(response.status).toBe(200); - expect(response.body.constructoras).toHaveLength(2); - expect(response.body.accessToken).toBeDefined(); - }); - - it('POST /auth/register-by-invitation - success', async () => { - const invitation = await createInvitation({ email: 'new@obra.com', role: 'resident' }); - - const response = await request(app) - .post('/api/auth/register-by-invitation') - .send({ - invitationToken: invitation.token, - password: 'SecurePass123', - firstName: 'Juan', - lastName: 'P茅rez' - }); - - expect(response.status).toBe(201); - expect(response.body.user.email).toBe('new@obra.com'); - expect(response.body.accessToken).toBeDefined(); - }); -}); -``` - ---- - -## 馃幆 Estimaci贸n - -**Desglose de Esfuerzo (8 SP = ~3-4 d铆as):** -- Backend: multi-tenancy y invitaciones: 1.5 d铆as -- Frontend: selector de constructora: 1 d铆a -- Testing: 0.75 d铆as -- Ajustes y documentaci贸n: 0.75 d铆as - -**Riesgos:** -- Selector de constructora puede requerir iteraciones de UX -- Multi-tenancy en RLS requiere validaciones exhaustivas - -**Mitigaciones:** -- Mockups de selector antes de implementar -- Tests E2E de multi-tenancy desde d铆a 1 - ---- - -**Creado:** 2025-11-17 -**Actualizado:** 2025-11-17 -**Responsable:** Equipo Backend + Frontend -**Sprint:** Sprint 1 (Semanas 2-3) -**脡pica:** MAI-001 - Fundamentos diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-002-perfiles-usuario-construccion.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-002-perfiles-usuario-construccion.md deleted file mode 100644 index 59451dd9d..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-002-perfiles-usuario-construccion.md +++ /dev/null @@ -1,739 +0,0 @@ -# US-FUND-002: Perfiles de Usuario de Construcci贸n - -**脡pica:** MAI-001 - Fundamentos -**Sprint:** Sprint 1-2 (Semanas 1-2) -**Story Points:** 5 SP -**Presupuesto:** $1,800 MXN -**Prioridad:** Alta -**Estado:** 馃毀 Planificado - ---- - -## Descripci贸n - -Como **usuario del sistema de gesti贸n de obra**, quiero **ver y editar mi perfil profesional** para **mantener mi informaci贸n de contacto actualizada y mostrar mi rol en la constructora**. - -**Contexto del Alcance Inicial:** -El MVP incluye perfiles b谩sicos con informaci贸n esencial para construcci贸n: nombre, email, rol en constructora(s), foto, tel茅fono. No incluye curr铆culum, certificaciones, historial de proyectos o configuraciones avanzadas, que se agregar谩n en extensiones futuras. - -**Diferencias con GAMILIT:** -- Multi-tenancy: Usuario puede tener diferentes roles en diferentes constructoras -- Informaci贸n adicional: tel茅fono, especialidad (para ingenieros/residentes) -- Sin gamificaci贸n (no hay XP, coins, badges en perfil) - ---- - -## Criterios de Aceptaci贸n - -- [ ] **CA-01:** El usuario puede ver su perfil con: nombre completo, email, tel茅fono, foto, constructoras asociadas -- [ ] **CA-02:** El usuario puede editar: fullName, phone, foto de perfil -- [ ] **CA-03:** El email NO es editable (requerir铆a re-verificaci贸n) -- [ ] **CA-04:** El rol NO es editable por el usuario (solo admin puede cambiar) -- [ ] **CA-05:** La foto de perfil puede subirse (max 5MB, formatos: jpg, png, webp) -- [ ] **CA-06:** Si no hay foto, se muestra avatar por defecto (iniciales del nombre) -- [ ] **CA-07:** Se muestra lista de constructoras donde el usuario tiene acceso con su rol en cada una -- [ ] **CA-08:** Los cambios se guardan en la base de datos -- [ ] **CA-09:** Se muestra mensaje de confirmaci贸n al guardar cambios -- [ ] **CA-10:** Se valida que fullName no est茅 vac铆o -- [ ] **CA-11:** Se valida formato de tel茅fono (10 d铆gitos, M茅xico) -- [ ] **CA-12:** La foto se redimensiona autom谩ticamente a 200x200px -- [ ] **CA-13:** Usuario puede marcar una constructora como "principal" (pre-seleccionada al login) - ---- - -## Especificaciones T茅cnicas - -### Backend (NestJS) - -**Endpoints:** -``` -GET /api/user/profile -- Headers: Authorization: Bearer {token} -- Response: { - user: { - id, - email, - fullName, - phone, - photoUrl, - createdAt - }, - constructoras: [ - { - constructoraId, - nombre, - logoUrl, - role, - isPrimary, - status - } - ] - } - -PATCH /api/user/profile -- Headers: Authorization: Bearer {token} -- Body: { fullName?, phone? } -- Response: { user: { ... } } - -POST /api/user/profile/photo -- Headers: Authorization: Bearer {token}, Content-Type: multipart/form-data -- Body: FormData with 'photo' file -- Response: { photoUrl: string } - -DELETE /api/user/profile/photo -- Headers: Authorization: Bearer {token} -- Response: { message: "Photo deleted" } - -PATCH /api/user/set-primary-constructora -- Headers: Authorization: Bearer {token} -- Body: { constructoraId: string } -- Response: { message: "Primary constructora updated" } -``` - -**Servicios:** -- **ProfileService:** Gesti贸n de perfiles de usuario -- **FileUploadService:** Manejo de uploads de im谩genes (reutilizado) -- **ImageProcessingService:** Redimensionamiento de im谩genes (reutilizado) -- **ConstructoraService:** Obtener constructoras del usuario - -**Entidades:** -```typescript -// Perfil global -@Entity('profiles', { schema: 'auth_management' }) -class Profile { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ unique: true }) - email: string; - - @Column({ name: 'full_name' }) - fullName: string; - - @Column({ nullable: true }) - phone: string; - - @Column({ name: 'photo_url', nullable: true }) - photoUrl?: string; - - @Column({ name: 'photo_key', nullable: true }) - photoKey?: string; // Para storage local o S3 - - @Column({ type: 'enum', enum: UserStatus, default: 'pending' }) - status: UserStatus; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; -} - -// Relaci贸n con constructoras (ya existe) -@Entity('user_constructoras', { schema: 'auth_management' }) -class UserConstructora { - // ... campos existentes de ET-AUTH-003 - role: ConstructionRole; - isPrimary: boolean; - status: UserStatus; -} -``` - -**Validaciones:** -```typescript -// apps/backend/src/modules/user/dto/update-profile.dto.ts -import { IsString, IsOptional, Length, Matches } from 'class-validator'; - -export class UpdateProfileDto { - @IsString() - @IsOptional() - @Length(3, 255) - fullName?: string; - - @IsString() - @IsOptional() - @Matches(/^[0-9]{10}$/, { - message: 'Tel茅fono debe tener 10 d铆gitos (formato M茅xico)', - }) - phone?: string; -} -``` - -### Frontend (React + Vite) - -**Componentes:** -```typescript -// apps/frontend/src/features/profile/ProfileView.tsx -- Muestra informaci贸n del usuario (solo lectura) -- Lista de constructoras con badges de rol -- Badge especial para constructora principal -- Bot贸n "Editar perfil" - -// apps/frontend/src/features/profile/ProfileEditForm.tsx -- Formulario de edici贸n con React Hook Form + Zod -- Campos: fullName, phone -- Validaci贸n en tiempo real -- Preview de cambios antes de guardar - -// apps/frontend/src/features/profile/PhotoUpload.tsx -- Upload drag-and-drop con react-dropzone -- Preview de imagen -- Crop/ajuste de imagen (opcional en MVP) -- Indicador de progreso - -// apps/frontend/src/components/ui/AvatarWithInitials.tsx -- Avatar con iniciales si no hay foto -- Colores generados por hash del nombre -- Tama帽os: sm, md, lg, xl - -// apps/frontend/src/features/profile/ConstructorasList.tsx -- Lista de constructoras del usuario -- Badge de rol (director, engineer, etc.) -- Icono de estrella para constructora principal -- Click para marcar como principal -``` - -**Rutas:** -```typescript -/profile 鈫 P谩gina de perfil (vista) -/profile/edit 鈫 Editar perfil -/settings/account 鈫 Configuraci贸n de cuenta (incluye perfil) -``` - -**Estado (Zustand):** -```typescript -// apps/frontend/src/stores/profile-store.ts -interface ProfileStore { - profile: UserProfile | null; - constructoras: UserConstructoraAccess[]; - loading: boolean; - uploadingPhoto: boolean; - - // Actions - fetchProfile: () => Promise; - updateProfile: (data: UpdateProfileDto) => Promise; - uploadPhoto: (file: File) => Promise; - deletePhoto: () => Promise; - setPrimaryConstructora: (constructoraId: string) => Promise; -} - -interface UserProfile { - id: string; - email: string; - fullName: string; - phone: string | null; - photoUrl: string | null; - createdAt: string; -} - -interface UserConstructoraAccess { - constructoraId: string; - nombre: string; - logoUrl: string | null; - role: ConstructionRole; - isPrimary: boolean; - status: UserStatus; -} -``` - -**UI/UX:** -- Card con informaci贸n del usuario -- Grid de 2 columnas: Info personal | Constructoras -- Botones: "Editar perfil", "Cambiar foto" -- Upload drag-and-drop con preview -- Loading states durante operaciones -- Toast notifications para confirmaciones - -### Almacenamiento de Archivos - -**Opci贸n Inicial (Alcance MVP):** -- Archivos guardados localmente en `/uploads/profile-photos/` -- Nombres generados con UUID: `{userId}-{timestamp}-{uuid}.jpg` -- Public URL servida por Express: `/static/profile-photos/{photoKey}` -- Organizaci贸n: `/uploads/profile-photos/YYYY/MM/` (por mes) - -**Limpieza:** -- Al subir nueva foto, eliminar foto anterior del disco -- Orphan cleanup job: eliminar fotos sin usuario (cron semanal) - -**Opci贸n Futura:** -- Migraci贸n a AWS S3 o CloudFlare R2 (Fase 2) - ---- - -## Dependencias - -**Antes:** -- 鉁 US-FUND-001 (Autenticaci贸n JWT - requiere usuario autenticado) -- 鉁 RF-AUTH-003 (Multi-tenancy - necesita constructoras) - -**Despu茅s:** -- US-FUND-003 (Dashboard - muestra foto de perfil y nombre) -- Todas las historias usan la foto y nombre del usuario - -**Bloqueos:** -- Ninguno (puede implementarse en Sprint 1-2) - ---- - -## Definici贸n de Hecho (DoD) - -- [ ] Endpoints implementados y documentados (Swagger) -- [ ] Validaciones en backend (DTO, file size, file type) -- [ ] Upload de archivos funcional con Sharp -- [ ] Redimensionamiento autom谩tico a 200x200px -- [ ] Componentes de frontend implementados -- [ ] Zustand store con acciones de perfil -- [ ] Tests unitarios backend (>80% coverage) -- [ ] Tests E2E para edici贸n de perfil -- [ ] Tests frontend (React Testing Library) -- [ ] Responsive design (mobile, tablet, desktop) -- [ ] Manejo de errores (file too large, invalid format, network error) -- [ ] Loading states y feedback visual -- [ ] Documentaci贸n de API en Swagger -- [ ] Code review aprobado -- [ ] Desplegado en staging y validado - ---- - -## Notas del Alcance Inicial - -### Incluido en MVP 鉁 -- 鉁 Campos b谩sicos: fullName, email, phone, photo -- 鉁 Lista de constructoras con roles -- 鉁 Marcar constructora como principal -- 鉁 Upload y preview de foto -- 鉁 Avatar con iniciales si no hay foto -- 鉁 Storage local de im谩genes - -### NO Incluido en MVP 鉂 -- 鉂 Curr铆culum vitae o bio extensa -- 鉂 Certificaciones profesionales (ingeniero civil, arquitecto, etc.) -- 鉂 Historial de proyectos completados -- 鉂 Estad铆sticas de desempe帽o -- 鉂 Configuraciones de privacidad -- 鉂 Redes sociales (LinkedIn, etc.) -- 鉂 Preferencias de notificaciones (en US-FUND-005) -- 鉂 Crop avanzado de imagen (solo redimensionamiento) -- 鉂 Storage en la nube (S3) - -### Extensiones Futuras 鈿狅笍 -- 鈿狅笍 **Fase 2:** Perfil profesional extendido (bio, certificaciones, experiencia) -- 鈿狅笍 **Fase 2:** Historial de proyectos y m茅tricas -- 鈿狅笍 **Fase 2:** Integraci贸n con LinkedIn -- 鈿狅笍 **Fase 2:** Migraci贸n a S3 para storage - ---- - -## Tareas de Implementaci贸n - -### Backend (Estimado: 10h) - -**Total Backend:** 10h (~2.5 SP) - -- [ ] **Tarea B.1:** Endpoints de perfil - Estimado: 4h - - [ ] Subtarea B.1.1: GET /user/profile con datos de perfil + constructoras - 1.5h - - [ ] Subtarea B.1.2: PATCH /user/profile con validaci贸n de DTO - 1h - - [ ] Subtarea B.1.3: PATCH /user/set-primary-constructora - 1h - - [ ] Subtarea B.1.4: Documentaci贸n Swagger de endpoints - 0.5h - -- [ ] **Tarea B.2:** Sistema de upload de archivos - Estimado: 4h - - [ ] Subtarea B.2.1: Reutilizar FileUploadService de GAMILIT - 0.5h - - [ ] Subtarea B.2.2: POST /user/profile/photo con validaci贸n (5MB, jpg/png/webp) - 1.5h - - [ ] Subtarea B.2.3: DELETE /user/profile/photo y cleanup de archivo - 1h - - [ ] Subtarea B.2.4: Organizaci贸n por mes: /uploads/profile-photos/YYYY/MM/ - 0.5h - - [ ] Subtarea B.2.5: Cleanup de foto anterior al subir nueva - 0.5h - -- [ ] **Tarea B.3:** Procesamiento de im谩genes - Estimado: 2h - - [ ] Subtarea B.3.1: Reutilizar ImageProcessingService con Sharp - 0.5h - - [ ] Subtarea B.3.2: Redimensionamiento a 200x200 manteniendo aspect ratio - 1h - - [ ] Subtarea B.3.3: Optimizaci贸n de calidad y compresi贸n - 0.5h - -### Frontend (Estimado: 7h) - -**Total Frontend:** 7h (~1.75 SP) - -- [ ] **Tarea F.1:** Componentes de perfil - Estimado: 4h - - [ ] Subtarea F.1.1: ProfileView con grid de info personal + constructoras - 1.5h - - [ ] Subtarea F.1.2: ProfileEditForm con React Hook Form + Zod - 1.5h - - [ ] Subtarea F.1.3: AvatarWithInitials reutilizado de GAMILIT - 0.5h - - [ ] Subtarea F.1.4: ConstructorasList con badges de rol y estrella primary - 1h - - [ ] Subtarea F.1.5: Navegaci贸n /profile y /profile/edit - 0.5h - -- [ ] **Tarea F.2:** Upload de foto de perfil - Estimado: 2h - - [ ] Subtarea F.2.1: PhotoUpload con react-dropzone - 1h - - [ ] Subtarea F.2.2: Preview de imagen antes de guardar - 0.5h - - [ ] Subtarea F.2.3: ProfileStore en Zustand con m茅todos CRUD - 0.5h - -- [ ] **Tarea F.3:** Responsive y UX - Estimado: 1h - - [ ] Subtarea F.3.1: Dise帽o responsive (mobile-first) - 0.5h - - [ ] Subtarea F.3.2: Loading states y skeleton loaders - 0.25h - - [ ] Subtarea F.3.3: Toast notifications para confirmaciones - 0.25h - -### Testing (Estimado: 3h) - -**Total Testing:** 3h (~0.75 SP) - -- [ ] **Tarea T.1:** Tests unitarios backend - Estimado: 1.5h - - [ ] Subtarea T.1.1: Tests de ProfileService (fetch, update) - 0.5h - - [ ] Subtarea T.1.2: Tests de FileUploadService (validaci贸n) - 0.5h - - [ ] Subtarea T.1.3: Tests de setPrimaryConstructora - 0.5h - -- [ ] **Tarea T.2:** Tests E2E - Estimado: 1h - - [ ] Subtarea T.2.1: Tests de endpoints GET/PATCH profile - 0.5h - - [ ] Subtarea T.2.2: Tests de upload de foto (success y errores) - 0.5h - -- [ ] **Tarea T.3:** Tests frontend - Estimado: 0.5h - - [ ] Subtarea T.3.1: Tests de ProfileEditForm (React Testing Library) - 0.25h - - [ ] Subtarea T.3.2: Tests de PhotoUpload - 0.25h - ---- - -## Resumen de Horas - -| Categor铆a | Estimado | Story Points | -|-----------|----------|--------------| -| Backend | 10h | 2.5 SP | -| Frontend | 7h | 1.75 SP | -| Testing | 3h | 0.75 SP | -| **TOTAL** | **20h** | **5 SP** | - -**Validaci贸n:** 5 SP 脳 4h/SP = 20 horas estimadas 鉁 - ---- - -## Cronograma Propuesto - -**Sprint:** Sprint 1-2 (Semanas 1-2) -**Duraci贸n:** 2.5 d铆as -**Equipo:** -- 1 Backend developer (10h) -- 1 Frontend developer (7h) -- QA compartido (3h) - -**Hitos:** -- D铆a 1: Endpoints backend + upload de foto -- D铆a 2: Frontend (componentes + store) -- D铆a 2.5: Testing + ajustes - ---- - -## Testing - -### Tests Unitarios Backend - -```typescript -// apps/backend/src/modules/user/services/profile.service.spec.ts -describe('ProfileService', () => { - it('should fetch user profile with constructoras', async () => { - const profile = await profileService.getProfile(userId); - expect(profile.user).toBeDefined(); - expect(profile.constructoras).toBeArray(); - }); - - it('should update fullName and phone', async () => { - const updated = await profileService.updateProfile(userId, { - fullName: 'Juan P茅rez Garc铆a', - phone: '5512345678', - }); - expect(updated.fullName).toBe('Juan P茅rez Garc铆a'); - }); - - it('should NOT update email', async () => { - await expect( - profileService.updateProfile(userId, { email: 'new@email.com' } as any) - ).rejects.toThrow(); - }); - - it('should handle photo upload', async () => { - const result = await profileService.uploadPhoto(userId, mockFile); - expect(result.photoUrl).toContain('/static/profile-photos/'); - }); - - it('should delete old photo when uploading new one', async () => { - await profileService.uploadPhoto(userId, mockFile1); - const oldPhotoKey = (await getProfile(userId)).photoKey; - - await profileService.uploadPhoto(userId, mockFile2); - - expect(fs.existsSync(`/uploads/profile-photos/${oldPhotoKey}`)).toBe(false); - }); - - it('should set primary constructora', async () => { - await profileService.setPrimaryConstructora(userId, constructoraId); - - const uc = await getUserConstructora(userId, constructoraId); - expect(uc.isPrimary).toBe(true); - - // Solo una debe ser primary - const allUc = await getAllUserConstructoras(userId); - const primaryCount = allUc.filter(c => c.isPrimary).length; - expect(primaryCount).toBe(1); - }); -}); - -describe('FileUploadService', () => { - it('should validate file size (max 5MB)', async () => { - const largeFile = createMockFile(6 * 1024 * 1024); // 6MB - await expect(fileUploadService.validateFile(largeFile)).rejects.toThrow('File too large'); - }); - - it('should validate file type (jpg, png, webp)', async () => { - const pdfFile = createMockFile(1024, 'application/pdf'); - await expect(fileUploadService.validateFile(pdfFile)).rejects.toThrow('Invalid file type'); - }); - - it('should generate unique filename', () => { - const filename1 = fileUploadService.generateFilename(userId, 'image.jpg'); - const filename2 = fileUploadService.generateFilename(userId, 'image.jpg'); - expect(filename1).not.toBe(filename2); - }); -}); - -describe('ImageProcessingService', () => { - it('should resize image to 200x200', async () => { - const processed = await imageService.resize(mockBuffer, 200, 200); - const metadata = await sharp(processed).metadata(); - expect(metadata.width).toBe(200); - expect(metadata.height).toBe(200); - }); - - it('should maintain aspect ratio with cover mode', async () => { - const processed = await imageService.resize(mockWideBuffer, 200, 200); - const metadata = await sharp(processed).metadata(); - expect(metadata.width).toBe(200); - expect(metadata.height).toBe(200); - }); -}); -``` - -### Tests E2E - -```typescript -// apps/backend/test/user/profile.e2e-spec.ts -describe('Profile API (E2E)', () => { - let app: INestApplication; - let user: User; - let token: string; - - beforeAll(async () => { - // Setup app - user = await createUser(); - token = await getAuthToken(user); - }); - - describe('GET /user/profile', () => { - it('should return user profile with constructoras', async () => { - const response = await request(app.getHttpServer()) - .get('/user/profile') - .set('Authorization', `Bearer ${token}`) - .expect(200); - - expect(response.body.user).toMatchObject({ - id: user.id, - email: user.email, - fullName: expect.any(String), - }); - - expect(response.body.constructoras).toBeArray(); - }); - - it('should return 401 without token', async () => { - await request(app.getHttpServer()) - .get('/user/profile') - .expect(401); - }); - }); - - describe('PATCH /user/profile', () => { - it('should update fullName and phone', async () => { - const response = await request(app.getHttpServer()) - .patch('/user/profile') - .set('Authorization', `Bearer ${token}`) - .send({ - fullName: 'Juan P茅rez Actualizado', - phone: '5512345678', - }) - .expect(200); - - expect(response.body.fullName).toBe('Juan P茅rez Actualizado'); - expect(response.body.phone).toBe('5512345678'); - }); - - it('should reject invalid phone format', async () => { - await request(app.getHttpServer()) - .patch('/user/profile') - .set('Authorization', `Bearer ${token}`) - .send({ phone: '123' }) // Muy corto - .expect(400); - }); - - it('should not allow email change', async () => { - await request(app.getHttpServer()) - .patch('/user/profile') - .set('Authorization', `Bearer ${token}`) - .send({ email: 'new@email.com' }) - .expect(400); - }); - }); - - describe('POST /user/profile/photo', () => { - it('should upload photo successfully', async () => { - const response = await request(app.getHttpServer()) - .post('/user/profile/photo') - .set('Authorization', `Bearer ${token}`) - .attach('photo', './test/fixtures/avatar.jpg') - .expect(201); - - expect(response.body.photoUrl).toMatch(/\/static\/profile-photos\/.+\.jpg$/); - }); - - it('should reject file larger than 5MB', async () => { - await request(app.getHttpServer()) - .post('/user/profile/photo') - .set('Authorization', `Bearer ${token}`) - .attach('photo', './test/fixtures/large-image.jpg') // 6MB - .expect(400); - }); - - it('should reject non-image files', async () => { - await request(app.getHttpServer()) - .post('/user/profile/photo') - .set('Authorization', `Bearer ${token}`) - .attach('photo', './test/fixtures/document.pdf') - .expect(400); - }); - }); - - describe('DELETE /user/profile/photo', () => { - it('should delete photo and revert to default', async () => { - // Upload first - await request(app.getHttpServer()) - .post('/user/profile/photo') - .set('Authorization', `Bearer ${token}`) - .attach('photo', './test/fixtures/avatar.jpg'); - - // Then delete - const response = await request(app.getHttpServer()) - .delete('/user/profile/photo') - .set('Authorization', `Bearer ${token}`) - .expect(200); - - // Verify photo is null - const profile = await request(app.getHttpServer()) - .get('/user/profile') - .set('Authorization', `Bearer ${token}`); - - expect(profile.body.user.photoUrl).toBeNull(); - }); - }); -}); -``` - -### Tests Frontend - -```typescript -// apps/frontend/src/features/profile/ProfileEditForm.spec.tsx -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { ProfileEditForm } from './ProfileEditForm'; - -describe('ProfileEditForm', () => { - const mockProfile = { - id: '123', - email: 'user@test.com', - fullName: 'Juan P茅rez', - phone: '5512345678', - }; - - it('renders current user data', () => { - render(); - - expect(screen.getByDisplayValue('Juan P茅rez')).toBeInTheDocument(); - expect(screen.getByDisplayValue('5512345678')).toBeInTheDocument(); - }); - - it('submits updated data on save', async () => { - const onSave = jest.fn(); - render(); - - const nameInput = screen.getByLabelText(/nombre completo/i); - fireEvent.change(nameInput, { target: { value: 'Juan P茅rez Garc铆a' } }); - - const saveButton = screen.getByText(/guardar/i); - fireEvent.click(saveButton); - - await waitFor(() => { - expect(onSave).toHaveBeenCalledWith({ - fullName: 'Juan P茅rez Garc铆a', - phone: '5512345678', - }); - }); - }); - - it('shows validation errors for invalid phone', async () => { - render(); - - const phoneInput = screen.getByLabelText(/tel茅fono/i); - fireEvent.change(phoneInput, { target: { value: '123' } }); - fireEvent.blur(phoneInput); - - await waitFor(() => { - expect(screen.getByText(/10 d铆gitos/i)).toBeInTheDocument(); - }); - }); - - it('disables email field', () => { - render(); - - const emailInput = screen.getByDisplayValue('user@test.com'); - expect(emailInput).toBeDisabled(); - }); -}); -``` - ---- - -## Estimaci贸n - -**Desglose de Esfuerzo (5 SP = ~2.5 d铆as = 20h):** -- Backend endpoints + validations: 0.5 d铆as (4h) -- File upload + processing: 0.5 d铆as (4h) -- Multi-tenancy (set primary): 0.25 d铆as (2h) -- Frontend components: 0.5 d铆as (4h) -- Photo upload UI: 0.375 d铆as (3h) -- Testing: 0.375 d铆as (3h) - -**Riesgos:** -- 鈿狅笍 File upload puede tener edge cases (conexi贸n lenta, archivos corruptos) -- 鈿狅笍 L贸gica de "primary constructora" requiere cuidado (constraint unique) -- 鈿狅笍 Cleanup de fotos antiguas debe ser robusto (no eliminar si falla upload) - -**Mitigaciones:** -- 鉁 Reutilizar FileUploadService y ImageProcessingService de GAMILIT -- 鉁 Transacci贸n para cambiar primary constructora -- 鉁 Tests E2E exhaustivos de upload - ---- - -## Recursos Externos - -**Librer铆as Backend:** -- `multer` (file upload middleware para Express) -- `sharp` (procesamiento de im谩genes - resize, optimize) -- `uuid` (generar nombres 煤nicos de archivo) - -**Librer铆as Frontend:** -- `react-dropzone` (drag & drop upload con preview) -- `react-hook-form` (manejo de formularios) -- `zod` (validaci贸n de esquemas) - -**Assets:** -- Avatar placeholder SVG (si no hay foto) -- Iconos de c谩mara para bot贸n de upload - ---- - -**Creado:** 2025-11-17 -**Actualizado:** 2025-11-17 -**Responsable:** Equipo Fullstack -**Reutilizaci贸n GAMILIT:** 75% (estructura similar, adaptar multi-tenancy) diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-003-dashboard-por-rol.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-003-dashboard-por-rol.md deleted file mode 100644 index 7edcd3659..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-003-dashboard-por-rol.md +++ /dev/null @@ -1,569 +0,0 @@ -# US-FUND-003: Dashboard Principal por Rol - -**脡pica:** MAI-001 - Fundamentos -**Sprint:** Sprint 2-3 (Semanas 2-3) -**Story Points:** 8 SP -**Presupuesto:** $2,900 MXN -**Prioridad:** Alta -**Estado:** 馃毀 Planificado - ---- - -## Descripci贸n - -Como **usuario del sistema de gesti贸n de obra**, quiero **ver un dashboard personalizado seg煤n mi rol** para **visualizar la informaci贸n y m茅tricas relevantes a mis responsabilidades en la constructora**. - -**Contexto del Alcance Inicial:** -El MVP incluye 7 dashboards diferentes (uno por rol de construcci贸n), cada uno mostrando KPIs y widgets relevantes. Los datos son reales de la base de datos (proyectos, presupuestos, empleados). No incluye gr谩ficas avanzadas interactivas ni personalizaci贸n de widgets, que se agregar谩n en extensiones futuras. - -**Diferencias con GAMILIT:** -- 7 variantes de dashboard vs 2 (estudiante/profesor) -- Sin gamificaci贸n (no hay niveles, XP, coins, badges) -- KPIs de construcci贸n (presupuestos, avances de obra, compras, n贸mina) -- Dashboard var铆a por constructora activa (multi-tenancy) - ---- - -## Criterios de Aceptaci贸n Generales - -### Para Todos los Roles -- [ ] **CA-01:** El dashboard muestra un saludo personalizado con nombre del usuario y rol actual -- [ ] **CA-02:** Se indica la constructora activa (nombre y logo) con selector para cambiar -- [ ] **CA-03:** El dashboard es responsive (mobile, tablet, desktop) -- [ ] **CA-04:** Se muestran widgets relevantes seg煤n el rol del usuario -- [ ] **CA-05:** Cada widget tiene un t铆tulo claro y acci贸n r谩pida (ver m谩s) -- [ ] **CA-06:** Los n煤meros y m茅tricas se actualizan en tiempo real -- [ ] **CA-07:** Se muestran alertas y notificaciones importantes en un panel lateral -- [ ] **CA-08:** El layout se adapta autom谩ticamente al cambiar de rol o constructora -- [ ] **CA-09:** Skeleton loaders mientras carga la informaci贸n -- [ ] **CA-10:** Manejo de estado vac铆o ("No hay proyectos activos") - ---- - -## Especificaciones T茅cnicas - -### Backend (NestJS) - -**Endpoints por Rol:** -``` -GET /api/dashboard/director -GET /api/dashboard/engineer -GET /api/dashboard/resident -GET /api/dashboard/purchases -GET /api/dashboard/finance -GET /api/dashboard/hr -GET /api/dashboard/post-sales - -Todos comparten estructura base: -- Headers: Authorization: Bearer {token} -- Response: { - user: { id, fullName, photoUrl, role }, - constructora: { id, nombre, logoUrl }, - widgets: [...], // Espec铆fico por rol - alerts: [...], // Alertas importantes - recentActivity: [...], // Actividad reciente - quickActions: [...] // Acciones r谩pidas - } -``` - -**Servicios:** -- **DashboardService:** Factory que delega a service espec铆fico por rol -- **DirectorDashboardService:** KPIs para director -- **EngineerDashboardService:** KPIs para ingeniero -- **ResidentDashboardService:** KPIs para residente -- ... (uno por cada rol) - -**Optimizaci贸n:** -- Queries SQL optimizadas con agregaciones -- Caching de datos que cambian poco (configuraci贸n, logos) -- 脥ndices en columnas de filtrado frecuente -- Lazy loading de widgets secundarios - -### Frontend (React + Vite) - -**Arquitectura de Componentes:** -```typescript -components/ -鈹溾攢鈹 dashboards/ -鈹 鈹溾攢鈹 DashboardContainer.tsx // Container principal -鈹 鈹溾攢鈹 DirectorDashboard.tsx // Dashboard espec铆fico -鈹 鈹溾攢鈹 EngineerDashboard.tsx -鈹 鈹溾攢鈹 ResidentDashboard.tsx -鈹 鈹溾攢鈹 PurchasesDashboard.tsx -鈹 鈹溾攢鈹 FinanceDashboard.tsx -鈹 鈹溾攢鈹 HRDashboard.tsx -鈹 鈹斺攢鈹 PostSalesDashboard.tsx -鈹溾攢鈹 widgets/ -鈹 鈹溾攢鈹 WidgetCard.tsx // Base card reutilizable -鈹 鈹溾攢鈹 StatWidget.tsx // N煤mero + icono + trend -鈹 鈹溾攢鈹 ChartWidget.tsx // Gr谩fica simple -鈹 鈹溾攢鈹 ListWidget.tsx // Lista de items -鈹 鈹斺攢鈹 TableWidget.tsx // Tabla b谩sica -鈹斺攢鈹 shared/ - 鈹溾攢鈹 DashboardHeader.tsx // Saludo + selector de constructora - 鈹溾攢鈹 AlertsPanel.tsx // Panel lateral de alertas - 鈹溾攢鈹 QuickActionsBar.tsx // Barra de acciones r谩pidas - 鈹斺攢鈹 EmptyState.tsx // Estado vac铆o gen茅rico -``` - -**Estado (Zustand):** -```typescript -interface DashboardStore { - dashboardData: DashboardData | null; - loading: boolean; - error: string | null; - fetchDashboard: (role: ConstructionRole) => Promise; - refreshDashboard: () => Promise; -} -``` - ---- - -## Dashboards por Rol - -### 1. Dashboard Director - -**KPIs Principales:** -- Total de proyectos activos -- Valor total de cartera (suma de presupuestos) -- Margen de utilidad promedio -- Proyectos con retraso -- Flujo de efectivo del mes - -**Widgets:** -```typescript -[ - { type: 'stat', title: 'Proyectos Activos', value: 12, trend: +2 }, - { type: 'stat', title: 'Cartera Total', value: '$45.2M', trend: +8.5 }, - { type: 'stat', title: 'Margen Promedio', value: '18.5%', trend: -1.2 }, - { type: 'chart', title: 'Avance de Proyectos', data: [...] }, - { type: 'list', title: 'Proyectos en Riesgo', items: [...] }, - { type: 'table', title: 'Top 5 Proyectos por Margen', rows: [...] }, -] -``` - -**Alertas T铆picas:** -- "Proyecto Residencial Valle: presupuesto al 95%, avance f铆sico 78%" -- "3 贸rdenes de compra pendientes de aprobaci贸n" -- "Flujo de efectivo negativo proyectado para pr贸ximo mes" - -**Acciones R谩pidas:** -- Crear nuevo proyecto -- Ver reportes ejecutivos -- Aprobar presupuestos pendientes -- Ver flujo de efectivo - ---- - -### 2. Dashboard Engineer (Ingeniero) - -**KPIs Principales:** -- Proyectos asignados -- Presupuestos en revisi贸n -- Avance promedio de proyectos -- Requisiciones pendientes de validar - -**Widgets:** -```typescript -[ - { type: 'stat', title: 'Mis Proyectos', value: 5 }, - { type: 'stat', title: 'Presupuestos Activos', value: 8 }, - { type: 'chart', title: 'Avance vs Programado', data: [...] }, - { type: 'list', title: 'Requisiciones Pendientes', items: [...] }, - { type: 'table', title: 'Proyectos con Desviaci贸n', rows: [...] }, -] -``` - -**Alertas T铆picas:** -- "Proyecto Torre Norte: avance 5% por debajo de lo programado" -- "Presupuesto Plaza Comercial requiere revisi贸n" -- "Nueva requisici贸n de materiales en Proyecto Residencial" - -**Acciones R谩pidas:** -- Ver programaci贸n de obra -- Revisar presupuestos -- Validar requisiciones -- Actualizar avances - ---- - -### 3. Dashboard Resident (Residente de Obra) - -**KPIs Principales:** -- Obra(s) asignada(s) -- Avance f铆sico esta semana -- Empleados activos en obra -- Incidencias abiertas - -**Widgets:** -```typescript -[ - { type: 'stat', title: 'Mi Obra', value: 'Torre Norte' }, - { type: 'stat', title: 'Avance Hoy', value: '+2.5%' }, - { type: 'stat', title: 'Personal en Obra', value: 45 }, - { type: 'list', title: 'Checklists Pendientes', items: [...] }, - { type: 'list', title: 'Incidencias Abiertas', items: [...] }, -] -``` - -**Alertas T铆picas:** -- "Checklist de seguridad pendiente para hoy" -- "Entrega de material programada para ma帽ana 8 AM" -- "2 empleados sin registro de asistencia hoy" - -**Acciones R谩pidas:** -- Capturar avance de obra -- Registrar asistencia (m贸vil) -- Reportar incidencia -- Ver programaci贸n semanal - ---- - -### 4. Dashboard Purchases (Compras) - -**KPIs Principales:** -- 脫rdenes de compra activas -- Presupuesto de compras del mes -- 脫rdenes pendientes de recepci贸n -- Inventario bajo en stock - -**Widgets:** -```typescript -[ - { type: 'stat', title: 'OC Activas', value: 28 }, - { type: 'stat', title: 'Presupuesto Mes', value: '$1.2M', trend: -15 }, - { type: 'list', title: 'OC Pendientes de Aprobar', items: [...] }, - { type: 'list', title: 'Recepciones Pendientes', items: [...] }, - { type: 'table', title: 'Materiales en Stock Bajo', rows: [...] }, -] -``` - ---- - -### 5. Dashboard Finance (Finanzas) - -**KPIs Principales:** -- Flujo de efectivo del mes -- Cuentas por pagar vencidas -- Presupuesto vs Real -- Pagos programados esta semana - -**Widgets:** -```typescript -[ - { type: 'stat', title: 'Flujo Mes', value: '$2.3M', trend: +12 }, - { type: 'stat', title: 'Pagos Vencidos', value: 5, alert: true }, - { type: 'chart', title: 'Presupuesto vs Real', data: [...] }, - { type: 'list', title: 'Pagos Esta Semana', items: [...] }, - { type: 'table', title: 'CxP Vencidas', rows: [...] }, -] -``` - ---- - -### 6. Dashboard HR (Recursos Humanos) - -**KPIs Principales:** -- Empleados activos -- Asistencias registradas hoy -- N贸mina del periodo -- Incidencias laborales - -**Widgets:** -```typescript -[ - { type: 'stat', title: 'Empleados Activos', value: 156 }, - { type: 'stat', title: 'Asistencias Hoy', value: 142, percent: 91 }, - { type: 'stat', title: 'N贸mina Periodo', value: '$456K' }, - { type: 'list', title: 'Faltas Sin Justificar', items: [...] }, - { type: 'list', title: 'Altas Pendientes IMSS', items: [...] }, -] -``` - ---- - -### 7. Dashboard Post Sales (Postventa) - -**KPIs Principales:** -- Incidencias abiertas -- Garant铆as activas -- Tiempo promedio de respuesta -- Clientes satisfechos (NPS) - -**Widgets:** -```typescript -[ - { type: 'stat', title: 'Incidencias Abiertas', value: 12 }, - { type: 'stat', title: 'Garant铆as Activas', value: 34 }, - { type: 'stat', title: 'Tiempo Respuesta', value: '2.3 d铆as' }, - { type: 'list', title: 'Incidencias Urgentes', items: [...] }, - { type: 'table', title: 'Garant铆as por Vencer', rows: [...] }, -] -``` - ---- - -## Dependencias - -**Antes:** -- 鉁 US-FUND-001 (Autenticaci贸n JWT - requiere usuario autenticado) -- 鉁 US-FUND-002 (Perfil - usa foto y nombre) -- 鉁 RF-AUTH-001 (Sistema de roles - necesita roles definidos) -- 鉁 RF-AUTH-003 (Multi-tenancy - dashboard por constructora) - -**Despu茅s:** -- Punto de entrada principal del sistema despu茅s del login -- Base para navegaci贸n a m贸dulos espec铆ficos -- Todos los m贸dulos se acceden desde widgets del dashboard - ---- - -## Definici贸n de Hecho (DoD) - -- [ ] 7 endpoints de dashboard implementados (uno por rol) -- [ ] Services espec铆ficos por rol con l贸gica de KPIs -- [ ] Queries SQL optimizadas (m谩x 5 queries por dashboard) -- [ ] 7 componentes de dashboard frontend -- [ ] Widgets reutilizables (StatWidget, ChartWidget, ListWidget, TableWidget) -- [ ] Responsive design probado en 3 breakpoints -- [ ] Loading states con skeletons -- [ ] Error handling y empty states -- [ ] Tests unitarios backend (>80% coverage) -- [ ] Tests E2E para carga de dashboard por rol -- [ ] Performance: dashboard carga en <2 segundos -- [ ] Documentaci贸n Swagger de endpoints -- [ ] Code review aprobado - ---- - -## Notas del Alcance Inicial - -### Incluido en MVP 鉁 -- 鉁 7 dashboards espec铆ficos por rol -- 鉁 KPIs y m茅tricas en tiempo real -- 鉁 Widgets b谩sicos (stat, list, table) -- 鉁 Alertas y notificaciones inline -- 鉁 Acciones r谩pidas por rol -- 鉁 Selector de constructora en header -- 鉁 Responsive design - -### NO Incluido en MVP 鉂 -- 鉂 Gr谩ficas interactivas avanzadas (solo barras/l铆neas simples) -- 鉂 Filtros de tiempo (煤ltima semana, mes, a帽o) -- 鉂 Exportaci贸n de datos -- 鉂 Personalizaci贸n de widgets (drag & drop) -- 鉂 Comparativas entre proyectos/per铆odos -- 鉂 Drill-down en gr谩ficas -- 鉂 Notificaciones push en tiempo real -- 鉂 Dashboard multi-constructora (vista consolidada) - -### Extensiones Futuras 鈿狅笍 -- 鈿狅笍 **Fase 2:** Gr谩ficas avanzadas con Chart.js o Recharts -- 鈿狅笍 **Fase 2:** Dashboard personalizable (agregar/quitar widgets) -- 鈿狅笍 **Fase 2:** Exportar dashboards a PDF/Excel -- 鈿狅笍 **Fase 2:** Comparativas y an谩lisis hist贸ricos -- 鈿狅笍 **Fase 2:** Vista consolidada multi-constructora (para directores) - ---- - -## Tareas de Implementaci贸n - -### Backend (Estimado: 18h) - -**Total Backend:** 18h (~4.5 SP) - -- [ ] **Tarea B.1:** Services de dashboard por rol - Estimado: 10h - - [ ] Subtarea B.1.1: DirectorDashboardService con KPIs principales - 1.5h - - [ ] Subtarea B.1.2: EngineerDashboardService con presupuestos y programaci贸n - 1.5h - - [ ] Subtarea B.1.3: ResidentDashboardService con avances y asistencias - 1.5h - - [ ] Subtarea B.1.4: PurchasesDashboardService con OC e inventario - 1.5h - - [ ] Subtarea B.1.5: FinanceDashboardService con flujo y CxP - 1.5h - - [ ] Subtarea B.1.6: HRDashboardService con n贸mina y asistencias - 1h - - [ ] Subtarea B.1.7: PostSalesDashboardService con incidencias - 1h - -- [ ] **Tarea B.2:** Endpoints REST - Estimado: 5h - - [ ] Subtarea B.2.1: DashboardController con 7 endpoints (GET por rol) - 2h - - [ ] Subtarea B.2.2: Guard para validar que usuario tenga el rol - 1h - - [ ] Subtarea B.2.3: Serializaci贸n de respuesta consistente - 1h - - [ ] Subtarea B.2.4: Documentaci贸n Swagger completa - 1h - -- [ ] **Tarea B.3:** Optimizaci贸n y queries - Estimado: 3h - - [ ] Subtarea B.3.1: Queries SQL optimizadas con agregaciones - 1.5h - - [ ] Subtarea B.3.2: 脥ndices en base de datos - 0.5h - - [ ] Subtarea B.3.3: Caching de datos est谩ticos (logos, configuraci贸n) - 1h - -### Frontend (Estimado: 12h) - -**Total Frontend:** 12h (~3 SP) - -- [ ] **Tarea F.1:** Widgets reutilizables - Estimado: 4h - - [ ] Subtarea F.1.1: WidgetCard base con header y body - 0.5h - - [ ] Subtarea F.1.2: StatWidget (n煤mero, icono, trend) - 1h - - [ ] Subtarea F.1.3: ListWidget (lista con iconos y badges) - 1h - - [ ] Subtarea F.1.4: TableWidget (tabla b谩sica responsive) - 1h - - [ ] Subtarea F.1.5: ChartWidget con Recharts (barras/l铆neas simples) - 0.5h - -- [ ] **Tarea F.2:** Dashboards espec铆ficos - Estimado: 6h - - [ ] Subtarea F.2.1: DashboardContainer con l贸gica de routing por rol - 1h - - [ ] Subtarea F.2.2: DirectorDashboard con 6 widgets - 1h - - [ ] Subtarea F.2.3: EngineerDashboard con 5 widgets - 0.75h - - [ ] Subtarea F.2.4: ResidentDashboard con 5 widgets - 0.75h - - [ ] Subtarea F.2.5: PurchasesDashboard con 5 widgets - 0.75h - - [ ] Subtarea F.2.6: FinanceDashboard con 5 widgets - 0.75h - - [ ] Subtarea F.2.7: HR y PostSales Dashboards - 1h - -- [ ] **Tarea F.3:** Layout y componentes compartidos - Estimado: 2h - - [ ] Subtarea F.3.1: DashboardHeader con saludo y selector - 0.75h - - [ ] Subtarea F.3.2: AlertsPanel lateral - 0.5h - - [ ] Subtarea F.3.3: QuickActionsBar - 0.5h - - [ ] Subtarea F.3.4: EmptyState gen茅rico - 0.25h - -### Testing (Estimado: 4h) - -**Total Testing:** 4h (~1 SP) - -- [ ] **Tarea T.1:** Tests unitarios backend - Estimado: 2h - - [ ] Subtarea T.1.1: Tests de DirectorDashboardService (KPIs) - 0.5h - - [ ] Subtarea T.1.2: Tests de otros services (sampling) - 1h - - [ ] Subtarea T.1.3: Tests de DashboardController - 0.5h - -- [ ] **Tarea T.2:** Tests E2E - Estimado: 1.5h - - [ ] Subtarea T.2.1: Tests de carga de dashboard por rol - 1h - - [ ] Subtarea T.2.2: Tests de cambio de constructora (recarga dashboard) - 0.5h - -- [ ] **Tarea T.3:** Tests frontend - Estimado: 0.5h - - [ ] Subtarea T.3.1: Tests de widgets reutilizables - 0.25h - - [ ] Subtarea T.3.2: Tests de DashboardContainer - 0.25h - ---- - -## Resumen de Horas - -| Categor铆a | Estimado | Story Points | -|-----------|----------|--------------| -| Backend | 18h | 4.5 SP | -| Frontend | 12h | 3 SP | -| Testing | 4h | 1 SP | -| **TOTAL** | **34h** | **8.5 SP 鈮 8 SP** | - -**Ajuste:** Se redondea a 8 SP considerando reutilizaci贸n de componentes. - ---- - -## Cronograma Propuesto - -**Sprint:** Sprint 2-3 (Semanas 2-3) -**Duraci贸n:** 4.5 d铆as -**Equipo:** -- 1 Backend developer (18h) -- 1 Frontend developer (12h) -- QA compartido (4h) - -**Hitos:** -- D铆as 1-2: Services backend + endpoints -- D铆as 3-4: Widgets y dashboards frontend -- D铆a 4.5: Testing + ajustes - ---- - -## Testing - -### Tests E2E - -```typescript -describe('Dashboard API (E2E)', () => { - describe('Director Dashboard', () => { - it('should return KPIs for director role', async () => { - const director = await createUser({ role: 'director' }); - const token = await getAuthToken(director); - - const response = await request(app.getHttpServer()) - .get('/dashboard/director') - .set('Authorization', `Bearer ${token}`) - .expect(200); - - expect(response.body.widgets).toBeArray(); - expect(response.body.widgets[0]).toHaveProperty('type', 'stat'); - expect(response.body.widgets[0]).toHaveProperty('title', 'Proyectos Activos'); - }); - - it('should reject non-director users', async () => { - const engineer = await createUser({ role: 'engineer' }); - const token = await getAuthToken(engineer); - - await request(app.getHttpServer()) - .get('/dashboard/director') - .set('Authorization', `Bearer ${token}`) - .expect(403); // Forbidden - }); - }); - - describe('Multi-tenancy', () => { - it('should show different data when switching constructora', async () => { - const user = await createUser(); - await assignToConstructora(user.id, 'constructora-a'); - await assignToConstructora(user.id, 'constructora-b'); - - // Dashboard en constructora A - const tokenA = await getAuthToken(user, 'constructora-a'); - const responseA = await request(app.getHttpServer()) - .get('/dashboard/director') - .set('Authorization', `Bearer ${tokenA}`) - .expect(200); - - // Dashboard en constructora B - const tokenB = await getAuthToken(user, 'constructora-b'); - const responseB = await request(app.getHttpServer()) - .get('/dashboard/director') - .set('Authorization', `Bearer ${tokenB}`) - .expect(200); - - // Datos deben ser diferentes - expect(responseA.body.constructora.id).toBe('constructora-a'); - expect(responseB.body.constructora.id).toBe('constructora-b'); - expect(responseA.body.widgets[0].value).not.toBe(responseB.body.widgets[0].value); - }); - }); -}); -``` - ---- - -## Estimaci贸n - -**Desglose de Esfuerzo (8 SP = ~4 d铆as = 32h):** -- Backend services (7 roles): 2.5 d铆as (18h) -- Frontend widgets + dashboards: 1.5 d铆as (12h) -- Testing: 0.5 d铆as (4h) -- Ajustes: -0.5 d铆as (reutilizaci贸n) - -**Riesgos:** -- 鈿狅笍 Complejidad de queries SQL para KPIs (m煤ltiples joins, agregaciones) -- 鈿狅笍 Performance en constructoras con muchos proyectos (>100) -- 鈿狅笍 Mantenimiento de 7 dashboards diferentes (DRY) - -**Mitigaciones:** -- 鉁 Reutilizar widgets entre dashboards -- 鉁 Optimizar queries con 铆ndices y caching -- 鉁 Lazy loading de widgets no cr铆ticos -- 鉁 Tests de performance con datos de producci贸n simulados - ---- - -## Recursos Externos - -**Librer铆as Frontend:** -- `recharts` (gr谩ficas simples - barras, l铆neas, 谩reas) -- `react-icons` (iconos para widgets) -- `date-fns` (formateo de fechas) - -**Referencias de Dise帽o:** -- Tailwind UI Dashboard components -- Material-UI Dashboard examples - ---- - -**Creado:** 2025-11-17 -**Actualizado:** 2025-11-17 -**Responsable:** Equipo Fullstack -**Reutilizaci贸n GAMILIT:** 50% (estructura de dashboard, adaptado a 7 roles vs 2) diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-004-infraestructura-base.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-004-infraestructura-base.md deleted file mode 100644 index 322fe2a19..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-004-infraestructura-base.md +++ /dev/null @@ -1,1612 +0,0 @@ -# US-FUND-004: Infraestructura T茅cnica Base - -**Epic:** MAI-001 - Fundamentos del Sistema -**Story Points:** 12 -**Prioridad:** Alta -**Dependencias:** -- Ninguna (es la base del proyecto) - -**Estado:** Pendiente -**Asignado a:** DevOps + Backend Lead + Frontend Lead - ---- - -## 馃搵 Historia de Usuario - -**Como** equipo de desarrollo -**Quiero** tener configurada toda la infraestructura t茅cnica base del proyecto -**Para** poder comenzar a desarrollar las funcionalidades del sistema de construcci贸n sobre una base s贸lida, escalable y mantenible. - ---- - -## 馃幆 Contexto y Objetivos - -### Contexto - -Esta historia cubre la configuraci贸n inicial completa del proyecto antes de comenzar el desarrollo de funcionalidades. Incluye: - -- **Base de datos PostgreSQL** con schemas organizados -- **Backend NestJS** con estructura modular -- **Frontend React + Vite** con TypeScript -- **Herramientas de desarrollo** (linting, formatting, testing) -- **CI/CD pipeline** b谩sico -- **Docker containers** para desarrollo local - -### Objetivos - -1. 鉁 Proyecto ejecutable localmente en < 5 minutos (para nuevos devs) -2. 鉁 Estructura modular lista para escalar -3. 鉁 Database migrations autom谩ticas -4. 鉁 Hot reload en desarrollo (backend y frontend) -5. 鉁 Code quality garantizada (pre-commit hooks) -6. 鉁 Tests automatizados en CI/CD - ---- - -## 鉁 Criterios de Aceptaci贸n - -### CA-1: Database Setup - -**Dado** un PostgreSQL 15+ instalado localmente o en Docker -**Cuando** ejecuto el script de setup inicial -**Entonces**: - -- 鉁 Se crea la base de datos `erp_construccion` -- 鉁 Se crean los schemas: `auth_management`, `projects`, `budgets`, `purchases`, `hr`, `gamification_system` -- 鉁 Se ejecutan todas las migraciones iniciales -- 鉁 Se crean las funciones de utilidad (get_current_constructora_id, etc.) -- 鉁 Se habilita RLS en todas las tablas de negocio - -### CA-2: Backend Structure - -**Dado** el proyecto de backend -**Cuando** examino la estructura de carpetas -**Entonces**: - -- 鉁 Existe una estructura modular clara: - ``` - apps/backend/src/ - 鈹溾攢鈹 modules/ - 鈹 鈹溾攢鈹 auth/ - 鈹 鈹溾攢鈹 users/ - 鈹 鈹溾攢鈹 constructoras/ - 鈹 鈹溾攢鈹 projects/ - 鈹 鈹斺攢鈹 ... (m贸dulos por dominio) - 鈹溾攢鈹 common/ - 鈹 鈹溾攢鈹 guards/ - 鈹 鈹溾攢鈹 decorators/ - 鈹 鈹溾攢鈹 interceptors/ - 鈹 鈹斺攢鈹 filters/ - 鈹溾攢鈹 config/ - 鈹 鈹溾攢鈹 database.config.ts - 鈹 鈹溾攢鈹 jwt.config.ts - 鈹 鈹斺攢鈹 ... - 鈹斺攢鈹 main.ts - ``` -- 鉁 Cada m贸dulo sigue el patr贸n: `module`, `controller`, `service`, `entity`, `dto` -- 鉁 TypeORM configurado con migrations autom谩ticas -- 鉁 Swagger UI disponible en `/api/docs` - -### CA-3: Frontend Structure - -**Dado** el proyecto de frontend -**Cuando** examino la estructura de carpetas -**Entonces**: - -- 鉁 Existe una estructura clara: - ``` - apps/frontend/src/ - 鈹溾攢鈹 features/ - 鈹 鈹溾攢鈹 auth/ - 鈹 鈹溾攢鈹 dashboard/ - 鈹 鈹溾攢鈹 projects/ - 鈹 鈹斺攢鈹 ... (features por m贸dulo) - 鈹溾攢鈹 components/ - 鈹 鈹溾攢鈹 ui/ (componentes reutilizables) - 鈹 鈹斺攢鈹 layout/ - 鈹溾攢鈹 stores/ (Zustand stores) - 鈹溾攢鈹 services/ (API clients) - 鈹溾攢鈹 hooks/ - 鈹溾攢鈹 utils/ - 鈹斺攢鈹 App.tsx - ``` -- 鉁 Vite configurado con hot reload -- 鉁 TypeScript strict mode habilitado -- 鉁 Path aliases configurados (`@/components`, `@/features`, etc.) - -### CA-4: Development Tools - -**Dado** el proyecto completo -**Cuando** un nuevo desarrollador clona el repo -**Entonces**: - -- 鉁 `npm install` instala todas las dependencias -- 鉁 `npm run dev` levanta backend + frontend + database -- 鉁 ESLint + Prettier configurados y funcionando -- 鉁 Husky pre-commit hooks ejecutan lint + format -- 鉁 Tests pueden ejecutarse con `npm test` - -### CA-5: Docker Setup - -**Dado** Docker instalado en el sistema -**Cuando** ejecuto `docker-compose up` -**Entonces**: - -- 鉁 Se levanta PostgreSQL en puerto 5432 -- 鉁 Se levanta backend en puerto 3000 -- 鉁 Se levanta frontend en puerto 5173 -- 鉁 Hot reload funciona dentro de los containers -- 鉁 Migrations se ejecutan autom谩ticamente al iniciar backend - -### CA-6: CI/CD Pipeline - -**Dado** un commit pusheado a GitHub -**Cuando** se activa el pipeline de CI -**Entonces**: - -- 鉁 Se ejecutan linters (ESLint) -- 鉁 Se ejecutan formatters (Prettier) -- 鉁 Se ejecutan tests unitarios -- 鉁 Se ejecutan tests de integraci贸n -- 鉁 Se genera reporte de cobertura -- 鉁 El pipeline falla si cualquier check no pasa - ---- - -## 馃敡 Especificaci贸n T茅cnica Detallada - -### 1. Database Setup - -#### Script de Inicializaci贸n - -**Archivo:** `apps/database/scripts/init-database.sh` - -```bash -#!/bin/bash - -# Configuraci贸n -DB_NAME="gamilit_construction" -DB_USER="gamilit_user" -DB_PASSWORD="secure_password_here" -DB_HOST="localhost" -DB_PORT="5432" - -# Crear base de datos -psql -U postgres -h $DB_HOST -p $DB_PORT < { - await queryRunner.query(` - CREATE TABLE auth_management.constructoras ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - name VARCHAR(255) NOT NULL, - rfc VARCHAR(13) UNIQUE NOT NULL, - business_name VARCHAR(255) NOT NULL, - phone VARCHAR(20), - email VARCHAR(255), - address TEXT, - city VARCHAR(100), - state VARCHAR(100), - country VARCHAR(100) DEFAULT 'M茅xico', - postal_code VARCHAR(10), - logo_url TEXT, - settings JSONB DEFAULT '{}'::jsonb, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - deleted_at TIMESTAMPTZ - ); - - -- 脥ndices - CREATE INDEX idx_constructoras_rfc ON auth_management.constructoras(rfc); - CREATE INDEX idx_constructoras_is_active ON auth_management.constructoras(is_active) WHERE deleted_at IS NULL; - - -- Trigger de actualizaci贸n - CREATE TRIGGER set_constructoras_updated_at - BEFORE UPDATE ON auth_management.constructoras - FOR EACH ROW - EXECUTE FUNCTION auth_management.update_updated_at_column(); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE auth_management.constructoras CASCADE;`); - } -} -``` - ---- - -### 2. Backend NestJS - Estructura - -#### Main Application Bootstrap - -**Archivo:** `apps/backend/src/main.ts` - -```typescript -import { NestFactory } from '@nestjs/core'; -import { ValidationPipe, VersioningType } from '@nestjs/common'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import { ConfigService } from '@nestjs/config'; -import helmet from 'helmet'; -import * as compression from 'compression'; -import { AppModule } from './app.module'; -import { HttpExceptionFilter } from './common/filters/http-exception.filter'; -import { TransformInterceptor } from './common/interceptors/transform.interceptor'; -import { LoggingInterceptor } from './common/interceptors/logging.interceptor'; - -async function bootstrap() { - const app = await NestFactory.create(AppModule, { - logger: ['error', 'warn', 'log', 'debug', 'verbose'], - }); - - const configService = app.get(ConfigService); - - // Security - app.use(helmet()); - app.enableCors({ - origin: configService.get('CORS_ORIGINS')?.split(',') || ['http://localhost:5173'], - credentials: true, - }); - - // Compression - app.use(compression()); - - // Global prefix - app.setGlobalPrefix('api'); - - // API Versioning - app.enableVersioning({ - type: VersioningType.URI, - defaultVersion: '1', - }); - - // Global pipes - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - forbidNonWhitelisted: true, - transform: true, - transformOptions: { - enableImplicitConversion: true, - }, - }), - ); - - // Global filters - app.useGlobalFilters(new HttpExceptionFilter()); - - // Global interceptors - app.useGlobalInterceptors( - new LoggingInterceptor(), - new TransformInterceptor(), - ); - - // Swagger documentation - if (configService.get('NODE_ENV') !== 'production') { - const config = new DocumentBuilder() - .setTitle('Sistema de Gesti贸n de Obra - API') - .setDescription('API RESTful para gesti贸n integral de proyectos de construcci贸n') - .setVersion('1.0') - .addBearerAuth( - { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - name: 'JWT', - description: 'Enter JWT token', - in: 'header', - }, - 'JWT-auth', - ) - .addTag('Auth', 'Autenticaci贸n y autorizaci贸n') - .addTag('Users', 'Gesti贸n de usuarios') - .addTag('Constructoras', 'Gesti贸n de constructoras (multi-tenancy)') - .addTag('Projects', 'Gesti贸n de proyectos') - .addTag('Budgets', 'Gesti贸n de presupuestos') - .addTag('Purchases', 'Gesti贸n de compras y proveedores') - .addTag('HR', 'Recursos humanos y asistencias') - .addTag('Finance', 'Finanzas y tesorer铆a') - .addTag('Post-Sales', 'Post-venta y garant铆as') - .build(); - - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api/docs', app, document); - } - - const port = configService.get('PORT') || 3000; - await app.listen(port); - - console.log(`馃殌 Application is running on: http://localhost:${port}/api`); - console.log(`馃摎 Swagger docs available at: http://localhost:${port}/api/docs`); -} - -bootstrap(); -``` - -#### Database Configuration - -**Archivo:** `apps/backend/src/config/database.config.ts` - -```typescript -import { TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { ConfigService } from '@nestjs/config'; - -export const getDatabaseConfig = ( - configService: ConfigService, -): TypeOrmModuleOptions => ({ - type: 'postgres', - host: configService.get('DB_HOST', 'localhost'), - port: configService.get('DB_PORT', 5432), - username: configService.get('DB_USERNAME', 'gamilit_user'), - password: configService.get('DB_PASSWORD'), - database: configService.get('DB_DATABASE', 'gamilit_construction'), - entities: [__dirname + '/../**/*.entity{.ts,.js}'], - migrations: [__dirname + '/../migrations/*{.ts,.js}'], - synchronize: false, // SIEMPRE false en producci贸n - logging: configService.get('NODE_ENV') === 'development' ? ['query', 'error'] : ['error'], - migrationsRun: true, // Auto-run migrations on startup - ssl: configService.get('DB_SSL') === 'true' ? { rejectUnauthorized: false } : false, - extra: { - max: 20, // Max pool size - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, - }, -}); -``` - -#### App Module - -**Archivo:** `apps/backend/src/app.module.ts` - -```typescript -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ThrottlerModule } from '@nestjs/throttler'; -import { ScheduleModule } from '@nestjs/schedule'; -import { EventEmitterModule } from '@nestjs/event-emitter'; - -import { getDatabaseConfig } from './config/database.config'; - -// Modules -import { AuthModule } from './modules/auth/auth.module'; -import { UsersModule } from './modules/users/users.module'; -import { ConstructorasModule } from './modules/constructoras/constructoras.module'; -import { ProjectsModule } from './modules/projects/projects.module'; -import { BudgetsModule } from './modules/budgets/budgets.module'; -import { PurchasesModule } from './modules/purchases/purchases.module'; -import { HrModule } from './modules/hr/hr.module'; -import { FinanceModule } from './modules/finance/finance.module'; -import { PostSalesModule } from './modules/post-sales/post-sales.module'; -import { DashboardModule } from './modules/dashboard/dashboard.module'; -import { NotificationsModule } from './modules/notifications/notifications.module'; - -// Common -import { APP_GUARD } from '@nestjs/core'; -import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; - -@Module({ - imports: [ - // Configuration - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: `.env.${process.env.NODE_ENV || 'development'}`, - }), - - // Database - TypeOrmModule.forRootAsync({ - inject: [ConfigService], - useFactory: getDatabaseConfig, - }), - - // Rate limiting - ThrottlerModule.forRoot([ - { - ttl: 60000, // 1 minuto - limit: 100, // 100 requests por minuto - }, - ]), - - // Scheduled tasks - ScheduleModule.forRoot(), - - // Event emitter - EventEmitterModule.forRoot(), - - // Feature modules - AuthModule, - UsersModule, - ConstructorasModule, - ProjectsModule, - BudgetsModule, - PurchasesModule, - HrModule, - FinanceModule, - PostSalesModule, - DashboardModule, - NotificationsModule, - ], - providers: [ - // Global JWT guard - { - provide: APP_GUARD, - useClass: JwtAuthGuard, - }, - ], -}) -export class AppModule {} -``` - ---- - -### 3. Frontend React + Vite - Estructura - -#### Vite Configuration - -**Archivo:** `apps/frontend/vite.config.ts` - -```typescript -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - '@/components': path.resolve(__dirname, './src/components'), - '@/features': path.resolve(__dirname, './src/features'), - '@/stores': path.resolve(__dirname, './src/stores'), - '@/services': path.resolve(__dirname, './src/services'), - '@/hooks': path.resolve(__dirname, './src/hooks'), - '@/utils': path.resolve(__dirname, './src/utils'), - '@/types': path.resolve(__dirname, './src/types'), - }, - }, - server: { - port: 5173, - host: true, - strictPort: true, - proxy: { - '/api': { - target: 'http://localhost:3000', - changeOrigin: true, - }, - }, - }, - build: { - outDir: 'dist', - sourcemap: true, - rollupOptions: { - output: { - manualChunks: { - 'react-vendor': ['react', 'react-dom', 'react-router-dom'], - 'zustand-vendor': ['zustand'], - 'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'], - }, - }, - }, - }, -}); -``` - -#### TypeScript Configuration - -**Archivo:** `apps/frontend/tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - /* Path aliases */ - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"], - "@/components/*": ["./src/components/*"], - "@/features/*": ["./src/features/*"], - "@/stores/*": ["./src/stores/*"], - "@/services/*": ["./src/services/*"], - "@/hooks/*": ["./src/hooks/*"], - "@/utils/*": ["./src/utils/*"], - "@/types/*": ["./src/types/*"] - } - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} -``` - -#### Main App Component - -**Archivo:** `apps/frontend/src/App.tsx` - -```typescript -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { Toaster } from 'sonner'; - -// Layouts -import { AuthLayout } from '@/components/layout/AuthLayout'; -import { DashboardLayout } from '@/components/layout/DashboardLayout'; - -// Features -import { LoginPage } from '@/features/auth/pages/LoginPage'; -import { RegisterPage } from '@/features/auth/pages/RegisterPage'; -import { DashboardPage } from '@/features/dashboard/pages/DashboardPage'; -import { ProjectsPage } from '@/features/projects/pages/ProjectsPage'; -import { BudgetsPage } from '@/features/budgets/pages/BudgetsPage'; - -// Guards -import { ProtectedRoute } from '@/components/guards/ProtectedRoute'; -import { RoleGuard } from '@/components/guards/RoleGuard'; - -// Create query client -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 60 * 1000, // 5 minutos - retry: 1, - refetchOnWindowFocus: false, - }, - }, -}); - -function App() { - return ( - - - - {/* Public routes */} - }> - } /> - } /> - - - {/* Protected routes */} - - - - } - > - } /> - - {/* Projects - Director, Engineer, Resident */} - - - - } - /> - - {/* Budgets - Director, Engineer */} - - - - } - /> - - {/* More routes... */} - - - {/* Redirect */} - } /> - } /> - - - - {/* Toasts */} - - - {/* React Query Devtools */} - - - ); -} - -export default App; -``` - -#### API Client Service - -**Archivo:** `apps/frontend/src/services/api.service.ts` - -```typescript -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; -import { toast } from 'sonner'; - -class ApiService { - private client: AxiosInstance; - - constructor() { - this.client = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api', - timeout: 30000, - headers: { - 'Content-Type': 'application/json', - }, - }); - - this.setupInterceptors(); - } - - private setupInterceptors() { - // Request interceptor - Agregar token - this.client.interceptors.request.use( - (config) => { - const token = localStorage.getItem('accessToken'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => Promise.reject(error), - ); - - // Response interceptor - Manejo de errores - this.client.interceptors.response.use( - (response) => response, - async (error) => { - if (error.response?.status === 401) { - // Token expirado o inv谩lido - localStorage.removeItem('accessToken'); - localStorage.removeItem('constructora-storage'); - window.location.href = '/login'; - toast.error('Sesi贸n expirada. Por favor inicia sesi贸n nuevamente.'); - } else if (error.response?.status === 403) { - toast.error('No tienes permisos para realizar esta acci贸n.'); - } else if (error.response?.status >= 500) { - toast.error('Error del servidor. Intenta nuevamente m谩s tarde.'); - } - - return Promise.reject(error); - }, - ); - } - - async get(url: string, config?: AxiosRequestConfig): Promise { - const response: AxiosResponse = await this.client.get(url, config); - return response.data; - } - - async post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - const response: AxiosResponse = await this.client.post(url, data, config); - return response.data; - } - - async put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - const response: AxiosResponse = await this.client.put(url, data, config); - return response.data; - } - - async patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - const response: AxiosResponse = await this.client.patch(url, data, config); - return response.data; - } - - async delete(url: string, config?: AxiosRequestConfig): Promise { - const response: AxiosResponse = await this.client.delete(url, config); - return response.data; - } -} - -export const apiService = new ApiService(); -``` - ---- - -### 4. Docker Setup - -#### Docker Compose - -**Archivo:** `docker-compose.yml` - -```yaml -version: '3.8' - -services: - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: gamilit-construction-db - restart: unless-stopped - environment: - POSTGRES_USER: gamilit_user - POSTGRES_PASSWORD: secure_password_here - POSTGRES_DB: gamilit_construction - PGDATA: /var/lib/postgresql/data/pgdata - ports: - - '5432:5432' - volumes: - - postgres_data:/var/lib/postgresql/data - - ./apps/database/scripts:/docker-entrypoint-initdb.d - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U gamilit_user -d gamilit_construction'] - interval: 10s - timeout: 5s - retries: 5 - - # Backend NestJS - backend: - build: - context: . - dockerfile: ./apps/backend/Dockerfile - target: development - container_name: gamilit-construction-backend - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - environment: - NODE_ENV: development - PORT: 3000 - DB_HOST: postgres - DB_PORT: 5432 - DB_USERNAME: gamilit_user - DB_PASSWORD: secure_password_here - DB_DATABASE: gamilit_construction - JWT_SECRET: your_jwt_secret_key_here - JWT_EXPIRES_IN: 15m - ports: - - '3000:3000' - volumes: - - ./apps/backend:/app/apps/backend - - /app/apps/backend/node_modules - - /app/node_modules - command: npm run start:dev - - # Frontend React - frontend: - build: - context: . - dockerfile: ./apps/frontend/Dockerfile - target: development - container_name: gamilit-construction-frontend - restart: unless-stopped - depends_on: - - backend - environment: - VITE_API_URL: http://localhost:3000/api - ports: - - '5173:5173' - volumes: - - ./apps/frontend:/app/apps/frontend - - /app/apps/frontend/node_modules - - /app/node_modules - command: npm run dev - -volumes: - postgres_data: - driver: local -``` - -#### Backend Dockerfile - -**Archivo:** `apps/backend/Dockerfile` - -```dockerfile -# Development stage -FROM node:20-alpine AS development - -WORKDIR /app - -# Copy package files -COPY package*.json ./ -COPY apps/backend/package*.json ./apps/backend/ - -# Install dependencies -RUN npm ci - -# Copy source code -COPY . . - -# Expose port -EXPOSE 3000 - -# Start development server -CMD ["npm", "run", "start:dev"] - -# =============================== - -# Production build stage -FROM node:20-alpine AS build - -WORKDIR /app - -COPY package*.json ./ -COPY apps/backend/package*.json ./apps/backend/ - -RUN npm ci --only=production - -COPY . . - -RUN npm run build - -# =============================== - -# Production stage -FROM node:20-alpine AS production - -WORKDIR /app - -ENV NODE_ENV=production - -COPY --from=build /app/dist ./dist -COPY --from=build /app/node_modules ./node_modules -COPY --from=build /app/package*.json ./ - -EXPOSE 3000 - -CMD ["node", "dist/main"] -``` - ---- - -### 5. Code Quality Tools - -#### ESLint Configuration - -**Archivo:** `.eslintrc.json` - -```json -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "sourceType": "module" - }, - "plugins": ["@typescript-eslint", "import", "prettier"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - "prettier" - ], - "rules": { - "@typescript-eslint/interface-name-prefix": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_" - } - ], - "import/order": [ - "error", - { - "groups": [ - "builtin", - "external", - "internal", - "parent", - "sibling", - "index" - ], - "newlines-between": "always", - "alphabetize": { - "order": "asc", - "caseInsensitive": true - } - } - ], - "prettier/prettier": "error" - }, - "settings": { - "import/resolver": { - "typescript": { - "alwaysTryTypes": true - } - } - } -} -``` - -#### Prettier Configuration - -**Archivo:** `.prettierrc` - -```json -{ - "semi": true, - "trailingComma": "all", - "singleQuote": true, - "printWidth": 100, - "tabWidth": 2, - "arrowParens": "always", - "endOfLine": "lf" -} -``` - -#### Husky Pre-commit Hook - -**Archivo:** `.husky/pre-commit` - -```bash -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -# Run lint-staged -npx lint-staged - -# Run type check -npm run type-check -``` - -**Archivo:** `package.json` (lint-staged config) - -```json -{ - "lint-staged": { - "*.{ts,tsx}": [ - "eslint --fix", - "prettier --write" - ], - "*.{json,md,yml,yaml}": [ - "prettier --write" - ] - } -} -``` - ---- - -### 6. CI/CD Pipeline - -#### GitHub Actions Workflow - -**Archivo:** `.github/workflows/ci.yml` - -```yaml -name: CI Pipeline - -on: - push: - branches: [main, develop] - pull_request: - branches: [main, develop] - -jobs: - lint-and-test: - name: Lint and Test - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.x] - - services: - postgres: - image: postgres:15 - env: - POSTGRES_USER: gamilit_user - POSTGRES_PASSWORD: test_password - POSTGRES_DB: gamilit_construction_test - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run ESLint - run: npm run lint - - - name: Run Prettier check - run: npm run format:check - - - name: Run TypeScript type check - run: npm run type-check - - - name: Run backend unit tests - run: npm run test:backend - env: - DB_HOST: localhost - DB_PORT: 5432 - DB_USERNAME: gamilit_user - DB_PASSWORD: test_password - DB_DATABASE: gamilit_construction_test - JWT_SECRET: test_secret_key - NODE_ENV: test - - - name: Run frontend tests - run: npm run test:frontend - - - name: Run e2e tests - run: npm run test:e2e - env: - DB_HOST: localhost - DB_PORT: 5432 - DB_USERNAME: gamilit_user - DB_PASSWORD: test_password - DB_DATABASE: gamilit_construction_test - JWT_SECRET: test_secret_key - NODE_ENV: test - - - name: Generate coverage report - run: npm run test:cov - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: ./coverage/lcov.info - flags: unittests - name: codecov-umbrella - - build: - name: Build - runs-on: ubuntu-latest - needs: lint-and-test - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20.x - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build backend - run: npm run build:backend - - - name: Build frontend - run: npm run build:frontend - - - name: Archive production artifacts - uses: actions/upload-artifact@v3 - with: - name: build-artifacts - path: | - apps/backend/dist - apps/frontend/dist -``` - ---- - -## 馃И Test Cases - -### TC-INFRA-001: Instalaci贸n Limpia - -**Pre-condiciones:** -- Sistema con Node.js 20+ y Docker instalados -- Puerto 5432, 3000, 5173 disponibles - -**Pasos:** -1. Clonar repositorio: `git clone ` -2. Ejecutar: `npm install` -3. Ejecutar: `docker-compose up -d postgres` -4. Esperar a que PostgreSQL est茅 healthy -5. Ejecutar: `npm run migrate` -6. Ejecutar: `npm run dev` - -**Resultado esperado:** -- 鉁 Backend corriendo en http://localhost:3000 -- 鉁 Frontend corriendo en http://localhost:5173 -- 鉁 Swagger docs en http://localhost:3000/api/docs -- 鉁 No hay errores en consola - ---- - -### TC-INFRA-002: Hot Reload Backend - -**Pre-condiciones:** -- Backend corriendo en modo desarrollo - -**Pasos:** -1. Abrir archivo `apps/backend/src/modules/users/users.controller.ts` -2. Modificar el mensaje de retorno de un endpoint -3. Guardar archivo -4. Observar la consola del backend - -**Resultado esperado:** -- 鉁 NestJS detecta el cambio -- 鉁 Recompila autom谩ticamente -- 鉁 Servidor se reinicia en < 3 segundos -- 鉁 Endpoint refleja el cambio sin reinicio manual - ---- - -### TC-INFRA-003: Hot Reload Frontend - -**Pre-condiciones:** -- Frontend corriendo en modo desarrollo - -**Pasos:** -1. Abrir archivo `apps/frontend/src/features/dashboard/pages/DashboardPage.tsx` -2. Modificar un texto en el componente -3. Guardar archivo -4. Observar el navegador - -**Resultado esperado:** -- 鉁 Vite detecta el cambio -- 鉁 Hot Module Replacement (HMR) se ejecuta -- 鉁 Componente se actualiza sin recargar la p谩gina -- 鉁 El cambio es visible instant谩neamente - ---- - -### TC-INFRA-004: Pre-commit Hooks - -**Pre-condiciones:** -- Husky instalado y configurado -- Archivo con errores de lint - -**Pasos:** -1. Modificar un archivo TypeScript introduciendo errores de lint: - ```typescript - // Agregar variable no utilizada - const unusedVar = 'test'; - - // Agregar l铆nea sin punto y coma - const x = 5 - ``` -2. Stage el archivo: `git add .` -3. Intentar commit: `git commit -m "Test commit"` - -**Resultado esperado:** -- 鉁 Pre-commit hook se ejecuta -- 鉁 ESLint detecta errores -- 鉁 El commit es rechazado -- 鉁 Se muestra mensaje con los errores encontrados - ---- - -### TC-INFRA-005: Database Migrations - -**Pre-condiciones:** -- PostgreSQL corriendo -- Backend configurado - -**Pasos:** -1. Crear nueva migraci贸n: `npm run migration:create CreateTestTable` -2. Implementar migraci贸n: - ```typescript - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE test ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) - ); - `); - } - ``` -3. Ejecutar: `npm run migration:run` -4. Conectar a la base de datos y verificar - -**Resultado esperado:** -- 鉁 Migraci贸n se crea correctamente -- 鉁 Migraci贸n se ejecuta sin errores -- 鉁 Tabla `test` existe en la base de datos -- 鉁 Entry en tabla `migrations` registra la ejecuci贸n - ---- - -### TC-INFRA-006: CI Pipeline - -**Pre-condiciones:** -- C贸digo pusheado a GitHub -- GitHub Actions configurado - -**Pasos:** -1. Crear PR con cambios -2. Observar GitHub Actions - -**Resultado esperado:** -- 鉁 Pipeline de CI se ejecuta autom谩ticamente -- 鉁 Job de lint pasa -- 鉁 Job de type-check pasa -- 鉁 Job de tests pasa -- 鉁 Job de build pasa -- 鉁 Reporte de cobertura se genera -- 鉁 PR muestra status check verde - ---- - -### TC-INFRA-007: RLS Context Injection - -**Pre-condiciones:** -- Backend corriendo -- Usuario autenticado -- Constructora activa - -**Pasos:** -1. Hacer request a cualquier endpoint protegido con token JWT v谩lido -2. Inspeccionar logs de PostgreSQL (query log habilitado) -3. Verificar que se ejecuten los `set_config` antes de la query principal - -**Resultado esperado:** -- 鉁 `set_config('app.current_user_id', '...', true)` se ejecuta -- 鉁 `set_config('app.current_constructora_id', '...', true)` se ejecuta -- 鉁 `set_config('app.current_user_role', '...', true)` se ejecuta -- 鉁 Query principal usa estas variables en RLS policies - ---- - -### TC-INFRA-008: Swagger Documentation - -**Pre-condiciones:** -- Backend corriendo en modo desarrollo - -**Pasos:** -1. Abrir navegador en http://localhost:3000/api/docs -2. Explorar endpoints documentados -3. Intentar ejecutar endpoint `/api/auth/login` desde Swagger UI - -**Resultado esperado:** -- 鉁 Swagger UI carga correctamente -- 鉁 Todos los m贸dulos est谩n listados con sus tags -- 鉁 Schemas de DTOs est谩n documentados -- 鉁 Es posible ejecutar requests desde la UI -- 鉁 Bearer token puede configurarse para endpoints protegidos - ---- - -## 馃搵 Tareas de Implementaci贸n - -### Sprint 0 - Semana 1 - -#### Backend - -- [ ] **INFRA-BE-001:** Crear estructura de carpetas del proyecto NestJS - - Estimado: 1h - - Asignado a: Backend Lead - -- [ ] **INFRA-BE-002:** Configurar TypeORM con migraciones - - Estimado: 2h - - Asignado a: Backend Lead - -- [ ] **INFRA-BE-003:** Crear script de inicializaci贸n de base de datos - - Estimado: 2h - - Asignado a: DevOps - -- [ ] **INFRA-BE-004:** Implementar main.ts con configuraci贸n completa - - Estimado: 2h - - Asignado a: Backend Lead - -- [ ] **INFRA-BE-005:** Crear m贸dulos base (Auth, Users, Constructoras) - - Estimado: 3h - - Asignado a: Backend Dev - -- [ ] **INFRA-BE-006:** Configurar Swagger documentation - - Estimado: 1h - - Asignado a: Backend Dev - -- [ ] **INFRA-BE-007:** Implementar guards globales (JWT, Roles) - - Estimado: 2h - - Asignado a: Backend Dev - -- [ ] **INFRA-BE-008:** Crear interceptors (Logging, Transform, RLS Context) - - Estimado: 2h - - Asignado a: Backend Dev - -#### Frontend - -- [ ] **INFRA-FE-001:** Crear estructura de carpetas del proyecto React - - Estimado: 1h - - Asignado a: Frontend Lead - -- [ ] **INFRA-FE-002:** Configurar Vite con path aliases - - Estimado: 1h - - Asignado a: Frontend Lead - -- [ ] **INFRA-FE-003:** Configurar React Router con layouts - - Estimado: 2h - - Asignado a: Frontend Dev - -- [ ] **INFRA-FE-004:** Implementar API service con interceptors - - Estimado: 2h - - Asignado a: Frontend Dev - -- [ ] **INFRA-FE-005:** Crear guards de navegaci贸n (ProtectedRoute, RoleGuard) - - Estimado: 2h - - Asignado a: Frontend Dev - -- [ ] **INFRA-FE-006:** Configurar React Query - - Estimado: 1h - - Asignado a: Frontend Dev - -- [ ] **INFRA-FE-007:** Crear componentes de layout base - - Estimado: 2h - - Asignado a: Frontend Dev - -#### DevOps - -- [ ] **INFRA-DO-001:** Configurar Docker Compose para desarrollo - - Estimado: 2h - - Asignado a: DevOps - -- [ ] **INFRA-DO-002:** Crear Dockerfiles (backend y frontend) - - Estimado: 2h - - Asignado a: DevOps - -- [ ] **INFRA-DO-003:** Configurar variables de entorno (.env templates) - - Estimado: 1h - - Asignado a: DevOps - -- [ ] **INFRA-DO-004:** Configurar GitHub Actions CI pipeline - - Estimado: 3h - - Asignado a: DevOps - -#### Code Quality - -- [ ] **INFRA-CQ-001:** Configurar ESLint para backend y frontend - - Estimado: 1h - - Asignado a: Tech Lead - -- [ ] **INFRA-CQ-002:** Configurar Prettier - - Estimado: 0.5h - - Asignado a: Tech Lead - -- [ ] **INFRA-CQ-003:** Configurar Husky + lint-staged - - Estimado: 1h - - Asignado a: Tech Lead - -- [ ] **INFRA-CQ-004:** Configurar Jest para testing (backend y frontend) - - Estimado: 2h - - Asignado a: QA Lead - -#### Documentation - -- [ ] **INFRA-DOC-001:** Crear README.md con instrucciones de instalaci贸n - - Estimado: 1h - - Asignado a: Tech Lead - -- [ ] **INFRA-DOC-002:** Documentar estructura de carpetas - - Estimado: 1h - - Asignado a: Tech Lead - -- [ ] **INFRA-DOC-003:** Crear CONTRIBUTING.md con gu铆a de desarrollo - - Estimado: 1h - - Asignado a: Tech Lead - -**Total estimado:** ~40 horas (distribuidas en equipo de 4 devs = 2 semanas) - ---- - -## 馃敆 Dependencias - -### Dependencias Externas - -- **Ninguna** (esta es la base del proyecto) - -### Bloqueantes para - -- 鉁 Todas las dem谩s historias de usuario -- 鉁 MAI-002 (Gesti贸n de Proyectos) -- 鉁 MAI-003 (Gesti贸n de Presupuestos) -- 鉁 MAI-004 (Gesti贸n de Compras) -- 鉁 MAI-005 (Gamificaci贸n) -- 鉁 MAI-006 (RRHH) - ---- - -## 馃搳 Definici贸n de Hecho (DoD) - -- 鉁 Backend ejecutable localmente en < 5 minutos -- 鉁 Frontend ejecutable localmente en < 5 minutos -- 鉁 Base de datos PostgreSQL configurada con schemas -- 鉁 Migrations funcionando correctamente -- 鉁 Hot reload funcional en backend y frontend -- 鉁 Docker Compose levanta todos los servicios -- 鉁 ESLint + Prettier configurados y funcionando -- 鉁 Pre-commit hooks funcionando -- 鉁 CI pipeline ejecut谩ndose en GitHub Actions -- 鉁 Swagger documentation accesible en `/api/docs` -- 鉁 Todos los test cases (TC-INFRA-001 a TC-INFRA-008) pasan -- 鉁 README.md con instrucciones de instalaci贸n completas -- 鉁 Variables de entorno documentadas (`.env.example`) -- 鉁 No hay warnings ni errores en consola (dev mode) - ---- - -## 馃摑 Notas Adicionales - -### Performance Targets - -- **Backend startup:** < 5 segundos -- **Frontend startup:** < 3 segundos -- **Hot reload backend:** < 3 segundos -- **Hot reload frontend:** < 1 segundo -- **Build backend:** < 30 segundos -- **Build frontend:** < 20 segundos - -### Security Considerations - -- 鉁 No commits de archivos `.env` (usar `.env.example`) -- 鉁 Secrets en variables de entorno, no hardcoded -- 鉁 PostgreSQL password fuerte en producci贸n -- 鉁 JWT secret diferente por ambiente -- 鉁 Helmet configurado para seguridad HTTP -- 鉁 CORS configurado restrictivamente - -### Monitoring & Logging - -- 鉁 Winston logger configurado (backend) -- 鉁 Request logging con timestamps -- 鉁 Error logging con stack traces -- 鉁 Query logging en desarrollo (deshabilitado en prod) - ---- - -## 馃帗 Lecciones de GAMILIT - -### Qu茅 Reutilizar (80%) - -鉁 **Estructura completa del proyecto:** -- Organizaci贸n de carpetas backend/frontend -- Configuraci贸n de TypeORM -- Guards, decorators, interceptors -- API service con interceptors -- Docker setup - -鉁 **Herramientas de desarrollo:** -- ESLint + Prettier config -- Husky hooks -- GitHub Actions CI -- Swagger configuration - -### Qu茅 Adaptar (20%) - -馃攧 **Schemas de base de datos:** -- GAMILIT: `auth_management`, `gamification_system`, `educational_content` -- Construcci贸n: `auth_management`, `projects`, `budgets`, `purchases`, `hr`, `finance` - -馃攧 **M贸dulos de negocio:** -- GAMILIT: Students, Teachers, Courses -- Construcci贸n: Projects, Budgets, Employees - ---- - -**Fecha de creaci贸n:** 2025-11-17 -**脷ltima actualizaci贸n:** 2025-11-17 -**Versi贸n:** 1.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-005-sistema-sesiones.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-005-sistema-sesiones.md deleted file mode 100644 index 7aa4d1368..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-005-sistema-sesiones.md +++ /dev/null @@ -1,1065 +0,0 @@ -# US-FUND-005: Sistema de Sesiones y Estado Global - -**Epic:** MAI-001 - Fundamentos del Sistema -**Story Points:** 6 -**Prioridad:** Media -**Dependencias:** -- US-FUND-001 (Autenticaci贸n JWT) -- US-FUND-004 (Infraestructura Base) - -**Estado:** Pendiente -**Asignado a:** Frontend Lead + Backend Dev - ---- - -## 馃搵 Historia de Usuario - -**Como** usuario autenticado del sistema -**Quiero** que mi sesi贸n se mantenga activa mientras uso la aplicaci贸n -**Para** no tener que iniciar sesi贸n repetidamente y poder cambiar de constructora sin perder mi trabajo. - ---- - -## 馃幆 Contexto y Objetivos - -### Contexto - -El sistema de sesiones es cr铆tico para la experiencia del usuario en una aplicaci贸n multi-tenant. Debe manejar: - -- **Persistencia de sesi贸n** al recargar la p谩gina -- **Refresh tokens** para renovar access tokens sin re-login -- **Estado global** (usuario, constructora activa, permisos) -- **Switch de constructora** sin p茅rdida de estado -- **Logout limpio** con invalidaci贸n de tokens -- **Session timeout** por inactividad - -### Objetivos - -1. 鉁 Sesi贸n persiste al recargar la p谩gina -2. 鉁 Access token se renueva autom谩ticamente antes de expirar -3. 鉁 Usuario puede cambiar de constructora sin re-login -4. 鉁 Logout invalida tokens en backend y limpia estado frontend -5. 鉁 Session timeout despu茅s de 30 minutos de inactividad -6. 鉁 Estado global accesible desde cualquier componente - ---- - -## 鉁 Criterios de Aceptaci贸n - -### CA-1: Persistencia de Sesi贸n - -**Dado** un usuario autenticado -**Cuando** recarga la p谩gina (F5) o cierra y vuelve a abrir el navegador -**Entonces**: - -- 鉁 La sesi贸n se mantiene activa -- 鉁 El usuario sigue en la misma constructora -- 鉁 El dashboard carga directamente (no vuelve a login) -- 鉁 El estado global se restaura (nombre, rol, permisos) - -**Excepciones:** -- 鉂 Si el refresh token expir贸, se redirige a login -- 鉂 Si el usuario fue suspendido/baneado, se redirige a login con mensaje - ---- - -### CA-2: Refresh Token Autom谩tico - -**Dado** un usuario con sesi贸n activa -**Cuando** el access token est谩 pr贸ximo a expirar (falta < 2 minutos) -**Entonces**: - -- 鉁 El sistema solicita autom谩ticamente un nuevo access token -- 鉁 El refresh se realiza en background (sin interferir con la UX) -- 鉁 Si el refresh es exitoso, el nuevo token se guarda -- 鉁 Si el refresh falla (token inv谩lido), se redirige a login - -**Timeout:** -- Access Token: 15 minutos -- Refresh Token: 7 d铆as - ---- - -### CA-3: Switch de Constructora - -**Dado** un usuario con acceso a m煤ltiples constructoras -**Cuando** selecciona una constructora diferente desde el switcher -**Entonces**: - -- 鉁 Se solicita un nuevo JWT con el nuevo `constructoraId` -- 鉁 El estado global se actualiza con la nueva constructora -- 鉁 La p谩gina se recarga para aplicar el nuevo contexto RLS -- 鉁 El dashboard muestra datos de la nueva constructora - ---- - -### CA-4: Logout Completo - -**Dado** un usuario autenticado -**Cuando** hace clic en "Cerrar sesi贸n" -**Entonces**: - -- 鉁 Se invalida el refresh token en backend (blacklist) -- 鉁 Se elimina el access token de localStorage -- 鉁 Se limpia todo el estado global (Zustand stores) -- 鉁 Se redirige a la p谩gina de login -- 鉁 No es posible volver atr谩s con el bot贸n "Back" - ---- - -### CA-5: Session Timeout por Inactividad - -**Dado** un usuario autenticado -**Cuando** est谩 inactivo por 30 minutos (sin interacci贸n) -**Entonces**: - -- 鉁 Se muestra un modal de advertencia: "Tu sesi贸n expirar谩 en 60 segundos" -- 鉁 El usuario puede hacer clic en "Mantener sesi贸n" para extender -- 鉁 Si no responde, la sesi贸n se cierra autom谩ticamente -- 鉁 Se redirige a login con mensaje: "Sesi贸n cerrada por inactividad" - -**Definici贸n de actividad:** -- Clicks, tecleo, scroll, movimiento del mouse - ---- - -### CA-6: Estado Global Reactivo - -**Dado** la aplicaci贸n en ejecuci贸n -**Cuando** cualquier componente actualiza el estado global -**Entonces**: - -- 鉁 Todos los componentes suscritos se re-renderizan autom谩ticamente -- 鉁 Los cambios se reflejan inmediatamente en la UI -- 鉁 Los cambios persisten en localStorage (si es necesario) - -**Stores disponibles:** -- `useAuthStore`: usuario, token, isAuthenticated -- `useConstructoraStore`: constructora activa, lista de constructoras -- `usePermissionsStore`: permisos del usuario - ---- - -## 馃敡 Especificaci贸n T茅cnica Detallada - -### 1. Backend - Refresh Token Endpoint - -#### Refresh Token Entity - -**Archivo:** `apps/backend/src/modules/auth/entities/refresh-token.entity.ts` - -```typescript -import { - Entity, - Column, - PrimaryGeneratedColumn, - ManyToOne, - JoinColumn, - CreateDateColumn, -} from 'typeorm'; -import { User } from '../../users/entities/user.entity'; - -@Entity('refresh_tokens', { schema: 'auth_management' }) -export class RefreshToken { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'uuid' }) - userId: string; - - @ManyToOne(() => User, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'userId' }) - user: User; - - @Column({ type: 'varchar', length: 500, unique: true }) - token: string; - - @Column({ type: 'timestamptz' }) - expiresAt: Date; - - @Column({ type: 'boolean', default: false }) - isRevoked: boolean; - - @Column({ type: 'varchar', length: 255, nullable: true }) - userAgent: string; - - @Column({ type: 'inet', nullable: true }) - ipAddress: string; - - @CreateDateColumn({ type: 'timestamptz' }) - createdAt: Date; -} -``` - -#### Refresh Token Service - -**Archivo:** `apps/backend/src/modules/auth/auth.service.ts` (m茅todo adicional) - -```typescript -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { ConfigService } from '@nestjs/config'; -import { randomBytes } from 'crypto'; -import { RefreshToken } from './entities/refresh-token.entity'; -import { User } from '../users/entities/user.entity'; - -@Injectable() -export class AuthService { - constructor( - @InjectRepository(RefreshToken) - private refreshTokenRepo: Repository, - @InjectRepository(User) - private userRepo: Repository, - private jwtService: JwtService, - private configService: ConfigService, - ) {} - - /** - * Genera access token y refresh token - */ - async generateTokens( - user: User, - constructoraId: string, - role: string, - userAgent?: string, - ipAddress?: string, - ) { - // Access Token (15 minutos) - const accessToken = this.jwtService.sign( - { - sub: user.id, - email: user.email, - fullName: user.fullName, - constructoraId, - role, - }, - { - secret: this.configService.get('JWT_SECRET'), - expiresIn: '15m', - }, - ); - - // Refresh Token (7 d铆as) - const refreshTokenValue = randomBytes(64).toString('hex'); - const expiresAt = new Date(); - expiresAt.setDate(expiresAt.getDate() + 7); - - const refreshToken = this.refreshTokenRepo.create({ - userId: user.id, - token: refreshTokenValue, - expiresAt, - userAgent, - ipAddress, - }); - - await this.refreshTokenRepo.save(refreshToken); - - return { - accessToken, - refreshToken: refreshTokenValue, - expiresIn: 900, // 15 minutos en segundos - }; - } - - /** - * Renueva el access token usando el refresh token - */ - async refreshAccessToken(refreshTokenValue: string, userAgent?: string) { - // Buscar refresh token - const refreshToken = await this.refreshTokenRepo.findOne({ - where: { token: refreshTokenValue }, - relations: ['user', 'user.constructoras'], - }); - - if (!refreshToken) { - throw new UnauthorizedException('Refresh token inv谩lido'); - } - - // Validar que no est茅 revocado - if (refreshToken.isRevoked) { - throw new UnauthorizedException('Refresh token revocado'); - } - - // Validar que no est茅 expirado - if (new Date() > refreshToken.expiresAt) { - throw new UnauthorizedException('Refresh token expirado'); - } - - // Validar que el user agent coincida (seguridad adicional) - if (userAgent && refreshToken.userAgent !== userAgent) { - throw new UnauthorizedException('Dispositivo no coincide'); - } - - // Obtener la constructora principal del usuario - const user = refreshToken.user; - const primaryConstructora = user.constructoras.find((uc) => uc.isPrimary); - - if (!primaryConstructora) { - throw new UnauthorizedException('Usuario sin constructora asignada'); - } - - // Generar nuevo access token - const accessToken = this.jwtService.sign( - { - sub: user.id, - email: user.email, - fullName: user.fullName, - constructoraId: primaryConstructora.constructoraId, - role: primaryConstructora.role, - }, - { - secret: this.configService.get('JWT_SECRET'), - expiresIn: '15m', - }, - ); - - return { - accessToken, - expiresIn: 900, - }; - } - - /** - * Revoca un refresh token (logout) - */ - async revokeRefreshToken(refreshTokenValue: string) { - const refreshToken = await this.refreshTokenRepo.findOne({ - where: { token: refreshTokenValue }, - }); - - if (refreshToken) { - refreshToken.isRevoked = true; - await this.refreshTokenRepo.save(refreshToken); - } - } - - /** - * Revoca todos los refresh tokens de un usuario - */ - async revokeAllUserTokens(userId: string) { - await this.refreshTokenRepo.update( - { userId, isRevoked: false }, - { isRevoked: true }, - ); - } - - /** - * Limpieza de refresh tokens expirados (ejecutar con cron) - */ - async cleanExpiredTokens() { - const result = await this.refreshTokenRepo - .createQueryBuilder() - .delete() - .where('expiresAt < NOW()') - .orWhere('isRevoked = true AND createdAt < NOW() - INTERVAL \'30 days\'') - .execute(); - - return { deleted: result.affected }; - } -} -``` - -#### Refresh Token Controller - -**Archivo:** `apps/backend/src/modules/auth/auth.controller.ts` (endpoints adicionales) - -```typescript -import { Controller, Post, Body, Req, Ip, UnauthorizedException } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { Public } from '../../common/decorators/public.decorator'; -import { AuthService } from './auth.service'; -import { Request } from 'express'; - -@ApiTags('Auth') -@Controller('auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @Public() - @Post('refresh') - @ApiOperation({ summary: 'Renovar access token usando refresh token' }) - @ApiResponse({ status: 200, description: 'Access token renovado exitosamente' }) - @ApiResponse({ status: 401, description: 'Refresh token inv谩lido o expirado' }) - async refresh( - @Body('refreshToken') refreshToken: string, - @Req() req: Request, - @Ip() ip: string, - ) { - if (!refreshToken) { - throw new UnauthorizedException('Refresh token requerido'); - } - - const userAgent = req.headers['user-agent']; - const tokens = await this.authService.refreshAccessToken(refreshToken, userAgent); - - return { - statusCode: 200, - message: 'Token renovado exitosamente', - data: tokens, - }; - } - - @Post('logout') - @ApiOperation({ summary: 'Cerrar sesi贸n y revocar refresh token' }) - @ApiResponse({ status: 200, description: 'Sesi贸n cerrada exitosamente' }) - async logout(@Body('refreshToken') refreshToken: string) { - if (refreshToken) { - await this.authService.revokeRefreshToken(refreshToken); - } - - return { - statusCode: 200, - message: 'Sesi贸n cerrada exitosamente', - }; - } - - @Post('logout-all') - @ApiOperation({ summary: 'Cerrar todas las sesiones del usuario' }) - @ApiResponse({ status: 200, description: 'Todas las sesiones cerradas' }) - async logoutAll(@Req() req: Request) { - const userId = req.user['sub']; - await this.authService.revokeAllUserTokens(userId); - - return { - statusCode: 200, - message: 'Todas las sesiones han sido cerradas', - }; - } -} -``` - ---- - -### 2. Frontend - Zustand Stores - -#### Auth Store - -**Archivo:** `apps/frontend/src/stores/useAuthStore.ts` - -```typescript -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import { jwtDecode } from 'jwt-decode'; - -interface JwtPayload { - sub: string; - email: string; - fullName: string; - constructoraId: string; - role: string; - iat: number; - exp: number; -} - -interface User { - id: string; - email: string; - fullName: string; - constructoraId: string; - role: string; -} - -interface AuthState { - // State - accessToken: string | null; - refreshToken: string | null; - user: User | null; - isAuthenticated: boolean; - - // Actions - setTokens: (accessToken: string, refreshToken: string) => void; - setAccessToken: (accessToken: string) => void; - logout: () => void; - getUser: () => User | null; - isTokenExpiring: () => boolean; -} - -export const useAuthStore = create()( - persist( - (set, get) => ({ - accessToken: null, - refreshToken: null, - user: null, - isAuthenticated: false, - - setTokens: (accessToken, refreshToken) => { - try { - const decoded = jwtDecode(accessToken); - const user: User = { - id: decoded.sub, - email: decoded.email, - fullName: decoded.fullName, - constructoraId: decoded.constructoraId, - role: decoded.role, - }; - - set({ - accessToken, - refreshToken, - user, - isAuthenticated: true, - }); - } catch (error) { - console.error('Error decoding token:', error); - } - }, - - setAccessToken: (accessToken) => { - try { - const decoded = jwtDecode(accessToken); - const user: User = { - id: decoded.sub, - email: decoded.email, - fullName: decoded.fullName, - constructoraId: decoded.constructoraId, - role: decoded.role, - }; - - set({ - accessToken, - user, - isAuthenticated: true, - }); - } catch (error) { - console.error('Error decoding token:', error); - } - }, - - logout: () => { - set({ - accessToken: null, - refreshToken: null, - user: null, - isAuthenticated: false, - }); - }, - - getUser: () => { - return get().user; - }, - - isTokenExpiring: () => { - const { accessToken } = get(); - if (!accessToken) return true; - - try { - const decoded = jwtDecode(accessToken); - const expiresAt = decoded.exp * 1000; // Convertir a milisegundos - const now = Date.now(); - const timeUntilExpiry = expiresAt - now; - - // Token expira en menos de 2 minutos - return timeUntilExpiry < 2 * 60 * 1000; - } catch { - return true; - } - }, - }), - { - name: 'auth-storage', - partialize: (state) => ({ - // Solo persistir tokens y user - accessToken: state.accessToken, - refreshToken: state.refreshToken, - user: state.user, - isAuthenticated: state.isAuthenticated, - }), - }, - ), -); -``` - ---- - -### 3. Frontend - Token Refresh Service - -#### Token Refresh Hook - -**Archivo:** `apps/frontend/src/hooks/useTokenRefresh.ts` - -```typescript -import { useEffect, useRef } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; -import { useAuthStore } from '@/stores/useAuthStore'; -import { apiService } from '@/services/api.service'; - -/** - * Hook para renovar autom谩ticamente el access token - * Se ejecuta cada 60 segundos y verifica si el token est谩 pr贸ximo a expirar - */ -export function useTokenRefresh() { - const navigate = useNavigate(); - const { isTokenExpiring, refreshToken, setAccessToken, logout } = useAuthStore(); - const intervalRef = useRef(null); - const isRefreshing = useRef(false); - - useEffect(() => { - if (!refreshToken) return; - - // Revisar cada 60 segundos - intervalRef.current = setInterval(async () => { - if (isRefreshing.current) return; - - if (isTokenExpiring()) { - isRefreshing.current = true; - - try { - const response = await apiService.post<{ - accessToken: string; - expiresIn: number; - }>('/auth/refresh', { - refreshToken, - }); - - setAccessToken(response.accessToken); - console.log('鉁 Access token renovado autom谩ticamente'); - } catch (error) { - console.error('鉂 Error al renovar token:', error); - logout(); - navigate('/login'); - toast.error('Sesi贸n expirada. Por favor inicia sesi贸n nuevamente.'); - } finally { - isRefreshing.current = false; - } - } - }, 60 * 1000); // Cada 60 segundos - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [refreshToken, isTokenExpiring, setAccessToken, logout, navigate]); -} -``` - ---- - -### 4. Frontend - Session Timeout por Inactividad - -#### Inactivity Timeout Hook - -**Archivo:** `apps/frontend/src/hooks/useInactivityTimeout.ts` - -```typescript -import { useEffect, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; -import { useAuthStore } from '@/stores/useAuthStore'; -import { apiService } from '@/services/api.service'; - -const INACTIVITY_TIMEOUT = 30 * 60 * 1000; // 30 minutos -const WARNING_TIME = 60 * 1000; // Advertir 60 segundos antes - -export function useInactivityTimeout() { - const navigate = useNavigate(); - const { isAuthenticated, refreshToken, logout } = useAuthStore(); - const [showWarning, setShowWarning] = useState(false); - - const timeoutRef = useRef(null); - const warningTimeoutRef = useRef(null); - - const resetTimer = () => { - // Limpiar timers existentes - if (timeoutRef.current) clearTimeout(timeoutRef.current); - if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current); - setShowWarning(false); - - // Timer de advertencia (a los 29 minutos) - warningTimeoutRef.current = setTimeout(() => { - setShowWarning(true); - }, INACTIVITY_TIMEOUT - WARNING_TIME); - - // Timer de logout (a los 30 minutos) - timeoutRef.current = setTimeout(() => { - handleLogout(); - }, INACTIVITY_TIMEOUT); - }; - - const handleLogout = async () => { - try { - await apiService.post('/auth/logout', { refreshToken }); - } catch (error) { - console.error('Error al cerrar sesi贸n:', error); - } finally { - logout(); - navigate('/login'); - toast.info('Sesi贸n cerrada por inactividad'); - } - }; - - const handleStayLoggedIn = () => { - setShowWarning(false); - resetTimer(); - toast.success('Sesi贸n extendida'); - }; - - useEffect(() => { - if (!isAuthenticated) return; - - // Eventos que reinician el timer - const events = ['mousedown', 'keydown', 'scroll', 'touchstart', 'mousemove']; - - const resetTimerHandler = () => { - if (!showWarning) { - resetTimer(); - } - }; - - events.forEach((event) => { - window.addEventListener(event, resetTimerHandler); - }); - - // Iniciar timer - resetTimer(); - - return () => { - events.forEach((event) => { - window.removeEventListener(event, resetTimerHandler); - }); - if (timeoutRef.current) clearTimeout(timeoutRef.current); - if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current); - }; - }, [isAuthenticated, showWarning]); - - return { - showWarning, - handleStayLoggedIn, - }; -} -``` - -#### Inactivity Warning Modal Component - -**Archivo:** `apps/frontend/src/components/auth/InactivityWarningModal.tsx` - -```typescript -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { AlertTriangle } from 'lucide-react'; - -interface InactivityWarningModalProps { - isOpen: boolean; - onStayLoggedIn: () => void; -} - -export function InactivityWarningModal({ - isOpen, - onStayLoggedIn, -}: InactivityWarningModalProps) { - return ( - {}}> - - -
- - Tu sesi贸n est谩 por expirar -
-
- -
-

- Por inactividad, tu sesi贸n se cerrar谩 autom谩ticamente en{' '} - 60 segundos. -

- -

- 驴Deseas mantener tu sesi贸n activa? -

- -
- -
-
-
-
- ); -} -``` - ---- - -### 5. Integration en App - -#### App Component con Hooks de Sesi贸n - -**Archivo:** `apps/frontend/src/App.tsx` (actualizado) - -```typescript -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { Toaster } from 'sonner'; - -// Hooks de sesi贸n -import { useTokenRefresh } from '@/hooks/useTokenRefresh'; -import { useInactivityTimeout } from '@/hooks/useInactivityTimeout'; - -// Components -import { InactivityWarningModal } from '@/components/auth/InactivityWarningModal'; - -// ... (resto de imports) - -function App() { - return ( - - - - - - {/* ... (rutas) */} - - - - - - ); -} - -function SessionManager() { - // Token refresh autom谩tico - useTokenRefresh(); - - // Timeout por inactividad - const { showWarning, handleStayLoggedIn } = useInactivityTimeout(); - - return ( - - ); -} - -export default App; -``` - ---- - -## 馃И Test Cases - -### TC-SESSION-001: Persistencia de Sesi贸n - -**Pre-condiciones:** -- Usuario autenticado - -**Pasos:** -1. Iniciar sesi贸n -2. Navegar a cualquier p谩gina del dashboard -3. Recargar la p谩gina (F5) - -**Resultado esperado:** -- 鉁 Usuario sigue autenticado -- 鉁 Permanece en la misma p谩gina -- 鉁 Datos del usuario visibles en header -- 鉁 No se redirige a login - ---- - -### TC-SESSION-002: Refresh Token Autom谩tico - -**Pre-condiciones:** -- Usuario autenticado con access token pr贸ximo a expirar - -**Pasos:** -1. Mockear fecha para simular token expirando en 1 minuto -2. Esperar a que el hook detecte expiraci贸n -3. Observar network requests - -**Resultado esperado:** -- 鉁 Se ejecuta POST `/api/auth/refresh` autom谩ticamente -- 鉁 Nuevo access token se guarda en localStorage -- 鉁 Usuario no percibe ninguna interrupci贸n -- 鉁 Requests subsiguientes usan el nuevo token - ---- - -### TC-SESSION-003: Logout Completo - -**Pre-condiciones:** -- Usuario autenticado - -**Pasos:** -1. Hacer clic en bot贸n "Cerrar sesi贸n" -2. Observar network y localStorage -3. Intentar navegar a ruta protegida - -**Resultado esperado:** -- 鉁 Se ejecuta POST `/api/auth/logout` -- 鉁 localStorage vac铆o (tokens eliminados) -- 鉁 Zustand store limpio -- 鉁 Redirige a `/login` -- 鉁 Intentar acceder a `/dashboard` redirige a login - ---- - -### TC-SESSION-004: Session Timeout - -**Pre-condiciones:** -- Usuario autenticado - -**Pasos:** -1. No interactuar con la aplicaci贸n por 29 minutos -2. Observar el modal de advertencia -3. No hacer clic en "Mantener sesi贸n" -4. Esperar 60 segundos - -**Resultado esperado:** -- 鉁 A los 29 min, aparece modal de advertencia -- 鉁 A los 30 min, sesi贸n se cierra autom谩ticamente -- 鉁 Toast muestra "Sesi贸n cerrada por inactividad" -- 鉁 Redirige a login - ---- - -### TC-SESSION-005: Extender Sesi贸n - -**Pre-condiciones:** -- Usuario con modal de inactividad visible - -**Pasos:** -1. Modal de inactividad aparece -2. Hacer clic en "Mantener sesi贸n activa" -3. Esperar 1 minuto - -**Resultado esperado:** -- 鉁 Modal se cierra -- 鉁 Toast muestra "Sesi贸n extendida" -- 鉁 Timer de inactividad se reinicia -- 鉁 Sesi贸n sigue activa - ---- - -### TC-SESSION-006: Switch Constructora - -**Pre-condiciones:** -- Usuario con acceso a m煤ltiples constructoras - -**Pasos:** -1. Abrir selector de constructoras -2. Seleccionar constructora diferente -3. Confirmar cambio - -**Resultado esperado:** -- 鉁 Se ejecuta POST `/api/auth/switch-constructora` -- 鉁 Nuevo access token se guarda -- 鉁 P谩gina se recarga -- 鉁 Dashboard muestra datos de nueva constructora -- 鉁 Zustand store actualizado con nueva constructora - ---- - -## 馃搵 Tareas de Implementaci贸n - -### Backend - -- [ ] **SESSION-BE-001:** Crear entity `RefreshToken` - - Estimado: 1h - -- [ ] **SESSION-BE-002:** Implementar `generateTokens()` en AuthService - - Estimado: 1.5h - -- [ ] **SESSION-BE-003:** Implementar endpoint POST `/auth/refresh` - - Estimado: 1h - -- [ ] **SESSION-BE-004:** Implementar endpoint POST `/auth/logout` - - Estimado: 0.5h - -- [ ] **SESSION-BE-005:** Implementar endpoint POST `/auth/logout-all` - - Estimado: 0.5h - -- [ ] **SESSION-BE-006:** Crear cron job para limpiar tokens expirados - - Estimado: 1h - -### Frontend - -- [ ] **SESSION-FE-001:** Crear `useAuthStore` con Zustand - - Estimado: 1.5h - -- [ ] **SESSION-FE-002:** Implementar `useTokenRefresh` hook - - Estimado: 2h - -- [ ] **SESSION-FE-003:** Implementar `useInactivityTimeout` hook - - Estimado: 2h - -- [ ] **SESSION-FE-004:** Crear componente `InactivityWarningModal` - - Estimado: 1h - -- [ ] **SESSION-FE-005:** Integrar hooks en App.tsx - - Estimado: 0.5h - -- [ ] **SESSION-FE-006:** Actualizar API service para usar tokens del store - - Estimado: 1h - -- [ ] **SESSION-FE-007:** Implementar logout en todos los componentes - - Estimado: 1h - -### Testing - -- [ ] **SESSION-TEST-001:** Unit tests para AuthService (backend) - - Estimado: 2h - -- [ ] **SESSION-TEST-002:** Integration tests para endpoints de sesi贸n - - Estimado: 2h - -- [ ] **SESSION-TEST-003:** Unit tests para useAuthStore - - Estimado: 1h - -- [ ] **SESSION-TEST-004:** E2E test para flujo completo de sesi贸n - - Estimado: 2h - -**Total estimado:** ~21 horas - ---- - -## 馃敆 Dependencias - -### Depende de - -- 鉁 US-FUND-001 (Autenticaci贸n JWT) -- 鉁 US-FUND-004 (Infraestructura Base) - -### Bloqueante para - -- Todas las funcionalidades que requieren estado global persistente -- Switch de constructora -- Refresh autom谩tico de datos - ---- - -## 馃搳 Definici贸n de Hecho (DoD) - -- 鉁 Refresh token se guarda en base de datos -- 鉁 Endpoint `/auth/refresh` funcional -- 鉁 Endpoint `/auth/logout` invalida tokens -- 鉁 Zustand store `useAuthStore` implementado y persistente -- 鉁 Token refresh autom谩tico funciona -- 鉁 Timeout por inactividad funciona (30 min) -- 鉁 Modal de advertencia se muestra correctamente -- 鉁 Logout limpia todo el estado (backend + frontend) -- 鉁 Todos los test cases (TC-SESSION-001 a TC-SESSION-006) pasan -- 鉁 Documentaci贸n actualizada en Swagger -- 鉁 Code coverage > 80% en funcionalidad de sesi贸n - ---- - -## 馃摑 Notas Adicionales - -### Security Considerations - -- 鉁 Refresh tokens almacenados con hash en BD -- 鉁 Refresh tokens asociados a user agent (anti-hijacking) -- 鉁 Refresh tokens con expiraci贸n de 7 d铆as -- 鉁 Tokens revocados al logout -- 鉁 Limpieza autom谩tica de tokens expirados - -### Performance - -- 鉁 Token refresh en background (no bloquea UI) -- 鉁 Interval check cada 60 segundos (no cada segundo) -- 鉁 Zustand store optimizado (no re-renders innecesarios) - ---- - -**Fecha de creaci贸n:** 2025-11-17 -**脷ltima actualizaci贸n:** 2025-11-17 -**Versi贸n:** 1.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-006-api-restful-base.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-006-api-restful-base.md deleted file mode 100644 index 4890ec67e..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-006-api-restful-base.md +++ /dev/null @@ -1,1187 +0,0 @@ -# US-FUND-006: API RESTful B谩sica - -**Epic:** MAI-001 - Fundamentos del Sistema -**Story Points:** 8 -**Prioridad:** Alta -**Dependencias:** -- US-FUND-004 (Infraestructura Base) - -**Estado:** Pendiente -**Asignado a:** Backend Lead + Backend Dev - ---- - -## 馃搵 Historia de Usuario - -**Como** desarrollador frontend -**Quiero** consumir una API RESTful bien estructurada y documentada -**Para** integrar el frontend con el backend de manera eficiente, predecible y con manejo de errores robusto. - ---- - -## 馃幆 Contexto y Objetivos - -### Contexto - -Este documento define los est谩ndares y convenciones para TODAS las APIs del sistema de construcci贸n. Incluye: - -- **Convenciones REST** (verbos HTTP, c贸digos de estado, naming) -- **Formato de respuestas** est谩ndar -- **Manejo de errores** consistente -- **Validaci贸n de requests** con DTOs -- **Paginaci贸n, filtrado y ordenamiento** -- **Documentaci贸n Swagger** autom谩tica -- **Rate limiting** para prevenir abuso -- **Versionado de API** - -### Objetivos - -1. 鉁 Todas las APIs siguen convenciones REST est谩ndar -2. 鉁 Respuestas consistentes (mismo formato en todos los endpoints) -3. 鉁 Errores informativos con c贸digos HTTP apropiados -4. 鉁 Validaci贸n autom谩tica con class-validator -5. 鉁 Paginaci贸n est谩ndar para listados -6. 鉁 Swagger docs generadas autom谩ticamente -7. 鉁 Rate limiting configurado globalmente - ---- - -## 鉁 Criterios de Aceptaci贸n - -### CA-1: Convenciones REST - -**Dado** un endpoint de la API -**Cuando** se dise帽a siguiendo las convenciones REST -**Entonces**: - -- 鉁 Usa los verbos HTTP correctos: - - `GET`: Obtener recursos (sin side effects) - - `POST`: Crear recursos - - `PUT`: Actualizar recurso completo - - `PATCH`: Actualizar recurso parcial - - `DELETE`: Eliminar recurso -- 鉁 URLs en plural: `/api/projects`, `/api/budgets`, `/api/users` -- 鉁 IDs en la URL: `/api/projects/:id` -- 鉁 Recursos anidados cuando corresponde: `/api/projects/:id/budgets` -- 鉁 Query params para filtros: `/api/projects?status=active&page=2` - ---- - -### CA-2: C贸digos de Estado HTTP - -**Dado** una respuesta de la API -**Cuando** se retorna al cliente -**Entonces** usa el c贸digo HTTP apropiado: - -- 鉁 **200 OK**: Operaci贸n exitosa (GET, PUT, PATCH) -- 鉁 **201 Created**: Recurso creado (POST) -- 鉁 **204 No Content**: Operaci贸n exitosa sin contenido (DELETE) -- 鉁 **400 Bad Request**: Validaci贸n fall贸 o par谩metros inv谩lidos -- 鉁 **401 Unauthorized**: No autenticado (token faltante o inv谩lido) -- 鉁 **403 Forbidden**: Autenticado pero sin permisos -- 鉁 **404 Not Found**: Recurso no existe -- 鉁 **409 Conflict**: Conflicto (ej: email duplicado) -- 鉁 **429 Too Many Requests**: Rate limit excedido -- 鉁 **500 Internal Server Error**: Error del servidor - ---- - -### CA-3: Formato de Respuesta Est谩ndar - -**Dado** cualquier endpoint exitoso -**Cuando** retorna datos -**Entonces** sigue este formato: - -```typescript -{ - "statusCode": 200, - "message": "Mensaje descriptivo", - "data": { ... } // o [ ... ] para listas -} -``` - -**Para errores:** - -```typescript -{ - "statusCode": 400, - "message": "Mensaje de error", - "error": "Bad Request", - "timestamp": "2025-11-17T10:30:00.000Z", - "path": "/api/projects", - "validationErrors": [ // Opcional, solo para errores de validaci贸n - { - "field": "name", - "message": "El nombre es requerido" - } - ] -} -``` - ---- - -### CA-4: Paginaci贸n Est谩ndar - -**Dado** un endpoint que retorna listas -**Cuando** se solicitan datos paginados -**Entonces**: - -- 鉁 Acepta query params: `?page=1&limit=20` -- 鉁 Defaults: `page=1`, `limit=20`, `maxLimit=100` -- 鉁 Retorna metadata de paginaci贸n: - -```typescript -{ - "statusCode": 200, - "message": "Proyectos obtenidos exitosamente", - "data": { - "items": [ ... ], - "meta": { - "page": 1, - "limit": 20, - "totalItems": 150, - "totalPages": 8, - "hasNextPage": true, - "hasPreviousPage": false - } - } -} -``` - ---- - -### CA-5: Filtrado y Ordenamiento - -**Dado** un endpoint de listado -**Cuando** se aplican filtros y orden -**Entonces**: - -- 鉁 Filtros con query params: `?status=active&role=engineer` -- 鉁 Ordenamiento: `?sortBy=createdAt&order=DESC` -- 鉁 B煤squeda: `?search=proyecto+nuevo` -- 鉁 Rango de fechas: `?startDate=2025-01-01&endDate=2025-12-31` - ---- - -### CA-6: Validaci贸n con DTOs - -**Dado** un request con datos inv谩lidos -**Cuando** se env铆a al endpoint -**Entonces**: - -- 鉁 Validaci贸n se ejecuta autom谩ticamente (ValidationPipe) -- 鉁 Retorna 400 Bad Request -- 鉁 Muestra errores espec铆ficos por campo -- 鉁 Mensajes en espa帽ol y descriptivos - ---- - -### CA-7: Documentaci贸n Swagger - -**Dado** la aplicaci贸n en modo desarrollo -**Cuando** accedo a `/api/docs` -**Entonces**: - -- 鉁 Swagger UI carga correctamente -- 鉁 Todos los endpoints est谩n documentados -- 鉁 DTOs muestran sus propiedades y validaciones -- 鉁 Responses documentadas con ejemplos -- 鉁 Autenticaci贸n JWT configurada -- 鉁 Es posible ejecutar requests desde Swagger - ---- - -### CA-8: Rate Limiting - -**Dado** un cliente realizando m煤ltiples requests -**Cuando** excede el l铆mite permitido -**Entonces**: - -- 鉁 Retorna 429 Too Many Requests -- 鉁 Headers incluyen informaci贸n del l铆mite: - - `X-RateLimit-Limit`: L铆mite m谩ximo - - `X-RateLimit-Remaining`: Requests restantes - - `X-RateLimit-Reset`: Timestamp de reset -- 鉁 L铆mite por defecto: 100 requests/minuto por IP - ---- - -## 馃敡 Especificaci贸n T茅cnica Detallada - -### 1. Response Transform Interceptor - -**Archivo:** `apps/backend/src/common/interceptors/transform.interceptor.ts` - -```typescript -import { - Injectable, - NestInterceptor, - ExecutionContext, - CallHandler, -} from '@nestjs/common'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -export interface Response { - statusCode: number; - message: string; - data: T; -} - -@Injectable() -export class TransformInterceptor implements NestInterceptor> { - intercept(context: ExecutionContext, next: CallHandler): Observable> { - const ctx = context.switchToHttp(); - const response = ctx.getResponse(); - - return next.handle().pipe( - map((data) => { - // Si el controller ya retorna el formato esperado, no transformar - if (data && typeof data === 'object' && 'statusCode' in data) { - return data; - } - - // Transformar a formato est谩ndar - return { - statusCode: response.statusCode, - message: data?.message || 'Operaci贸n exitosa', - data: data?.data !== undefined ? data.data : data, - }; - }), - ); - } -} -``` - ---- - -### 2. HTTP Exception Filter - -**Archivo:** `apps/backend/src/common/filters/http-exception.filter.ts` - -```typescript -import { - ExceptionFilter, - Catch, - ArgumentsHost, - HttpException, - HttpStatus, - Logger, -} from '@nestjs/common'; -import { Request, Response } from 'express'; -import { QueryFailedError } from 'typeorm'; - -interface ValidationError { - field: string; - message: string; -} - -@Catch() -export class HttpExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(HttpExceptionFilter.name); - - catch(exception: unknown, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - - let status = HttpStatus.INTERNAL_SERVER_ERROR; - let message = 'Error interno del servidor'; - let error = 'Internal Server Error'; - let validationErrors: ValidationError[] | undefined; - - // Manejo de HttpException (NestJS) - if (exception instanceof HttpException) { - status = exception.getStatus(); - const exceptionResponse = exception.getResponse(); - - if (typeof exceptionResponse === 'string') { - message = exceptionResponse; - } else if (typeof exceptionResponse === 'object') { - const responseObj = exceptionResponse as any; - message = responseObj.message || exception.message; - error = responseObj.error || exception.name; - - // Errores de validaci贸n - if (Array.isArray(responseObj.message)) { - validationErrors = this.formatValidationErrors(responseObj.message); - message = 'Error de validaci贸n'; - } - } - } - // Manejo de errores de TypeORM - else if (exception instanceof QueryFailedError) { - status = HttpStatus.BAD_REQUEST; - message = this.handleDatabaseError(exception); - error = 'Database Error'; - } - // Otros errores - else if (exception instanceof Error) { - message = exception.message; - error = exception.name; - } - - // Log del error - this.logger.error( - `${request.method} ${request.url} - ${status} - ${message}`, - exception instanceof Error ? exception.stack : undefined, - ); - - // Respuesta al cliente - const errorResponse: any = { - statusCode: status, - message, - error, - timestamp: new Date().toISOString(), - path: request.url, - }; - - if (validationErrors) { - errorResponse.validationErrors = validationErrors; - } - - response.status(status).json(errorResponse); - } - - private formatValidationErrors(errors: any[]): ValidationError[] { - return errors.map((err) => ({ - field: err.property || 'unknown', - message: Object.values(err.constraints || {}).join(', '), - })); - } - - private handleDatabaseError(error: QueryFailedError): string { - const message = error.message; - - // Unique constraint violation - if (message.includes('unique constraint')) { - if (message.includes('email')) { - return 'El email ya est谩 registrado'; - } - if (message.includes('rfc')) { - return 'El RFC ya est谩 registrado'; - } - return 'El valor ya existe en la base de datos'; - } - - // Foreign key constraint violation - if (message.includes('foreign key constraint')) { - return 'El registro est谩 relacionado con otros datos y no puede ser eliminado'; - } - - // Not null constraint violation - if (message.includes('null value')) { - return 'Falta un campo requerido'; - } - - return 'Error en la base de datos'; - } -} -``` - ---- - -### 3. Pagination DTO - -**Archivo:** `apps/backend/src/common/dto/pagination.dto.ts` - -```typescript -import { IsOptional, IsInt, Min, Max } from 'class-validator'; -import { Type } from 'class-transformer'; -import { ApiPropertyOptional } from '@nestjs/swagger'; - -export class PaginationDto { - @ApiPropertyOptional({ - description: 'N煤mero de p谩gina (inicia en 1)', - minimum: 1, - default: 1, - example: 1, - }) - @IsOptional() - @Type(() => Number) - @IsInt({ message: 'La p谩gina debe ser un n煤mero entero' }) - @Min(1, { message: 'La p谩gina debe ser mayor o igual a 1' }) - page?: number = 1; - - @ApiPropertyOptional({ - description: 'Cantidad de elementos por p谩gina', - minimum: 1, - maximum: 100, - default: 20, - example: 20, - }) - @IsOptional() - @Type(() => Number) - @IsInt({ message: 'El l铆mite debe ser un n煤mero entero' }) - @Min(1, { message: 'El l铆mite debe ser mayor o igual a 1' }) - @Max(100, { message: 'El l铆mite no puede ser mayor a 100' }) - limit?: number = 20; - - get skip(): number { - return (this.page - 1) * this.limit; - } -} - -export class SortDto { - @ApiPropertyOptional({ - description: 'Campo por el cual ordenar', - example: 'createdAt', - }) - @IsOptional() - sortBy?: string = 'createdAt'; - - @ApiPropertyOptional({ - description: 'Orden (ASC o DESC)', - enum: ['ASC', 'DESC'], - default: 'DESC', - example: 'DESC', - }) - @IsOptional() - order?: 'ASC' | 'DESC' = 'DESC'; -} - -export class PaginatedDto extends PaginationDto { - @ApiPropertyOptional({ - description: 'T茅rmino de b煤squeda', - example: 'proyecto nuevo', - }) - @IsOptional() - search?: string; -} -``` - -**Archivo:** `apps/backend/src/common/dto/paginated-response.dto.ts` - -```typescript -import { ApiProperty } from '@nestjs/swagger'; - -export class PaginationMetaDto { - @ApiProperty({ description: 'P谩gina actual', example: 1 }) - page: number; - - @ApiProperty({ description: 'Elementos por p谩gina', example: 20 }) - limit: number; - - @ApiProperty({ description: 'Total de elementos', example: 150 }) - totalItems: number; - - @ApiProperty({ description: 'Total de p谩ginas', example: 8 }) - totalPages: number; - - @ApiProperty({ description: 'Tiene p谩gina siguiente', example: true }) - hasNextPage: boolean; - - @ApiProperty({ description: 'Tiene p谩gina anterior', example: false }) - hasPreviousPage: boolean; -} - -export class PaginatedResponseDto { - @ApiProperty({ description: 'Lista de elementos', isArray: true }) - items: T[]; - - @ApiProperty({ description: 'Metadata de paginaci贸n', type: PaginationMetaDto }) - meta: PaginationMetaDto; - - constructor(items: T[], total: number, page: number, limit: number) { - this.items = items; - - const totalPages = Math.ceil(total / limit); - - this.meta = { - page, - limit, - totalItems: total, - totalPages, - hasNextPage: page < totalPages, - hasPreviousPage: page > 1, - }; - } -} -``` - ---- - -### 4. Ejemplo de Controller con Todas las Convenciones - -**Archivo:** `apps/backend/src/modules/projects/projects.controller.ts` - -```typescript -import { - Controller, - Get, - Post, - Put, - Patch, - Delete, - Body, - Param, - Query, - ParseUUIDPipe, - HttpCode, - HttpStatus, -} from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBearerAuth, - ApiQuery, -} from '@nestjs/swagger'; -import { ProjectsService } from './projects.service'; -import { CreateProjectDto } from './dto/create-project.dto'; -import { UpdateProjectDto } from './dto/update-project.dto'; -import { ProjectResponseDto } from './dto/project-response.dto'; -import { PaginationDto } from '../../common/dto/pagination.dto'; -import { PaginatedResponseDto } from '../../common/dto/paginated-response.dto'; -import { Roles } from '../../common/decorators/roles.decorator'; -import { ConstructionRole } from '../../common/enums/construction-role.enum'; -import { CurrentUser } from '../../common/decorators/current-user.decorator'; -import { CurrentConstructora } from '../../common/decorators/current-constructora.decorator'; - -@ApiTags('Projects') -@ApiBearerAuth('JWT-auth') -@Controller('projects') -export class ProjectsController { - constructor(private readonly projectsService: ProjectsService) {} - - /** - * Listar proyectos con paginaci贸n, filtros y ordenamiento - */ - @Get() - @Roles(ConstructionRole.DIRECTOR, ConstructionRole.ENGINEER, ConstructionRole.FINANCE) - @ApiOperation({ summary: 'Listar proyectos con paginaci贸n' }) - @ApiResponse({ - status: 200, - description: 'Lista de proyectos', - type: PaginatedResponseDto, - }) - @ApiQuery({ name: 'page', required: false, example: 1 }) - @ApiQuery({ name: 'limit', required: false, example: 20 }) - @ApiQuery({ name: 'status', required: false, enum: ['planning', 'active', 'completed'] }) - @ApiQuery({ name: 'search', required: false, example: 'proyecto nuevo' }) - async findAll( - @Query() paginationDto: PaginationDto, - @Query('status') status?: string, - @Query('search') search?: string, - @CurrentConstructora() constructoraId?: string, - ) { - const { items, total } = await this.projectsService.findAll({ - ...paginationDto, - status, - search, - constructoraId, - }); - - return { - statusCode: 200, - message: 'Proyectos obtenidos exitosamente', - data: new PaginatedResponseDto(items, total, paginationDto.page, paginationDto.limit), - }; - } - - /** - * Obtener proyecto por ID - */ - @Get(':id') - @Roles(ConstructionRole.DIRECTOR, ConstructionRole.ENGINEER, ConstructionRole.RESIDENT) - @ApiOperation({ summary: 'Obtener proyecto por ID' }) - @ApiResponse({ - status: 200, - description: 'Proyecto encontrado', - type: ProjectResponseDto, - }) - @ApiResponse({ status: 404, description: 'Proyecto no encontrado' }) - async findOne(@Param('id', ParseUUIDPipe) id: string) { - const project = await this.projectsService.findOne(id); - - return { - statusCode: 200, - message: 'Proyecto obtenido exitosamente', - data: project, - }; - } - - /** - * Crear nuevo proyecto - */ - @Post() - @Roles(ConstructionRole.DIRECTOR) - @ApiOperation({ summary: 'Crear nuevo proyecto' }) - @ApiResponse({ - status: 201, - description: 'Proyecto creado exitosamente', - type: ProjectResponseDto, - }) - @ApiResponse({ status: 400, description: 'Datos inv谩lidos' }) - async create( - @Body() createProjectDto: CreateProjectDto, - @CurrentUser('id') userId: string, - @CurrentConstructora() constructoraId: string, - ) { - const project = await this.projectsService.create({ - ...createProjectDto, - constructoraId, - createdBy: userId, - }); - - return { - statusCode: 201, - message: 'Proyecto creado exitosamente', - data: project, - }; - } - - /** - * Actualizar proyecto completo (PUT) - */ - @Put(':id') - @Roles(ConstructionRole.DIRECTOR) - @ApiOperation({ summary: 'Actualizar proyecto completo' }) - @ApiResponse({ - status: 200, - description: 'Proyecto actualizado exitosamente', - type: ProjectResponseDto, - }) - @ApiResponse({ status: 404, description: 'Proyecto no encontrado' }) - async update( - @Param('id', ParseUUIDPipe) id: string, - @Body() updateProjectDto: UpdateProjectDto, - ) { - const project = await this.projectsService.update(id, updateProjectDto); - - return { - statusCode: 200, - message: 'Proyecto actualizado exitosamente', - data: project, - }; - } - - /** - * Actualizar proyecto parcial (PATCH) - */ - @Patch(':id') - @Roles(ConstructionRole.DIRECTOR, ConstructionRole.ENGINEER) - @ApiOperation({ summary: 'Actualizar proyecto parcialmente' }) - @ApiResponse({ - status: 200, - description: 'Proyecto actualizado exitosamente', - type: ProjectResponseDto, - }) - @ApiResponse({ status: 404, description: 'Proyecto no encontrado' }) - async partialUpdate( - @Param('id', ParseUUIDPipe) id: string, - @Body() updateProjectDto: Partial, - ) { - const project = await this.projectsService.update(id, updateProjectDto); - - return { - statusCode: 200, - message: 'Proyecto actualizado exitosamente', - data: project, - }; - } - - /** - * Eliminar proyecto (soft delete) - */ - @Delete(':id') - @Roles(ConstructionRole.DIRECTOR) - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: 'Eliminar proyecto (soft delete)' }) - @ApiResponse({ status: 204, description: 'Proyecto eliminado exitosamente' }) - @ApiResponse({ status: 404, description: 'Proyecto no encontrado' }) - async remove(@Param('id', ParseUUIDPipe) id: string) { - await this.projectsService.remove(id); - // No retorna data (204 No Content) - } -} -``` - ---- - -### 5. Ejemplo de DTO con Validaciones - -**Archivo:** `apps/backend/src/modules/projects/dto/create-project.dto.ts` - -```typescript -import { - IsString, - IsNotEmpty, - IsEnum, - IsOptional, - IsDateString, - IsNumber, - Min, - Max, - Length, - IsUUID, -} from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ProjectStatus } from '../enums/project-status.enum'; - -export class CreateProjectDto { - @ApiProperty({ - description: 'Nombre del proyecto', - example: 'Residencial Las Palmas', - minLength: 3, - maxLength: 255, - }) - @IsString({ message: 'El nombre debe ser un texto' }) - @IsNotEmpty({ message: 'El nombre es requerido' }) - @Length(3, 255, { message: 'El nombre debe tener entre 3 y 255 caracteres' }) - name: string; - - @ApiPropertyOptional({ - description: 'Descripci贸n del proyecto', - example: 'Desarrollo habitacional de 150 unidades', - }) - @IsOptional() - @IsString({ message: 'La descripci贸n debe ser un texto' }) - description?: string; - - @ApiProperty({ - description: 'Estado del proyecto', - enum: ProjectStatus, - example: ProjectStatus.PLANNING, - }) - @IsEnum(ProjectStatus, { message: 'Estado inv谩lido' }) - status: ProjectStatus; - - @ApiProperty({ - description: 'Fecha de inicio estimada', - example: '2025-01-15', - }) - @IsDateString({}, { message: 'La fecha de inicio debe ser v谩lida (YYYY-MM-DD)' }) - startDate: string; - - @ApiProperty({ - description: 'Fecha de fin estimada', - example: '2026-06-30', - }) - @IsDateString({}, { message: 'La fecha de fin debe ser v谩lida (YYYY-MM-DD)' }) - endDate: string; - - @ApiProperty({ - description: 'Presupuesto total en MXN', - example: 45000000, - minimum: 0, - }) - @IsNumber({}, { message: 'El presupuesto debe ser un n煤mero' }) - @Min(0, { message: 'El presupuesto no puede ser negativo' }) - budget: number; - - @ApiPropertyOptional({ - description: 'Ubicaci贸n del proyecto', - example: 'Av. Reforma 123, CDMX', - }) - @IsOptional() - @IsString({ message: 'La ubicaci贸n debe ser un texto' }) - location?: string; - - @ApiPropertyOptional({ - description: 'Ingeniero responsable (UUID)', - example: '123e4567-e89b-12d3-a456-426614174000', - }) - @IsOptional() - @IsUUID('4', { message: 'El ID del ingeniero debe ser un UUID v谩lido' }) - engineerId?: string; -} -``` - ---- - -### 6. Rate Limiting Configuration - -**Archivo:** `apps/backend/src/common/guards/throttle.guard.ts` - -```typescript -import { Injectable } from '@nestjs/common'; -import { ThrottlerGuard as NestThrottlerGuard } from '@nestjs/throttler'; - -@Injectable() -export class ThrottleGuard extends NestThrottlerGuard { - protected async getTracker(req: Record): Promise { - // Rate limiting por usuario autenticado (si existe) - if (req.user?.sub) { - return `user-${req.user.sub}`; - } - - // Rate limiting por IP para requests no autenticados - return req.ip; - } -} -``` - -**Configuraci贸n en AppModule:** - -```typescript -import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; -import { APP_GUARD } from '@nestjs/core'; - -@Module({ - imports: [ - ThrottlerModule.forRoot([ - { - ttl: 60000, // 1 minuto - limit: 100, // 100 requests - }, - ]), - ], - providers: [ - { - provide: APP_GUARD, - useClass: ThrottlerGuard, - }, - ], -}) -export class AppModule {} -``` - ---- - -### 7. Custom Decorators para Documentaci贸n - -**Archivo:** `apps/backend/src/common/decorators/api-paginated-response.decorator.ts` - -```typescript -import { applyDecorators, Type } from '@nestjs/common'; -import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger'; -import { PaginatedResponseDto } from '../dto/paginated-response.dto'; - -export const ApiPaginatedResponse = >(model: TModel) => { - return applyDecorators( - ApiOkResponse({ - schema: { - allOf: [ - { - properties: { - statusCode: { type: 'number', example: 200 }, - message: { type: 'string', example: 'Operaci贸n exitosa' }, - data: { - properties: { - items: { - type: 'array', - items: { $ref: getSchemaPath(model) }, - }, - meta: { - properties: { - page: { type: 'number', example: 1 }, - limit: { type: 'number', example: 20 }, - totalItems: { type: 'number', example: 150 }, - totalPages: { type: 'number', example: 8 }, - hasNextPage: { type: 'boolean', example: true }, - hasPreviousPage: { type: 'boolean', example: false }, - }, - }, - }, - }, - }, - }, - ], - }, - }), - ); -}; -``` - -**Uso:** - -```typescript -@Get() -@ApiPaginatedResponse(ProjectResponseDto) -async findAll() { - // ... -} -``` - ---- - -## 馃И Test Cases - -### TC-API-001: Respuesta Exitosa - -**Request:** -```http -GET /api/projects/123e4567-e89b-12d3-a456-426614174000 -Authorization: Bearer -``` - -**Resultado esperado:** -```json -{ - "statusCode": 200, - "message": "Proyecto obtenido exitosamente", - "data": { - "id": "123e4567-e89b-12d3-a456-426614174000", - "name": "Residencial Las Palmas", - "status": "active", - ... - } -} -``` - ---- - -### TC-API-002: Error de Validaci贸n - -**Request:** -```http -POST /api/projects -Authorization: Bearer -Content-Type: application/json - -{ - "name": "AB", // Muy corto (m铆nimo 3) - "status": "invalid_status", // Estado inv谩lido - "budget": -1000 // Negativo -} -``` - -**Resultado esperado:** -```json -{ - "statusCode": 400, - "message": "Error de validaci贸n", - "error": "Bad Request", - "timestamp": "2025-11-17T10:30:00.000Z", - "path": "/api/projects", - "validationErrors": [ - { - "field": "name", - "message": "El nombre debe tener entre 3 y 255 caracteres" - }, - { - "field": "status", - "message": "Estado inv谩lido" - }, - { - "field": "budget", - "message": "El presupuesto no puede ser negativo" - } - ] -} -``` - ---- - -### TC-API-003: Paginaci贸n - -**Request:** -```http -GET /api/projects?page=2&limit=10&status=active&sortBy=createdAt&order=DESC -Authorization: Bearer -``` - -**Resultado esperado:** -```json -{ - "statusCode": 200, - "message": "Proyectos obtenidos exitosamente", - "data": { - "items": [ ... ], // 10 proyectos - "meta": { - "page": 2, - "limit": 10, - "totalItems": 45, - "totalPages": 5, - "hasNextPage": true, - "hasPreviousPage": true - } - } -} -``` - ---- - -### TC-API-004: Rate Limiting - -**Pasos:** -1. Realizar 101 requests en < 60 segundos -2. Observar respuesta del request #101 - -**Resultado esperado:** -```json -{ - "statusCode": 429, - "message": "Too Many Requests", - "error": "ThrottlerException" -} -``` - -**Headers de respuesta:** -``` -X-RateLimit-Limit: 100 -X-RateLimit-Remaining: 0 -X-RateLimit-Reset: 1700123456 -``` - ---- - -### TC-API-005: Recurso No Encontrado - -**Request:** -```http -GET /api/projects/00000000-0000-0000-0000-000000000000 -Authorization: Bearer -``` - -**Resultado esperado:** -```json -{ - "statusCode": 404, - "message": "Proyecto no encontrado", - "error": "Not Found", - "timestamp": "2025-11-17T10:30:00.000Z", - "path": "/api/projects/00000000-0000-0000-0000-000000000000" -} -``` - ---- - -### TC-API-006: Sin Permisos - -**Request:** -```http -POST /api/projects -Authorization: Bearer // Solo DIRECTOR puede crear -Content-Type: application/json - -{ - "name": "Nuevo Proyecto", - "status": "planning", - ... -} -``` - -**Resultado esperado:** -```json -{ - "statusCode": 403, - "message": "No tienes permisos para realizar esta acci贸n", - "error": "Forbidden", - "timestamp": "2025-11-17T10:30:00.000Z", - "path": "/api/projects" -} -``` - ---- - -## 馃搵 Tareas de Implementaci贸n - -### Backend - -- [ ] **API-BE-001:** Implementar TransformInterceptor - - Estimado: 1h - -- [ ] **API-BE-002:** Implementar HttpExceptionFilter - - Estimado: 2h - -- [ ] **API-BE-003:** Crear DTOs de paginaci贸n (PaginationDto, PaginatedResponseDto) - - Estimado: 1.5h - -- [ ] **API-BE-004:** Configurar ThrottlerGuard globalmente - - Estimado: 1h - -- [ ] **API-BE-005:** Crear decorator @ApiPaginatedResponse - - Estimado: 1h - -- [ ] **API-BE-006:** Configurar ValidationPipe global con mensajes en espa帽ol - - Estimado: 1h - -- [ ] **API-BE-007:** Documentar todos los endpoints con Swagger decorators - - Estimado: 3h - -### Testing - -- [ ] **API-TEST-001:** Unit tests para TransformInterceptor - - Estimado: 1h - -- [ ] **API-TEST-002:** Unit tests para HttpExceptionFilter - - Estimado: 1.5h - -- [ ] **API-TEST-003:** Integration tests para paginaci贸n - - Estimado: 2h - -- [ ] **API-TEST-004:** E2E tests para rate limiting - - Estimado: 1.5h - -### Documentation - -- [ ] **API-DOC-001:** Crear gu铆a de convenciones REST para el equipo - - Estimado: 2h - -- [ ] **API-DOC-002:** Documentar formato de respuestas est谩ndar - - Estimado: 1h - -**Total estimado:** ~20 horas - ---- - -## 馃敆 Dependencias - -### Depende de - -- 鉁 US-FUND-004 (Infraestructura Base) - -### Bloqueante para - -- Todos los m贸dulos de negocio (Projects, Budgets, Purchases, etc.) -- Frontend (necesita API estable y predecible) - ---- - -## 馃搳 Definici贸n de Hecho (DoD) - -- 鉁 TransformInterceptor aplicado globalmente -- 鉁 HttpExceptionFilter aplicado globalmente -- 鉁 ValidationPipe configurado con mensajes en espa帽ol -- 鉁 ThrottlerGuard funcionando (100 req/min) -- 鉁 Paginaci贸n implementada en al menos 3 endpoints -- 鉁 Swagger docs accesibles en `/api/docs` -- 鉁 Todos los test cases (TC-API-001 a TC-API-006) pasan -- 鉁 Code coverage > 80% en interceptors y filters -- 鉁 Gu铆a de convenciones REST documentada - ---- - -## 馃摑 Notas Adicionales - -### Versionado de API - -Para futuras versiones: - -```typescript -@Controller({ path: 'projects', version: '1' }) -export class ProjectsV1Controller {} - -@Controller({ path: 'projects', version: '2' }) -export class ProjectsV2Controller {} -``` - -Acceso: `/api/v1/projects`, `/api/v2/projects` - -### HATEOAS (Opcional) - -Para nivel de madurez REST avanzado: - -```typescript -{ - "statusCode": 200, - "data": { - "id": "123", - "name": "Proyecto X", - "_links": { - "self": "/api/projects/123", - "budgets": "/api/projects/123/budgets", - "update": "/api/projects/123", - "delete": "/api/projects/123" - } - } -} -``` - ---- - -**Fecha de creaci贸n:** 2025-11-17 -**脷ltima actualizaci贸n:** 2025-11-17 -**Versi贸n:** 1.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-007-navegacion-routing.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-007-navegacion-routing.md deleted file mode 100644 index ddd7009cf..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-007-navegacion-routing.md +++ /dev/null @@ -1,976 +0,0 @@ -# US-FUND-007: Navegaci贸n y Routing - -**Epic:** MAI-001 - Fundamentos del Sistema -**Story Points:** 5 -**Prioridad:** Baja -**Dependencias:** -- US-FUND-004 (Infraestructura Base) -- US-FUND-001 (Autenticaci贸n JWT) - -**Estado:** Pendiente -**Asignado a:** Frontend Lead - ---- - -## 馃搵 Historia de Usuario - -**Como** usuario del sistema -**Quiero** navegar de forma fluida entre las diferentes secciones de la aplicaci贸n -**Para** acceder r谩pidamente a las funcionalidades que necesito seg煤n mi rol. - ---- - -## 馃幆 Contexto y Objetivos - -### Contexto - -Este documento define la estructura de navegaci贸n y routing de la aplicaci贸n frontend. Incluye: - -- **Estructura de rutas** por m贸dulo -- **Layouts** reutilizables (Auth, Dashboard) -- **Rutas protegidas** (autenticaci贸n requerida) -- **Guards por rol** (acceso basado en permisos) -- **Navegaci贸n lateral** (sidebar con men煤 din谩mico) -- **Breadcrumbs** para orientaci贸n del usuario -- **404 y manejo de errores** de navegaci贸n - -### Objetivos - -1. 鉁 Rutas organizadas por m贸dulo de negocio -2. 鉁 Protecci贸n de rutas que requieren autenticaci贸n -3. 鉁 Restricci贸n de rutas por rol (RBAC) -4. 鉁 Sidebar din谩mico seg煤n rol del usuario -5. 鉁 Breadcrumbs actualizados autom谩ticamente -6. 鉁 Deep linking funcional (URLs compartibles) -7. 鉁 404 page para rutas inexistentes - ---- - -## 鉁 Criterios de Aceptaci贸n - -### CA-1: Estructura de Rutas - -**Dado** la aplicaci贸n frontend -**Cuando** se examinan las rutas configuradas -**Entonces**: - -- 鉁 Rutas p煤blicas (no requieren auth): - - `/login` - - `/register` - - `/forgot-password` - - `/reset-password/:token` - -- 鉁 Rutas protegidas (requieren auth): - - `/dashboard` - - `/profile` - - `/projects` - - `/projects/:id` - - `/budgets` - - `/purchases` - - `/hr` (solo HR) - - `/finance` (solo Finance) - - `/post-sales` (solo Post-Sales) - ---- - -### CA-2: Rutas Protegidas por Autenticaci贸n - -**Dado** un usuario no autenticado -**Cuando** intenta acceder a `/dashboard` -**Entonces**: - -- 鉁 Es redirigido a `/login` -- 鉁 URL de destino se guarda en query param: `/login?redirect=/dashboard` -- 鉁 Despu茅s de login, es redirigido a `/dashboard` - ---- - -### CA-3: Rutas Protegidas por Rol - -**Dado** un usuario con rol `resident` -**Cuando** intenta acceder a `/budgets` (solo Director/Engineer) -**Entonces**: - -- 鉁 Es redirigido a `/dashboard` -- 鉁 Toast muestra: "No tienes permisos para acceder a esta secci贸n" -- 鉁 No se muestra contenido de la ruta restringida - ---- - -### CA-4: Sidebar Din谩mico - -**Dado** un usuario autenticado -**Cuando** visualiza el sidebar -**Entonces**: - -- 鉁 Solo muestra secciones permitidas para su rol -- 鉁 Secci贸n activa est谩 resaltada -- 鉁 Iconos representativos para cada secci贸n -- 鉁 Sidebar colapsable en pantallas peque帽as - -**Ejemplo para rol `engineer`:** -- 鉁 Dashboard 鉁旓笍 -- 鉁 Proyectos 鉁旓笍 -- 鉁 Presupuestos 鉁旓笍 -- 鉁 Compras 鉁旓笍 -- 鉂 Finanzas (oculto) -- 鉂 RRHH (oculto) -- 鉂 Post-venta (oculto) - ---- - -### CA-5: Breadcrumbs - -**Dado** un usuario en `/projects/123e4567-e89b-12d3-a456-426614174000` -**Cuando** visualiza los breadcrumbs -**Entonces**: - -- 鉁 Muestra: `Dashboard > Proyectos > Residencial Las Palmas` -- 鉁 Cada nivel es clickeable (excepto el actual) -- 鉁 Click en "Proyectos" navega a `/projects` -- 鉁 Click en "Dashboard" navega a `/dashboard` - ---- - -### CA-6: Deep Linking - -**Dado** un usuario autenticado -**Cuando** accede directamente a `/projects/123e4567-e89b-12d3-a456-426614174000` (URL copiada) -**Entonces**: - -- 鉁 La p谩gina carga correctamente -- 鉁 Sidebar muestra "Proyectos" como activo -- 鉁 Breadcrumbs muestra la ruta completa -- 鉁 Datos del proyecto se cargan desde la API - ---- - -### CA-7: 404 Not Found - -**Dado** un usuario autenticado -**Cuando** accede a `/ruta-inexistente` -**Entonces**: - -- 鉁 Se muestra p谩gina 404 personalizada -- 鉁 Mensaje: "La p谩gina que buscas no existe" -- 鉁 Bot贸n "Ir al Dashboard" redirige a `/dashboard` -- 鉁 URL en el navegador sigue siendo `/ruta-inexistente` - ---- - -## 馃敡 Especificaci贸n T茅cnica Detallada - -### 1. Estructura de Rutas - -**Archivo:** `apps/frontend/src/routes/routes.tsx` - -```typescript -import { Routes, Route, Navigate } from 'react-router-dom'; - -// Layouts -import { AuthLayout } from '@/components/layout/AuthLayout'; -import { DashboardLayout } from '@/components/layout/DashboardLayout'; - -// Guards -import { ProtectedRoute } from '@/components/guards/ProtectedRoute'; -import { RoleGuard } from '@/components/guards/RoleGuard'; - -// Pages - Auth -import { LoginPage } from '@/features/auth/pages/LoginPage'; -import { RegisterPage } from '@/features/auth/pages/RegisterPage'; -import { ForgotPasswordPage } from '@/features/auth/pages/ForgotPasswordPage'; -import { ResetPasswordPage } from '@/features/auth/pages/ResetPasswordPage'; - -// Pages - Dashboard -import { DashboardPage } from '@/features/dashboard/pages/DashboardPage'; -import { ProfilePage } from '@/features/profile/pages/ProfilePage'; - -// Pages - Projects -import { ProjectsListPage } from '@/features/projects/pages/ProjectsListPage'; -import { ProjectDetailPage } from '@/features/projects/pages/ProjectDetailPage'; -import { CreateProjectPage } from '@/features/projects/pages/CreateProjectPage'; - -// Pages - Budgets -import { BudgetsListPage } from '@/features/budgets/pages/BudgetsListPage'; -import { BudgetDetailPage } from '@/features/budgets/pages/BudgetDetailPage'; - -// Pages - Purchases -import { PurchasesListPage } from '@/features/purchases/pages/PurchasesListPage'; -import { SuppliersPage } from '@/features/purchases/pages/SuppliersPage'; - -// Pages - HR -import { HrDashboardPage } from '@/features/hr/pages/HrDashboardPage'; -import { EmployeesPage } from '@/features/hr/pages/EmployeesPage'; -import { AttendancePage } from '@/features/hr/pages/AttendancePage'; - -// Pages - Finance -import { FinanceDashboardPage } from '@/features/finance/pages/FinanceDashboardPage'; -import { CashFlowPage } from '@/features/finance/pages/CashFlowPage'; - -// Pages - Post-Sales -import { PostSalesDashboardPage } from '@/features/post-sales/pages/PostSalesDashboardPage'; -import { WarrantiesPage } from '@/features/post-sales/pages/WarrantiesPage'; - -// Pages - Errors -import { NotFoundPage } from '@/features/errors/pages/NotFoundPage'; - -export function AppRoutes() { - return ( - - {/* ==================== PUBLIC ROUTES ==================== */} - }> - } /> - } /> - } /> - } /> - - - {/* ==================== PROTECTED ROUTES ==================== */} - - - - } - > - {/* Dashboard - Todos los roles */} - } /> - } /> - - {/* Projects - Director, Engineer, Resident */} - - - - } - /> - - - - } - /> - - - - } - /> - - {/* Budgets - Director, Engineer */} - - - - } - /> - - - - } - /> - - {/* Purchases - Director, Engineer, Purchases */} - - - - } - /> - - - - } - /> - - {/* HR - Director, HR */} - - - - } - /> - - - - } - /> - - - - } - /> - - {/* Finance - Director, Finance */} - - - - } - /> - - - - } - /> - - {/* Post-Sales - Director, Post-Sales */} - - - - } - /> - - - - } - /> - - - {/* ==================== REDIRECTS ==================== */} - } /> - - {/* ==================== 404 NOT FOUND ==================== */} - } /> - - ); -} -``` - ---- - -### 2. Protected Route Guard - -**Archivo:** `apps/frontend/src/components/guards/ProtectedRoute.tsx` - -```typescript -import { Navigate, useLocation } from 'react-router-dom'; -import { useAuthStore } from '@/stores/useAuthStore'; - -interface ProtectedRouteProps { - children: React.ReactNode; -} - -export function ProtectedRoute({ children }: ProtectedRouteProps) { - const { isAuthenticated } = useAuthStore(); - const location = useLocation(); - - if (!isAuthenticated) { - // Redirigir a login con URL de retorno - return ; - } - - return <>{children}; -} -``` - ---- - -### 3. Role Guard - -**Archivo:** `apps/frontend/src/components/guards/RoleGuard.tsx` - -```typescript -import { Navigate } from 'react-router-dom'; -import { toast } from 'sonner'; -import { useEffect } from 'react'; -import { useAuthStore } from '@/stores/useAuthStore'; - -interface RoleGuardProps { - allowedRoles: string[]; - children: React.ReactNode; -} - -export function RoleGuard({ allowedRoles, children }: RoleGuardProps) { - const { user } = useAuthStore(); - - useEffect(() => { - if (user && !allowedRoles.includes(user.role)) { - toast.error('No tienes permisos para acceder a esta secci贸n'); - } - }, [user, allowedRoles]); - - if (!user || !allowedRoles.includes(user.role)) { - return ; - } - - return <>{children}; -} -``` - ---- - -### 4. Dashboard Layout con Sidebar - -**Archivo:** `apps/frontend/src/components/layout/DashboardLayout.tsx` - -```typescript -import { Outlet } from 'react-router-dom'; -import { Sidebar } from './Sidebar'; -import { Header } from './Header'; -import { Breadcrumbs } from './Breadcrumbs'; - -export function DashboardLayout() { - return ( -
- {/* Sidebar */} - - - {/* Main Content */} -
- {/* Header */} -
- - {/* Breadcrumbs */} -
- -
- - {/* Page Content */} -
- -
-
-
- ); -} -``` - ---- - -### 5. Sidebar con Men煤 Din谩mico - -**Archivo:** `apps/frontend/src/components/layout/Sidebar.tsx` - -```typescript -import { NavLink } from 'react-router-dom'; -import { - LayoutDashboard, - FolderKanban, - FileText, - ShoppingCart, - Users, - DollarSign, - Headphones, -} from 'lucide-react'; -import { useAuthStore } from '@/stores/useAuthStore'; -import { cn } from '@/lib/utils'; - -interface MenuItem { - label: string; - path: string; - icon: React.ElementType; - allowedRoles: string[]; -} - -const menuItems: MenuItem[] = [ - { - label: 'Dashboard', - path: '/dashboard', - icon: LayoutDashboard, - allowedRoles: ['director', 'engineer', 'resident', 'purchases', 'finance', 'hr', 'post_sales'], - }, - { - label: 'Proyectos', - path: '/projects', - icon: FolderKanban, - allowedRoles: ['director', 'engineer', 'resident'], - }, - { - label: 'Presupuestos', - path: '/budgets', - icon: FileText, - allowedRoles: ['director', 'engineer'], - }, - { - label: 'Compras', - path: '/purchases', - icon: ShoppingCart, - allowedRoles: ['director', 'engineer', 'purchases'], - }, - { - label: 'RRHH', - path: '/hr', - icon: Users, - allowedRoles: ['director', 'hr'], - }, - { - label: 'Finanzas', - path: '/finance', - icon: DollarSign, - allowedRoles: ['director', 'finance'], - }, - { - label: 'Post-venta', - path: '/post-sales', - icon: Headphones, - allowedRoles: ['director', 'post_sales'], - }, -]; - -export function Sidebar() { - const { user } = useAuthStore(); - - // Filtrar men煤 seg煤n rol - const visibleItems = menuItems.filter((item) => - item.allowedRoles.includes(user?.role || ''), - ); - - return ( - - ); -} -``` - ---- - -### 6. Breadcrumbs Component - -**Archivo:** `apps/frontend/src/components/layout/Breadcrumbs.tsx` - -```typescript -import { Link, useLocation } from 'react-router-dom'; -import { ChevronRight, Home } from 'lucide-react'; -import { useMemo } from 'react'; - -interface BreadcrumbItem { - label: string; - path: string; -} - -// Mapa de rutas a labels -const routeLabels: Record = { - dashboard: 'Dashboard', - projects: 'Proyectos', - budgets: 'Presupuestos', - purchases: 'Compras', - suppliers: 'Proveedores', - hr: 'RRHH', - employees: 'Empleados', - attendance: 'Asistencias', - finance: 'Finanzas', - 'cash-flow': 'Flujo de Caja', - 'post-sales': 'Post-venta', - warranties: 'Garant铆as', - profile: 'Mi Perfil', - new: 'Nuevo', -}; - -export function Breadcrumbs() { - const location = useLocation(); - - const breadcrumbs = useMemo(() => { - const paths = location.pathname.split('/').filter(Boolean); - - const items: BreadcrumbItem[] = []; - let currentPath = ''; - - paths.forEach((segment, index) => { - currentPath += `/${segment}`; - - // Si es un UUID, obtener nombre del recurso (requiere data del contexto) - const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( - segment, - ); - - if (isUUID) { - // TODO: Obtener nombre real del recurso desde el store o API - items.push({ - label: 'Detalles', - path: currentPath, - }); - } else { - items.push({ - label: routeLabels[segment] || segment, - path: currentPath, - }); - } - }); - - return items; - }, [location.pathname]); - - return ( - - ); -} -``` - ---- - -### 7. 404 Not Found Page - -**Archivo:** `apps/frontend/src/features/errors/pages/NotFoundPage.tsx` - -```typescript -import { useNavigate } from 'react-router-dom'; -import { Button } from '@/components/ui/button'; -import { Home, ArrowLeft } from 'lucide-react'; - -export function NotFoundPage() { - const navigate = useNavigate(); - - return ( -
-
-

404

- -

- P谩gina no encontrada -

- -

- Lo sentimos, la p谩gina que buscas no existe o ha sido movida. -

- -
- - - -
-
-
- ); -} -``` - ---- - -### 8. Login Redirect After Auth - -**Archivo:** `apps/frontend/src/features/auth/pages/LoginPage.tsx` (fragmento) - -```typescript -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { useAuthStore } from '@/stores/useAuthStore'; - -export function LoginPage() { - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const { setTokens } = useAuthStore(); - - const handleLogin = async (credentials: LoginDto) => { - try { - const response = await apiService.post('/auth/login', credentials); - - setTokens(response.accessToken, response.refreshToken); - - // Redirigir a la URL original o al dashboard - const redirectUrl = searchParams.get('redirect') || '/dashboard'; - navigate(redirectUrl); - - toast.success('Inicio de sesi贸n exitoso'); - } catch (error) { - toast.error('Credenciales inv谩lidas'); - } - }; - - return ( - // ... form - ); -} -``` - ---- - -## 馃И Test Cases - -### TC-NAV-001: Redirecci贸n a Login - -**Pre-condiciones:** -- Usuario no autenticado - -**Pasos:** -1. Navegar a `/dashboard` - -**Resultado esperado:** -- 鉁 Redirige a `/login?redirect=/dashboard` -- 鉁 No se muestra contenido del dashboard - ---- - -### TC-NAV-002: Login con Redirect - -**Pre-condiciones:** -- Usuario en `/login?redirect=/projects/123` - -**Pasos:** -1. Completar login exitoso - -**Resultado esperado:** -- 鉁 Redirige a `/projects/123` -- 鉁 P谩gina de proyecto carga correctamente - ---- - -### TC-NAV-003: Restricci贸n por Rol - -**Pre-condiciones:** -- Usuario con rol `resident` autenticado - -**Pasos:** -1. Navegar a `/budgets` (solo Director/Engineer) - -**Resultado esperado:** -- 鉁 Redirige a `/dashboard` -- 鉁 Toast: "No tienes permisos para acceder a esta secci贸n" - ---- - -### TC-NAV-004: Sidebar Din谩mico - -**Pre-condiciones:** -- Usuario con rol `engineer` autenticado - -**Pasos:** -1. Observar el sidebar - -**Resultado esperado:** -- 鉁 Visible: Dashboard, Proyectos, Presupuestos, Compras -- 鉂 Oculto: RRHH, Finanzas, Post-venta - ---- - -### TC-NAV-005: Navegaci贸n Activa - -**Pre-condiciones:** -- Usuario en `/projects` - -**Pasos:** -1. Observar el sidebar - -**Resultado esperado:** -- 鉁 Item "Proyectos" resaltado con bg-primary -- 鉁 Otros items con estilo normal - ---- - -### TC-NAV-006: Breadcrumbs - -**Pre-condiciones:** -- Usuario en `/projects/123/budgets/456` - -**Pasos:** -1. Observar breadcrumbs - -**Resultado esperado:** -- 鉁 Muestra: `Inicio > Proyectos > Detalles > Presupuestos > Detalles` -- 鉁 "Inicio" clickeable 鈫 navega a `/dashboard` -- 鉁 "Proyectos" clickeable 鈫 navega a `/projects` -- 鉁 脷ltimo elemento NO clickeable (activo) - ---- - -### TC-NAV-007: 404 Page - -**Pre-condiciones:** -- Usuario autenticado - -**Pasos:** -1. Navegar a `/ruta-que-no-existe` - -**Resultado esperado:** -- 鉁 Se muestra p谩gina 404 -- 鉁 T铆tulo: "404" -- 鉁 Bot贸n "Volver atr谩s" navega a p谩gina anterior -- 鉁 Bot贸n "Ir al Dashboard" navega a `/dashboard` - ---- - -## 馃搵 Tareas de Implementaci贸n - -### Frontend - -- [ ] **NAV-FE-001:** Configurar React Router v6 - - Estimado: 1h - -- [ ] **NAV-FE-002:** Crear archivo de rutas (routes.tsx) - - Estimado: 2h - -- [ ] **NAV-FE-003:** Implementar ProtectedRoute guard - - Estimado: 1h - -- [ ] **NAV-FE-004:** Implementar RoleGuard - - Estimado: 1.5h - -- [ ] **NAV-FE-005:** Crear DashboardLayout con sidebar - - Estimado: 2h - -- [ ] **NAV-FE-006:** Crear AuthLayout - - Estimado: 1h - -- [ ] **NAV-FE-007:** Implementar Sidebar con men煤 din谩mico - - Estimado: 2h - -- [ ] **NAV-FE-008:** Implementar Breadcrumbs component - - Estimado: 2h - -- [ ] **NAV-FE-009:** Crear NotFoundPage - - Estimado: 1h - -- [ ] **NAV-FE-010:** Implementar redirect despu茅s de login - - Estimado: 1h - -### Testing - -- [ ] **NAV-TEST-001:** Unit tests para guards - - Estimado: 2h - -- [ ] **NAV-TEST-002:** E2E tests para navegaci贸n - - Estimado: 2h - -**Total estimado:** ~18.5 horas - ---- - -## 馃敆 Dependencias - -### Depende de - -- 鉁 US-FUND-004 (Infraestructura Base) -- 鉁 US-FUND-001 (Autenticaci贸n JWT) - -### Bloqueante para - -- Todas las p谩ginas y features del sistema -- UX completa - ---- - -## 馃搳 Definici贸n de Hecho (DoD) - -- 鉁 React Router v6 configurado -- 鉁 Rutas p煤blicas y protegidas definidas -- 鉁 ProtectedRoute guard funcional -- 鉁 RoleGuard funcional -- 鉁 Sidebar muestra men煤 din谩mico seg煤n rol -- 鉁 Breadcrumbs actualizados autom谩ticamente -- 鉁 404 page implementada -- 鉁 Deep linking funcional -- 鉁 Redirect despu茅s de login funcional -- 鉁 Todos los test cases (TC-NAV-001 a TC-NAV-007) pasan -- 鉁 Navegaci贸n fluida sin flickering - ---- - -## 馃摑 Notas Adicionales - -### Mobile Responsive - -- 鉁 Sidebar colapsable en pantallas < 768px -- 鉁 Men煤 hamburguesa en mobile -- 鉁 Breadcrumbs ocultos en mobile (opcional) - -### Accesibilidad - -- 鉁 Navegaci贸n con teclado (Tab, Enter) -- 鉁 ARIA labels en links -- 鉁 Focus visible en elementos - -### Performance - -- 鉁 Lazy loading de p谩ginas con React.lazy() -- 鉁 Code splitting por ruta -- 鉁 Suspense boundaries para cargas as铆ncronas - ---- - -**Fecha de creaci贸n:** 2025-11-17 -**脷ltima actualizaci贸n:** 2025-11-17 -**Versi贸n:** 1.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-008-ui-ux-base.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-008-ui-ux-base.md deleted file mode 100644 index 5a4ab2592..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-008-ui-ux-base.md +++ /dev/null @@ -1,972 +0,0 @@ -# US-FUND-008: UI/UX Base y Sistema de Dise帽o - -**Epic:** MAI-001 - Fundamentos del Sistema -**Story Points:** 3 -**Prioridad:** Baja -**Dependencias:** -- US-FUND-004 (Infraestructura Base) - -**Estado:** Pendiente -**Asignado a:** Frontend Lead + UI/UX Designer - ---- - -## 馃搵 Historia de Usuario - -**Como** usuario del sistema -**Quiero** una interfaz intuitiva, consistente y visualmente atractiva -**Para** navegar y trabajar de forma eficiente en la plataforma de gesti贸n de obra. - ---- - -## 馃幆 Contexto y Objetivos - -### Contexto - -Este documento define el sistema de dise帽o base de la aplicaci贸n. Incluye: - -- **Paleta de colores** (primary, secondary, neutrals) -- **Tipograf铆a** (fuentes, tama帽os, weights) -- **Espaciado y grid** (sistema de 8px) -- **Componentes reutilizables** (Button, Input, Card, etc.) -- **Estados de carga** (Skeletons, Spinners) -- **Estados vac铆os** (Empty States) -- **Notificaciones** (Toasts, Alerts) -- **Responsive design** (mobile-first) - -### Objetivos - -1. 鉁 Dise帽o consistente en toda la aplicaci贸n -2. 鉁 Componentes reutilizables para acelerar desarrollo -3. 鉁 Paleta de colores definida y documentada -4. 鉁 Tipograf铆a clara y legible -5. 鉁 Estados de loading bien definidos -6. 鉁 Responsive en desktop, tablet y mobile -7. 鉁 Accesible (WCAG 2.1 AA) - ---- - -## 鉁 Criterios de Aceptaci贸n - -### CA-1: Paleta de Colores - -**Dado** la aplicaci贸n en ejecuci贸n -**Cuando** se visualizan componentes -**Entonces**: - -- 鉁 Color primario (construcci贸n/obra): - - Primary: `#E97A20` (naranja construcci贸n) - - Primary Hover: `#D46B17` - - Primary Light: `#FFF4E6` - -- 鉁 Colores de estado: - - Success: `#10B981` (verde) - - Warning: `#F59E0B` (amarillo) - - Error: `#EF4444` (rojo) - - Info: `#3B82F6` (azul) - -- 鉁 Colores neutrales: - - Gray-50 a Gray-900 (escala de grises) - ---- - -### CA-2: Tipograf铆a - -**Dado** cualquier p谩gina de la aplicaci贸n -**Cuando** se visualiza texto -**Entonces**: - -- 鉁 Fuente principal: `Inter` (Google Fonts) -- 鉁 Fallback: `system-ui, -apple-system, sans-serif` -- 鉁 Tama帽os de texto: - - `text-xs`: 12px - - `text-sm`: 14px - - `text-base`: 16px - - `text-lg`: 18px - - `text-xl`: 20px - - `text-2xl`: 24px - - `text-3xl`: 30px - - `text-4xl`: 36px - -- 鉁 Pesos (weights): - - Regular: 400 - - Medium: 500 - - Semibold: 600 - - Bold: 700 - ---- - -### CA-3: Componentes Reutilizables - -**Dado** el sistema de componentes -**Cuando** se utiliza en cualquier p谩gina -**Entonces** est谩n disponibles: - -- 鉁 **Button** (variants: primary, secondary, outline, ghost, destructive) -- 鉁 **Input** (text, email, password, number) -- 鉁 **Select** (dropdown) -- 鉁 **Checkbox** y **Radio** -- 鉁 **Card** (contenedor con sombra) -- 鉁 **Badge** (etiquetas de estado) -- 鉁 **Table** (tablas de datos) -- 鉁 **Modal/Dialog** -- 鉁 **Dropdown Menu** -- 鉁 **Tabs** -- 鉁 **Tooltip** - ---- - -### CA-4: Estados de Loading - -**Dado** una operaci贸n as铆ncrona en ejecuci贸n -**Cuando** se est谩n cargando datos -**Entonces**: - -- 鉁 Botones muestran spinner cuando est谩n en loading -- 鉁 Listas muestran skeleton loaders -- 鉁 P谩ginas completas muestran spinner centrado -- 鉁 No se permiten doble-clicks durante loading - ---- - -### CA-5: Estados Vac铆os - -**Dado** una lista sin datos -**Cuando** se visualiza la p谩gina -**Entonces**: - -- 鉁 Se muestra ilustraci贸n o icono grande -- 鉁 Mensaje descriptivo: "No hay proyectos todav铆a" -- 鉁 Call-to-action: Bot贸n "Crear Proyecto" -- 鉁 No se muestra tabla/grid vac铆o - ---- - -### CA-6: Notificaciones (Toasts) - -**Dado** una acci贸n exitosa/fallida -**Cuando** se completa -**Entonces**: - -- 鉁 Toast aparece en top-right -- 鉁 Auto-dismiss despu茅s de 5 segundos -- 鉁 Se puede cerrar manualmente (X) -- 鉁 Iconos seg煤n tipo (success: 鉁, error: 鉁, warning: 鈿, info: 鈩) -- 鉁 Colores seg煤n tipo - ---- - -### CA-7: Responsive Design - -**Dado** la aplicaci贸n en diferentes dispositivos -**Cuando** se ajusta el viewport -**Entonces**: - -- 鉁 Desktop (鈮1024px): - - Sidebar visible - - Grid de 12 columnas - - Tablas completas - -- 鉁 Tablet (768px - 1023px): - - Sidebar colapsable - - Grid de 8 columnas - - Tablas con scroll horizontal - -- 鉁 Mobile (<768px): - - Sidebar como men煤 hamburguesa - - Grid de 4 columnas - - Tablas adaptadas (cards) - - Inputs full-width - ---- - -## 馃敡 Especificaci贸n T茅cnica Detallada - -### 1. Tailwind Configuration - -**Archivo:** `apps/frontend/tailwind.config.js` - -```javascript -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: ['class'], - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], - theme: { - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: '#E97A20', - foreground: '#FFFFFF', - hover: '#D46B17', - light: '#FFF4E6', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - success: { - DEFAULT: '#10B981', - foreground: '#FFFFFF', - }, - warning: { - DEFAULT: '#F59E0B', - foreground: '#FFFFFF', - }, - error: { - DEFAULT: '#EF4444', - foreground: '#FFFFFF', - }, - info: { - DEFAULT: '#3B82F6', - foreground: '#FFFFFF', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - fontFamily: { - sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'], - }, - spacing: { - 18: '4.5rem', - 72: '18rem', - 84: '21rem', - 96: '24rem', - }, - }, - }, - plugins: [require('tailwindcss-animate')], -}; -``` - ---- - -### 2. CSS Variables - -**Archivo:** `apps/frontend/src/index.css` - -```css -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - - --radius: 0.5rem; - } - - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - font-feature-settings: 'rlig' 1, 'calt' 1; - } -} -``` - ---- - -### 3. Button Component (shadcn/ui) - -**Archivo:** `apps/frontend/src/components/ui/button.tsx` - -```typescript -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; -import { Loader2 } from 'lucide-react'; - -const buttonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary-hover', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: - 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', - }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; - loading?: boolean; -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, loading = false, children, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - - return ( - - {loading && } - {children} - - ); - }, -); -Button.displayName = 'Button'; - -export { Button, buttonVariants }; -``` - -**Uso:** - -```typescript - - - - -``` - ---- - -### 4. Card Component - -**Archivo:** `apps/frontend/src/components/ui/card.tsx` - -```typescript -import * as React from 'react'; -import { cn } from '@/lib/utils'; - -const Card = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), -); -Card.displayName = 'Card'; - -const CardHeader = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), -); -CardHeader.displayName = 'CardHeader'; - -const CardTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ), -); -CardTitle.displayName = 'CardTitle'; - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); -CardDescription.displayName = 'CardDescription'; - -const CardContent = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ), -); -CardContent.displayName = 'CardContent'; - -const CardFooter = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), -); -CardFooter.displayName = 'CardFooter'; - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; -``` - -**Uso:** - -```typescript - - - Proyecto Residencial - 150 unidades habitacionales - - -

Contenido del proyecto...

-
- - - -
-``` - ---- - -### 5. Skeleton Loader - -**Archivo:** `apps/frontend/src/components/ui/skeleton.tsx` - -```typescript -import { cn } from '@/lib/utils'; - -function Skeleton({ className, ...props }: React.HTMLAttributes) { - return
; -} - -export { Skeleton }; -``` - -**Archivo:** `apps/frontend/src/components/skeletons/ProjectCardSkeleton.tsx` - -```typescript -import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/card'; -import { Skeleton } from '@/components/ui/skeleton'; - -export function ProjectCardSkeleton() { - return ( - - - - - - - - - - - - - - - ); -} -``` - -**Uso:** - -```typescript -{isLoading ? ( - -) : ( - -)} -``` - ---- - -### 6. Empty State Component - -**Archivo:** `apps/frontend/src/components/ui/empty-state.tsx` - -```typescript -import { Button } from '@/components/ui/button'; -import { LucideIcon } from 'lucide-react'; - -interface EmptyStateProps { - icon: LucideIcon; - title: string; - description?: string; - action?: { - label: string; - onClick: () => void; - }; -} - -export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) { - return ( -
-
- -
- -

{title}

- - {description &&

{description}

} - - {action && ( - - )} -
- ); -} -``` - -**Uso:** - -```typescript -import { FolderKanban } from 'lucide-react'; - -{projects.length === 0 && ( - navigate('/projects/new'), - }} - /> -)} -``` - ---- - -### 7. Toast Notifications (Sonner) - -**Instalaci贸n:** - -```bash -npm install sonner -``` - -**Configuraci贸n en App.tsx:** - -```typescript -import { Toaster } from 'sonner'; - -function App() { - return ( - <> - {/* App content */} - - - ); -} -``` - -**Uso:** - -```typescript -import { toast } from 'sonner'; - -// Success -toast.success('Proyecto creado exitosamente'); - -// Error -toast.error('Error al guardar los cambios'); - -// Warning -toast.warning('El presupuesto excede el l铆mite'); - -// Info -toast.info('Nuevo comentario en el proyecto'); - -// Loading -const toastId = toast.loading('Guardando cambios...'); -// ... despu茅s de completar -toast.success('Cambios guardados', { id: toastId }); - -// Con acci贸n -toast.success('Proyecto actualizado', { - action: { - label: 'Ver', - onClick: () => navigate(`/projects/${id}`), - }, -}); -``` - ---- - -### 8. Badge Component - -**Archivo:** `apps/frontend/src/components/ui/badge.tsx` - -```typescript -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; - -const badgeVariants = cva( - 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', - { - variants: { - variant: { - default: 'border-transparent bg-primary text-primary-foreground', - secondary: 'border-transparent bg-secondary text-secondary-foreground', - success: 'border-transparent bg-success text-success-foreground', - warning: 'border-transparent bg-warning text-warning-foreground', - error: 'border-transparent bg-error text-error-foreground', - outline: 'text-foreground', - }, - }, - defaultVariants: { - variant: 'default', - }, - }, -); - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return
; -} - -export { Badge, badgeVariants }; -``` - -**Uso para estados de proyecto:** - -```typescript -const statusBadgeVariant = { - planning: 'secondary', - active: 'success', - completed: 'default', - cancelled: 'error', -}; - - - {project.status} - -``` - ---- - -### 9. Confirmation Dialog Component - -**Archivo:** `apps/frontend/src/components/ui/confirmation-dialog.tsx` - -```typescript -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; - -interface ConfirmationDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - title: string; - description: string; - confirmLabel?: string; - cancelLabel?: string; - onConfirm: () => void; - variant?: 'default' | 'destructive'; -} - -export function ConfirmationDialog({ - open, - onOpenChange, - title, - description, - confirmLabel = 'Confirmar', - cancelLabel = 'Cancelar', - onConfirm, - variant = 'default', -}: ConfirmationDialogProps) { - return ( - - - - {title} - {description} - - - {cancelLabel} - - {confirmLabel} - - - - - ); -} -``` - -**Uso:** - -```typescript -const [showDeleteDialog, setShowDeleteDialog] = useState(false); - - { - await deleteProject(projectId); - toast.success('Proyecto eliminado'); - }} -/> -``` - ---- - -## 馃И Test Cases - -### TC-UI-001: Botones con Loading - -**Pasos:** -1. Click en bot贸n "Guardar" -2. Observar estado durante request - -**Resultado esperado:** -- 鉁 Bot贸n muestra spinner -- 鉁 Bot贸n est谩 deshabilitado -- 鉁 Texto cambia a "Guardando..." -- 鉁 Doble-click no ejecuta acci贸n dos veces - ---- - -### TC-UI-002: Empty State - -**Pasos:** -1. Navegar a `/projects` sin proyectos creados - -**Resultado esperado:** -- 鉁 Se muestra icono de carpeta grande -- 鉁 T铆tulo: "No hay proyectos todav铆a" -- 鉁 Descripci贸n visible -- 鉁 Bot贸n "Crear Proyecto" presente - ---- - -### TC-UI-003: Toast Notifications - -**Pasos:** -1. Crear un proyecto exitosamente -2. Observar notificaci贸n - -**Resultado esperado:** -- 鉁 Toast aparece en top-right -- 鉁 Color verde (success) -- 鉁 Icono de checkmark -- 鉁 Auto-dismiss despu茅s de 5 segundos -- 鉁 Se puede cerrar manualmente - ---- - -### TC-UI-004: Responsive Design - -**Pasos:** -1. Abrir app en desktop (1920px) -2. Reducir viewport a tablet (768px) -3. Reducir viewport a mobile (375px) - -**Resultado esperado:** -- 鉁 Desktop: Sidebar visible, grid 12 columnas -- 鉁 Tablet: Sidebar colapsable, grid 8 columnas -- 鉁 Mobile: Men煤 hamburguesa, grid 4 columnas - ---- - -### TC-UI-005: Skeleton Loaders - -**Pasos:** -1. Navegar a `/projects` -2. Observar durante carga - -**Resultado esperado:** -- 鉁 Se muestran 3 skeletons de cards -- 鉁 Animaci贸n de pulse -- 鉁 Una vez cargados, se reemplazan por cards reales - ---- - -## 馃搵 Tareas de Implementaci贸n - -### Frontend - -- [ ] **UI-FE-001:** Configurar Tailwind CSS con theme custom - - Estimado: 1h - -- [ ] **UI-FE-002:** Instalar y configurar shadcn/ui - - Estimado: 1h - -- [ ] **UI-FE-003:** Crear componentes base (Button, Input, Card) - - Estimado: 2h - -- [ ] **UI-FE-004:** Crear Skeleton loaders para cards y tablas - - Estimado: 1.5h - -- [ ] **UI-FE-005:** Crear EmptyState component - - Estimado: 1h - -- [ ] **UI-FE-006:** Configurar Sonner para toasts - - Estimado: 0.5h - -- [ ] **UI-FE-007:** Crear Badge component con variants - - Estimado: 0.5h - -- [ ] **UI-FE-008:** Crear ConfirmationDialog component - - Estimado: 1h - -- [ ] **UI-FE-009:** Documentar sistema de dise帽o en Storybook (opcional) - - Estimado: 3h - -### Design - -- [ ] **UI-DESIGN-001:** Definir paleta de colores final - - Estimado: 2h - -- [ ] **UI-DESIGN-002:** Crear design tokens en Figma - - Estimado: 2h - -**Total estimado:** ~15.5 horas - ---- - -## 馃敆 Dependencias - -### Depende de - -- 鉁 US-FUND-004 (Infraestructura Base) - -### Bloqueante para - -- Todas las p谩ginas y features del sistema -- UX completa - ---- - -## 馃搳 Definici贸n de Hecho (DoD) - -- 鉁 Tailwind configurado con paleta de colores -- 鉁 Componentes base instalados (shadcn/ui) -- 鉁 Button component con loading state -- 鉁 Card component funcional -- 鉁 Skeleton loaders implementados -- 鉁 EmptyState component reutilizable -- 鉁 Toasts configurados (Sonner) -- 鉁 Badge component con variants -- 鉁 ConfirmationDialog funcional -- 鉁 Responsive en desktop, tablet, mobile -- 鉁 Todos los test cases (TC-UI-001 a TC-UI-005) pasan - ---- - -## 馃摑 Notas Adicionales - -### Accesibilidad - -- 鉁 Contraste de colores WCAG AA (4.5:1) -- 鉁 Focus visible en todos los elementos interactivos -- 鉁 ARIA labels en iconos -- 鉁 Keyboard navigation funcional - -### Dark Mode (Opcional) - -- 鉁 CSS variables preparadas para dark mode -- 鉁 Toggle en user settings -- 鉁 Persistencia en localStorage - -### Icons - -- 鉁 Librer铆a: Lucide React -- 鉁 Tama帽os est谩ndar: 16px, 20px, 24px -- 鉁 Stroke width: 2px - ---- - -**Fecha de creaci贸n:** 2025-11-17 -**脷ltima actualizaci贸n:** 2025-11-17 -**Versi贸n:** 1.0 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/implementacion/TRACEABILITY.yml b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/implementacion/TRACEABILITY.yml deleted file mode 100644 index 508b25201..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/implementacion/TRACEABILITY.yml +++ /dev/null @@ -1,525 +0,0 @@ -# TRACEABILITY.yml - MAI-001: Fundamentos -# Matriz completa de trazabilidad: Requerimientos 鈫 Especificaciones 鈫 Historias 鈫 Implementaci贸n - -epic_code: MAI-001 -epic_name: Fundamentos -phase: 1 -phase_name: Alcance Inicial -budget_mxn: 25000 -story_points: 50 -status: planned -sprint: 0-2 -period: "Semanas 1-2" -reused_from_gamilit: 90% - -# ============================================================================ -# DOCUMENTACI脫N -# ============================================================================ - -documentation: - requirements: - - id: RF-AUTH-001 - file: requerimientos/RF-AUTH-001-roles-construccion.md - title: Sistema de Roles de Construcci贸n - status: planned - reused_from: EAI-001/RF-AUTH-001 - adaptations: - - "3 roles 鈫 7 roles espec铆ficos de construcci贸n" - - "Permisos ajustados por m贸dulo de obra" - - - id: RF-AUTH-002 - file: requerimientos/RF-AUTH-002-estados-cuenta.md - title: Estados de Cuenta de Usuario - status: planned - reused_from: EAI-001/RF-AUTH-002 - adaptations: - - "Estados espec铆ficos para usuarios de obra" - - - id: RF-AUTH-003 - file: requerimientos/RF-AUTH-003-multi-tenancy.md - title: Multi-tenancy por Constructora - status: planned - reused_from: EAI-001/RF-AUTH-003 (concepto) - adaptations: - - "Soporte de m煤ltiples constructoras (tenants)" - - "RLS por constructora + proyecto" - - specifications: - - id: ET-AUTH-001 - file: especificaciones/ET-AUTH-001-rbac.md - rf: RF-AUTH-001 - title: RBAC Implementation para Construcci贸n - status: planned - reused_from: EAI-001/ET-AUTH-001 - adaptations: - - "Implementaci贸n de 7 roles vs 3 de GAMILIT" - - "Matriz de permisos por m贸dulo de obra" - - - id: ET-AUTH-002 - file: especificaciones/ET-AUTH-002-estados-cuenta.md - rf: RF-AUTH-002 - title: Estados de Cuenta de Usuario - status: planned - reused_from: EAI-001/ET-AUTH-002 - adaptations: ["M铆nimas - Concepto igual"] - - - id: ET-AUTH-003 - file: especificaciones/ET-AUTH-003-multi-tenancy.md - rf: RF-AUTH-003 - title: Multi-tenancy Implementation - status: planned - reused_from: EAI-001 (concepto de multi-tenancy) - adaptations: - - "Aislamiento de datos por constructora" - - "RLS policies por tenant" - - user_stories: - - id: US-FUND-001 - file: historias-usuario/US-FUND-001-autenticacion-basica-jwt.md - title: Autenticaci贸n B谩sica JWT - rf: [RF-AUTH-001, RF-AUTH-002] - story_points: 8 - status: planned - reused_from: EAI-001/US-FUND-001 - adaptations: ["M铆nimas - 90% reutilizable"] - - - id: US-FUND-002 - file: historias-usuario/US-FUND-002-perfiles-usuario-construccion.md - title: Perfiles de Usuario de Construcci贸n - rf: RF-AUTH-001 - story_points: 5 - status: planned - reused_from: EAI-001/US-FUND-002 - adaptations: ["Perfiles espec铆ficos de construcci贸n"] - - - id: US-FUND-003 - file: historias-usuario/US-FUND-003-dashboard-por-rol.md - title: Dashboard Principal por Rol - rf: RF-AUTH-001 - story_points: 8 - status: planned - reused_from: EAI-001/US-FUND-003 - adaptations: ["7 variantes de dashboard por rol"] - - - id: US-FUND-004 - file: historias-usuario/US-FUND-004-infraestructura-base.md - title: Infraestructura T茅cnica Base - rf: [RF-AUTH-001, RF-AUTH-003] - story_points: 12 - status: planned - reused_from: EAI-001/US-FUND-004 - adaptations: ["Setup de DB, API, Frontend desde GAMILIT"] - - - id: US-FUND-005 - file: historias-usuario/US-FUND-005-sistema-sesiones.md - title: Sistema de Sesiones y Estado - rf: RF-AUTH-002 - story_points: 6 - status: planned - reused_from: EAI-001/US-FUND-005 - adaptations: ["Ninguna - Reutilizaci贸n directa"] - - - id: US-FUND-006 - file: historias-usuario/US-FUND-006-api-restful-base.md - title: API RESTful B谩sica - rf: [RF-AUTH-001, RF-AUTH-003] - story_points: 8 - status: planned - reused_from: EAI-001/US-FUND-006 - adaptations: ["Endpoints espec铆ficos de construcci贸n"] - - - id: US-FUND-007 - file: historias-usuario/US-FUND-007-navegacion-routing.md - title: Navegaci贸n y Routing - rf: RF-AUTH-001 - story_points: 5 - status: planned - reused_from: EAI-001/US-FUND-007 - adaptations: ["Rutas espec铆ficas de obra/proyecto"] - - - id: US-FUND-008 - file: historias-usuario/US-FUND-008-ui-ux-base.md - title: UI/UX Base - rf: RF-AUTH-001 - story_points: 3 - status: planned - reused_from: EAI-001/US-FUND-008 - adaptations: ["Branding de constructora, tema personalizado"] - -# ============================================================================ -# IMPLEMENTACI脫N - BASE DE DATOS -# ============================================================================ - -implementation: - database: - schemas: - - name: auth - path: apps/database/ddl/schemas/auth/ - description: Schema de autenticaci贸n (usuarios, sesiones) - reused_from_gamilit: true - - - name: auth_management - path: apps/database/ddl/schemas/auth_management/ - description: Schema de gesti贸n de autenticaci贸n (perfiles, roles) - reused_from_gamilit: true - - - name: audit_logging - path: apps/database/ddl/schemas/audit_logging/ - description: Schema de auditor铆a - reused_from_gamilit: true - - - name: constructoras - path: apps/database/ddl/schemas/constructoras/ - description: Schema de multi-tenancy (constructoras) - reused_from_gamilit: false - note: "Nuevo schema para multi-tenancy" - - enums: - - name: construction_role - schema: auth_management - file: apps/database/ddl/00-prerequisites.sql - lines: "30-39" - values: [director, engineer, resident, purchases, finance, hr, post_sales] - rf: RF-AUTH-001 - reused_from: gamilit_role (adaptado) - note: "7 roles espec铆ficos de construcci贸n vs 3 de GAMILIT" - - - name: account_status - schema: auth_management - file: apps/database/ddl/00-prerequisites.sql - lines: "40-44" - values: [active, suspended, banned, pending_verification, inactive] - rf: RF-AUTH-002 - reused_from: account_status (igual) - - tables: - - name: constructoras - schema: constructoras - file: apps/database/ddl/schemas/constructoras/tables/01-constructoras.sql - lines: 80 - description: Cat谩logo de constructoras (tenants) - rf: RF-AUTH-003 - reused_from_gamilit: false - note: "Nueva tabla para multi-tenancy" - columns: - - id (UUID, PK) - - nombre (TEXT) - - razon_social (TEXT) - - rfc (TEXT UNIQUE) - - logo_url (TEXT) - - active (BOOLEAN) - - settings (JSONB) - - created_at (TIMESTAMPTZ) - - updated_at (TIMESTAMPTZ) - - - name: profiles - schema: auth_management - file: apps/database/ddl/schemas/auth_management/tables/03-profiles.sql - lines: 125 - description: Perfiles de usuario con rol de construcci贸n - rf: RF-AUTH-001 - reused_from_gamilit: true - adaptations: - - "Agregar constructora_id FK" - - "Cambiar role a construction_role ENUM" - columns_using_enums: - - column: role - enum: construction_role - - column: account_status - enum: account_status - - - name: user_constructoras - schema: auth_management - file: apps/database/ddl/schemas/auth_management/tables/04-user_constructoras.sql - lines: 70 - description: Relaci贸n usuario-constructora (un usuario puede estar en m煤ltiples constructoras) - rf: RF-AUTH-003 - reused_from_gamilit: false - note: "Nueva tabla para multi-tenancy" - columns: - - id (UUID, PK) - - user_id (UUID, FK) - - constructora_id (UUID, FK) - - role_in_constructora (construction_role) - - is_primary (BOOLEAN) - - active (BOOLEAN) - - created_at (TIMESTAMPTZ) - - - name: audit_logs - schema: audit_logging - file: apps/database/ddl/schemas/audit_logging/tables/01-audit_logs.sql - lines: 95 - description: Logs de auditor铆a de acciones cr铆ticas - rf: RF-AUTH-002 - reused_from_gamilit: true - adaptations: ["Agregar constructora_id para filtrado"] - - functions: - - name: get_current_user_id - schema: public - file: apps/database/ddl/schemas/public/functions/get_current_user_id.sql - lines: "10-15" - description: Obtiene el user_id del usuario en contexto - rf: RF-AUTH-001 - reused_from_gamilit: true - adaptations: [] - - - name: get_current_user_role - schema: public - file: apps/database/ddl/schemas/public/functions/get_current_user_role.sql - lines: "10-20" - description: Obtiene el rol del usuario en contexto - rf: RF-AUTH-001 - reused_from_gamilit: true - adaptations: ["Retornar construction_role en lugar de gamilit_role"] - - - name: get_current_constructora_id - schema: public - file: apps/database/ddl/schemas/public/functions/get_current_constructora_id.sql - lines: "10-20" - description: Obtiene la constructora activa del usuario - rf: RF-AUTH-003 - reused_from_gamilit: false - note: "Nueva funci贸n para multi-tenancy" - - - name: user_has_access_to_constructora - schema: public - file: apps/database/ddl/schemas/public/functions/user_has_access_to_constructora.sql - lines: "10-25" - description: Verifica si usuario tiene acceso a una constructora - rf: RF-AUTH-003 - reused_from_gamilit: false - note: "Nueva funci贸n para multi-tenancy" - - rls_policies: - - table: constructoras.constructoras - policy: constructoras_select_own - description: Usuarios solo ven constructoras a las que pertenecen - rf: RF-AUTH-003 - reused_from_gamilit: false - sql: | - CREATE POLICY "constructoras_select_own" ON constructoras.constructoras - FOR SELECT - TO authenticated - USING ( - id IN ( - SELECT constructora_id - FROM auth_management.user_constructoras - WHERE user_id = get_current_user_id() - AND active = true - ) - ); - - - table: auth_management.profiles - policy: profiles_select_all - description: Todos pueden ver perfiles b谩sicos dentro de su constructora - rf: RF-AUTH-001 - reused_from_gamilit: true - adaptations: ["Filtrar por constructora"] - -# ============================================================================ -# IMPLEMENTACI脫N - BACKEND -# ============================================================================ - - backend: - modules: - - name: auth - path: apps/backend/src/modules/auth/ - description: M贸dulo de autenticaci贸n y autorizaci贸n - rf: [RF-AUTH-001, RF-AUTH-002, RF-AUTH-003] - reused_from_gamilit: true - adaptations: - - "L贸gica de multi-tenancy" - - "7 roles en lugar de 3" - - services: - - name: AuthService - path: apps/backend/src/modules/auth/auth.service.ts - description: L贸gica de autenticaci贸n (login, register, JWT) - rf: [RF-AUTH-001, RF-AUTH-002] - reused_from_gamilit: true - adaptations: ["Validar constructora al login"] - - - name: ConstructoraService - path: apps/backend/src/modules/auth/constructora.service.ts - description: L贸gica de gesti贸n de constructoras - rf: RF-AUTH-003 - reused_from_gamilit: false - note: "Nuevo servicio para multi-tenancy" - - guards: - - name: RolesGuard - path: apps/backend/src/shared/guards/roles.guard.ts - description: Guard para validar roles de usuario - rf: RF-AUTH-001 - reused_from_gamilit: true - adaptations: ["Soportar 7 roles de construcci贸n"] - - - name: ConstructoraGuard - path: apps/backend/src/shared/guards/constructora.guard.ts - description: Guard para validar acceso a recursos por constructora - rf: RF-AUTH-003 - reused_from_gamilit: false - note: "Nuevo guard para multi-tenancy" - - enums: - - name: ConstructionRole - path: apps/backend/src/shared/enums/construction-role.enum.ts - description: Enum TypeScript de roles de construcci贸n - rf: RF-AUTH-001 - reused_from: GamilitRole (adaptado) - values: - - DIRECTOR = 'director' - - ENGINEER = 'engineer' - - RESIDENT = 'resident' - - PURCHASES = 'purchases' - - FINANCE = 'finance' - - HR = 'hr' - - POST_SALES = 'post_sales' - -# ============================================================================ -# IMPLEMENTACI脫N - FRONTEND -# ============================================================================ - - frontend: - features: - - name: auth - path: apps/frontend/src/features/auth/ - description: Feature de autenticaci贸n (login, register, perfil) - rf: [RF-AUTH-001, RF-AUTH-002] - reused_from_gamilit: true - adaptations: ["Selector de constructora al login"] - - components: - - name: LoginForm - path: apps/frontend/src/features/auth/components/LoginForm.tsx - description: Formulario de login con selector de constructora - rf: RF-AUTH-001 - reused_from_gamilit: true - adaptations: ["Agregar dropdown de constructora"] - - - name: ConstructoraSelector - path: apps/frontend/src/features/auth/components/ConstructoraSelector.tsx - description: Selector de constructora activa - rf: RF-AUTH-003 - reused_from_gamilit: false - note: "Nuevo componente para multi-tenancy" - - - name: RoleBasedDashboard - path: apps/frontend/src/features/dashboard/components/RoleBasedDashboard.tsx - description: Dashboard principal con 7 variantes por rol - rf: RF-AUTH-001 - reused_from_gamilit: true - adaptations: ["7 variantes en lugar de 3"] - - stores: - - name: authStore - path: apps/frontend/src/stores/authStore.ts - description: Store de autenticaci贸n y usuario - rf: [RF-AUTH-001, RF-AUTH-002] - reused_from_gamilit: true - adaptations: ["Agregar constructora activa"] - - - name: constructoraStore - path: apps/frontend/src/stores/constructoraStore.ts - description: Store de constructora activa - rf: RF-AUTH-003 - reused_from_gamilit: false - note: "Nuevo store para multi-tenancy" - -# ============================================================================ -# TESTING -# ============================================================================ - - testing: - unit_tests: - - module: AuthService - file: apps/backend/src/modules/auth/auth.service.spec.ts - coverage_target: 85% - reused_from_gamilit: true - - - module: RolesGuard - file: apps/backend/src/shared/guards/roles.guard.spec.ts - coverage_target: 90% - reused_from_gamilit: true - - - module: ConstructoraGuard - file: apps/backend/src/shared/guards/constructora.guard.spec.ts - coverage_target: 90% - reused_from_gamilit: false - - e2e_tests: - - name: Auth E2E - file: apps/backend/test/auth/auth.e2e-spec.ts - scenarios: - - Login con credenciales v谩lidas - - Login con constructora inv谩lida - - Acceso a recurso sin permisos - - Cambio de constructora activa - reused_from_gamilit: true - - integration_tests: - - name: Multi-tenancy Integration - file: apps/backend/test/integration/multi-tenancy.spec.ts - scenarios: - - Aislamiento de datos entre constructoras - - RLS policies funcionan correctamente - reused_from_gamilit: false - -# ============================================================================ -# M脡TRICAS -# ============================================================================ - -metrics: - story_points: - planned: 50 - completed: 0 - variance: 0% - - budget: - planned: 25000 - actual: 0 - variance: 0% - - reuse_from_gamilit: - infrastructure: 90% - database: 75% - backend: 85% - frontend: 85% - overall: 84% - - time_saved_weeks: 2.5 - -# ============================================================================ -# ROADMAP -# ============================================================================ - -roadmap: - sprint_0: - week: 1 - goal: "Migraci贸n de componentes GAMILIT" - tasks: - - Migrar sistema de autenticaci贸n JWT - - Migrar guards y middleware - - Migrar componentes UI base - - Setup de base de datos con schemas - - sprint_1: - week: 2 - goal: "Implementaci贸n de MAI-001" - tasks: - - Implementar 7 roles de construcci贸n - - Implementar multi-tenancy - - Crear dashboards por rol - - Tests E2E de autenticaci贸n - -# ============================================================================ -# NOTAS -# ============================================================================ - -notes: - - "Reutilizaci贸n masiva de GAMILIT (90%) reduce tiempo significativamente" - - "Multi-tenancy es adici贸n cr铆tica vs GAMILIT" - - "7 roles requieren matriz de permisos detallada por m贸dulo" - - "Tests de GAMILIT sirven como base, adaptarlos" - - "Documentar todas las adaptaciones para mantenibilidad" diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-001-roles-construccion.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-001-roles-construccion.md deleted file mode 100644 index 2b08d98e1..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-001-roles-construccion.md +++ /dev/null @@ -1,568 +0,0 @@ -# RF-AUTH-001: Sistema de Roles de Construcci贸n - -## 馃搵 Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | RF-AUTH-001 | -| **M贸dulo** | Autenticaci贸n y Autorizaci贸n | -| **Prioridad** | P0 - Cr铆tica | -| **Estado** | 馃毀 Planificado | -| **Versi贸n** | 1.0 | -| **Fecha creaci贸n** | 2025-11-17 | -| **脷ltima actualizaci贸n** | 2025-11-17 | - -## 馃敆 Referencias - -### Especificaci贸n T茅cnica -馃搻 [ET-AUTH-001: RBAC Implementation](../especificaciones/ET-AUTH-001-rbac.md) - -### Implementaci贸n DDL -馃梽锔 **ENUM Can贸nico:** -- **Ubicaci贸n:** `apps/database/ddl/00-prerequisites.sql:30-39` -- **Tipo:** `auth_management.construction_role` -- **Valores:** `director`, `engineer`, `resident`, `purchases`, `finance`, `hr`, `post_sales` - -馃梽锔 **Tablas que usan el ENUM:** -1. `auth_management.profiles` 鈫 `apps/database/ddl/schemas/auth_management/tables/03-profiles.sql:15` -2. `auth.users` 鈫 `apps/database/ddl/schemas/auth/tables/01-users.sql:15` -3. `constructoras.user_constructoras` 鈫 `apps/database/ddl/schemas/constructoras/tables/02-user_constructoras.sql:20` - -### Backend -馃捇 **Implementaci贸n:** -- **Enum:** `apps/backend/src/shared/enums/construction-role.enum.ts` -- **Guard:** `apps/backend/src/shared/guards/roles.guard.ts` -- **Decorator:** `@Roles('engineer', 'director')` - -### Frontend -馃帹 **Componentes:** -- **Types:** `apps/frontend/src/types/auth.types.ts` -- **Componentes:** - - `apps/frontend/src/components/auth/RoleBasedRoute.tsx` - - `apps/frontend/src/components/ui/UserRoleBadge.tsx` - - `apps/frontend/src/components/admin/AdminPanel.tsx` - -### Reutilizaci贸n de Cat谩logo -鈾伙笍 **Componente base:** `core/catalog/auth/` *(Patr贸n de roles RBAC)* -**Adaptaci贸n:** 3 roles base 鈫 7 roles espec铆ficos de construcci贸n - ---- - -## 馃摑 Descripci贸n del Requerimiento - -### Contexto - -El Sistema de Administraci贸n de Obra necesita diferenciar entre **7 tipos de usuarios** con permisos y funcionalidades espec铆ficas para la gesti贸n de proyectos de construcci贸n. Esta diferenciaci贸n es cr铆tica para: -- Proteger datos sensibles de obras y presupuestos -- Permitir gesti贸n efectiva de recursos por perfil especializado -- Cumplir con segregaci贸n de funciones (SOD - Separation of Duties) -- Garantizar trazabilidad de acciones por responsable - -### Necesidad del Negocio - -**Problema:** -En proyectos de construcci贸n, diferentes actores tienen responsabilidades y necesidades de informaci贸n distintas: -- **Direcci贸n** necesita visi贸n global de m谩rgenes y riesgos -- **Ingenier铆a** necesita control de presupuestos y programaci贸n -- **Residentes** necesitan capturar avances desde campo -- **Compras** necesita gestionar proveedores e inventarios -- **Finanzas** necesita controlar flujo de efectivo -- **RRHH** necesita gestionar n贸mina y asistencias -- **Postventa** necesita atender incidencias y garant铆as - -Sin un sistema de roles bien definido, todos tendr铆an el mismo nivel de acceso, lo cual: -- Comprometer铆a la confidencialidad de datos financieros -- Dificultar铆a la asignaci贸n de responsabilidades -- Impedir铆a auditor铆as efectivas -- Generar铆a confusi贸n en permisos - -**Soluci贸n:** -Implementar un sistema de roles con **7 niveles especializados** de autorizaci贸n que permita acceso granular basado en funci贸n y responsabilidad en la obra. - ---- - -## 馃幆 Requerimiento Funcional - -### RF-AUTH-001.1: Roles Disponibles - -El sistema **DEBE** soportar exactamente 7 roles de usuario especializados en construcci贸n: - -#### 1. Director (`director`) -**Descripci贸n:** Director general o de proyectos con visi贸n estrat茅gica - -**Permisos:** -- 鉁 Visi贸n global de todos los proyectos de la constructora -- 鉁 Acceso a m谩rgenes de utilidad y rentabilidad -- 鉁 Ver todos los reportes financieros -- 鉁 Aprobar presupuestos y modificaciones mayores -- 鉁 Acceso a analytics y dashboards ejecutivos -- 鉁 Gestionar equipo de proyecto (asignar responsables) -- 鉁 Aprobar estimaciones mayores -- 鉂 NO edita datos operativos (lo hace ingenier铆a) -- 鉂 NO captura avances (lo hacen residentes) - -**Restricciones RLS (Row Level Security):** -```sql --- Pol铆tica: Director ve todos los proyectos de su constructora -CREATE POLICY "directors_view_all_projects" ON projects.projects - FOR SELECT - TO authenticated - USING ( - constructora_id = get_current_constructora_id() - AND get_current_user_role() = 'director' - ); -``` - -**Casos de Uso:** -- Revisar estado financiero de todas las obras -- Analizar desviaciones de costos -- Tomar decisiones estrat茅gicas (cancelar/continuar obras) -- Aprobar 贸rdenes de cambio mayores - ---- - -#### 2. Ingeniero (`engineer`) -**Descripci贸n:** Ingeniero de planeaci贸n, control de obra, o jefe de proyecto - -**Permisos:** -- 鉁 Todos los permisos de residente -- 鉁 Crear y editar presupuestos -- 鉁 Gestionar cat谩logo de conceptos de obra -- 鉁 Ver y ajustar programaci贸n de obra -- 鉁 Generar reportes de avance y desviaciones -- 鉁 Revisar estimaciones antes de aprobar -- 鉁 Asignar cuadrillas a frentes de trabajo -- 鉁 Acceso a m贸dulo de planeaci贸n -- 鉂 NO puede aprobar estimaciones (solo director/finanzas) -- 鉂 NO puede ver m谩rgenes de utilidad detallados -- 鉂 NO gestiona compras (lo hace departamento de compras) - -**Restricciones RLS:** -```sql --- Pol铆tica: Ingeniero ve proyectos donde es responsable o de su 谩rea -CREATE POLICY "engineers_view_assigned_projects" ON projects.projects - FOR SELECT - TO authenticated - USING ( - get_current_user_role() = 'engineer' - AND ( - id IN ( - SELECT project_id - FROM projects.project_team_assignments - WHERE user_id = get_current_user_id() - AND role IN ('engineer', 'project_manager') - ) - OR constructora_id = get_current_constructora_id() - ) - ); -``` - -**Casos de Uso:** -- Elaborar presupuestos por prototipo -- Ajustar precios unitarios -- Revisar avances vs programado -- Generar curva S -- Revisar checklists de calidad - ---- - -#### 3. Residente (`resident`) -**Descripci贸n:** Residente de obra o supervisor de campo - -**Permisos:** -- 鉁 Capturar avances f铆sicos de obra (desde app m贸vil) -- 鉁 Registrar incidencias y no conformidades -- 鉁 Tomar evidencias fotogr谩ficas geolocalizadas -- 鉁 Completar checklists de actividades -- 鉁 Ver programaci贸n de su obra -- 鉁 Registrar asistencia de empleados (app m贸vil con biom茅trico) -- 鉁 Solicitar materiales (requisiciones) -- 鉁 Ver inventario de su almac茅n de obra -- 鉂 NO puede editar presupuestos -- 鉂 NO puede aprobar 贸rdenes de compra -- 鉂 NO puede ver datos financieros (costos, m谩rgenes) -- 鉂 NO puede ver otras obras (solo las asignadas) - -**Restricciones RLS:** -```sql --- Pol铆tica: Residente solo ve su(s) obra(s) asignada(s) -CREATE POLICY "residents_view_own_projects" ON projects.projects - FOR SELECT - TO authenticated - USING ( - get_current_user_role() = 'resident' - AND id IN ( - SELECT project_id - FROM projects.project_team_assignments - WHERE user_id = get_current_user_id() - AND role = 'resident' - AND active = true - ) - ); -``` - -**Casos de Uso:** -- Capturar avance de cimentaci贸n al 80% -- Registrar incidencia de filtraci贸n en lote 23 -- Tomar foto de avance con GPS -- Completar checklist de instalaciones hidrosanitarias -- Registrar asistencia de cuadrilla de alba帽iles con huella dactilar - ---- - -#### 4. Compras (`purchases`) -**Descripci贸n:** Encargado de compras y almac茅n - -**Permisos:** -- 鉁 Ver requisiciones de todas las obras -- 鉁 Crear y gestionar 贸rdenes de compra -- 鉁 Comparar cotizaciones de proveedores -- 鉁 Gestionar cat谩logo de proveedores -- 鉁 Autorizar entregas parciales/completas -- 鉁 Gestionar movimientos de inventario (entradas, salidas, traspasos) -- 鉁 Ver kardex de materiales -- 鉁 Configurar alertas de stock m铆nimo -- 鉂 NO puede editar presupuestos -- 鉂 NO puede ver m谩rgenes de utilidad -- 鉂 NO puede aprobar estimaciones - -**Restricciones RLS:** -```sql --- Pol铆tica: Compras ve requisiciones de todas las obras de la constructora -CREATE POLICY "purchases_view_all_requisitions" ON purchases.purchase_requisitions - FOR SELECT - TO authenticated - USING ( - get_current_user_role() = 'purchases' - AND constructora_id = get_current_constructora_id() - ); -``` - -**Casos de Uso:** -- Comparar 3 cotizaciones de cemento -- Generar orden de compra por $50,000 -- Autorizar entrega parcial de acero -- Traspasar materiales entre obras -- Alertar por stock bajo de alambr贸n - ---- - -#### 5. Finanzas (`finance`) -**Descripci贸n:** Administraci贸n, contabilidad o finanzas - -**Permisos:** -- 鉁 Ver presupuestos y costos reales de todas las obras -- 鉁 Aprobar estimaciones hacia clientes -- 鉁 Aprobar estimaciones hacia subcontratistas -- 鉁 Ver flujo de efectivo de obra -- 鉁 Gestionar pagos y cobranzas -- 鉁 Ver reportes financieros detallados -- 鉁 Exportar datos contables -- 鉁 Configurar centros de costo -- 鉂 NO puede editar avances f铆sicos -- 鉂 NO puede crear 贸rdenes de compra (solo autorizar pagos) - -**Restricciones RLS:** -```sql --- Pol铆tica: Finanzas ve datos financieros de toda la constructora -CREATE POLICY "finance_view_all_financials" ON budgets.budgets - FOR SELECT - TO authenticated - USING ( - get_current_user_role() = 'finance' - AND constructora_id = get_current_constructora_id() - ); -``` - -**Casos de Uso:** -- Revisar desviaciones de presupuesto por obra -- Aprobar estimaci贸n 3 de obra A por $2M -- Ver flujo de efectivo proyectado -- Generar reporte de cartera vencida -- Exportar p贸lizas contables - ---- - -#### 6. RRHH (`hr`) -**Descripci贸n:** Recursos Humanos y n贸mina - -**Permisos:** -- 鉁 Gestionar empleados y cuadrillas -- 鉁 Ver asistencias de todas las obras -- 鉁 Generar reportes de n贸mina -- 鉁 Exportar datos para IMSS/INFONAVIT -- 鉁 Ver costeo de mano de obra por obra -- 鉁 Gestionar oficios y categor铆as -- 鉁 Configurar par谩metros de n贸mina -- 鉂 NO puede ver presupuestos completos -- 鉂 NO puede aprobar estimaciones -- 鉂 NO puede ver m谩rgenes de utilidad - -**Restricciones RLS:** -```sql --- Pol铆tica: RRHH ve datos de empleados de toda la constructora -CREATE POLICY "hr_view_all_employees" ON hr.employees - FOR SELECT - TO authenticated - USING ( - get_current_user_role() = 'hr' - AND constructora_id = get_current_constructora_id() - ); -``` - -**Casos de Uso:** -- Dar de alta nuevo empleado en IMSS -- Generar archivo SUA mensual -- Revisar asistencias de obra B -- Calcular costeo de mano de obra por partida -- Exportar n贸mina semanal - ---- - -#### 7. Postventa (`post_sales`) -**Descripci贸n:** Coordinador de postventa y garant铆as - -**Permisos:** -- 鉁 Ver viviendas entregadas -- 鉁 Gestionar tickets de postventa -- 鉁 Registrar incidencias de clientes -- 鉁 Dar seguimiento a garant铆as -- 鉁 Programar visitas de reparaci贸n -- 鉁 Gestionar cuadrillas de postventa -- 鉁 Ver historial completo de vivienda -- 鉂 NO puede ver presupuestos -- 鉂 NO puede ver datos de obras en ejecuci贸n -- 鉂 NO puede gestionar compras - -**Restricciones RLS:** -```sql --- Pol铆tica: Postventa solo ve viviendas entregadas -CREATE POLICY "post_sales_view_delivered_units" ON projects.housing_units - FOR SELECT - TO authenticated - USING ( - get_current_user_role() = 'post_sales' - AND status = 'delivered' - AND constructora_id = get_current_constructora_id() - ); -``` - -**Casos de Uso:** -- Registrar incidencia de filtraci贸n en vivienda entregada -- Programar reparaci贸n de grieta en muro -- Ver historial de tickets de lote 45 -- Generar reporte de garant铆as vencidas -- Asignar cuadrilla de plomer铆a para reparaci贸n - ---- - -### RF-AUTH-001.2: Asignaci贸n de Roles - -**Reglas de Asignaci贸n:** - -1. **Al registrarse (self-service):** - - Usuarios nuevos reciben rol `resident` por defecto - - No es posible auto-asignarse otros roles - - Registro requiere invitaci贸n de constructora - -2. **Promoci贸n a otros roles:** - - Solo `director` puede asignar/cambiar roles - - Requiere justificaci贸n documentada en audit_logs - - Cambio de rol env铆a notificaci贸n por email - -3. **Relaci贸n con Constructoras:** - - Un usuario puede pertenecer a m煤ltiples constructoras (diferentes roles) - - Ejemplo: Juan es `engineer` en Constructora A y `resident` en Constructora B - - Usuario selecciona constructora activa al login - -4. **Degradaci贸n de rol:** - - `director` puede degradar cualquier rol - - Usuario puede solicitar cambio de rol - - Cambio de rol no elimina acceso hist贸rico (solo auditor铆a) - ---- - -### RF-AUTH-001.3: Validaci贸n de Roles - -El sistema **DEBE** validar roles en 3 capas: - -1. **Backend (Guards):** -```typescript -// apps/backend/src/modules/budgets/budgets.controller.ts -@Roles('director', 'engineer', 'finance') -@Get('budgets/:id') -getBudget(@Param('id') id: string) { - // Solo director, engineer y finance pueden ver presupuestos -} -``` - -2. **Database (RLS Policies):** -```sql --- Cada tabla sensible debe tener pol铆ticas RLS por rol --- Ejemplo: presupuestos solo visibles por director, engineer, finance -CREATE POLICY "budgets_select_by_role" ON budgets.budgets - FOR SELECT - TO authenticated - USING ( - get_current_user_role() IN ('director', 'engineer', 'finance') - AND constructora_id = get_current_constructora_id() - ); -``` - -3. **Frontend (Routing y UI):** -```typescript -// apps/frontend/src/routes/ProtectedRoute.tsx - - - -``` - ---- - -## 馃搳 Matriz de Permisos por M贸dulo - -| M贸dulo | director | engineer | resident | purchases | finance | hr | post_sales | -|--------|----------|----------|----------|-----------|---------|----|-----------:| -| **Proyectos** | -| Ver todos los proyectos | 鉁 | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| Ver proyectos asignados | 鉁 | 鉁 | 鉁 | 鉂 | 鉁 | 鉂 | 鉂 | -| Crear proyecto | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Editar proyecto | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| **Presupuestos** | -| Ver presupuestos | 鉁 | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| Crear presupuesto | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Editar presupuesto | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| Ver m谩rgenes | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| **Compras** | -| Ver requisiciones | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | 鉂 | 鉂 | -| Crear requisici贸n | 鉁 | 鉁 | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | -| Crear orden de compra | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | 鉂 | -| Aprobar orden | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| **Inventarios** | -| Ver inventario | 鉁 | 鉁 | 鉁 | 鉁 | 鉁 | 鉂 | 鉂 | -| Movimientos | 鉁 | 鉁 | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | -| **Control de Obra** | -| Capturar avances | 鉁 | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | -| Ver avances | 鉁 | 鉁 | 鉁 | 鉂 | 鉁 | 鉂 | 鉂 | -| Editar avances | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | -| **RRHH** | -| Ver empleados | 鉁 | 鉁 | 鉁 | 鉂 | 鉁 | 鉁 | 鉂 | -| Gestionar empleados | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | -| Ver asistencias | 鉁 | 鉁 | 鉁 | 鉂 | 鉁 | 鉁 | 鉂 | -| Registrar asistencia | 鉁 | 鉁 | 鉁 | 鉂 | 鉂 | 鉁 | 鉂 | -| Exportar IMSS/INFONAVIT | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉁 | 鉂 | -| **Postventa** | -| Ver tickets | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | -| Crear ticket | 鉁 | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | -| Asignar cuadrilla | 鉁 | 鉂 | 鉂 | 鉂 | 鉂 | 鉂 | 鉁 | -| **Reportes** | -| Dashboard ejecutivo | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | -| Reportes de obra | 鉁 | 鉁 | 鉁 | 鉂 | 鉁 | 鉂 | 鉂 | -| Reportes financieros | 鉁 | 鉂 | 鉂 | 鉂 | 鉁 | 鉂 | 鉂 | - ---- - -## 鉁 Criterios de Aceptaci贸n - -### AC-001: Roles Implementados -- [ ] ENUM `construction_role` existe en DDL con 7 valores -- [ ] Backend tiene enum TypeScript espejo -- [ ] Frontend tiene types TypeScript -- [ ] Documentaci贸n de cada rol completa - -### AC-002: RLS Policies Activas -- [ ] Tabla `projects.projects` tiene policies por rol -- [ ] Tabla `budgets.budgets` tiene policies por rol -- [ ] Tabla `hr.employees` tiene policies por rol -- [ ] 10+ tablas cr铆ticas con RLS implementado - -### AC-003: Guards Funcionales -- [ ] `@Roles()` decorator implementado en backend -- [ ] Endpoints sensibles protegidos con guards -- [ ] Tests E2E validan autorizaci贸n por rol - -### AC-004: UI Adapta por Rol -- [ ] Dashboard muestra diferentes opciones seg煤n rol (7 variantes) -- [ ] Men煤 de navegaci贸n adapta seg煤n rol -- [ ] Componentes restringidos no se muestran a roles no autorizados - -### AC-005: Auditor铆a -- [ ] Cambios de rol se auditan en `audit_logs` -- [ ] Timestamp, admin que realiz贸 cambio, y justificaci贸n registrados - ---- - -## 馃И Testing - -### Test Case 1: Residente NO puede ver presupuestos -```typescript -test('Resident cannot view budgets', async () => { - const resident = await createUser({ role: 'resident' }); - const project = await createProject(); - - await loginAs(resident); - const response = await api.get(`/budgets/project/${project.id}`); - - expect(response.status).toBe(403); // Forbidden -}); -``` - -### Test Case 2: Ingeniero puede crear presupuesto -```typescript -test('Engineer can create budget', async () => { - const engineer = await createUser({ role: 'engineer' }); - const project = await createProject(); - - await loginAs(engineer); - const response = await api.post('/budgets', { - project_id: project.id, - name: 'Presupuesto base', - total: 1000000 - }); - - expect(response.status).toBe(201); - expect(response.data.budget.id).toBeDefined(); -}); -``` - -### Test Case 3: Director tiene acceso completo -```typescript -test('Director can access all projects', async () => { - const director = await createUser({ role: 'director' }); - await createMultipleProjects(5); - - await loginAs(director); - const response = await api.get('/projects'); - - expect(response.status).toBe(200); - expect(response.data.projects.length).toBe(5); -}); -``` - ---- - -## 馃摎 Referencias Adicionales - -### Documentos Relacionados -- 馃搫 [RF-AUTH-002: Estados de Cuenta de Usuario](./RF-AUTH-002-estados-cuenta.md) -- 馃搫 [RF-AUTH-003: Multi-tenancy por Constructora](./RF-AUTH-003-multi-tenancy.md) -- 馃搻 [ET-AUTH-001: RBAC Implementation](../especificaciones/ET-AUTH-001-rbac.md) - -### Est谩ndares de Industria -- [NIST RBAC Model](https://csrc.nist.gov/projects/role-based-access-control) -- [OWASP: Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html) - ---- - -## 馃搮 Historial de Cambios - -| Versi贸n | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-11-17 | Tech Lead | Creaci贸n inicial basada en EAI-001/RF-AUTH-001 de GAMILIT | - ---- - -**Documento:** `docs/01-fase-alcance-inicial/MAI-001-fundamentos/requerimientos/RF-AUTH-001-roles-construccion.md` -**Ruta relativa:** `MAI-001-fundamentos/requerimientos/RF-AUTH-001-roles-construccion.md` -**脡pica:** MAI-001 -**Sprint:** Sprint 1 (Semanas 2-3) diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md deleted file mode 100644 index 15e98376b..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md +++ /dev/null @@ -1,1644 +0,0 @@ -# RF-AUTH-002: Estados de Cuenta de Usuario - -## 馃搵 Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | RF-AUTH-002 | -| **脡pica** | MAI-001 - Fundamentos | -| **M贸dulo** | Autenticaci贸n y Autorizaci贸n | -| **Prioridad** | Alta | -| **Estado** | 馃毀 Planificado | -| **Versi贸n** | 1.0 | -| **Fecha creaci贸n** | 2025-11-17 | -| **脷ltima actualizaci贸n** | 2025-11-17 | -| **Esfuerzo estimado** | 12h (vs 15h GAMILIT - 20% ahorro por reutilizaci贸n) | -| **Story Points** | 5 SP | - -## 馃敆 Referencias - -### Especificaci贸n T茅cnica -馃搻 [ET-AUTH-002: Gesti贸n de Estados de Cuenta](../especificaciones/ET-AUTH-002-estados-cuenta.md) *(Pendiente)* - -### Reutilizaci贸n de Cat谩logo -鈾伙笍 **Reutilizaci贸n:** 85% -- **Cat谩logo de referencia:** `core/catalog/auth/` *(Patr贸n estados de cuenta)* -- **Diferencias clave:** - - Estados adaptados a contexto de construcci贸n - - Casos de uso espec铆ficos para roles de obra - - Integraci贸n con sistema de constructoras - -### Implementaci贸n DDL -馃梽锔 **ENUM Can贸nico:** -```sql --- Location: apps/database/ddl/00-prerequisites.sql -CREATE TYPE auth_management.user_status AS ENUM ( - 'active', -- Usuario verificado, acceso completo - 'inactive', -- Usuario desactiv贸 su cuenta temporalmente - 'suspended', -- Admin suspendi贸 cuenta (reversible) - 'banned', -- Admin bane贸 cuenta (permanente) - 'pending' -- Email no verificado -); -``` - -馃梽锔 **Tablas que usan el ENUM:** -1. `auth_management.profiles` - - Columna: `status auth_management.user_status NOT NULL DEFAULT 'pending'` - -2. `auth_management.user_constructoras` - - Columna: `status auth_management.user_status NOT NULL DEFAULT 'active'` - - **Nota:** Permite diferentes estados por constructora - -馃梽锔 **Funciones:** -```sql --- Validar estado del usuario -CREATE FUNCTION auth_management.verify_user_status( - p_user_id UUID, - p_constructora_id UUID -) RETURNS BOOLEAN; - --- Suspender usuario -CREATE FUNCTION auth_management.suspend_user( - p_user_id UUID, - p_reason TEXT, - p_duration_days INTEGER, - p_suspended_by UUID -) RETURNS VOID; - --- Banear usuario permanentemente -CREATE FUNCTION auth_management.ban_user( - p_user_id UUID, - p_reason TEXT, - p_banned_by UUID -) RETURNS VOID; - --- Reactivar usuario (desde inactive o suspended) -CREATE FUNCTION auth_management.reactivate_user( - p_user_id UUID, - p_reactivated_by UUID -) RETURNS VOID; -``` - -馃梽锔 **Triggers:** -```sql --- Auditar cambios de estado -CREATE TRIGGER trg_profiles_status_change - AFTER UPDATE OF status ON auth_management.profiles - FOR EACH ROW - WHEN (OLD.status IS DISTINCT FROM NEW.status) - EXECUTE FUNCTION audit_logging.log_status_change(); -``` - -### Backend -馃捇 **Implementaci贸n:** -- **Enum:** `apps/backend/src/modules/auth/enums/user-status.enum.ts` -```typescript -export enum UserStatus { - ACTIVE = 'active', - INACTIVE = 'inactive', - SUSPENDED = 'suspended', - BANNED = 'banned', - PENDING = 'pending', -} -``` - -- **Middleware:** `apps/backend/src/modules/auth/middleware/user-status.middleware.ts` -- **Service:** `apps/backend/src/modules/auth/services/user-management.service.ts` -- **DTOs:** - - `apps/backend/src/modules/auth/dto/update-user-status.dto.ts` - - `apps/backend/src/modules/auth/dto/suspend-user.dto.ts` - -### Frontend -馃帹 **Componentes:** -- **Types:** `apps/frontend/src/types/auth.types.ts` -```typescript -interface UserProfile { - id: string; - email: string; - status: UserStatus; - role: ConstructionRole; - constructoraId: string; - // ...otros campos -} -``` - -- **Componentes:** - - `apps/frontend/src/components/ui/UserStatusBadge.tsx` - Badge visual del estado - - `apps/frontend/src/features/admin/UserManagementPanel.tsx` - Panel de gesti贸n - - `apps/frontend/src/features/admin/SuspendUserModal.tsx` - Modal para suspender - - `apps/frontend/src/features/auth/AccountStatusPage.tsx` - P谩gina de estado de cuenta - -### Trazabilidad -馃搳 [TRACEABILITY.yml](../implementacion/TRACEABILITY.yml#L45-L78) - ---- - -## 馃摑 Descripci贸n del Requerimiento - -### Contexto - -En un sistema de gesti贸n de obra, las cuentas de usuario requieren un control riguroso del ciclo de vida completo: - -- **Verificaci贸n inicial:** Usuario invitado debe verificar su email antes de acceder -- **Seguridad:** Suspender cuentas comprometidas o con comportamiento inadecuado -- **Compliance:** Dar de baja usuarios que ya no trabajan en la constructora -- **Flexibilidad:** Permitir desactivaci贸n temporal voluntaria -- **Multi-tenancy:** Un usuario puede tener diferentes estados en diferentes constructoras - -### Necesidad del Negocio - -**Problema:** -Sin un sistema de estados bien definido: -- 鉂 No se puede verificar email antes de dar acceso a datos sensibles de obra -- 鉂 No hay forma de suspender temporalmente usuarios problem谩ticos -- 鉂 No se puede diferenciar entre baja temporal (inactivo) y baja permanente (baneado) -- 鉂 Empleados que ya no trabajan siguen teniendo acceso a informaci贸n confidencial -- 鉂 No hay auditor铆a de cambios de estado - -**Soluci贸n:** -Implementar un sistema de 5 estados que modela el ciclo de vida completo, con transiciones controladas, auditadas y espec铆ficas para construcci贸n. - ---- - -## 馃幆 Requerimiento Funcional - -### RF-AUTH-002.1: Estados Disponibles - -El sistema **DEBE** soportar exactamente 5 estados de cuenta: - -#### 1. Pendiente (`pending`) -**Descripci贸n:** Cuenta reci茅n creada por invitaci贸n, email no verificado - -**Caracter铆sticas:** -- Estado inicial al ser invitado a una constructora -- Usuario no puede acceder al sistema -- Se env铆a email de verificaci贸n autom谩ticamente -- 鈴憋笍 **Expira despu茅s de 7 d铆as** si no se verifica -- 馃敀 No tiene acceso a datos de obra - -**Acceso Permitido:** -- 鉂 Dashboard -- 鉂 Proyectos/Obras -- 鉂 M贸dulos del sistema -- 鉁 P谩gina de verificaci贸n de email -- 鉁 Reenviar email de verificaci贸n - -**Transiciones:** -- 鈫 `active`: Al verificar email exitosamente -- 鈫 鈭 (eliminaci贸n autom谩tica): Despu茅s de 7 d铆as sin verificar - -**Caso de Uso T铆pico:** -> Residente de Obra es invitado por el Director. Recibe email, tiene 7 d铆as para verificar antes de que la invitaci贸n expire. - ---- - -#### 2. Activo (`active`) -**Descripci贸n:** Cuenta verificada, acceso completo seg煤n rol asignado - -**Caracter铆sticas:** -- 鉁 Email verificado exitosamente -- 鉁 Acceso completo seg煤n rol (director, engineer, resident, etc.) -- 鉁 Puede usar app m贸vil de asistencias -- 鉁 Recibe notificaciones del sistema -- 鉁 Aparece en b煤squedas de usuarios - -**Acceso Permitido:** -- 鉁 Todo el sistema seg煤n permisos de rol -- 鉁 Dashboard personalizado por rol -- 鉁 M贸dulos asignados (proyectos, presupuestos, RRHH, etc.) -- 鉁 App m贸vil (si tiene rol de resident o hr) - -**Transiciones:** -- 鈫 `inactive`: Usuario desactiva su propia cuenta -- 鈫 `suspended`: Admin suspende la cuenta (reversible) -- 鈫 `banned`: Admin banea la cuenta (irreversible) - -**Caso de Uso T铆pico:** -> Ingeniero accede diariamente para revisar presupuestos, actualizar programaci贸n y aprobar requisiciones. - ---- - -#### 3. Inactivo (`inactive`) -**Descripci贸n:** Usuario desactiv贸 temporalmente su cuenta (voluntario) - -**Caracter铆sticas:** -- 馃數 Desactivaci贸n **voluntaria** por el usuario -- 馃捑 Datos conservados intactos -- 馃攧 **Reversible** por el propio usuario en cualquier momento -- 馃摟 No recibe notificaciones -- 馃毇 No aparece en b煤squedas de usuarios - -**Acceso Permitido:** -- 鉂 Dashboard y funcionalidades principales -- 鉂 M贸dulos del sistema -- 鉁 P谩gina de reactivaci贸n de cuenta -- 鉁 Descargar datos personales (GDPR compliance) - -**Transiciones:** -- 鈫 `active`: Usuario reactiva su cuenta (m谩ximo 3 veces por d铆a) -- 鈫 鈭 (eliminaci贸n voluntaria): Usuario solicita eliminaci贸n de cuenta - -**Caso de Uso T铆pico:** -> Residente que toma vacaciones de 2 semanas desactiva temporalmente su cuenta para no recibir notificaciones. - -**Rate Limiting:** -- M谩ximo **3 reactivaciones por d铆a** (prevenir abuso) - ---- - -#### 4. Suspendido (`suspended`) -**Descripci贸n:** Admin suspendi贸 la cuenta (reversible) - -**Caracter铆sticas:** -- 馃敶 Suspensi贸n por admin debido a comportamiento inapropiado -- 馃攧 **REVERSIBLE** (diferencia clave con `banned`) -- 馃摑 Requiere **raz贸n documentada** obligatoria -- 馃攳 Requiere revisi贸n de admin para levantar suspensi贸n -- 馃摤 Usuario recibe notificaci贸n con raz贸n de suspensi贸n - -**Acceso Permitido:** -- 鉂 Todo el sistema (bloqueado completamente) -- 鉁 Ver notificaci贸n de suspensi贸n con raz贸n -- 鉁 Contactar soporte - -**Restricciones:** -- 馃毇 No puede iniciar sesi贸n -- 馃摟 No recibe notificaciones del sistema -- 馃攳 No aparece en b煤squedas de usuarios -- 馃摫 App m贸vil bloqueada - -**Transiciones:** -- 鈫 `active`: Admin levanta suspensi贸n despu茅s de revisi贸n -- 鈫 `banned`: Admin decide hacer baneo permanente - -**Duraci贸n:** -- T铆picamente: **7-30 d铆as** -- Requiere revisi贸n peri贸dica por directores/admins - -**Razones Comunes (Construcci贸n):** -- Registro de asistencia fraudulento (check-in sin estar en obra) -- Captura incorrecta de avances de obra -- Modificaci贸n no autorizada de presupuestos -- Comportamiento inapropiado en obra - -**Caso de Uso T铆pico:** -> Residente registr贸 asistencias falsas de empleados. Director lo suspende 14 d铆as mientras investiga. - ---- - -#### 5. Baneado (`banned`) -**Descripci贸n:** Admin bane贸 la cuenta **permanentemente** (irreversible) - -**Caracter铆sticas:** -- 馃敶 Baneo por violaci贸n **grave** de t茅rminos -- 鉂 **IRREVERSIBLE** (no hay transici贸n de vuelta) -- 馃摟 Email y username quedan **bloqueados permanentemente** -- 馃捑 Datos se conservan por auditor铆a pero cuenta **inaccesible** -- 馃摑 Requiere raz贸n grave documentada - -**Acceso Permitido:** -- 鉂 Todo el sistema -- 鉁 Ver notificaci贸n de baneo con raz贸n - -**Restricciones:** -- 馃毇 No puede iniciar sesi贸n -- 馃毇 No puede crear nueva cuenta con mismo email -- 馃毇 Username queda reservado permanentemente -- 馃毇 No puede ser invitado nuevamente a ninguna constructora - -**Razones Comunes (Construcci贸n):** -- Fraude financiero (apropiaci贸n indebida de recursos) -- Robo de informaci贸n confidencial de la constructora -- Suplantaci贸n de identidad (firmar como otro usuario) -- Alteraci贸n de documentos oficiales (contratos, presupuestos) -- Actividad criminal relacionada con la obra - -**Caso de Uso T铆pico:** -> Empleado de compras desvi贸 recursos, realiz贸 贸rdenes de compra falsas. Director lo banea permanentemente y procede legalmente. - ---- - -### RF-AUTH-002.2: Flujo de Estados - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 INVITACI脫N A 鈹 -鈹 CONSTRUCTORA 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 - pending 鈹鈹鈹鈹鈹鈹(verificar email)鈹鈹鈹鈹鈹> active - 鈹 鈹 - 鈹 鈹溾攢鈹(usuario desactiva)鈹鈹> inactive - 鈹 鈹 鈹 - 鈹 鈹 鈹斺攢鈹(reactiva)鈹鈹> active - 鈹 鈹 - 鈹 鈹溾攢鈹(admin suspende)鈹鈹> suspended - 鈹 鈹 鈹 - 鈹 鈹 鈹溾攢鈹(admin levanta)鈹鈹> active - 鈹 鈹 鈹斺攢鈹(admin decide)鈹鈹> banned - 鈹 鈹 - 鈹斺攢鈹鈹鈹(7 d铆as)鈹鈹鈹> 鈭 鈹斺攢鈹(admin banea)鈹鈹> banned - (eliminaci贸n) 鈹 - 鈹斺攢鈹(permanente)鈹鈹> 鈭 -``` - -**Leyenda:** -- 鈭 = Registro eliminado/cuenta no recuperable -- Flechas s贸lidas = Transiciones permitidas -- Flechas punteadas = Eliminaci贸n autom谩tica - ---- - -### RF-AUTH-002.3: Reglas de Transici贸n - -#### Transiciones Permitidas - -| Estado Actual | Puede Transicionar A | Qui茅n Puede Hacerlo | Requiere | -|---------------|---------------------|---------------------|----------| -| `pending` | `active` | Usuario | Verificar email (click en link) | -| `pending` | 鈭 (eliminaci贸n) | Sistema | 7 d铆as sin verificar | -| `active` | `inactive` | Usuario | Confirmaci贸n + password | -| `active` | `suspended` | director, super_admin | Raz贸n documentada obligatoria | -| `active` | `banned` | director, super_admin | Raz贸n grave documentada + evidencia | -| `inactive` | `active` | Usuario | Click en reactivar (m谩x 3/d铆a) | -| `suspended` | `active` | director, super_admin | Revisi贸n completada + justificaci贸n | -| `suspended` | `banned` | director, super_admin | Decisi贸n justificada + evidencia | -| `banned` | 鈭 | N/A | **Irreversible** | - -#### Transiciones Prohibidas - -| Transici贸n | Raz贸n | -|------------|-------| -| 鉂 `pending` 鈫 `suspended` | No tiene sentido suspender cuenta no verificada (simplemente eliminar invitaci贸n) | -| 鉂 `pending` 鈫 `banned` | No tiene sentido banear cuenta no verificada | -| 鉂 `inactive` 鈫 `suspended` | Usuario ya desactiv贸 voluntariamente, no hay necesidad de suspender | -| 鉂 `inactive` 鈫 `banned` | Si se requiere baneo, reactivar primero y luego banear desde `active` | -| 鉂 `suspended` 鈫 `inactive` | Confusi贸n de estados (uno es admin-driven, otro user-driven) | -| 鉂 `banned` 鈫 cualquier otro | **Irreversible por dise帽o** | - ---- - -### RF-AUTH-002.4: Validaci贸n de Estado en Sistema Multi-tenancy - -**Importante:** En este sistema, un usuario puede pertenecer a **m煤ltiples constructoras** con diferentes estados en cada una. - -#### Escenario Multi-tenancy -```typescript -// Usuario puede tener diferentes estados en diferentes constructoras -const userConstructoras = [ - { constructoraId: 'A', status: 'active', role: 'director' }, - { constructoraId: 'B', status: 'suspended', role: 'engineer' }, - { constructoraId: 'C', status: 'active', role: 'resident' }, -]; - -// Al hacer login, usuario ve solo constructoras donde status = 'active' -const availableConstructoras = userConstructoras.filter( - uc => uc.status === 'active' -); // ['A', 'C'] -``` - -#### 1. Validaci贸n en Login -```typescript -// POST /api/auth/login -// Retorna solo constructoras donde user.status = 'active' - -async login(email: string, password: string) { - const user = await this.findByEmail(email); - - // Validar estado global del perfil - if (user.profile.status === 'banned') { - throw new UnauthorizedException( - 'Tu cuenta ha sido baneada permanentemente. Contacta soporte para m谩s informaci贸n.' - ); - } - - if (user.profile.status === 'pending') { - throw new UnauthorizedException( - 'Debes verificar tu email antes de acceder. Revisa tu bandeja de entrada.' - ); - } - - // Obtener constructoras donde el usuario est谩 activo - const activeConstructoras = await this.getActiveConstructoras(user.id); - - if (activeConstructoras.length === 0) { - throw new UnauthorizedException( - 'No tienes acceso activo a ninguna constructora. Contacta al administrador.' - ); - } - - return { - user: user, - accessToken: this.generateToken(user, activeConstructoras[0]), - constructoras: activeConstructoras, // Usuario puede elegir - }; -} -``` - -#### 2. Middleware en Cada Request -```typescript -// UserStatusMiddleware valida en CADA request autenticado -@Injectable() -export class UserStatusMiddleware implements NestMiddleware { - use(req: any, res: any, next: () => void) { - const user = req.user; // Incluye constructoraId del JWT - - // Excepciones: endpoints de reactivaci贸n y cambio de constructora - const allowedPaths = [ - '/auth/reactivate', - '/auth/status', - '/auth/switch-constructora', - ]; - if (allowedPaths.some(path => req.path.startsWith(path))) { - return next(); - } - - // Validar estado global - if (user.profileStatus === 'banned') { - throw new ForbiddenException('Cuenta baneada permanentemente.'); - } - - if (user.profileStatus === 'pending') { - throw new ForbiddenException('Email no verificado.'); - } - - // Validar estado en constructora actual - if (user.constructoraStatus !== 'active') { - throw new ForbiddenException( - `Tu acceso a esta constructora est谩 ${user.constructoraStatus}. ` + - `Contacta al administrador o cambia a otra constructora.` - ); - } - - next(); - } -} -``` - -#### 3. Frontend - Validaci贸n Temprana -```typescript -// Redirigir seg煤n estado -useEffect(() => { - if (!user) return; - - // Estado global del perfil - if (user.profile.status === 'pending') { - navigate('/verify-email'); - return; - } - - if (user.profile.status === 'banned') { - navigate('/account-banned'); - return; - } - - // Estado en constructora actual - const currentConstructora = user.constructoras.find( - c => c.id === user.currentConstructoraId - ); - - if (!currentConstructora) { - navigate('/select-constructora'); - return; - } - - if (currentConstructora.status === 'suspended') { - navigate('/account-suspended-in-constructora'); - return; - } - - if (currentConstructora.status === 'inactive') { - navigate('/reactivate-account'); - return; - } - - // Estado activo, continuar -}, [user, navigate]); -``` - ---- - -## 馃搳 Casos de Uso - -### UC-AUTH-003: Usuario verifica email tras invitaci贸n -**Actor:** Residente de Obra (nuevo usuario) -**Precondiciones:** -- Usuario invitado a constructora por Director -- Email de invitaci贸n enviado -- `profiles.status` = `pending` - -**Flujo Principal:** -1. Usuario recibe email de invitaci贸n a constructora "Constructora ABC" -2. Usuario hace click en link de verificaci贸n -3. Sistema valida token de verificaci贸n (v谩lido por 24h) -4. Sistema actualiza `profiles.status` de `pending` 鈫 `active` -5. Sistema actualiza `user_constructoras.status` de `pending` 鈫 `active` -6. Sistema audita cambio en `audit_logs`: - ```json - { - "action": "update", - "resource_type": "user_status", - "resource_id": "user_id", - "details": { - "old_status": "pending", - "new_status": "active", - "constructora_id": "abc-123", - "verified_at": "2025-11-17T10:30:00Z" - } - } - ``` -7. Sistema env铆a email de bienvenida con instrucciones -8. Usuario redirigido a `/auth/login` -9. Usuario hace login y selecciona constructora "Constructora ABC" -10. Usuario accede a dashboard seg煤n su rol (resident) - -**Resultado:** Usuario con status `active` puede acceder al sistema - -**Variantes:** -- **V1: Token expirado (>24h):** - 1. Sistema detecta token expirado - 2. Sistema muestra mensaje: "Link expirado" - 3. Sistema ofrece bot贸n "Reenviar email de verificaci贸n" - 4. Usuario hace click - 5. Sistema env铆a nuevo email con nuevo token - 6. Volver a flujo principal paso 2 - -- **V2: Token inv谩lido:** - 1. Sistema detecta token inv谩lido - 2. Sistema muestra error: "Link inv谩lido" - 3. Sistema ofrece contactar soporte - 4. Sistema audita intento de verificaci贸n inv谩lido - -- **V3: Usuario ya verificado:** - 1. Sistema detecta que `status` ya es `active` - 2. Sistema muestra: "Ya has verificado tu email" - 3. Sistema redirige a login - ---- - -### UC-ADMIN-002: Director suspende cuenta de Residente -**Actor:** Director de Construcci贸n -**Precondiciones:** -- Usuario con rol `director` -- Residente con status `active` -- Comportamiento inapropiado detectado (ej: asistencias falsas) - -**Flujo Principal:** -1. Director navega a **Panel de Gesti贸n de Usuarios** (`/admin/users`) -2. Director filtra usuarios por constructora actual -3. Director busca residente por nombre: "Juan P茅rez" -4. Director hace click en bot贸n "Acciones" 鈫 "Suspender cuenta" -5. Sistema muestra **Modal de Suspensi贸n** con formulario: - - **Raz贸n** (textarea obligatorio, min 20 caracteres) - - **Duraci贸n sugerida** (select: 7 d铆as, 14 d铆as, 30 d铆as, Indefinido) - - **Evidencia** (opcional: subir screenshots, documentos) - - **Fecha de revisi贸n** (autom谩tica seg煤n duraci贸n) -6. Director completa formulario: - - Raz贸n: "Registr贸 asistencias de empleados que no estaban en obra seg煤n GPS" - - Duraci贸n: 14 d铆as - - Evidencia: Sube screenshots de GPS -7. Director hace click en "Confirmar Suspensi贸n" -8. Sistema valida: - - Raz贸n tiene m铆nimo 20 caracteres 鉁 - - Usuario tiene permisos (rol = director) 鉁 - - Usuario a suspender no es director 鉁 (no puede suspender otro director) -9. Sistema ejecuta transacci贸n: - ```sql - BEGIN; - - -- Actualizar estado - UPDATE auth_management.user_constructoras - SET status = 'suspended', - suspended_at = NOW(), - suspended_by = :director_id, - suspended_reason = :reason, - suspended_until = NOW() + INTERVAL '14 days' - WHERE user_id = :resident_id - AND constructora_id = :constructora_id; - - -- Auditar - INSERT INTO audit_logging.audit_logs ( - action, resource_type, resource_id, performed_by, details - ) VALUES ( - 'update', 'user_status', :resident_id, :director_id, - jsonb_build_object( - 'old_status', 'active', - 'new_status', 'suspended', - 'reason', :reason, - 'evidence_urls', :evidence_urls, - 'suspended_by_name', 'Director L贸pez', - 'review_date', NOW() + INTERVAL '14 days', - 'constructora_id', :constructora_id - ) - ); - - COMMIT; - ``` -10. Sistema cierra todas las sesiones activas del residente en esa constructora -11. Sistema env铆a **Notificaci贸n Push + Email** al residente: - ``` - Asunto: Tu cuenta ha sido suspendida temporalmente - - Hola Juan, - - Tu cuenta en Constructora ABC ha sido suspendida por 14 d铆as. - - Raz贸n: Registr贸 asistencias de empleados que no estaban en obra seg煤n GPS - - Fecha de revisi贸n: 2025-12-01 - - Si crees que esto es un error, puedes contactar a soporte en soporte@constructora.com - - --- - Sistema de Gesti贸n de Obra - ``` -12. Sistema registra en timeline del usuario el evento de suspensi贸n -13. Sistema muestra confirmaci贸n al director: "Usuario suspendido exitosamente" - -**Resultado:** -- Residente no puede acceder a esa constructora por 14 d铆as -- Director puede revisar suspensi贸n antes de la fecha -- Suspensi贸n queda auditada con evidencia - -**Variantes:** -- **V1: Director intenta suspender otro director:** - - Sistema lanza error: "No puedes suspender a otro director" - - Solo super_admin puede suspender directores - -- **V2: Residente ya est谩 suspendido:** - - Sistema detecta status = 'suspended' - - Sistema muestra opciones: - - Levantar suspensi贸n - - Extender suspensi贸n - - Convertir en baneo - -**Postcondiciones:** -- `user_constructoras.status` = 'suspended' -- Registro en `audit_logs` -- Email enviado -- Sesiones cerradas - ---- - -### UC-AUTH-004: Usuario desactiva su propia cuenta -**Actor:** Ingeniero -**Precondiciones:** Usuario con status `active` - -**Flujo Principal:** -1. Ingeniero navega a **Configuraci贸n** 鈫 **Mi Cuenta** (`/settings/account`) -2. Ingeniero hace scroll hasta secci贸n "Zona Peligrosa" -3. Ingeniero hace click en bot贸n "Desactivar mi cuenta" -4. Sistema muestra **Modal de Confirmaci贸n** con: - - **T铆tulo:** "驴Est谩s seguro que quieres desactivar tu cuenta?" - - **Explicaci贸n:** - - 鉁 Esta acci贸n es temporal - - 鉁 Tus datos NO se eliminar谩n - - 鉁 Puedes reactivar en cualquier momento - - 鈿狅笍 No recibir谩s notificaciones mientras est茅 desactivada - - 鈿狅笍 No aparecer谩s en b煤squedas de usuarios - - **Campo:** Contrase帽a (requerido para confirmar) -5. Ingeniero ingresa su contrase帽a -6. Ingeniero hace click en "Confirmar Desactivaci贸n" -7. Sistema valida contrase帽a 鉁 -8. Sistema actualiza `profiles.status` de `active` 鈫 `inactive` -9. Sistema audita cambio: - ```json - { - "action": "update", - "resource_type": "user_status", - "resource_id": "user_id", - "details": { - "old_status": "active", - "new_status": "inactive", - "deactivated_by": "self", - "reason": "voluntary_deactivation", - "deactivated_at": "2025-11-17T15:45:00Z" - } - } - ``` -10. Sistema env铆a email de confirmaci贸n: - ``` - Asunto: Cuenta desactivada temporalmente - - Hola, - - Tu cuenta ha sido desactivada exitosamente. - - Para reactivarla, simplemente haz login de nuevo en: - https://app.constructora.com/auth/login - - Y haz click en "Reactivar cuenta". - - --- - Sistema de Gesti贸n de Obra - ``` -11. Sistema cierra sesi贸n del usuario (`logout`) -12. Usuario redirigido a p谩gina de login con mensaje: - > "Tu cuenta ha sido desactivada. Puedes reactivarla en cualquier momento haciendo login." - -**Resultado:** Cuenta desactivada temporalmente, usuario puede reactivar cuando quiera - -**Reactivaci贸n (Flujo Secundario):** -1. Usuario navega a `/auth/login` -2. Usuario ingresa email + password -3. Sistema detecta `status` = `inactive` -4. Sistema muestra p谩gina de reactivaci贸n con: - - Mensaje: "Tu cuenta est谩 desactivada temporalmente" - - Bot贸n grande: "Reactivar mi cuenta" -5. Usuario hace click en "Reactivar mi cuenta" -6. Sistema valida rate limiting (m谩ximo 3 reactivaciones por d铆a) -7. Sistema actualiza `status` de `inactive` 鈫 `active` -8. Sistema audita reactivaci贸n -9. Sistema muestra: "Cuenta reactivada exitosamente" -10. Usuario redirigido a dashboard - -**Postcondiciones:** -- `profiles.status` = 'inactive' -- Email enviado -- Sesi贸n cerrada - ---- - -### UC-ADMIN-003: Director banea cuenta permanentemente -**Actor:** Director -**Precondiciones:** -- Usuario con rol `director` -- Empleado de compras con status `active` o `suspended` -- Violaci贸n grave detectada (fraude) - -**Flujo Principal:** -1. Director detecta fraude: Empleado de compras cre贸 贸rdenes de compra falsas por $500,000 MXN -2. Director navega a **Panel de Gesti贸n de Usuarios** 鈫 Usuario "Carlos Ram铆rez" -3. Director hace click en "Acciones" 鈫 "Banear cuenta permanentemente" -4. Sistema muestra **Modal de Baneo** con advertencia: - ``` - 鈿狅笍 ADVERTENCIA: Esta acci贸n es IRREVERSIBLE - - El usuario NO podr谩: - - Acceder al sistema nunca m谩s - - Crear nueva cuenta con este email - - Ser invitado a ninguna constructora - - Esta acci贸n debe reservarse para violaciones GRAVES: - - Fraude financiero - - Robo de informaci贸n - - Actividad criminal - - 驴Est谩s completamente seguro? - ``` -5. Sistema requiere: - - **Raz贸n grave** (textarea obligatorio, min 50 caracteres) - - **Evidencia** (obligatorio: m铆nimo 1 archivo) - - **Confirmaci贸n:** Usuario debe escribir "BANEAR PERMANENTEMENTE" -6. Director completa: - - Raz贸n: "Empleado cre贸 贸rdenes de compra falsas por $500,000 MXN a proveedores ficticios. Se detect贸 desv铆o de recursos. Se proceder谩 legalmente." - - Evidencia: Sube PDFs de 贸rdenes falsas, capturas de pantalla, reporte de auditor铆a - - Confirmaci贸n: Escribe "BANEAR PERMANENTEMENTE" -7. Director hace click en "Confirmar Baneo" -8. Sistema ejecuta: - ```sql - BEGIN; - - -- Banear en todas las constructoras - UPDATE auth_management.profiles - SET status = 'banned', - banned_at = NOW(), - banned_by = :director_id, - banned_reason = :reason - WHERE id = :user_id; - - -- Marcar email como bloqueado - INSERT INTO auth_management.banned_emails (email, reason, banned_by) - VALUES (:user_email, :reason, :director_id); - - -- Auditar con m谩xima prioridad - INSERT INTO audit_logging.audit_logs ( - action, resource_type, resource_id, performed_by, details, priority - ) VALUES ( - 'ban', 'user_status', :user_id, :director_id, - jsonb_build_object( - 'old_status', 'active', - 'new_status', 'banned', - 'reason', :reason, - 'evidence_urls', :evidence_urls, - 'banned_by_name', 'Director L贸pez', - 'legal_action', true - ), - 'critical' - ); - - -- Cerrar sesiones en TODAS las constructoras - DELETE FROM auth_management.user_sessions - WHERE user_id = :user_id; - - COMMIT; - ``` -9. Sistema env铆a notificaci贸n CR脥TICA a: - - Usuario baneado (email) - - Todos los directores de todas las constructoras donde estaba el usuario - - Super admins del sistema -10. Sistema env铆a email al usuario baneado: - ``` - Asunto: Cuenta baneada permanentemente - - Tu cuenta ha sido baneada permanentemente. - - Raz贸n: [Raz贸n documentada] - - Esta acci贸n es irreversible. No podr谩s acceder al sistema ni crear nueva cuenta. - - Si tienes preguntas, contacta a legal@constructora.com - ``` - -**Resultado:** -- Usuario baneado en TODAS las constructoras -- Email bloqueado permanentemente -- Username reservado permanentemente -- No puede ser invitado nunca m谩s - -**Postcondiciones:** -- `profiles.status` = 'banned' -- `banned_emails` contiene email -- Sesiones cerradas en todas las constructoras -- Notificaciones cr铆ticas enviadas - ---- - -## 馃攼 Consideraciones de Seguridad - -### 1. Prevenci贸n de Bypass de Estado - -**Problema:** Usuario suspendido podr铆a intentar acceder v铆a API directamente, salt谩ndose frontend - -**Soluci贸n: Middleware en CADA request autenticado** - -```typescript -// apps/backend/src/modules/auth/middleware/user-status.middleware.ts -import { Injectable, NestMiddleware, ForbiddenException } from '@nestjs/common'; -import { UserStatus } from '../enums/user-status.enum'; - -@Injectable() -export class UserStatusMiddleware implements NestMiddleware { - use(req: any, res: any, next: () => void) { - const user = req.user; // Inyectado por JwtAuthGuard - - // Excepciones: endpoints que permiten ciertos estados - const allowedPaths = [ - '/auth/reactivate', // inactive puede reactivar - '/auth/status', // consultar estado - '/auth/switch-constructora', // cambiar constructora - '/auth/download-data', // GDPR: inactive puede descargar datos - ]; - - if (allowedPaths.some(path => req.path.startsWith(path))) { - return next(); - } - - // Validar estado global del perfil - if (user.profileStatus === 'banned') { - throw new ForbiddenException({ - statusCode: 403, - message: 'Tu cuenta ha sido baneada permanentemente.', - errorCode: 'ACCOUNT_BANNED', - contactSupport: true, - }); - } - - if (user.profileStatus === 'pending') { - throw new ForbiddenException({ - statusCode: 403, - message: 'Debes verificar tu email antes de acceder.', - errorCode: 'EMAIL_NOT_VERIFIED', - action: 'verify_email', - }); - } - - // Validar estado en constructora actual - if (user.constructoraStatus !== UserStatus.ACTIVE) { - throw new ForbiddenException({ - statusCode: 403, - message: `Tu acceso a esta constructora est谩 ${user.constructoraStatus}.`, - errorCode: 'CONSTRUCTORA_ACCESS_DENIED', - constructoraId: user.constructoraId, - status: user.constructoraStatus, - }); - } - - next(); - } -} -``` - -**Aplicaci贸n global:** -```typescript -// apps/backend/src/main.ts -app.use(UserStatusMiddleware); // Aplica a TODOS los endpoints -``` - ---- - -### 2. Auditor铆a Obligatoria de Cambios de Estado - -**Problema:** Cambios de estado sin auditar = no hay trazabilidad - -**Soluci贸n: Trigger autom谩tico en base de datos** - -```sql --- apps/database/ddl/schemas/auth_management/triggers/audit-status-change.sql - -CREATE OR REPLACE FUNCTION audit_logging.log_status_change() -RETURNS TRIGGER AS $$ -BEGIN - -- Solo auditar si cambi贸 el estado - IF OLD.status IS DISTINCT FROM NEW.status THEN - INSERT INTO audit_logging.audit_logs ( - action, - resource_type, - resource_id, - performed_by, - details, - priority, - ip_address - ) VALUES ( - CASE NEW.status - WHEN 'suspended' THEN 'suspend' - WHEN 'banned' THEN 'ban' - WHEN 'active' THEN 'reactivate' - WHEN 'inactive' THEN 'deactivate' - ELSE 'update' - END, - 'user_status', - NEW.id, - COALESCE(current_setting('app.current_user_id', true)::UUID, NEW.id), - jsonb_build_object( - 'old_status', OLD.status, - 'new_status', NEW.status, - 'reason', NEW.suspended_reason, - 'table', TG_TABLE_NAME, - 'timestamp', NOW() - ), - CASE NEW.status - WHEN 'banned' THEN 'critical' - WHEN 'suspended' THEN 'high' - ELSE 'medium' - END, - inet_client_addr() - ); - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Aplicar a profiles -CREATE TRIGGER trg_audit_status_change_profiles - AFTER UPDATE OF status ON auth_management.profiles - FOR EACH ROW - EXECUTE FUNCTION audit_logging.log_status_change(); - --- Aplicar a user_constructoras -CREATE TRIGGER trg_audit_status_change_constructoras - AFTER UPDATE OF status ON auth_management.user_constructoras - FOR EACH ROW - EXECUTE FUNCTION audit_logging.log_status_change(); -``` - -**Resultado:** TODO cambio de estado queda auditado autom谩ticamente - ---- - -### 3. Rate Limiting en Reactivaci贸n - -**Problema:** Usuario abusa de desactivar/reactivar repetidamente - -**Soluci贸n: Limitar reactivaciones a 3 por d铆a** - -```typescript -// apps/backend/src/modules/auth/services/user-management.service.ts -import { TooManyRequestsException } from '@nestjs/common'; - -async reactivateAccount(userId: string): Promise { - // Contar reactivaciones hoy - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const reactivationCount = await this.auditLogRepository.count({ - where: { - resourceId: userId, - action: 'reactivate', - createdAt: MoreThan(today), - }, - }); - - // Limitar a 3 reactivaciones por d铆a - if (reactivationCount >= 3) { - throw new TooManyRequestsException({ - statusCode: 429, - message: 'Has alcanzado el l铆mite de reactivaciones por hoy (3 m谩ximo).', - errorCode: 'TOO_MANY_REACTIVATIONS', - retryAfter: this.getSecondsUntilMidnight(), - }); - } - - // Proceder con reactivaci贸n - await this.profileRepository.update( - { id: userId }, - { status: UserStatus.ACTIVE } - ); -} - -private getSecondsUntilMidnight(): number { - const now = new Date(); - const midnight = new Date(now); - midnight.setHours(24, 0, 0, 0); - return Math.floor((midnight.getTime() - now.getTime()) / 1000); -} -``` - ---- - -### 4. Notificaci贸n Obligatoria al Usuario - -**Problema:** Usuario no sabe por qu茅 cambi贸 su estado - -**Soluci贸n: Notificaci贸n autom谩tica en todo cambio de estado** - -```typescript -// apps/backend/src/modules/auth/services/user-status-notification.service.ts -import { Injectable } from '@nestjs/common'; -import { NotificationService } from '../../notifications/notification.service'; -import { EmailService } from '../../email/email.service'; - -@Injectable() -export class UserStatusNotificationService { - constructor( - private readonly notificationService: NotificationService, - private readonly emailService: EmailService, - ) {} - - async notifyStatusChange( - userId: string, - oldStatus: UserStatus, - newStatus: UserStatus, - reason: string, - changedBy: string, - ): Promise { - // Solo notificar si el cambio NO fue iniciado por el propio usuario - if (changedBy !== userId) { - // Notificaci贸n in-app (push) - await this.notificationService.send({ - userId, - type: 'system_announcement', - priority: newStatus === 'banned' ? 'critical' : 'high', - title: this.getNotificationTitle(newStatus), - body: reason, - icon: this.getNotificationIcon(newStatus), - actions: this.getNotificationActions(newStatus), - }); - - // Email - await this.emailService.send({ - to: await this.getUserEmail(userId), - subject: this.getEmailSubject(newStatus), - template: 'account-status-changed', - data: { - newStatus, - oldStatus, - reason, - changedByName: await this.getUserName(changedBy), - supportEmail: 'soporte@constructora.com', - }, - }); - } - } - - private getNotificationTitle(status: UserStatus): string { - switch (status) { - case UserStatus.SUSPENDED: - return 'Tu cuenta ha sido suspendida temporalmente'; - case UserStatus.BANNED: - return 'Tu cuenta ha sido baneada permanentemente'; - case UserStatus.ACTIVE: - return 'Tu cuenta ha sido reactivada'; - case UserStatus.INACTIVE: - return 'Tu cuenta ha sido desactivada'; - default: - return 'Tu estado de cuenta ha cambiado'; - } - } - - private getNotificationIcon(status: UserStatus): string { - switch (status) { - case UserStatus.SUSPENDED: - return '鈿狅笍'; - case UserStatus.BANNED: - return '馃毇'; - case UserStatus.ACTIVE: - return '鉁'; - case UserStatus.INACTIVE: - return '鈩癸笍'; - default: - return '馃敂'; - } - } - - private getNotificationActions(status: UserStatus): any[] { - switch (status) { - case UserStatus.SUSPENDED: - return [{ label: 'Contactar Soporte', action: 'contact_support' }]; - case UserStatus.BANNED: - return [{ label: 'Ver Detalles', action: 'view_ban_details' }]; - case UserStatus.INACTIVE: - return [{ label: 'Reactivar Cuenta', action: 'reactivate_account' }]; - default: - return []; - } - } -} -``` - ---- - -### 5. Prevenci贸n de Registro con Email Baneado - -**Problema:** Usuario baneado intenta crear nueva cuenta con mismo email - -**Soluci贸n: Validar email contra tabla `banned_emails` en registro** - -```typescript -// apps/backend/src/modules/auth/services/auth.service.ts -async registerByInvitation(token: string, password: string): Promise { - const invitation = await this.validateInvitationToken(token); - - // Verificar si el email est谩 baneado - const isBanned = await this.bannedEmailRepository.findOne({ - where: { email: invitation.email }, - }); - - if (isBanned) { - throw new ForbiddenException({ - statusCode: 403, - message: 'Este email est谩 bloqueado y no puede crear una cuenta.', - errorCode: 'EMAIL_BANNED', - contactSupport: true, - bannedReason: isBanned.reason, - }); - } - - // Continuar con registro... -} -``` - -```sql --- apps/database/ddl/schemas/auth_management/tables/banned-emails.sql -CREATE TABLE auth_management.banned_emails ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - email VARCHAR(255) UNIQUE NOT NULL, - reason TEXT NOT NULL, - banned_by UUID REFERENCES auth_management.profiles(id), - banned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - -CREATE INDEX idx_banned_emails_email ON auth_management.banned_emails(email); -``` - ---- - -## 鉁 Criterios de Aceptaci贸n - -### AC-001: ENUM Implementado -- [ ] ENUM `user_status` existe en schema `auth_management` -- [ ] ENUM tiene exactamente 5 valores: `active`, `inactive`, `suspended`, `banned`, `pending` -- [ ] Columna `profiles.status` usa el ENUM con default `'pending'` -- [ ] Columna `user_constructoras.status` usa el ENUM con default `'active'` - -### AC-002: Transiciones Validadas -- [ ] Solo transiciones permitidas pueden ejecutarse (validaci贸n en service) -- [ ] Transiciones prohibidas lanzan exception con mensaje claro -- [ ] Trigger audita TODOS los cambios de estado autom谩ticamente -- [ ] Raz贸n es obligatoria para `suspended` y `banned` -- [ ] Evidencia es obligatoria para `banned` - -### AC-003: Middleware Activo -- [ ] `UserStatusMiddleware` aplicado globalmente en backend -- [ ] Middleware valida estado en CADA request autenticado -- [ ] Excepciones configuradas para paths espec铆ficos (`/auth/reactivate`, etc.) -- [ ] Frontend redirige seg煤n status (pending 鈫 verify-email, banned 鈫 account-banned) -- [ ] Usuario suspendido no puede acceder ni v铆a API ni v铆a UI - -### AC-004: Notificaciones Enviadas -- [ ] Usuario recibe notificaci贸n push al cambiar status -- [ ] Usuario recibe email con raz贸n y pr贸ximos pasos -- [ ] Notificaci贸n visible en UI con icono correcto -- [ ] Email incluye informaci贸n de contacto (soporte, legal) - -### AC-005: Admin Panel Funcional -- [ ] Director puede suspender usuarios (excepto otros directores) -- [ ] Director puede banear usuarios permanentemente -- [ ] Raz贸n es campo obligatorio (min 20 caracteres para suspend, 50 para ban) -- [ ] Evidencia es obligatoria para baneo -- [ ] Historial de cambios visible en perfil de usuario -- [ ] Director puede levantar suspensi贸n con justificaci贸n - -### AC-006: Usuario Puede Desactivar/Reactivar -- [ ] Bot贸n "Desactivar cuenta" visible en `/settings/account` -- [ ] Modal de confirmaci贸n requiere contrase帽a -- [ ] Reactivaci贸n simple desde login con bot贸n "Reactivar cuenta" -- [ ] Rate limiting: m谩ximo 3 reactivaciones por d铆a -- [ ] Email de confirmaci贸n enviado al desactivar - -### AC-007: Multi-tenancy Soportado -- [ ] Usuario puede tener diferentes estados en diferentes constructoras -- [ ] Login muestra solo constructoras donde status = 'active' -- [ ] Suspensi贸n en una constructora no afecta otras constructoras -- [ ] Baneo global afecta TODAS las constructoras - -### AC-008: Email Baneado Bloqueado -- [ ] Tabla `banned_emails` creada -- [ ] Registro valida email contra `banned_emails` -- [ ] Email baneado no puede crear nueva cuenta -- [ ] Error claro al intentar registro con email baneado - ---- - -## 馃И Testing - -### Test Suite: User Status Management - -#### Test Case 1: Usuario pending no puede acceder -```typescript -// apps/backend/src/modules/auth/tests/user-status.spec.ts -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication, HttpStatus } from '@nestjs/common'; -import * as request from 'supertest'; - -describe('UserStatusMiddleware - Pending Users', () => { - let app: INestApplication; - - beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('should block pending user from accessing protected endpoints', async () => { - // Arrange: Crear usuario con status pending - const user = await createTestUser({ - email: 'pending@test.com', - status: UserStatus.PENDING, - }); - const token = await generateAuthToken(user); - - // Act: Intentar acceder a dashboard - const response = await request(app.getHttpServer()) - .get('/dashboard') - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.FORBIDDEN); - - // Assert - expect(response.body.message).toContain('verificar tu email'); - expect(response.body.errorCode).toBe('EMAIL_NOT_VERIFIED'); - }); - - it('should allow pending user to access /auth/resend-verification', async () => { - const user = await createTestUser({ status: UserStatus.PENDING }); - const token = await generateAuthToken(user); - - // Endpoint de excepci贸n debe funcionar - const response = await request(app.getHttpServer()) - .post('/auth/resend-verification') - .set('Authorization', `Bearer ${token}`) - .expect(HttpStatus.OK); - - expect(response.body.message).toContain('Email enviado'); - }); -}); -``` - ---- - -#### Test Case 2: Director puede suspender usuario -```typescript -describe('UserManagementService - Suspend User', () => { - it('should allow director to suspend resident with reason', async () => { - // Arrange - const director = await createTestUser({ - role: ConstructionRole.DIRECTOR, - constructoraId: 'constructora-a', - }); - const resident = await createTestUser({ - role: ConstructionRole.RESIDENT, - status: UserStatus.ACTIVE, - constructoraId: 'constructora-a', - }); - - await loginAs(director); - - // Act - const response = await request(app.getHttpServer()) - .post(`/admin/users/${resident.id}/suspend`) - .send({ - reason: 'Registr贸 asistencias falsas de empleados', - durationDays: 14, - evidence: ['https://s3.aws.com/screenshot1.png'], - }) - .expect(HttpStatus.OK); - - // Assert - expect(response.body.message).toContain('suspendido exitosamente'); - - // Verificar estado en DB - const updatedResident = await getUserById(resident.id); - expect(updatedResident.status).toBe(UserStatus.SUSPENDED); - expect(updatedResident.suspendedReason).toBe('Registr贸 asistencias falsas de empleados'); - - // Verificar auditor铆a - const auditLog = await getLatestAuditLog(resident.id, 'user_status'); - expect(auditLog.action).toBe('suspend'); - expect(auditLog.performedBy).toBe(director.id); - expect(auditLog.details.reason).toBe('Registr贸 asistencias falsas de empleados'); - expect(auditLog.priority).toBe('high'); - }); - - it('should NOT allow director to suspend another director', async () => { - const director1 = await createTestUser({ role: ConstructionRole.DIRECTOR }); - const director2 = await createTestUser({ role: ConstructionRole.DIRECTOR }); - - await loginAs(director1); - - const response = await request(app.getHttpServer()) - .post(`/admin/users/${director2.id}/suspend`) - .send({ reason: 'Test', durationDays: 7 }) - .expect(HttpStatus.FORBIDDEN); - - expect(response.body.message).toContain('No puedes suspender a otro director'); - }); - - it('should require reason with minimum length', async () => { - const director = await createTestUser({ role: ConstructionRole.DIRECTOR }); - const resident = await createTestUser({ role: ConstructionRole.RESIDENT }); - - await loginAs(director); - - const response = await request(app.getHttpServer()) - .post(`/admin/users/${resident.id}/suspend`) - .send({ - reason: 'Test', // Muy corto (< 20 caracteres) - durationDays: 7, - }) - .expect(HttpStatus.BAD_REQUEST); - - expect(response.body.message).toContain('Raz贸n debe tener m铆nimo 20 caracteres'); - }); -}); -``` - ---- - -#### Test Case 3: Usuario puede desactivar y reactivar -```typescript -describe('UserAccountService - Deactivate/Reactivate', () => { - it('should allow user to deactivate their own account', async () => { - // Arrange - const user = await createTestUser({ - email: 'engineer@test.com', - password: 'password123', - status: UserStatus.ACTIVE, - }); - await loginAs(user); - - // Act: Desactivar - const deactivateResponse = await request(app.getHttpServer()) - .post('/auth/deactivate') - .send({ password: 'password123' }) - .expect(HttpStatus.OK); - - expect(deactivateResponse.body.message).toContain('desactivada'); - - // Assert: Verificar estado - let updatedUser = await getUserById(user.id); - expect(updatedUser.status).toBe(UserStatus.INACTIVE); - - // Assert: Verificar auditor铆a - const auditLog = await getLatestAuditLog(user.id, 'user_status'); - expect(auditLog.action).toBe('deactivate'); - expect(auditLog.details.deactivatedBy).toBe('self'); - }); - - it('should allow user to reactivate their account', async () => { - // Arrange - const user = await createTestUser({ status: UserStatus.INACTIVE }); - await loginAs(user); - - // Act: Reactivar - const reactivateResponse = await request(app.getHttpServer()) - .post('/auth/reactivate') - .expect(HttpStatus.OK); - - expect(reactivateResponse.body.message).toContain('reactivada'); - - // Assert - const updatedUser = await getUserById(user.id); - expect(updatedUser.status).toBe(UserStatus.ACTIVE); - }); - - it('should enforce rate limiting on reactivation (max 3/day)', async () => { - const user = await createTestUser({ status: UserStatus.INACTIVE }); - await loginAs(user); - - // Reactivar 3 veces (m谩ximo permitido) - for (let i = 0; i < 3; i++) { - await request(app.getHttpServer()) - .post('/auth/reactivate') - .expect(HttpStatus.OK); - - await request(app.getHttpServer()) - .post('/auth/deactivate') - .send({ password: user.password }) - .expect(HttpStatus.OK); - } - - // Intentar 4ta reactivaci贸n (debe fallar) - const response = await request(app.getHttpServer()) - .post('/auth/reactivate') - .expect(HttpStatus.TOO_MANY_REQUESTS); - - expect(response.body.errorCode).toBe('TOO_MANY_REACTIVATIONS'); - expect(response.body.message).toContain('l铆mite de reactivaciones'); - }); -}); -``` - ---- - -#### Test Case 4: Banned user cannot login -```typescript -describe('AuthService - Banned User', () => { - it('should block banned user from logging in', async () => { - // Arrange - const user = await createTestUser({ - email: 'banned@test.com', - password: 'password123', - status: UserStatus.BANNED, - bannedReason: 'Fraude financiero', - }); - - // Act - const response = await request(app.getHttpServer()) - .post('/auth/login') - .send({ - email: 'banned@test.com', - password: 'password123', - }) - .expect(HttpStatus.UNAUTHORIZED); - - // Assert - expect(response.body.message).toContain('baneada permanentemente'); - expect(response.body.errorCode).toBe('ACCOUNT_BANNED'); - expect(response.body.contactSupport).toBe(true); - }); - - it('should block banned email from creating new account', async () => { - // Arrange: Crear email baneado - await createBannedEmail({ - email: 'fraud@test.com', - reason: 'Usuario anterior cometi贸 fraude', - }); - - // Act: Intentar registro con email baneado - const invitationToken = await createInvitation('fraud@test.com'); - - const response = await request(app.getHttpServer()) - .post('/auth/register-by-invitation') - .send({ - token: invitationToken, - password: 'newPassword123', - }) - .expect(HttpStatus.FORBIDDEN); - - // Assert - expect(response.body.errorCode).toBe('EMAIL_BANNED'); - expect(response.body.message).toContain('bloqueado'); - }); -}); -``` - ---- - -#### Test Case 5: Multi-tenancy status validation -```typescript -describe('Multi-tenancy Status', () => { - it('should allow user active in constructora A but suspended in B', async () => { - // Arrange - const user = await createTestUser({ email: 'multi@test.com' }); - - await assignUserToConstructora(user.id, 'constructora-a', { - status: UserStatus.ACTIVE, - role: ConstructionRole.DIRECTOR, - }); - - await assignUserToConstructora(user.id, 'constructora-b', { - status: UserStatus.SUSPENDED, - role: ConstructionRole.ENGINEER, - }); - - // Act: Login - const loginResponse = await request(app.getHttpServer()) - .post('/auth/login') - .send({ email: 'multi@test.com', password: 'password' }) - .expect(HttpStatus.OK); - - // Assert: Solo debe ver constructora A (activa) - expect(loginResponse.body.constructoras).toHaveLength(1); - expect(loginResponse.body.constructoras[0].id).toBe('constructora-a'); - - // Act: Acceder a constructora A (debe funcionar) - const tokenA = loginResponse.body.accessToken; - await request(app.getHttpServer()) - .get('/dashboard') - .set('Authorization', `Bearer ${tokenA}`) - .expect(HttpStatus.OK); - - // Act: Intentar cambiar a constructora B (debe fallar) - const switchResponse = await request(app.getHttpServer()) - .post('/auth/switch-constructora') - .set('Authorization', `Bearer ${tokenA}`) - .send({ constructoraId: 'constructora-b' }) - .expect(HttpStatus.FORBIDDEN); - - expect(switchResponse.body.message).toContain('suspended'); - }); -}); -``` - ---- - -#### Test Case 6: Trigger audits all status changes -```typescript -describe('Audit Trigger', () => { - it('should automatically audit status changes via trigger', async () => { - const user = await createTestUser({ status: UserStatus.ACTIVE }); - - // Cambiar estado directamente en DB (sin pasar por service) - await query(` - UPDATE auth_management.profiles - SET status = 'suspended' - WHERE id = $1 - `, [user.id]); - - // Verificar que trigger cre贸 audit log - const auditLogs = await query(` - SELECT * FROM audit_logging.audit_logs - WHERE resource_id = $1 - AND resource_type = 'user_status' - ORDER BY created_at DESC - LIMIT 1 - `, [user.id]); - - expect(auditLogs.rows).toHaveLength(1); - expect(auditLogs.rows[0].action).toBe('suspend'); - expect(auditLogs.rows[0].details.old_status).toBe('active'); - expect(auditLogs.rows[0].details.new_status).toBe('suspended'); - }); -}); -``` - ---- - -## 馃摎 Referencias Adicionales - -### Documentos Relacionados -- 馃搫 [RF-AUTH-001: Sistema de Roles de Construcci贸n](./RF-AUTH-001-roles-construccion.md) - Estados interact煤an con roles -- 馃搫 [RF-AUTH-003: Multi-tenancy por Constructora](./RF-AUTH-003-multi-tenancy.md) *(Pendiente)* - Estados por constructora -- 馃搫 [US-FUND-001: Autenticaci贸n B谩sica JWT](../historias-usuario/US-FUND-001-autenticacion-basica-jwt.md) - Login valida estado -- 馃搫 [US-FUND-005: Sistema de Sesiones](../historias-usuario/US-FUND-005-sistema-sesiones.md) *(Pendiente)* - Cerrar sesiones al suspender - -### Regulaciones y Compliance -- [GDPR Article 17: Right to Erasure](https://gdpr-info.eu/art-17-gdpr/) - Derecho al olvido -- [GDPR Article 20: Right to Data Portability](https://gdpr-info.eu/art-20-gdpr/) - Exportar datos personales -- [Ley Federal de Protecci贸n de Datos Personales (M茅xico)](https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf) - Protecci贸n de datos en M茅xico - -### Recursos T茅cnicos -- [PostgreSQL ENUM Types](https://www.postgresql.org/docs/current/datatype-enum.html) -- [NestJS Guards](https://docs.nestjs.com/guards) -- [NestJS Middleware](https://docs.nestjs.com/middleware) - ---- - -## 馃搮 Historial de Cambios - -| Versi贸n | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-11-17 | Tech Team | Creaci贸n inicial adaptada de GAMILIT con contexto de construcci贸n | - ---- - -**Documento:** `MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md` -**Ruta absoluta:** `[RUTA-LEGACY-ELIMINADA]/docs/01-fase-alcance-inicial/MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md` -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @frontend-team diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-003-multi-tenancy.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-003-multi-tenancy.md deleted file mode 100644 index f58a7a303..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/requerimientos/RF-AUTH-003-multi-tenancy.md +++ /dev/null @@ -1,1507 +0,0 @@ -# RF-AUTH-003: Multi-tenancy por Constructora - -## 馃搵 Metadata - -| Campo | Valor | -|-------|-------| -| **ID** | RF-AUTH-003 | -| **脡pica** | MAI-001 - Fundamentos | -| **M贸dulo** | Autenticaci贸n y Multi-tenancy | -| **Prioridad** | Cr铆tica | -| **Estado** | 馃毀 Planificado | -| **Versi贸n** | 1.0 | -| **Fecha creaci贸n** | 2025-11-17 | -| **脷ltima actualizaci贸n** | 2025-11-17 | -| **Esfuerzo estimado** | 18h | -| **Story Points** | 8 SP | - -## 馃敆 Referencias - -### Especificaci贸n T茅cnica -馃搻 [ET-AUTH-003: Multi-tenancy Implementation](../especificaciones/ET-AUTH-003-multi-tenancy.md) *(Pendiente)* - -### Origen (GAMILIT) -鈾伙笍 **Reutilizaci贸n:** 0% - Funcionalidad nueva -- **Justificaci贸n:** GAMILIT no requiere multi-tenancy a nivel de organizaci贸n -- **Adaptaci贸n:** Implementaci贸n completa desde cero para construcci贸n -- **Beneficio:** Permite que profesionales trabajen en m煤ltiples constructoras - -### Documentos Relacionados -- 馃搫 [RF-AUTH-001: Sistema de Roles](./RF-AUTH-001-roles-construccion.md) - Roles pueden variar por constructora -- 馃搫 [RF-AUTH-002: Estados de Cuenta](./RF-AUTH-002-estados-cuenta.md) - Estados pueden variar por constructora -- 馃搫 [US-FUND-001: Autenticaci贸n B谩sica JWT](../historias-usuario/US-FUND-001-autenticacion-basica-jwt.md) - Login con selector de constructora - -### Implementaci贸n DDL - -馃梽锔 **Tablas Principales:** - -```sql --- apps/database/ddl/schemas/auth_management/tables/constructoras.sql -CREATE TABLE auth_management.constructoras ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - nombre VARCHAR(255) NOT NULL, - razon_social VARCHAR(500) NOT NULL, - rfc VARCHAR(13) NOT NULL UNIQUE, - logo_url VARCHAR(1000), - settings JSONB DEFAULT '{}'::JSONB, - active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - -CREATE INDEX idx_constructoras_rfc ON auth_management.constructoras(rfc); -CREATE INDEX idx_constructoras_active ON auth_management.constructoras(active); - --- apps/database/ddl/schemas/auth_management/tables/user-constructoras.sql -CREATE TABLE auth_management.user_constructoras ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID NOT NULL REFERENCES auth_management.profiles(id) ON DELETE CASCADE, - constructora_id UUID NOT NULL REFERENCES auth_management.constructoras(id) ON DELETE CASCADE, - role construction_role NOT NULL, - status user_status NOT NULL DEFAULT 'active', - is_primary BOOLEAN DEFAULT FALSE, - invited_by UUID REFERENCES auth_management.profiles(id), - invited_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - joined_at TIMESTAMP WITH TIME ZONE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - - UNIQUE(user_id, constructora_id) -); - -CREATE INDEX idx_user_constructoras_user ON auth_management.user_constructoras(user_id); -CREATE INDEX idx_user_constructoras_constructora ON auth_management.user_constructoras(constructora_id); -CREATE INDEX idx_user_constructoras_status ON auth_management.user_constructoras(user_id, status); - --- Constraint: Solo una constructora primaria por usuario -CREATE UNIQUE INDEX idx_user_primary_constructora - ON auth_management.user_constructoras(user_id) - WHERE is_primary = TRUE; -``` - -馃梽锔 **Funciones de Context:** - -```sql --- apps/database/ddl/schemas/auth_management/functions/context.sql - --- Obtener constructora actual del usuario (desde JWT) -CREATE OR REPLACE FUNCTION auth_management.get_current_constructora_id() -RETURNS UUID AS $$ -BEGIN - RETURN NULLIF(current_setting('app.current_constructora_id', true), '')::UUID; -EXCEPTION WHEN OTHERS THEN - RETURN NULL; -END; -$$ LANGUAGE plpgsql STABLE; - --- Verificar si usuario tiene acceso a constructora -CREATE OR REPLACE FUNCTION auth_management.user_has_access_to_constructora( - p_user_id UUID, - p_constructora_id UUID -) RETURNS BOOLEAN AS $$ -BEGIN - RETURN EXISTS ( - SELECT 1 - FROM auth_management.user_constructoras - WHERE user_id = p_user_id - AND constructora_id = p_constructora_id - AND status = 'active' - ); -END; -$$ LANGUAGE plpgsql STABLE; - --- Obtener rol del usuario en constructora actual -CREATE OR REPLACE FUNCTION auth_management.get_user_role_in_constructora( - p_user_id UUID, - p_constructora_id UUID -) RETURNS construction_role AS $$ -DECLARE - v_role construction_role; -BEGIN - SELECT role INTO v_role - FROM auth_management.user_constructoras - WHERE user_id = p_user_id - AND constructora_id = p_constructora_id - AND status = 'active'; - - RETURN v_role; -END; -$$ LANGUAGE plpgsql STABLE; - --- Obtener constructoras activas del usuario -CREATE OR REPLACE FUNCTION auth_management.get_user_active_constructoras( - p_user_id UUID -) RETURNS TABLE ( - constructora_id UUID, - nombre VARCHAR, - role construction_role, - is_primary BOOLEAN -) AS $$ -BEGIN - RETURN QUERY - SELECT - c.id, - c.nombre, - uc.role, - uc.is_primary - FROM auth_management.user_constructoras uc - INNER JOIN auth_management.constructoras c ON c.id = uc.constructora_id - WHERE uc.user_id = p_user_id - AND uc.status = 'active' - AND c.active = TRUE - ORDER BY uc.is_primary DESC, c.nombre ASC; -END; -$$ LANGUAGE plpgsql STABLE; -``` - -馃梽锔 **Row Level Security (RLS) Policies:** - -```sql --- Ejemplo: Tabla de proyectos con RLS por constructora -CREATE POLICY "users_view_own_constructora_projects" - ON projects.projects - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.user_has_access_to_constructora( - auth_management.get_current_user_id(), - constructora_id - ) - ); - -CREATE POLICY "users_create_in_own_constructora" - ON projects.projects - FOR INSERT - TO authenticated - WITH CHECK ( - constructora_id = auth_management.get_current_constructora_id() - ); -``` - -### Backend - -馃捇 **Implementaci贸n:** - -```typescript -// apps/backend/src/modules/auth/entities/constructora.entity.ts -import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; - -@Entity('constructoras', { schema: 'auth_management' }) -export class Constructora { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 255 }) - nombre: string; - - @Column({ type: 'varchar', length: 500 }) - razonSocial: string; - - @Column({ type: 'varchar', length: 13, unique: true }) - rfc: string; - - @Column({ type: 'varchar', length: 1000, nullable: true }) - logoUrl: string; - - @Column({ type: 'jsonb', default: {} }) - settings: Record; - - @Column({ type: 'boolean', default: true }) - active: boolean; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} - -// apps/backend/src/modules/auth/entities/user-constructora.entity.ts -@Entity('user_constructoras', { schema: 'auth_management' }) -export class UserConstructora { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'uuid' }) - userId: string; - - @Column({ type: 'uuid' }) - constructoraId: string; - - @Column({ type: 'enum', enum: ConstructionRole }) - role: ConstructionRole; - - @Column({ type: 'enum', enum: UserStatus, default: UserStatus.ACTIVE }) - status: UserStatus; - - @Column({ type: 'boolean', default: false }) - isPrimary: boolean; - - @Column({ type: 'uuid', nullable: true }) - invitedBy: string; - - @Column({ type: 'timestamp with time zone', default: () => 'NOW()' }) - invitedAt: Date; - - @Column({ type: 'timestamp with time zone', nullable: true }) - joinedAt: Date; - - @ManyToOne(() => Profile) - @JoinColumn({ name: 'user_id' }) - user: Profile; - - @ManyToOne(() => Constructora) - @JoinColumn({ name: 'constructora_id' }) - constructora: Constructora; -} - -// apps/backend/src/modules/auth/guards/constructora.guard.ts -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; - -@Injectable() -export class ConstructoraGuard implements CanActivate { - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user = request.user; - - // Validar que el usuario tenga acceso a la constructora - if (!user.constructoraId) { - throw new ForbiddenException('No se ha seleccionado una constructora'); - } - - const hasAccess = await this.userConstructoraRepository.findOne({ - where: { - userId: user.id, - constructoraId: user.constructoraId, - status: UserStatus.ACTIVE, - }, - }); - - if (!hasAccess) { - throw new ForbiddenException( - 'No tienes acceso a esta constructora o tu acceso est谩 inactivo' - ); - } - - return true; - } -} -``` - -### Frontend - -馃帹 **Componentes:** - -```typescript -// apps/frontend/src/features/auth/ConstructoraSelector.tsx -interface ConstructoraSelectorProps { - constructoras: Constructora[]; - onSelect: (constructoraId: string) => void; -} - -export const ConstructoraSelector: React.FC = ({ - constructoras, - onSelect, -}) => { - return ( -
-

Selecciona una constructora

-
- {constructoras.map(constructora => ( - - ))} -
-
- ); -}; - -// apps/frontend/src/stores/constructora-store.ts -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; - -interface ConstructoraStore { - currentConstructora: Constructora | null; - constructoras: Constructora[]; - setCurrentConstructora: (constructora: Constructora) => void; - setConstructoras: (constructoras: Constructora[]) => void; - switchConstructora: (constructoraId: string) => Promise; -} - -export const useConstructoraStore = create()( - persist( - (set, get) => ({ - currentConstructora: null, - constructoras: [], - - setCurrentConstructora: (constructora) => set({ currentConstructora: constructora }), - - setConstructoras: (constructoras) => set({ constructoras }), - - switchConstructora: async (constructoraId) => { - const response = await api.post('/auth/switch-constructora', { - constructoraId, - }); - - const newToken = response.data.accessToken; - localStorage.setItem('accessToken', newToken); - - const constructora = get().constructoras.find(c => c.id === constructoraId); - set({ currentConstructora: constructora }); - - // Recargar p谩gina para actualizar contexto - window.location.reload(); - }, - }), - { - name: 'constructora-storage', - } - ) -); -``` - -### Trazabilidad -馃搳 [TRACEABILITY.yml](../implementacion/TRACEABILITY.yml#L15-L44) - ---- - -## 馃摑 Descripci贸n del Requerimiento - -### Contexto - -En la industria de la construcci贸n, es **com煤n** que profesionales trabajen simult谩neamente para **m煤ltiples empresas constructoras**: - -**Ejemplos del Mundo Real:** -- 馃彈锔 **Ingeniero Freelance:** Proporciona servicios de planeaci贸n a 3 constructoras diferentes -- 馃懆鈥嶐煉 **Residente de Obra:** Trabaja medio tiempo en "Constructora A" y medio tiempo en "Constructora B" -- 馃捈 **Director de Proyectos:** Socio en 2 constructoras y consultor externo en 1 m谩s -- 馃搳 **Contador Externo:** Lleva contabilidad de 5 constructoras peque帽as -- 馃洅 **Coordinador de Compras:** Trabaja para 2 constructoras del mismo grupo empresarial - -### Necesidad del Negocio - -**Problema (Sin Multi-tenancy):** -Sin un sistema multi-tenant robusto: - -1. 鉂 **Usuario necesita m煤ltiples cuentas:** Un ingeniero que trabaja en 3 constructoras necesitar铆a 3 emails diferentes -2. 鉂 **No hay aislamiento de datos:** Riesgo de que constructora A vea datos de constructora B -3. 鉂 **Complejidad en permisos:** No se puede modelar que un usuario sea "director" en una empresa y "residente" en otra -4. 鉂 **Experiencia de usuario pobre:** Usuario debe cerrar sesi贸n y volver a iniciar en cada empresa -5. 鉂 **Dif铆cil auditor铆a:** No queda claro en qu茅 contexto (constructora) se realiz贸 cada acci贸n - -**Soluci贸n (Multi-tenancy por Constructora):** -鉁 **Un email, m煤ltiples constructoras:** Usuario accede con un solo email a todas sus constructoras -鉁 **Aislamiento total de datos:** RLS garantiza que datos de cada constructora est茅n separados -鉁 **Roles por constructora:** Usuario puede ser "director" en A y "resident" en B simult谩neamente -鉁 **Cambio fluido:** Usuario cambia de constructora sin cerrar sesi贸n (switch token) -鉁 **Auditor铆a clara:** Cada acci贸n registra en qu茅 constructora se realiz贸 - ---- - -## 馃幆 Requerimiento Funcional - -### RF-AUTH-003.1: Modelo de Datos Multi-tenant - -El sistema **DEBE** implementar un modelo de multi-tenancy donde: - -#### Entidades Principales - -**1. Constructora (Tenant)** -```typescript -interface Constructora { - id: string; // UUID - nombre: string; // "Constructora ABC S.A. de C.V." - razonSocial: string; // Raz贸n social oficial - rfc: string; // RFC 煤nico (13 caracteres) - logoUrl: string; // URL del logo - settings: { - timezone?: string; // "America/Mexico_City" - currency?: string; // "MXN" - locale?: string; // "es-MX" - fiscalRegime?: string; // "601 - General de Ley Personas Morales" - mainAddress?: Address; - billingConfig?: BillingConfig; - }; - active: boolean; // true = operando, false = inactiva - createdAt: Date; - updatedAt: Date; -} -``` - -**Validaciones:** -- `rfc`: Debe ser RFC v谩lido mexicano (13 caracteres para persona moral) -- `nombre`: M铆nimo 3 caracteres, m谩ximo 255 -- `razonSocial`: M铆nimo 5 caracteres, m谩ximo 500 -- `active`: Solo super_admin puede cambiar - -**2. User-Constructora (Relaci贸n Many-to-Many)** -```typescript -interface UserConstructora { - id: string; - userId: string; // Usuario - constructoraId: string; // Constructora - role: ConstructionRole; // Rol del usuario EN ESTA constructora - status: UserStatus; // Estado EN ESTA constructora - isPrimary: boolean; // true = constructora principal del usuario - invitedBy: string; // UUID del usuario que invit贸 - invitedAt: Date; // Fecha de invitaci贸n - joinedAt: Date | null; // Fecha en que acept贸 invitaci贸n (verific贸 email) - createdAt: Date; - updatedAt: Date; -} -``` - -**Validaciones:** -- `userId` + `constructoraId`: Unique constraint (usuario no puede estar duplicado en misma constructora) -- `isPrimary`: Solo UNA constructora puede ser primaria por usuario -- `role`: Puede ser diferente en cada constructora -- `status`: Puede ser diferente en cada constructora (ej: activo en A, suspendido en B) - ---- - -### RF-AUTH-003.2: Flujo de Invitaci贸n a Constructora - -#### Caso 1: Usuario Nuevo (No existe en sistema) - -**Flujo:** -1. **Director de Constructora A** invita a "ingeniero@email.com" -2. Sistema verifica que email NO existe en `profiles` -3. Sistema crea registro en `invitations`: - ```typescript - { - email: "ingeniero@email.com", - constructoraId: "constructora-a-uuid", - role: "engineer", - invitedBy: "director-uuid", - token: "random-secure-token", - expiresAt: NOW() + 7 days - } - ``` -4. Sistema env铆a email: - ``` - Asunto: Has sido invitado a Constructora ABC - - Hola, - - El Director L贸pez te ha invitado a unirte a Constructora ABC como Ingeniero. - - Para aceptar la invitaci贸n, haz click aqu铆: - https://app.constructora.com/auth/accept-invitation?token=xyz123 - - Esta invitaci贸n expira en 7 d铆as. - ``` -5. Usuario hace click en link -6. Sistema muestra formulario de registro: - - Email: ingeniero@email.com (pre-llenado, readonly) - - Nombre completo - - Contrase帽a - - Confirmar contrase帽a -7. Usuario completa formulario y hace click en "Registrarme" -8. Sistema ejecuta transacci贸n: - ```sql - BEGIN; - - -- Crear perfil - INSERT INTO auth_management.profiles (email, password_hash, full_name, status) - VALUES ('ingeniero@email.com', hash, 'Juan P茅rez', 'pending'); - - -- Asociar a constructora - INSERT INTO auth_management.user_constructoras ( - user_id, constructora_id, role, status, is_primary, invited_by, joined_at - ) VALUES ( - new_user_id, 'constructora-a-uuid', 'engineer', 'pending', true, 'director-uuid', NOW() - ); - - -- Marcar invitaci贸n como aceptada - UPDATE invitations SET status = 'accepted' WHERE token = 'xyz123'; - - COMMIT; - ``` -9. Sistema env铆a email de verificaci贸n -10. Usuario verifica email 鈫 `profiles.status` y `user_constructoras.status` cambian a `'active'` -11. Usuario puede hacer login - -**Resultado:** Usuario nuevo creado y asociado a su primera constructora - ---- - -#### Caso 2: Usuario Existente (Ya registrado) - -**Flujo:** -1. **Director de Constructora B** invita a "ingeniero@email.com" (que ya trabaja en Constructora A) -2. Sistema detecta que email YA existe en `profiles` -3. Sistema crea invitaci贸n: - ```typescript - { - email: "ingeniero@email.com", - userId: "existing-user-uuid", // Ya conocido - constructoraId: "constructora-b-uuid", - role: "resident", // Diferente rol - invitedBy: "director-b-uuid", - token: "random-secure-token", - expiresAt: NOW() + 7 days - } - ``` -4. Sistema env铆a email: - ``` - Asunto: Nueva invitaci贸n a Constructora XYZ - - Hola Juan, - - El Director G贸mez te ha invitado a unirte a Constructora XYZ como Residente de Obra. - - Para aceptar la invitaci贸n, haz click aqu铆: - https://app.constructora.com/auth/accept-invitation?token=abc456 - - Esta invitaci贸n expira en 7 d铆as. - ``` -5. Usuario (ya tiene cuenta) hace click en link -6. Sistema detecta que usuario est谩 autenticado O solicita login -7. Sistema muestra confirmaci贸n: - ``` - Constructora XYZ te ha invitado como Residente de Obra - - 驴Deseas aceptar esta invitaci贸n? - - [Aceptar] [Rechazar] - ``` -8. Usuario hace click en "Aceptar" -9. Sistema asocia usuario a nueva constructora: - ```sql - INSERT INTO auth_management.user_constructoras ( - user_id, constructora_id, role, status, is_primary, invited_by, joined_at - ) VALUES ( - 'existing-user-uuid', 'constructora-b-uuid', 'resident', 'active', false, 'director-b-uuid', NOW() - ); - ``` -10. Sistema muestra: - ``` - 隆Listo! Ahora tienes acceso a Constructora XYZ - - Tus constructoras: - - Constructora ABC (Ingeniero) 猸 Principal - - Constructora XYZ (Residente de Obra) - - [Ir a Constructora XYZ] - ``` - -**Resultado:** Usuario existente asociado a nueva constructora con rol diferente - ---- - -### RF-AUTH-003.3: Login Multi-tenant - -El sistema **DEBE** manejar login de usuarios con acceso a m煤ltiples constructoras: - -#### Flujo de Login - -**Paso 1: Credenciales** -```typescript -// POST /api/auth/login -{ - "email": "ingeniero@email.com", - "password": "password123" -} -``` - -**Paso 2: Validaci贸n** -```typescript -async login(email: string, password: string) { - // 1. Buscar usuario - const user = await this.profileRepository.findOne({ where: { email } }); - - // 2. Validar password - const isPasswordValid = await bcrypt.compare(password, user.passwordHash); - if (!isPasswordValid) throw new UnauthorizedException('Invalid credentials'); - - // 3. Validar estado global - if (user.status === 'banned') { - throw new UnauthorizedException('Account banned'); - } - - // 4. Obtener constructoras activas - const constructoras = await this.getActiveConstructoras(user.id); - - if (constructoras.length === 0) { - throw new UnauthorizedException('No active access to any constructora'); - } - - // 5A. Si solo tiene 1 constructora: login directo - if (constructoras.length === 1) { - const token = this.generateJwt(user, constructoras[0]); - return { - accessToken: token, - user: user, - currentConstructora: constructoras[0], - }; - } - - // 5B. Si tiene m煤ltiples: retornar lista para que elija - return { - requiresConstructoraSelection: true, - user: user, - constructoras: constructoras, // Usuario debe elegir - }; -} -``` - -**Paso 3A: Respuesta (1 sola constructora)** -```json -{ - "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "user": { - "id": "user-uuid", - "email": "ingeniero@email.com", - "fullName": "Juan P茅rez" - }, - "currentConstructora": { - "id": "constructora-a-uuid", - "nombre": "Constructora ABC", - "role": "engineer" - } -} -``` - -**Paso 3B: Respuesta (M煤ltiples constructoras)** -```json -{ - "requiresConstructoraSelection": true, - "user": { - "id": "user-uuid", - "email": "ingeniero@email.com", - "fullName": "Juan P茅rez" - }, - "constructoras": [ - { - "id": "constructora-a-uuid", - "nombre": "Constructora ABC", - "logoUrl": "https://...", - "role": "engineer", - "isPrimary": true - }, - { - "id": "constructora-b-uuid", - "nombre": "Constructora XYZ", - "logoUrl": "https://...", - "role": "resident", - "isPrimary": false - } - ] -} -``` - -**Paso 4: Usuario selecciona constructora** -```typescript -// POST /api/auth/select-constructora -{ - "userId": "user-uuid", - "constructoraId": "constructora-b-uuid", - "tempToken": "temp-token-from-step-3" // Token temporal de 5 min -} - -// Respuesta: -{ - "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "currentConstructora": { - "id": "constructora-b-uuid", - "nombre": "Constructora XYZ", - "role": "resident" - } -} -``` - -**JWT Payload:** -```typescript -interface JwtPayload { - sub: string; // userId - email: string; - fullName: string; - constructoraId: string; // 馃攽 Constructora seleccionada - role: ConstructionRole; // 馃攽 Rol EN ESTA constructora - iat: number; - exp: number; -} -``` - ---- - -### RF-AUTH-003.4: Cambio de Constructora (Switch) - -El sistema **DEBE** permitir cambiar de constructora **sin cerrar sesi贸n**: - -#### Flujo de Switch - -**Paso 1: Usuario hace click en selector de constructora en UI** -```tsx - switchConstructora(constructoraId)} -/> -``` - -**Paso 2: Request al backend** -```typescript -// POST /api/auth/switch-constructora -// Headers: Authorization: Bearer -{ - "constructoraId": "constructora-b-uuid" -} -``` - -**Paso 3: Backend valida y genera nuevo token** -```typescript -@Post('switch-constructora') -@UseGuards(JwtAuthGuard) // Requiere estar autenticado -async switchConstructora( - @CurrentUser() user: User, - @Body() dto: SwitchConstructoraDto -): Promise<{ accessToken: string }> { - // 1. Validar que usuario tiene acceso a constructora destino - const hasAccess = await this.userConstructoraRepository.findOne({ - where: { - userId: user.id, - constructoraId: dto.constructoraId, - status: UserStatus.ACTIVE, - }, - relations: ['constructora'], - }); - - if (!hasAccess) { - throw new ForbiddenException('No tienes acceso a esta constructora'); - } - - // 2. Generar nuevo JWT con nueva constructora - const newToken = this.jwtService.sign({ - sub: user.id, - email: user.email, - fullName: user.fullName, - constructoraId: dto.constructoraId, - role: hasAccess.role, // Nuevo rol (puede ser diferente) - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60), // 24h - }); - - // 3. Auditar cambio de contexto - await this.auditService.log({ - action: 'switch_constructora', - userId: user.id, - details: { - from: user.constructoraId, - to: dto.constructoraId, - timestamp: new Date(), - }, - }); - - return { accessToken: newToken }; -} -``` - -**Paso 4: Frontend actualiza token y contexto** -```typescript -async function switchConstructora(constructoraId: string) { - const response = await api.post('/auth/switch-constructora', { - constructoraId, - }); - - // Actualizar token en localStorage - localStorage.setItem('accessToken', response.data.accessToken); - - // Actualizar estado global - useConstructoraStore.getState().setCurrentConstructora( - constructoras.find(c => c.id === constructoraId) - ); - - // Recargar aplicaci贸n para aplicar nuevo contexto - window.location.reload(); -} -``` - -**Resultado:** -- 鉁 Usuario cambia de constructora sin volver a hacer login -- 鉁 Nuevo token con `constructoraId` y `role` actualizados -- 鉁 RLS en base de datos usa nuevo contexto autom谩ticamente -- 鉁 UI se actualiza mostrando datos de nueva constructora - ---- - -### RF-AUTH-003.5: Aislamiento de Datos (Row Level Security) - -El sistema **DEBE** garantizar aislamiento TOTAL de datos entre constructoras usando RLS: - -#### Implementaci贸n RLS - -**Paso 1: Configurar contexto en cada request** -```typescript -// apps/backend/src/common/interceptors/set-rls-context.interceptor.ts -@Injectable() -export class SetRlsContextInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest(); - const user = request.user; // Del JWT - - if (user) { - // Configurar variables de sesi贸n de PostgreSQL - return from( - this.dataSource.query(` - SELECT set_config('app.current_user_id', $1, true), - set_config('app.current_constructora_id', $2, true), - set_config('app.current_user_role', $3, true) - `, [user.id, user.constructoraId, user.role]) - ).pipe( - switchMap(() => next.handle()) - ); - } - - return next.handle(); - } -} -``` - -**Paso 2: Pol铆ticas RLS en todas las tablas de negocio** - -```sql --- Ejemplo: Tabla de proyectos -CREATE POLICY "users_view_own_constructora_projects" - ON projects.projects - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - ); - -CREATE POLICY "users_create_in_own_constructora" - ON projects.projects - FOR INSERT - TO authenticated - WITH CHECK ( - constructora_id = auth_management.get_current_constructora_id() - ); - --- Ejemplo: Tabla de empleados -CREATE POLICY "users_view_own_constructora_employees" - ON hr.employees - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - ); - --- Ejemplo: Tabla de presupuestos -CREATE POLICY "directors_engineers_view_budgets" - ON budgets.budgets - FOR SELECT - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - AND auth_management.get_current_user_role() IN ('director', 'engineer', 'finance') - ); -``` - -**Paso 3: Testing de aislamiento** - -```typescript -describe('Multi-tenancy Data Isolation', () => { - it('should NOT allow user to see projects from other constructora', async () => { - // Setup: 2 constructoras con 1 proyecto cada una - const constructoraA = await createConstructora({ nombre: 'Constructora A' }); - const constructoraB = await createConstructora({ nombre: 'Constructora B' }); - - const projectA = await createProject({ - nombre: 'Proyecto A', - constructoraId: constructoraA.id, - }); - - const projectB = await createProject({ - nombre: 'Proyecto B', - constructoraId: constructoraB.id, - }); - - // Usuario con acceso SOLO a constructora A - const user = await createUser(); - await assignToConstructora(user.id, constructoraA.id, 'engineer'); - const token = await loginAs(user, constructoraA.id); - - // Act: Solicitar todos los proyectos - const response = await request(app.getHttpServer()) - .get('/projects') - .set('Authorization', `Bearer ${token}`) - .expect(200); - - // Assert: Solo debe ver proyecto de constructora A - expect(response.body.data).toHaveLength(1); - expect(response.body.data[0].id).toBe(projectA.id); - expect(response.body.data[0].nombre).toBe('Proyecto A'); - - // Proyecto B NO debe aparecer - const projectBInResponse = response.body.data.find(p => p.id === projectB.id); - expect(projectBInResponse).toBeUndefined(); - }); -}); -``` - ---- - -### RF-AUTH-003.6: Constructora Principal (Primary) - -El sistema **DEBE** soportar concepto de "constructora principal": - -#### Caracter铆sticas - -**Definici贸n:** -- Usuario designa UNA constructora como "principal" -- Al hacer login con m煤ltiples constructoras, se selecciona autom谩ticamente la principal -- Usuario puede cambiar cu谩l es la principal en cualquier momento - -**Reglas:** -- Solo UNA constructora puede ser principal por usuario -- Constraint a nivel de base de datos garantiza unicidad -- Si usuario elimina su 煤nica constructora principal, debe designar otra - -**Implementaci贸n:** - -```typescript -// PATCH /api/user/set-primary-constructora -async setPrimaryConstructora( - userId: string, - constructoraId: string -): Promise { - // 1. Validar que usuario tiene acceso - const access = await this.userConstructoraRepository.findOne({ - where: { userId, constructoraId, status: UserStatus.ACTIVE }, - }); - - if (!access) { - throw new NotFoundException('No tienes acceso a esta constructora'); - } - - // 2. Transacci贸n: quitar primary de anterior y asignar a nueva - await this.dataSource.transaction(async (manager) => { - // Quitar is_primary de todas las constructoras del usuario - await manager.update( - UserConstructora, - { userId }, - { isPrimary: false } - ); - - // Asignar is_primary a la nueva - await manager.update( - UserConstructora, - { userId, constructoraId }, - { isPrimary: true } - ); - }); - - // 3. Auditar - await this.auditService.log({ - action: 'set_primary_constructora', - userId, - details: { constructoraId }, - }); -} -``` - -**UI:** - -```tsx -// Componente de lista de constructoras del usuario -
- {userConstructoras.map(uc => ( -
- -

{uc.constructora.nombre}

- {uc.role} - - {uc.isPrimary ? ( - 猸 Principal - ) : ( - - )} -
- ))} -
-``` - ---- - -## 馃搳 Casos de Uso - -### UC-MT-001: Ingeniero freelance trabaja en 3 constructoras - -**Actor:** Ingeniero de Planeaci贸n -**Precondiciones:** Usuario registrado - -**Flujo:** -1. **Constructora A** invita a ingeniero@email.com como "engineer" -2. Ingeniero acepta, verifica email, tiene acceso a Constructora A -3. Ingeniero marca Constructora A como principal -4. **Constructora B** invita a ingeniero@email.com como "engineer" -5. Ingeniero (ya autenticado) acepta invitaci贸n desde panel -6. Ingeniero ahora tiene acceso a: - - Constructora A (Ingeniero) 猸 Principal - - Constructora B (Ingeniero) -7. **Constructora C** invita a ingeniero@email.com como "director" -8. Ingeniero acepta -9. Ingeniero ahora tiene: - - Constructora A (Ingeniero) 猸 Principal - - Constructora B (Ingeniero) - - Constructora C (Director) 鈫 Rol diferente -10. Ingeniero hace login una vez -11. Sistema le muestra selector de 3 constructoras -12. Ingeniero selecciona Constructora A (principal pre-seleccionada) -13. Ingeniero trabaja en Constructora A -14. Ingeniero hace click en selector de constructora en header -15. Ingeniero selecciona "Constructora C" -16. Sistema regenera token con `constructoraId=C` y `role=director` -17. UI se actualiza mostrando dashboard de director -18. Ingeniero trabaja en Constructora C con permisos de director - -**Resultado:** -- 鉁 Un email, 3 constructoras -- 鉁 Roles diferentes en cada constructora -- 鉁 Cambio fluido entre contextos -- 鉁 Datos aislados por constructora - ---- - -### UC-MT-002: Director crea nueva constructora e invita equipo - -**Actor:** Director de Construcci贸n -**Precondiciones:** Usuario con acceso al sistema - -**Flujo:** -1. Director navega a `/admin/constructoras/create` -2. Director completa formulario: - - Nombre: "Constructora Nueva S.A." - - Raz贸n Social: "Constructora Nueva Sociedad An贸nima de Capital Variable" - - RFC: "CNN123456ABC" - - Logo: (sube imagen) -3. Director hace click en "Crear Constructora" -4. Sistema crea constructora -5. Sistema autom谩ticamente asocia al director como primer usuario: - ```sql - INSERT INTO user_constructoras (user_id, constructora_id, role, status, is_primary) - VALUES (director_id, new_constructora_id, 'director', 'active', false); - ``` -6. Director navega a `/admin/users/invite` -7. Director invita usuarios: - - ingeniero@email.com 鈫 Ingeniero - - residente1@email.com 鈫 Residente de Obra - - residente2@email.com 鈫 Residente de Obra - - compras@email.com 鈫 Compras -8. Sistema env铆a 4 emails de invitaci贸n -9. Usuarios aceptan invitaciones y verifican emails -10. Director ve en panel de usuarios: - ``` - Usuarios activos en Constructora Nueva S.A.: - - Director L贸pez (Director) 猸 T煤 - - Ing. Juan P茅rez (Ingeniero) - - Residente Carlos G贸mez (Residente de Obra) - - Residente Ana Mart铆nez (Residente de Obra) - - Mar铆a Torres (Compras) - ``` - -**Resultado:** -- 鉁 Constructora creada -- 鉁 Equipo completo invitado -- 鉁 Usuarios pueden acceder con diferentes roles - ---- - -### UC-MT-003: Usuario suspendido en constructora A pero activo en B - -**Actor:** Director de Constructora A -**Precondiciones:** -- Residente trabaja en Constructora A y Constructora B -- Residente cometi贸 falta grave en Constructora A - -**Flujo:** -1. Director de Constructora A suspende al residente por 14 d铆as -2. Sistema actualiza: - ```sql - UPDATE user_constructoras - SET status = 'suspended' - WHERE user_id = residente_id - AND constructora_id = constructora_a_id; - ``` -3. Residente intenta hacer login -4. Sistema detecta que tiene: - - Constructora A: status = 'suspended' - - Constructora B: status = 'active' -5. Sistema muestra solo Constructora B en selector -6. Residente hace login en Constructora B exitosamente -7. Residente puede trabajar normalmente en Constructora B -8. Residente intenta cambiar a Constructora A desde selector -9. Sistema muestra error: "Tu acceso a Constructora A est谩 suspendido. Contacta al administrador." -10. Despu茅s de 14 d铆as, Director de Constructora A levanta suspensi贸n -11. Sistema actualiza status a 'active' -12. Residente ahora puede acceder a ambas constructoras - -**Resultado:** -- 鉁 Suspensi贸n aislada por constructora -- 鉁 No afecta acceso a otras constructoras -- 鉁 Usuario puede seguir trabajando donde no est谩 suspendido - ---- - -## 馃攼 Consideraciones de Seguridad - -### 1. Prevenci贸n de Data Leakage entre Constructoras - -**Problema:** Query malicioso podr铆a intentar acceder a datos de otra constructora - -**Soluci贸n: RLS aplicado en TODAS las tablas de negocio** - -```sql --- OBLIGATORIO en CADA tabla con constructora_id -ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY; -ALTER TABLE hr.employees ENABLE ROW LEVEL SECURITY; -ALTER TABLE budgets.budgets ENABLE ROW LEVEL SECURITY; --- ... etc - --- Pol铆tica base (repetir en cada tabla) -CREATE POLICY "constructora_isolation" - ON [tabla] - FOR ALL - TO authenticated - USING ( - constructora_id = auth_management.get_current_constructora_id() - ); -``` - -**Testing:** -```typescript -it('should prevent SQL injection to access other constructora data', async () => { - const userA = await createUserInConstructora('constructora-a'); - const projectB = await createProjectInConstructora('constructora-b'); - - // Intentar inyecci贸n SQL - const maliciousQuery = ` - SELECT * FROM projects.projects - WHERE constructora_id = '${projectB.constructoraId}' - -- Intentar bypass - `; - - // Debe retornar 0 resultados (RLS bloquea) - const result = await executeAsUser(userA, maliciousQuery); - expect(result).toHaveLength(0); -}); -``` - ---- - -### 2. Validaci贸n de Acceso en Cambio de Constructora - -**Problema:** Usuario intenta cambiar a constructora a la que no tiene acceso - -**Soluci贸n: Validar en backend antes de generar token** - -```typescript -async switchConstructora(userId: string, constructoraId: string) { - // 1. Verificar acceso - const hasAccess = await this.db.query(` - SELECT 1 - FROM auth_management.user_constructoras - WHERE user_id = $1 - AND constructora_id = $2 - AND status = 'active' - `, [userId, constructoraId]); - - if (hasAccess.rows.length === 0) { - throw new ForbiddenException({ - statusCode: 403, - message: 'No tienes acceso a esta constructora', - errorCode: 'CONSTRUCTORA_ACCESS_DENIED', - }); - } - - // 2. Generar token solo si tiene acceso - return this.generateJwt(userId, constructoraId); -} -``` - ---- - -### 3. Auditor铆a de Cambios de Contexto - -**Problema:** Dif铆cil rastrear en qu茅 constructora se realiz贸 cada acci贸n - -**Soluci贸n: Incluir `constructora_id` en TODOS los audit logs** - -```sql --- Trigger en cada tabla -CREATE TRIGGER trg_audit_with_constructora - AFTER INSERT OR UPDATE OR DELETE ON [tabla] - FOR EACH ROW - EXECUTE FUNCTION audit_logging.log_action_with_constructora(); - --- Funci贸n -CREATE OR REPLACE FUNCTION audit_logging.log_action_with_constructora() -RETURNS TRIGGER AS $$ -BEGIN - INSERT INTO audit_logging.audit_logs ( - action, - table_name, - record_id, - user_id, - constructora_id, -- 馃攽 Incluir siempre - old_data, - new_data, - timestamp - ) VALUES ( - TG_OP, - TG_TABLE_NAME, - COALESCE(NEW.id, OLD.id), - auth_management.get_current_user_id(), - auth_management.get_current_constructora_id(), -- 馃攽 Contexto - to_jsonb(OLD), - to_jsonb(NEW), - NOW() - ); - - RETURN COALESCE(NEW, OLD); -END; -$$ LANGUAGE plpgsql; -``` - -**Query de auditor铆a:** -```sql --- Ver todas las acciones del usuario en Constructora A -SELECT * -FROM audit_logging.audit_logs -WHERE user_id = 'user-uuid' - AND constructora_id = 'constructora-a-uuid' -ORDER BY timestamp DESC; -``` - ---- - -### 4. Expiraci贸n de Invitaciones - -**Problema:** Invitaciones pendientes indefinidamente - -**Soluci贸n: Cleanup autom谩tico de invitaciones expiradas** - -```typescript -// Cron job: cada d铆a a las 2 AM -@Cron('0 2 * * *') -async cleanupExpiredInvitations() { - const result = await this.invitationRepository.delete({ - expiresAt: LessThan(new Date()), - status: 'pending', - }); - - this.logger.log(`Deleted ${result.affected} expired invitations`); -} -``` - ---- - -## 鉁 Criterios de Aceptaci贸n - -### AC-001: Modelo de Datos -- [ ] Tabla `constructoras` creada con campos obligatorios -- [ ] Tabla `user_constructoras` creada con unique constraint (user_id, constructora_id) -- [ ] Constraint de una sola constructora principal por usuario funciona -- [ ] 脥ndices creados en columnas de b煤squeda frecuente - -### AC-002: Invitaciones -- [ ] Director puede invitar usuario nuevo (no registrado) -- [ ] Director puede invitar usuario existente -- [ ] Email de invitaci贸n enviado con token v谩lido -- [ ] Invitaci贸n expira despu茅s de 7 d铆as -- [ ] Usuario puede aceptar invitaci贸n y asociarse a constructora -- [ ] Usuario puede rechazar invitaci贸n - -### AC-003: Login Multi-tenant -- [ ] Usuario con 1 constructora: login directo -- [ ] Usuario con m煤ltiples constructoras: muestra selector -- [ ] JWT incluye `constructoraId` y `role` correcto -- [ ] Constructora principal se pre-selecciona -- [ ] Login valida que usuario tenga al menos 1 constructora activa - -### AC-004: Switch de Constructora -- [ ] Usuario puede cambiar de constructora sin cerrar sesi贸n -- [ ] Nuevo token generado con `constructoraId` y `role` actualizados -- [ ] UI se actualiza mostrando datos de nueva constructora -- [ ] Cambio auditado en `audit_logs` -- [ ] Error claro si usuario no tiene acceso a constructora destino - -### AC-005: Aislamiento de Datos (RLS) -- [ ] RLS habilitado en TODAS las tablas con `constructora_id` -- [ ] Usuario NO puede ver datos de otras constructoras -- [ ] Queries autom谩ticamente filtran por `constructora_id` actual -- [ ] Testing demuestra aislamiento completo - -### AC-006: Roles por Constructora -- [ ] Usuario puede tener diferentes roles en diferentes constructoras -- [ ] JWT incluye rol correcto seg煤n constructora actual -- [ ] Permisos se eval煤an seg煤n rol en constructora actual - -### AC-007: Estados por Constructora -- [ ] Usuario puede estar suspendido en A pero activo en B -- [ ] Login muestra solo constructoras donde status = 'active' -- [ ] Cambio a constructora suspendida bloqueado con error claro - -### AC-008: Constructora Principal -- [ ] Usuario puede marcar una constructora como principal -- [ ] Solo una constructora puede ser principal (constraint) -- [ ] Login pre-selecciona constructora principal - ---- - -## 馃И Testing - -### Test Suite: Multi-tenancy - -#### Test 1: Data isolation between constructoras -```typescript -describe('Multi-tenancy Data Isolation', () => { - it('should completely isolate data between constructoras', async () => { - // Setup - const constructoraA = await createConstructora({ nombre: 'A' }); - const constructoraB = await createConstructora({ nombre: 'B' }); - - const userA = await createUser({ email: 'usera@test.com' }); - const userB = await createUser({ email: 'userb@test.com' }); - - await assignToConstructora(userA.id, constructoraA.id, 'engineer'); - await assignToConstructora(userB.id, constructoraB.id, 'engineer'); - - const projectA = await createProject({ constructoraId: constructoraA.id }); - const projectB = await createProject({ constructoraId: constructoraB.id }); - - // Act: User A requests all projects - const tokenA = await loginAs(userA, constructoraA.id); - const responseA = await request(app.getHttpServer()) - .get('/projects') - .set('Authorization', `Bearer ${tokenA}`) - .expect(200); - - // Assert: User A sees ONLY projectA - expect(responseA.body.data).toHaveLength(1); - expect(responseA.body.data[0].id).toBe(projectA.id); - - // Act: User B requests all projects - const tokenB = await loginAs(userB, constructoraB.id); - const responseB = await request(app.getHttpServer()) - .get('/projects') - .set('Authorization', `Bearer ${tokenB}`) - .expect(200); - - // Assert: User B sees ONLY projectB - expect(responseB.body.data).toHaveLength(1); - expect(responseB.body.data[0].id).toBe(projectB.id); - }); -}); -``` - -#### Test 2: User with multiple constructoras can switch -```typescript -it('should allow user to switch between constructoras', async () => { - const user = await createUser(); - const constructoraA = await createConstructora({ nombre: 'A' }); - const constructoraB = await createConstructora({ nombre: 'B' }); - - await assignToConstructora(user.id, constructoraA.id, 'engineer'); - await assignToConstructora(user.id, constructoraB.id, 'director'); - - // Login in constructora A - let token = await loginAs(user, constructoraA.id); - let decoded = jwt.decode(token); - expect(decoded.constructoraId).toBe(constructoraA.id); - expect(decoded.role).toBe('engineer'); - - // Switch to constructora B - const switchResponse = await request(app.getHttpServer()) - .post('/auth/switch-constructora') - .set('Authorization', `Bearer ${token}`) - .send({ constructoraId: constructoraB.id }) - .expect(200); - - const newToken = switchResponse.body.accessToken; - decoded = jwt.decode(newToken); - expect(decoded.constructoraId).toBe(constructoraB.id); - expect(decoded.role).toBe('director'); // Role changed! -}); -``` - -#### Test 3: Invitation flow for new user -```typescript -it('should allow director to invite new user and user to accept', async () => { - const director = await createDirector(); - const constructora = director.constructoras[0]; - - // Director invites new user - await loginAs(director); - const inviteResponse = await request(app.getHttpServer()) - .post('/admin/users/invite') - .send({ - email: 'newuser@test.com', - constructoraId: constructora.id, - role: 'resident', - }) - .expect(201); - - const invitationToken = inviteResponse.body.token; - - // New user accepts invitation - const registerResponse = await request(app.getHttpServer()) - .post('/auth/register-by-invitation') - .send({ - token: invitationToken, - password: 'SecurePass123!', - fullName: 'Juan P茅rez', - }) - .expect(201); - - // Verify user was created and associated - const newUser = await getUserByEmail('newuser@test.com'); - expect(newUser).toBeDefined(); - expect(newUser.status).toBe('pending'); // Needs email verification - - const association = await getUserConstructoraAssociation(newUser.id, constructora.id); - expect(association.role).toBe('resident'); - expect(association.status).toBe('pending'); -}); -``` - ---- - -## 馃摎 Referencias Adicionales - -### Documentos Relacionados -- 馃搫 [RF-AUTH-001: Sistema de Roles](./RF-AUTH-001-roles-construccion.md) -- 馃搫 [RF-AUTH-002: Estados de Cuenta](./RF-AUTH-002-estados-cuenta.md) -- 馃搫 [US-FUND-001: Autenticaci贸n B谩sica JWT](../historias-usuario/US-FUND-001-autenticacion-basica-jwt.md) - -### Recursos T茅cnicos -- [PostgreSQL Row Level Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) -- [Multi-tenancy Patterns](https://docs.microsoft.com/en-us/azure/architecture/patterns/category/data-management) -- [JWT Best Practices](https://datatracker.ietf.org/doc/html/rfc8725) - ---- - -## 馃搮 Historial de Cambios - -| Versi贸n | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-11-17 | Tech Team | Creaci贸n inicial - Funcionalidad completamente nueva para construcci贸n | - ---- - -**Documento:** `MAI-001-fundamentos/requerimientos/RF-AUTH-003-multi-tenancy.md` -**Ruta absoluta:** `[RUTA-LEGACY-ELIMINADA]/docs/01-fase-alcance-inicial/MAI-001-fundamentos/requerimientos/RF-AUTH-003-multi-tenancy.md` -**Generado:** 2025-11-17 -**Mantenedores:** @tech-lead @backend-team @database-team diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/README.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/README.md deleted file mode 100644 index b2ee7477a..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/README.md +++ /dev/null @@ -1,481 +0,0 @@ -# MAI-002: Proyectos y Estructura de Obra - -**Vertical:** Construccion -**Modulo:** MAI-002 -**Nombre:** Proyectos y Estructura -**Fase:** Fase 2 - Core Business -**Prioridad:** P0 (Critica) -**Estado:** Completo -**Story Points:** 45 SP -**Ultima actualizacion:** 2025-11-17 - ---- - -## Descripcion - -El modulo **MAI-002 - Proyectos y Estructura** es el nucleo central del sistema de construccion, permitiendo la gestion completa de proyectos inmobiliarios con una jerarquia flexible que se adapta a diferentes tipos de desarrollos: - -- **Fraccionamiento Horizontal:** Proyecto 鈫 Etapa 鈫 Manzana 鈫 Lote 鈫 Vivienda -- **Conjunto Habitacional:** Proyecto 鈫 Etapa 鈫 Lote 鈫 Vivienda (sin manzanas) -- **Edificio Vertical:** Proyecto 鈫 Torre 鈫 Nivel 鈫 Departamento -- **Proyecto Mixto:** Combinacion de estructuras anteriores - -Este modulo gestiona desde el catalogo de proyectos hasta la estructura jerarquica detallada de cada desarrollo, incluyendo prototipos de vivienda, asignacion de equipos de trabajo y calendario de hitos del proyecto. - -### Caracteristicas Clave - -- Gestion de multiples proyectos simultaneos por constructora -- Jerarquia de 5 niveles: Proyecto 鈫 Etapa 鈫 Manzana 鈫 Lote 鈫 Vivienda -- Catalogo de prototipos de vivienda con versionado -- Asignacion de equipo con validacion de workload -- Calendario de hitos con dependencias y alertas automaticas -- Soporte para creacion masiva de lotes (hasta 500 por operacion) -- Estados del ciclo de vida: Licitacion 鈫 Adjudicado 鈫 Ejecucion 鈫 Entregado 鈫 Cerrado - ---- - -## Alcance Funcional - -El modulo MAI-002 cubre las siguientes areas funcionales: - -### 1. Catalogo de Proyectos (RF-PROJ-001) -- **CRUD completo** de proyectos inmobiliarios -- **4 tipos de proyectos:** Fraccionamiento horizontal, Conjunto habitacional, Edificio vertical, Mixto -- **5 estados del ciclo de vida:** Licitacion 鈫 Adjudicado 鈫 Ejecucion 鈫 Entregado 鈫 Cerrado -- **Datos completos:** Ubicacion geografica, cliente, contrato, permisos legales -- **Metricas automaticas:** Fisicas (viviendas, m2), financieras (presupuesto, avance), temporales (fechas clave) -- **Codigo auto-generado:** PROJ-2025-001, PROJ-2025-002, etc. - -### 2. Estructura Jerarquica de Obra (RF-PROJ-002) -- **Jerarquia de 5 niveles:** Proyecto 鈫 Etapa 鈫 Manzana 鈫 Lote 鈫 Vivienda -- **Estructuras flexibles:** - - Fraccionamiento: Con manzanas y lotes - - Conjunto: Sin manzanas (solo lotes directos) - - Torre vertical: Niveles y departamentos -- **Estados de lote:** Disponible 鈫 Vendido 鈫 En construccion 鈫 Terminado 鈫 Entregado -- **Avance fisico por vivienda:** Cimentacion, Estructura, Muros, Instalaciones, Acabados -- **Creacion masiva:** Hasta 500 lotes en una operacion (< 3 segundos) -- **Vista de arbol jerarquico:** Navegacion recursiva con expandir/colapsar - -### 3. Prototipos de Vivienda (RF-PROJ-003) -- **Catalogo de prototipos** por constructora -- **3 tipos principales:** Casa unifamiliar, Departamento, Duplex/Triplex -- **Segmentos:** Interes social, Interes medio, Residencial medio/alto, Premium -- **Datos completos:** Areas (construccion, terreno, vendible), distribucion (recamaras, banos), acabados, costos -- **Versionado automatico:** v1, v2, v3... con historial de cambios -- **Asignacion a lotes:** Individual o masiva (hasta 500 lotes) -- **Herencia de caracteristicas:** Snapshot de prototipo al momento de asignacion -- **Control de uso:** Impedir eliminacion si el prototipo esta asignado a viviendas - -### 4. Asignacion de Equipo y Calendario (RF-PROJ-004) -- **5 roles de equipo:** Director, Residente, Ingeniero, Supervisor, Gerente de Compras -- **Validacion de workload:** Limites por rol (Director 500%, Residente 200%, Ingeniero 800%) -- **Reglas de asignacion:** Solo un Director/Residente principal por proyecto -- **Hitos del proyecto:** 11 tipos desde arranque hasta cierre administrativo -- **Fases constructivas:** 9 fases (preliminares, cimentacion, estructura, albanileria, instalaciones, acabados, exteriores, urbanizacion, entrega) -- **Fechas criticas:** Contractuales, regulatorias, financieras con alertas automaticas -- **Alertas configurables:** 30, 15, 7, 3, 2, 1 dia(s) antes -- **Dashboard de equipo:** Visualizacion de carga de trabajo por rol - ---- - -## Reutilizacion del Core ERP - -El modulo MAI-002 reutiliza aproximadamente **40% de componentes** del ERP generico (GAMILIT): - -### Componentes Reutilizados del Core - -| Componente Core | Reutilizacion | Adaptacion Requerida | -|-----------------|---------------|----------------------| -| **Sistema de autenticacion (JWT)** | 95% | Roles de construccion | -| **Multi-tenancy (por constructora)** | 90% | RLS policies por proyecto | -| **Sistema de auditoria** | 85% | Eventos especificos de proyectos | -| **Gestion de catalogos** | 70% | Jerarquia de 5 niveles | -| **Sistema de notificaciones** | 80% | Alertas de fechas criticas | -| **Dashboard base** | 60% | Metricas especificas de obra | -| **Componentes UI** | 85% | TreeView, Forms, Cards | -| **API RESTful** | 90% | Endpoints especificos | - -### Componentes Nuevos (No Reutilizables) - -Los siguientes componentes son especificos del dominio de construccion: - -- **Jerarquia de 5 niveles:** Proyecto 鈫 Etapa 鈫 Manzana 鈫 Lote 鈫 Vivienda -- **Prototipos de vivienda:** Catalogo con versionado y herencia -- **Estructuras flexibles:** Soporte para horizontal, vertical y mixto -- **Creacion masiva de lotes:** Bulk operations hasta 500 elementos -- **Workload por rol:** Validacion de limites por tipo de ingeniero/residente -- **Hitos con dependencias:** Graph validation de milestones -- **Avance fisico ponderado:** Calculo automatico por etapas constructivas - ---- - -## Requerimientos Funcionales (RF) - -El modulo MAI-002 incluye 4 requerimientos funcionales completos: - -| ID | Titulo | Archivo | Prioridad | Estado | -|----|--------|---------|-----------|--------| -| RF-PROJ-001 | Catalogo de Proyectos | [RF-PROJ-001-catalogo-proyectos.md](./requerimientos-funcionales/RF-PROJ-001-catalogo-proyectos.md) | P0 | Completo | -| RF-PROJ-002 | Estructura Jerarquica de Obra | [RF-PROJ-002-estructura-jerarquica-obra.md](./requerimientos-funcionales/RF-PROJ-002-estructura-jerarquica-obra.md) | P0 | Completo | -| RF-PROJ-003 | Prototipos de Vivienda | [RF-PROJ-003-prototipos-vivienda.md](./requerimientos-funcionales/RF-PROJ-003-prototipos-vivienda.md) | P0 | Completo | -| RF-PROJ-004 | Asignacion de Equipo y Calendario | [RF-PROJ-004-asignacion-equipo-calendario.md](./requerimientos-funcionales/RF-PROJ-004-asignacion-equipo-calendario.md) | P0 | Completo | - -**Total:** 4 RFs (~104 KB de documentacion) - ---- - -## Especificaciones Tecnicas (ET) - -| ID | Titulo | Archivo | RF Base | Estado | -|----|--------|---------|---------|--------| -| ET-PROJ-001 | Implementacion de Catalogo de Proyectos | [ET-PROJ-001-implementacion-catalogo-proyectos.md](./especificaciones/ET-PROJ-001-implementacion-catalogo-proyectos.md) | RF-PROJ-001 | Completo | -| ET-PROJ-002 | Implementacion de Estructura Jerarquica | [ET-PROJ-002-implementacion-estructura-jerarquica.md](./especificaciones/ET-PROJ-002-implementacion-estructura-jerarquica.md) | RF-PROJ-002 | Completo | -| ET-PROJ-003 | Implementacion de Prototipos | [ET-PROJ-003-implementacion-prototipos.md](./especificaciones/ET-PROJ-003-implementacion-prototipos.md) | RF-PROJ-003 | Completo | -| ET-PROJ-004 | Implementacion de Equipo y Calendario | [ET-PROJ-004-implementacion-equipo-calendario.md](./especificaciones/ET-PROJ-004-implementacion-equipo-calendario.md) | RF-PROJ-004 | Completo | - -**Total:** 4 ETs (~164 KB de documentacion) - ---- - -## Historias de Usuario (US) - -| ID | Titulo | Archivo | SP | Estado | -|----|--------|---------|-------|--------| -| US-PROJ-001 | Catalogo de Proyectos | [US-PROJ-001-catalogo-proyectos.md](./historias-usuario/US-PROJ-001-catalogo-proyectos.md) | 8 | Completo | -| US-PROJ-002 | Transiciones de Estado | [US-PROJ-002-transiciones-estado.md](./historias-usuario/US-PROJ-002-transiciones-estado.md) | 5 | Completo | -| US-PROJ-003 | Crear Estructura de Fraccionamiento | [US-PROJ-003-estructura-fraccionamiento.md](./historias-usuario/US-PROJ-003-estructura-fraccionamiento.md) | 8 | Completo | -| US-PROJ-004 | Crear Estructura de Torre Vertical | [US-PROJ-004-estructura-torre-vertical.md](./historias-usuario/US-PROJ-004-estructura-torre-vertical.md) | 6 | Completo | -| US-PROJ-005 | Gestion de Prototipos | [US-PROJ-005-gestion-prototipos.md](./historias-usuario/US-PROJ-005-gestion-prototipos.md) | 5 | Completo | -| US-PROJ-006 | Asignacion de Prototipos a Lotes | [US-PROJ-006-asignacion-prototipos-lotes.md](./historias-usuario/US-PROJ-006-asignacion-prototipos-lotes.md) | 3 | Completo | -| US-PROJ-007 | Asignacion de Equipo | [US-PROJ-007-asignacion-equipo.md](./historias-usuario/US-PROJ-007-asignacion-equipo.md) | 4 | Completo | -| US-PROJ-008 | Calendario de Hitos | [US-PROJ-008-calendario-hitos.md](./historias-usuario/US-PROJ-008-calendario-hitos.md) | 3 | Completo | -| US-PROJ-009 | Alertas de Fechas Criticas | [US-PROJ-009-alertas-fechas-criticas.md](./historias-usuario/US-PROJ-009-alertas-fechas-criticas.md) | 3 | Completo | - -**Total:** 9 USs | 45 Story Points (~92 KB de documentacion) - ---- - -## Dependencias con Otros Modulos - -### Modulos Prerequisitos (Bloqueantes) - -| Modulo | Relacion | Detalles | -|--------|----------|----------| -| **MAI-001: Fundamentos** | BLOQUEANTE | Sistema de autenticacion, RBAC, multi-tenancy por constructora | - -### Modulos Dependientes (Consumen MAI-002) - -| Modulo | Relacion | Uso de MAI-002 | -|--------|----------|----------------| -| **MAI-003: Presupuestos y Costos** | DEPENDIENTE | Presupuestos maestros por proyecto, costos por prototipo | -| **MAI-004: Compras e Inventarios** | DEPENDIENTE | Requisiciones filtradas por proyecto | -| **MAI-005: Control de Obra y Avances** | DEPENDIENTE | Avances fisicos por vivienda, checklists por etapa | -| **MAI-007: RRHH y Asistencias** | DEPENDIENTE | Asistencias de cuadrillas asignadas al proyecto | -| **MAI-008: Estimaciones y Facturacion** | DEPENDIENTE | Estimaciones por avance de proyecto | -| **MAI-009: Calidad y Postventa** | DEPENDIENTE | Incidencias por lote/vivienda | -| **MAI-010: CRM Derechohabientes** | DEPENDIENTE | Clientes asignados a lotes | - -### Integraciones Clave - -```yaml -# Ejemplo de flujo de datos -MAI-002 (Proyecto) 鈫 MAI-003 (Presupuesto Maestro) -MAI-002 (Prototipo) 鈫 MAI-003 (Presupuesto por Prototipo) -MAI-002 (Lote) 鈫 MAI-010 (Cliente/Derechohabiente) -MAI-002 (Vivienda) 鈫 MAI-005 (Avance Fisico) -MAI-002 (Equipo) 鈫 MAI-007 (Asistencias Cuadrillas) -``` - ---- - -## Diagrama de Arquitectura - -### Vista Jerarquica de Datos - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 CONSTRUCTORA 鈹 -鈹 (Multi-tenant: RLS) 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 - 鈻 -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 PROYECTO 鈹 -鈹 Tipo: Fraccionamiento | Conjunto | Torre | Mixto 鈹 -鈹 Estado: Licitacion 鈫 Adjudicado 鈫 Ejecucion 鈫 Entregado 鈹 -鈹 Codigo: PROJ-2025-001 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 鈹 鈹 - 鈻 鈻 鈻 - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 ETAPA 1 鈹 鈹 ETAPA 2 鈹 鈹 PROTOTIPO 鈹 - 鈹 (Fase del 鈹 鈹 (Fase del 鈹 鈹 (Modelo de 鈹 - 鈹 proyecto) 鈹 鈹 proyecto) 鈹 鈹 vivienda) 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 鈹 鈹 - 鈻 鈻 鈹 (asignado a) - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 - 鈹 MANZANA A 鈹 鈹 MANZANA B 鈹 鈹 - 鈹 (Solo en 鈹 鈹 (Solo en 鈹 鈹 - 鈹 Fraccionam.) 鈹 鈹 Fraccionam.) 鈹 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹 鈹 - 鈹 鈹 鈹 - 鈻 鈻 鈹 - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 - 鈹 LOTE 1 鈹 鈹 LOTE 2 鈹傗梽鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 (Terreno 鈹 鈹 (Terreno 鈹 - 鈹 individual) 鈹 鈹 individual) 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹 - 鈹 鈹 - 鈻 鈻 - 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - 鈹 VIVIENDA 鈹 鈹 VIVIENDA 鈹 - 鈹 (Casa/Depto 鈹 鈹 (Casa/Depto 鈹 - 鈹 construido) 鈹 鈹 construido) 鈹 - 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -### Vista de Modulos Backend - -``` -apps/backend/src/modules/projects/ -鈹溾攢鈹 entities/ -鈹 鈹溾攢鈹 project.entity.ts (Proyecto principal) -鈹 鈹溾攢鈹 stage.entity.ts (Etapa del proyecto) -鈹 鈹溾攢鈹 block.entity.ts (Manzana) -鈹 鈹溾攢鈹 lot.entity.ts (Lote/Terreno) -鈹 鈹溾攢鈹 housing-unit.entity.ts (Vivienda construida) -鈹 鈹溾攢鈹 housing-prototype.entity.ts (Prototipo) -鈹 鈹溾攢鈹 team-assignment.entity.ts (Equipo) -鈹 鈹溾攢鈹 milestone.entity.ts (Hitos) -鈹 鈹斺攢鈹 critical-date.entity.ts (Fechas criticas) -鈹溾攢鈹 dto/ -鈹 鈹溾攢鈹 create-project.dto.ts -鈹 鈹溾攢鈹 update-project.dto.ts -鈹 鈹溾攢鈹 bulk-lot-creation.dto.ts -鈹 鈹斺攢鈹 ... -鈹溾攢鈹 services/ -鈹 鈹溾攢鈹 projects.service.ts (CRUD + metricas) -鈹 鈹溾攢鈹 stages.service.ts (Jerarquia) -鈹 鈹溾攢鈹 lots.service.ts (Creacion masiva) -鈹 鈹溾攢鈹 housing-units.service.ts (Avances) -鈹 鈹溾攢鈹 prototypes.service.ts (Versionado) -鈹 鈹溾攢鈹 team.service.ts (Workload) -鈹 鈹溾攢鈹 milestones.service.ts (Dependencias) -鈹 鈹斺攢鈹 alerts.service.ts (Cron jobs) -鈹溾攢鈹 controllers/ -鈹 鈹溾攢鈹 projects.controller.ts -鈹 鈹溾攢鈹 stages.controller.ts -鈹 鈹溾攢鈹 prototypes.controller.ts -鈹 鈹斺攢鈹 ... -鈹斺攢鈹 projects.module.ts -``` - -### Vista de Base de Datos - -```sql --- Schema: projects -CREATE SCHEMA IF NOT EXISTS projects; - --- Tablas principales (11 tablas) -projects.projects -- Proyectos -projects.stages -- Etapas -projects.blocks -- Manzanas -projects.lots -- Lotes -projects.housing_units -- Viviendas -projects.housing_prototypes -- Prototipos -projects.project_team_assignments -- Equipo -projects.project_milestones -- Hitos -projects.critical_dates -- Fechas criticas -projects.construction_phases -- Fases constructivas -projects.project_documents -- Documentos - --- ENUMs -CREATE TYPE project_type AS ENUM ( - 'fraccionamiento_horizontal', - 'conjunto_habitacional', - 'edificio_vertical', - 'mixto' -); - -CREATE TYPE project_status AS ENUM ( - 'licitacion', - 'adjudicado', - 'ejecucion', - 'entregado', - 'cerrado' -); - --- Row Level Security (RLS) -ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY; - -CREATE POLICY projects_tenant_isolation ON projects.projects - FOR ALL - USING (constructora_id = current_setting('app.current_constructora_id')::UUID); -``` - ---- - -## Stack Tecnologico - -### Backend -- **Framework:** NestJS 10.x (Node.js + TypeScript) -- **ORM:** TypeORM 0.3.x -- **Base de Datos:** PostgreSQL 15.x -- **Autenticacion:** JWT (passport-jwt) -- **Validacion:** class-validator + class-transformer -- **Documentacion API:** Swagger/OpenAPI 3.0 -- **Testing:** Jest + Supertest -- **Cron Jobs:** @nestjs/schedule (para alertas) - -### Frontend -- **Framework:** React 18.x + TypeScript -- **State Management:** Zustand 4.x -- **Forms:** React Hook Form + Zod -- **UI Components:** shadcn/ui + Tailwind CSS -- **Data Fetching:** TanStack Query (React Query) -- **Routing:** React Router v6 -- **Testing:** Vitest + React Testing Library -- **Visualizacion:** Recharts (graficas), react-big-calendar - -### Base de Datos -- **RDBMS:** PostgreSQL 15.x -- **Features:** - - Row Level Security (RLS) para multi-tenancy - - Triggers para calculos automaticos - - Funciones SQL para workload y metricas - - Indices GIN para busqueda full-text - - Particionamiento por constructora (futuro) - -### DevOps -- **Containerizacion:** Docker + Docker Compose -- **CI/CD:** GitHub Actions -- **Migraciones:** TypeORM migrations -- **Monitoreo:** Prometheus + Grafana -- **Logs:** Winston + ELK Stack - ---- - -## Metricas del Modulo - -| Metrica | Valor | -|---------|-------| -| **Story Points** | 45 SP | -| **Requerimientos Funcionales** | 4 RFs | -| **Especificaciones Tecnicas** | 4 ETs | -| **Historias de Usuario** | 9 USs | -| **Tablas de Base de Datos** | 11 tablas | -| **Entities TypeORM** | 8 entities | -| **Services NestJS** | 8+ services | -| **Controllers RESTful** | 6+ controllers | -| **Componentes React** | 20+ componentes | -| **Endpoints API** | 40+ endpoints | -| **Documentacion Total** | ~360 KB | -| **Reutilizacion Core ERP** | 40% | -| **Tiempo Estimado Desarrollo** | 8 semanas (4 sprints) | - ---- - -## Configuracion SaaS Multi-tenant - -### Activacion del Modulo - -MAI-002 es un **modulo core** incluido en los 3 planes de suscripcion: - -| Plan | Modulo MAI-002 | Limites | -|------|----------------|---------| -| **Basico** | Incluido | 5 proyectos activos simultaneos | -| **Profesional** | Incluido | 15 proyectos activos simultaneos | -| **Enterprise** | Incluido | Proyectos ilimitados | - -**Activacion automatica:** Este modulo se activa durante el onboarding de un nuevo tenant (constructora). - -### Provisioning Automatico - -Durante el onboarding, el sistema ejecuta: - -```sql --- 1. Activar modulo MAI-002 -INSERT INTO constructoras.constructora_modules ( - constructora_id, module_code, is_active, plan_included -) VALUES ( - $constructora_id, 'MAI-002', true, true -); - --- 2. Crear prototipos seed (3 prototipos predefinidos) -INSERT INTO projects.housing_prototypes ( - constructora_id, code, name, category, segment, ... -) VALUES - ($constructora_id, 'CASA-SEED-001', 'Casa Tipo A', 'unifamiliar', 'interes_social', ...), - ($constructora_id, 'CASA-SEED-002', 'Casa Tipo B', 'unifamiliar', 'interes_medio', ...), - ($constructora_id, 'DEPTO-SEED-001', 'Departamento Tipo A', 'departamento', 'interes_social', ...); - --- 3. Configurar limites por plan -INSERT INTO constructoras.constructora_limits ( - constructora_id, limit_key, limit_value -) VALUES - ($constructora_id, 'max_active_projects', 15); -- Plan Profesional -``` - -### Aislamiento de Datos (RLS) - -Todas las tablas del modulo MAI-002 estan protegidas con Row-Level Security (RLS): - -```sql --- Configuracion de contexto por sesion -SET app.current_constructora_id = 'uuid-de-constructora'; - --- Toda query SELECT filtra automaticamente por constructora -SELECT * FROM projects.projects; --- Retorna solo proyectos de la constructora actual -``` - -**Politicas RLS completas:** Ver archivos `implementacion/ET-PROJ-*-rls-policies.sql` - ---- - -## Documentacion Adicional - -### Resumen Ejecutivo -- [RESUMEN-EPICA-MAI-002.md](./RESUMEN-EPICA-MAI-002.md) - Resumen completo de la epica con estado de completitud - -### Implementacion -- [implementacion/](./implementacion/) - Codigo fuente, migraciones, tests - - Scripts SQL de creacion de tablas - - Politicas RLS por tabla - - Funciones SQL para calculos - - Triggers automaticos - - Seeds de datos iniciales - -### Documentacion de Referencia -- [Guia de Uso de Referencias Odoo](/home/isem/workspace/projects/erp-suite/apps/verticales/construccion/docs/GUIA-USO-REFERENCIAS-ODOO.md) -- [Mapeo MAI to MGN](/home/isem/workspace/projects/erp-suite/apps/verticales/construccion/docs/01-analisis-referencias/MAPEO-MAI-TO-MGN.md) -- [Analisis de Reutilizacion GAMILIT](../ANALISIS-REUTILIZACION-GAMILIT.md) - -### Roadmap -- **Fase 1 (Semanas 1-2):** Implementacion de RF-PROJ-001 y RF-PROJ-002 (Catalogo y Jerarquia) -- **Fase 2 (Semanas 3-4):** Implementacion de RF-PROJ-003 (Prototipos) -- **Fase 3 (Semanas 5-6):** Implementacion de RF-PROJ-004 (Equipo y Calendario) -- **Fase 4 (Semanas 7-8):** Testing, refinamiento y deployment - ---- - -## Contacto y Soporte - -**Equipo responsable:** -- Tech Lead: @tech-lead -- Backend Team: @backend-team -- Frontend Team: @frontend-team -- Database Team: @database-team - -**Documentacion generada:** 2025-11-17 -**Estado del modulo:** Completo (documentacion 100%) -**Proximo paso:** Iniciar implementacion (Sprint 3) - ---- - -## Licencia - -Copyright (c) 2025 - ERP Suite Construccion -Todos los derechos reservados. diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/RESUMEN-EPICA-MAI-002.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/RESUMEN-EPICA-MAI-002.md deleted file mode 100644 index 0e4ee2b87..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/RESUMEN-EPICA-MAI-002.md +++ /dev/null @@ -1,832 +0,0 @@ -# Resumen Ejecutivo: MAI-002 - Proyectos y Estructura de Obra - -**Fecha de generaci贸n:** 2025-11-17 -**Estado:** 鉁 COMPLETO (100%) -**Story Points:** 45 SP -**Prioridad:** P0 (Cr铆tica) - ---- - -## 馃搳 Estado de Completitud - -| Tipo de Documento | Completados | Total | % | -|-------------------|-------------|-------|---| -| Requerimientos Funcionales (RF) | 4 | 4 | 100% 鉁 | -| Especificaciones T茅cnicas (ET) | 4 | 4 | 100% 鉁 | -| Historias de Usuario (US) | 9 | 9 | 100% 鉁 | -| **Total Documentos** | **17** | **17** | **100%** 鉁 | - -**Tama帽o total generado:** ~230 KB - ---- - -## 鉁 Documentos Completados - -### Requerimientos Funcionales (4/4 - 100%) - -#### 1. RF-PROJ-001: Cat谩logo de Proyectos (~25 KB) -**Contenido clave:** -- 4 tipos de proyectos: Fraccionamiento Horizontal, Conjunto Habitacional, Edificio Vertical, Mixto -- 5 estados: Licitaci贸n 鈫 Adjudicado 鈫 Ejecuci贸n 鈫 Entregado 鈫 Cerrado -- Datos completos: ubicaci贸n, cliente, contrato, fechas, permisos -- M茅tricas: f铆sicas, financieras, temporales, recursos -- 4 casos de uso detallados -- Validaciones de negocio y t茅cnicas -- Permisos por rol - -#### 2. RF-PROJ-002: Estructura Jer谩rquica de Obra (~28 KB) -**Contenido clave:** -- Jerarqu铆a de 5 niveles: Proyecto 鈫 Etapa 鈫 Manzana 鈫 Lote 鈫 Vivienda -- Estructuras por tipo: Fraccionamiento (con manzanas), Conjunto (sin manzanas), Torre vertical (niveles) -- Estados de lote: Disponible 鈫 Vendido 鈫 En construcci贸n 鈫 Terminado 鈫 Entregado -- Avance f铆sico por vivienda (cimentaci贸n, estructura, muros, instalaciones, acabados) -- 3 casos de uso: Crear estructura, Crear torre, Cambiar estado -- Reportes: 脕rbol jer谩rquico, Resumen por etapa, Listado por estado - -#### 3. RF-PROJ-003: Prototipos de Vivienda (~26 KB) -**Contenido clave:** -- 3 tipos principales: Casa Unifamiliar, Departamento, D煤plex/Tr铆plex -- Segmentos: Inter茅s social, Inter茅s medio, Residencial medio/alto, Premium -- Datos: 脕reas, distribuci贸n, caracter铆sticas constructivas, acabados, costos -- Versionado de prototipos (v1, v2, etc.) -- Asignaci贸n a lotes en masa -- Herencia de caracter铆sticas a viviendas -- Cat谩logo por constructora - -#### 4. RF-PROJ-004: Asignaci贸n de Equipo y Calendario (~25 KB) -**Contenido clave:** -- 5 roles de equipo: Director, Residente, Ingeniero, Supervisor, Gerente de Compras -- Reglas de asignaci贸n: Director 煤nico, Residente principal, L铆mites de carga -- Hitos del proyecto (milestones): 11 tipos desde arranque hasta cierre -- Fases constructivas: 9 fases (preliminares, cimentaci贸n, estructura, etc.) -- Fechas cr铆ticas con alertas autom谩ticas -- Workload management (% de dedicaci贸n) -- Dashboard de equipo y calendario - -### Especificaciones T茅cnicas (4/4 - 100%) - -#### 1. ET-PROJ-001: Implementaci贸n de Cat谩logo de Proyectos (~20 KB) -**Contenido t茅cnico:** -- **Entity:** `Project` con 45+ columnas (TypeORM) -- **ENUMs:** ProjectType, ProjectStatus, ClientType, ContractType -- **Service:** `ProjectsService` con 8 m茅todos: - - create, findAll (con filtros), findOne, update, changeStatus - - calculateMetrics, generateProjectCode, validateStatusTransition -- **Controller:** RESTful con 6 endpoints -- **Frontend:** - - `ProjectForm` component (React Hook Form + Zod) - - `ProjectCard` component con badges de estado - - Validaciones completas -- **Tests:** Generaci贸n de c贸digos, transiciones de estado -- **Features:** - - C贸digo auto-generado: PROJ-2025-001 - - C谩lculo autom谩tico de scheduledEndDate - - Event emitters para status changes - - M茅tricas calculadas (f铆sico, financiero, temporal) - -#### 2. ET-PROJ-002: Implementaci贸n de Estructura Jer谩rquica (~48 KB) -**Contenido t茅cnico:** -- **Entities:** Stage, Block, Lot, HousingUnit (4 entidades completas) -- **ENUMs:** StageStatus, BlockStatus, LotStatus, ConstructionStatus, LotShape, LotOrientation -- **Services:** - - `StagesService`: create, findAll, getTreeStructure (recursivo), changeStatus - - `LotsService`: create, bulkCreate (hasta 500 lotes), assignPrototype, bulkAssignPrototype - - `HousingUnitsService`: create con herencia de prototipos, updateProgress con c谩lculo ponderado -- **Controllers:** 3 controllers RESTful con 20+ endpoints -- **Frontend:** - - `StructureTreeView` component con navegaci贸n jer谩rquica - - `BulkLotCreationForm` para creaci贸n masiva - - `HousingUnitProgressCard` con sliders por etapa constructiva -- **Database:** - - Triggers autom谩ticos para contadores (totalLots, totalBlocks, totalHousingUnits) - - Funciones SQL para c谩lculo de avances -- **Features:** - - Soporte de 3 estructuras: Fraccionamiento (con manzanas), Conjunto (sin manzanas), Torre vertical - - 脕rbol jer谩rquico recursivo con relaciones OneToMany - - Creaci贸n masiva de lotes con c贸digo secuencial - - Asignaci贸n de prototipos individual y en masa - - C谩lculo autom谩tico de avance f铆sico ponderado - -#### 3. ET-PROJ-003: Implementaci贸n de Prototipos (~45 KB) -**Contenido t茅cnico:** -- **Entity:** `HousingPrototype` con 50+ columnas -- **ENUMs:** PrototypeCategory, PrototypeSegment, PrototypeStatus, KitchenType -- **Service:** `HousingPrototypesService` con 10 m茅todos: - - create, createVersion (versionado), findAll, getCatalog, getVersionHistory - - deprecate, incrementUsageCount, cloneForHousingUnit -- **Controller:** RESTful con 8 endpoints -- **Frontend:** - - `PrototypeGallery` component con filtros por categor铆a y segmento - - `PrototypeForm` component con 60+ campos - - Visualizaci贸n de costos y caracter铆sticas -- **Database:** - - Triggers autom谩ticos para c谩lculo de totalBuiltArea y totalTurnkeyCost - - 脥ndices GIN para b煤squeda por tags -- **Features:** - - Versionado autom谩tico (v1, v2, v3...) - - Depreciaci贸n de versiones anteriores - - Cat谩logo agrupado por categor铆a o segmento - - C贸digo auto-generado: CASA-2025-001, DEPTO-2025-001 - - Herencia de caracter铆sticas a viviendas (snapshot) - - Control de usageCount para prevenir eliminaciones - -#### 4. ET-PROJ-004: Implementaci贸n de Equipo y Calendario (~51 KB) -**Contenido t茅cnico:** -- **Entities:** ProjectTeamAssignment, Milestone, CriticalDate, ConstructionPhase -- **ENUMs:** ProjectRole, Specialty, MilestoneType, MilestoneStatus, CommitmentType, CriticalDateStatus -- **Services:** - - `TeamAssignmentsService`: create con validaci贸n de workload, getUserTotalWorkload, getTeamDashboard - - `MilestonesService`: create, markComplete con validaci贸n de dependencias, getTimeline - - `CriticalDatesService`: create, updateStatus, sendAlerts - - `AlertsService`: Cron jobs para alertas autom谩ticas (diario a las 9:00 AM) -- **Controllers:** 3 controllers RESTful con 18+ endpoints -- **Frontend:** - - `TeamRoster` component con visualizaci贸n de workload por rol - - `MilestoneTimeline` component con l铆nea de tiempo visual - - `CriticalDatesCalendar` component con alertas -- **Database:** - - Funciones SQL: get_user_total_workload(), get_role_workload_limit() - - L铆mites por rol: Director 500%, Residente 200%, Ingeniero 800% -- **Features:** - - Validaci贸n de l铆mites de workload por rol - - Solo un Director principal por proyecto - - Milestones con dependencias (graph validation) - - Alertas autom谩ticas con cron jobs - - Sistema de notificaciones por correo/webhook - - Dashboard de disponibilidad de equipo - ---- - -## 馃搵 Documentos Pendientes - -### Especificaciones T茅cnicas (0 pendientes - 100% 鉁) - -Todas las especificaciones t茅cnicas han sido completadas. - -### Historias de Usuario (9/9 - 100%) - -#### 1. US-PROJ-001: Cat谩logo de Proyectos (8 SP) - ~17 KB -**Contenido clave:** -- CRUD completo de proyectos con formulario de 6 secciones -- Filtros por tipo, estado, cliente, a帽o + b煤squeda de texto libre -- Vista de detalle con 5 tabs: General, M茅tricas, Estructura, Equipo, Calendario -- Permisos por rol (Director/Admin: crear/editar, todos: ver) -- Eliminaci贸n con confirmaci贸n "ELIMINAR" -- C贸digo auto-generado secuencial por a帽o (PROJ-2025-001) -- 8 criterios de aceptaci贸n detallados -- 5 escenarios de prueba - -#### 2. US-PROJ-002: Transiciones de Estado (5 SP) - ~15 KB -**Contenido clave:** -- Flujo validado: Licitaci贸n 鈫 Adjudicado 鈫 Ejecuci贸n 鈫 Entregado 鈫 Cerrado -- Checklist de condiciones por transici贸n -- Actualizaci贸n autom谩tica de fechas (awardDate, actualStartDate, deliveryDate, closureDate) -- Timeline de historial con auditor铆a completa -- Notificaciones a equipo y cliente -- Solo Director/Admin pueden cambiar estados -- Regresi贸n solo para Admin con justificaci贸n -- 5 escenarios de prueba - -#### 3. US-PROJ-003: Crear Estructura de Fraccionamiento (8 SP) - ~11 KB -**Contenido clave:** -- Wizard de 4 pasos: Etapas 鈫 Manzanas 鈫 Lotes 鈫 Resumen -- Creaci贸n masiva de lotes (hasta 500 por operaci贸n) -- Vista de 谩rbol jer谩rquico con expandir/colapsar -- C贸digos 煤nicos validados (Etapa, Manzana, Lote) -- Calculadora de 谩reas y lotes -- Edici贸n y eliminaci贸n con validaci贸n de dependencias -- Transacci贸n at贸mica (todo o nada) -- Performance: 500 lotes en <3 segundos - -#### 4. US-PROJ-004: Crear Estructura de Torre Vertical (6 SP) - ~7 KB -**Contenido clave:** -- Wizard adaptado para edificios verticales -- Terminolog铆a: Torre 鈫 Niveles 鈫 Departamentos -- C贸digos departamento: DEPTO-101, DEPTO-201 (piso-n煤mero) -- Plantilla de niveles repetitivos (N niveles iguales) -- Orientaci贸n de departamentos (Norte/Sur/Este/Oeste) -- TreeView adaptado para torres -- Creaci贸n masiva por nivel - -#### 5. US-PROJ-005: Gesti贸n de Prototipos (5 SP) - ~9 KB -**Contenido clave:** -- Cat谩logo con galer铆a visual (filtros por categor铆a, segmento, precio) -- Formulario de 6 secciones: B谩sica, Caracter铆sticas, 脕reas, Distribuci贸n, Acabados, Costos -- Versionado autom谩tico (v1, v2, v3...) -- Depreciaci贸n de versiones anteriores -- Auto-c贸digo: CASA-2025-001, DEPTO-2025-001 -- Control de usageCount (no eliminar si >0) -- Historial de versiones con cambios documentados - -#### 6. US-PROJ-006: Asignaci贸n de Prototipos a Lotes (3 SP) - ~8 KB -**Contenido clave:** -- Asignaci贸n individual desde modal con cat谩logo -- Asignaci贸n en masa (hasta 500 lotes) -- Validaci贸n de 谩rea requerida vs disponible -- No permitir si lote tiene vivienda construida -- Incremento/decremento de usageCount -- Reasignaci贸n con confirmaci贸n -- Filtros en TreeView: "Sin prototipo", "Con prototipo X" -- Snapshot de versi贸n al momento de asignaci贸n - -#### 7. US-PROJ-007: Asignaci贸n de Equipo (4 SP) - ~9 KB -**Contenido clave:** -- Dashboard de equipo por roles: Director, Residentes, Ingenieros, Supervisores -- Validaci贸n de workload por rol (Director: 500%, Residente: 200%, Ingeniero: 800%) -- Solo un Director/Residente principal por proyecto -- C谩lculo en tiempo real de carga total -- Indicadores visuales: 馃煝 Verde (0-70%), 馃煛 Amarillo (70-90%), 馃敶 Rojo (90-100%) -- Edici贸n de dedicaci贸n y responsabilidades -- Desactivaci贸n de asignaciones con fecha fin -- Funci贸n SQL: get_user_total_workload() - -#### 8. US-PROJ-008: Calendario de Hitos (3 SP) - ~8 KB -**Contenido clave:** -- Timeline visual con 11 tipos de hitos predefinidos -- Estados: Completado, En progreso, Pr贸ximo, Pendiente, Retrasado -- Validaci贸n de dependencias entre hitos -- Marcar como completado con fecha real y notas -- C谩lculo de d铆as de retraso/adelanto -- Alertas autom谩ticas configurables (7 d铆as antes por defecto) -- Cron job diario a las 9:00 AM -- Notificaciones por email e in-app - -#### 9. US-PROJ-009: Alertas de Fechas Cr铆ticas (3 SP) - ~8 KB -**Contenido clave:** -- Registro de fechas contractuales, regulatorias, financieras -- Marcado de fechas inamovibles con consecuencias si se incumplen -- Sistema de alertas m煤ltiples: 30, 15, 7, 3, 2, 1 d铆a(s) antes -- Widget en dashboard: "鈿狅笍 Fechas Cr铆ticas Pr贸ximas" -- Colores: 馃敶 <7 d铆as, 馃煛 7-30 d铆as, 馃煝 >30 d铆as -- Marcar como cumplida o incumplida con notas -- Cron job diario para env铆o de alertas -- Email template completo con botones de acci贸n - -**Total USs:** 45 SP | ~90 KB de documentaci贸n - ---- - -## 馃搵 Documentos Pendientes - -**Ninguno - 脡pica 100% Completa** 鉁 - ---- - -## 馃彈锔 Arquitectura T茅cnica - -### Base de Datos (PostgreSQL) - -```sql --- Schema: projects -CREATE SCHEMA IF NOT EXISTS projects; - --- Tablas principales -projects.projects (proyecto) -projects.stages (etapas) -projects.blocks (manzanas) -projects.lots (lotes) -projects.housing_units (viviendas) -projects.housing_prototypes (prototipos) -projects.project_team_assignments (equipo) -projects.project_milestones (hitos) -projects.critical_dates (fechas cr铆ticas) -projects.construction_phases (fases) -projects.project_documents (documentos) - --- Total: 11 tablas -``` - -### Backend (NestJS) - -``` -apps/backend/src/modules/projects/ -鈹溾攢鈹 entities/ -鈹 鈹溾攢鈹 project.entity.ts -鈹 鈹溾攢鈹 stage.entity.ts -鈹 鈹溾攢鈹 block.entity.ts -鈹 鈹溾攢鈹 lot.entity.ts -鈹 鈹溾攢鈹 housing-unit.entity.ts -鈹 鈹溾攢鈹 housing-prototype.entity.ts -鈹 鈹溾攢鈹 team-assignment.entity.ts -鈹 鈹斺攢鈹 milestone.entity.ts -鈹溾攢鈹 dto/ -鈹 鈹溾攢鈹 create-project.dto.ts -鈹 鈹溾攢鈹 update-project.dto.ts -鈹 鈹斺攢鈹 ... -鈹溾攢鈹 services/ -鈹 鈹溾攢鈹 projects.service.ts -鈹 鈹溾攢鈹 stages.service.ts -鈹 鈹溾攢鈹 housing-units.service.ts -鈹 鈹溾攢鈹 prototypes.service.ts -鈹 鈹斺攢鈹 team.service.ts -鈹溾攢鈹 controllers/ -鈹 鈹溾攢鈹 projects.controller.ts -鈹 鈹溾攢鈹 stages.controller.ts -鈹 鈹斺攢鈹 ... -鈹斺攢鈹 projects.module.ts -``` - -### Frontend (React + TypeScript) - -``` -apps/frontend/src/features/projects/ -鈹溾攢鈹 pages/ -鈹 鈹溾攢鈹 ProjectsListPage.tsx -鈹 鈹溾攢鈹 ProjectDetailPage.tsx -鈹 鈹溾攢鈹 CreateProjectPage.tsx -鈹 鈹斺攢鈹 PrototypesPage.tsx -鈹溾攢鈹 components/ -鈹 鈹溾攢鈹 ProjectForm.tsx -鈹 鈹溾攢鈹 ProjectCard.tsx -鈹 鈹溾攢鈹 StructureTreeView.tsx -鈹 鈹溾攢鈹 HousingUnitCard.tsx -鈹 鈹溾攢鈹 PrototypeBuilder.tsx -鈹 鈹溾攢鈹 TeamRoster.tsx -鈹 鈹斺攢鈹 MilestoneTimeline.tsx -鈹溾攢鈹 stores/ -鈹 鈹斺攢鈹 projectStore.ts -鈹斺攢鈹 services/ - 鈹斺攢鈹 projects.api.ts -``` - ---- - -## 馃搱 Caracter铆sticas Clave Implementadas - -### 1. Multi-Proyecto -鉁 Una constructora gestiona m煤ltiples proyectos simult谩neos -鉁 Aislamiento por `constructoraId` (RLS) -鉁 C贸digo 煤nico auto-generado por a帽o - -### 2. Jerarqu铆a Flexible -鉁 Soporta fraccionamientos (con manzanas) -鉁 Soporta conjuntos (sin manzanas) -鉁 Soporta torres verticales (niveles) -鉁 Navegaci贸n de 谩rbol recursivo - -### 3. Gesti贸n de Equipo -鉁 M煤ltiples residentes por proyecto -鉁 Ingenieros compartidos (workload distribuido) -鉁 Validaci贸n de l铆mites de carga -鉁 Historial de asignaciones - -### 4. Calendario Inteligente -鉁 Hitos con dependencias -鉁 Fases constructivas -鉁 Fechas cr铆ticas con alertas -鉁 C谩lculo de desviaciones temporales - ---- - -## 鈿欙笍 Configuraci贸n SaaS Multi-tenant - -### Activaci贸n del M贸dulo - -MAI-002 es un **m贸dulo core** incluido en los 3 planes de suscripci贸n: - -| Plan | M贸dulo MAI-002 | L铆mites | -|------|----------------|---------| -| **B谩sico** | 鉁 Incluido | 5 proyectos activos simult谩neos | -| **Profesional** | 鉁 Incluido | 15 proyectos activos simult谩neos | -| **Enterprise** | 鉁 Incluido | Proyectos ilimitados | - -**Activaci贸n autom谩tica:** Este m贸dulo se activa autom谩ticamente durante el onboarding de un nuevo tenant (constructora). - -### Portal de Administraci贸n SaaS - -#### Para Super Admin (Equipo Interno) - -**Panel de gesti贸n del m贸dulo:** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 M贸dulo MAI-002: Proyectos y Estructura 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃搳 Uso Global (Todos los Tenants) 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Tenants con m贸dulo activo: 234/234 (100%) 鈹 -鈹 Proyectos totales creados: 2,847 鈹 -鈹 Viviendas gestionadas: 128,456 鈹 -鈹 Proyectos activos: 1,234 鈹 -鈹 鈹 -鈹 馃敡 Feature Flags 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈽戯笍 projects.bulk_lot_creation (ENABLED) 鈹 -鈹 Permite creaci贸n masiva de hasta 500 lotes 鈹 -鈹 鈹 -鈹 鈽戯笍 projects.housing_prototypes (ENABLED) 鈹 -鈹 Cat谩logo de prototipos de vivienda 鈹 -鈹 鈹 -鈹 鈽戯笍 projects.team_workload_validation (ENABLED) 鈹 -鈹 Validaci贸n de l铆mites de carga por rol 鈹 -鈹 鈹 -鈹 鈽 projects.ai_schedule_optimization (BETA) 鈹 -鈹 Optimizaci贸n de calendario con IA (Beta) 鈹 -鈹 鈹 -鈹 馃搱 M茅tricas de Rendimiento 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 API response time (p95): 145 ms 鉁 鈹 -鈹 Database query time (p95): 78 ms 鉁 鈹 -鈹 Bulk lot creation (500 lotes): 2.3 s 鉁 鈹 -鈹 TreeView render (200 nodos): 1.2 s 鉁 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -#### Para Tenant Admin (Cliente/Constructora) - -**Panel de configuraci贸n del m贸dulo:** -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 Configuraci贸n: M贸dulo de Proyectos 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈹 -鈹 馃搳 Uso Actual (Constructora ABC) 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Plan: Profesional ($799/mes) 鈹 -鈹 Proyectos activos: 8 / 15 (53%) 鈹 -鈹 Viviendas gestionadas: 342 鈹 -鈹 Usuarios usando m贸dulo: 12 / 25 鈹 -鈹 鈹 -鈹 鈿欙笍 Configuraciones Personalizadas 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 鈽戯笍 Generar c贸digo de proyecto autom谩ticamente 鈹 -鈹 Formato: PROJ-{a帽o}-{secuencial} 鈹 -鈹 鈹 -鈹 鈽戯笍 Requerir aprobaci贸n para eliminar proyectos 鈹 -鈹 Solo Director/Admin pueden eliminar 鈹 -鈹 鈹 -鈹 鈽 Notificar equipo en cambios de estado 鈹 -鈹 Email + In-app notification 鈹 -鈹 鈹 -鈹 鈽戯笍 Validar l铆mites de workload 鈹 -鈹 Director: 500%, Resident: 200%, Engineer: 800% 鈹 -鈹 鈹 -鈹 馃搨 Cat谩logos Personalizados 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Prototipos de vivienda: 15 activos 鈹 -鈹 Plantillas de milestones: 3 plantillas 鈹 -鈹 鈹 -鈹 [Gestionar Prototipos] [Configurar Workflows] 鈹 -鈹 鈹 -鈹 鈿狅笍 L铆mites del Plan 鈹 -鈹 鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 鈹 -鈹 Est谩s usando 8 de 15 proyectos permitidos. 鈹 -鈹 驴Necesitas m谩s proyectos? 鈹 -鈹 [Actualizar a Plan Enterprise] (100 proyectos) 鈹 -鈹 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - -### Provisioning Autom谩tico - -Durante el onboarding de un nuevo tenant (constructora), el sistema ejecuta: - -```bash -# 1. Validar que tenant existe -SELECT id FROM constructoras.constructoras WHERE id = $constructora_id; - -# 2. Activar m贸dulo MAI-002 -INSERT INTO constructoras.constructora_modules ( - constructora_id, - module_code, - is_active, - plan_included -) VALUES ( - $constructora_id, - 'MAI-002', - true, - true -- Incluido en todos los planes -); - -# 3. Crear cat谩logos seed (prototipos predefinidos) -INSERT INTO projects.housing_prototypes ( - constructora_id, - code, - name, - category, - segment, - ... -) VALUES - ($constructora_id, 'CASA-SEED-001', 'Casa Tipo A', 'unifamiliar', 'interes_social', ...), - ($constructora_id, 'CASA-SEED-002', 'Casa Tipo B', 'unifamiliar', 'interes_medio', ...), - ($constructora_id, 'DEPTO-SEED-001', 'Departamento Tipo A', 'departamento', 'interes_social', ...); - -# 4. Configurar feature flags por plan -INSERT INTO constructoras.constructora_feature_flags ( - constructora_id, - flag_key, - is_enabled -) VALUES - ($constructora_id, 'projects.bulk_lot_creation', true), - ($constructora_id, 'projects.housing_prototypes', true), - ($constructora_id, 'projects.team_workload_validation', true); - -# 5. Configurar l铆mites por plan -INSERT INTO constructoras.constructora_limits ( - constructora_id, - limit_key, - limit_value -) VALUES - ($constructora_id, 'max_active_projects', 15), -- Plan Profesional - ($constructora_id, 'max_housing_units', 5000), - ($constructora_id, 'max_team_assignments', 100); -``` - -### Aislamiento de Datos (RLS) - -**Garant铆a de seguridad multi-tenant:** - -Cada consulta a tablas del m贸dulo MAI-002 est谩 protegida por Row-Level Security (RLS): - -```sql --- Configuraci贸n de contexto por sesi贸n -SET app.current_constructora_id = 'uuid-de-constructora-abc'; -SET app.current_user_id = 'uuid-de-usuario'; -SET app.current_user_role = 'director'; - --- Toda query SELECT autom谩ticamente filtra por constructora -SELECT * FROM projects.projects; --- Internamente ejecuta: --- SELECT * FROM projects.projects --- WHERE constructora_id = current_setting('app.current_constructora_id')::UUID; - --- Imposible ver datos de otras constructoras -SELECT * FROM projects.projects WHERE constructora_id = 'otra-constructora-uuid'; --- Retorna: 0 rows (bloqueado por RLS policy) -``` - -**Pol铆ticas RLS completas:** Ver `implementacion/ET-PROJ-001-rls-policies.sql` y `ET-PROJ-002-rls-policies.sql` - -### Migraciones Multi-tenant - -Cuando se despliega una nueva versi贸n con cambios en schema: - -```typescript -// Migration ejemplo: Agregar columna nueva -export class AddProjectTypeToProjects1700000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - // Se ejecuta una sola vez, afecta a todos los tenants - await queryRunner.addColumn( - 'projects', - new TableColumn({ - name: 'project_subtype', - type: 'varchar', - length: '50', - isNullable: true, - }) - ); - - // Los datos de cada tenant est谩n aislados por constructora_id - // No hay riesgo de cross-contamination - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn('projects', 'project_subtype'); - } -} -``` - -**Proceso de deployment:** -1. **Pre-deployment checks**: Validar que migration no rompe RLS policies -2. **Staging deployment**: Ejecutar en tenant de prueba -3. **Production rollout**: Blue-green deployment sin downtime -4. **Post-deployment validation**: Verificar que RLS sigue activo - -### Monitoreo por Tenant - -**M茅tricas capturadas por constructora:** - -```javascript -// Ejemplo de evento de auditor铆a -{ - "event": "project.created", - "timestamp": "2025-11-17T10:30:00Z", - "constructora_id": "uuid-constructora-abc", - "user_id": "uuid-user-123", - "project_id": "uuid-project-456", - "metadata": { - "project_code": "PROJ-2025-015", - "project_type": "fraccionamiento_horizontal", - "contract_amount": 25000000 - } -} -``` - -**Dashboard de m茅tricas por tenant:** -- Proyectos creados por mes -- Viviendas gestionadas -- Usuarios activos en m贸dulo -- API calls por d铆a -- Errores y warnings - -### Upgrade de Plan - -Cuando un tenant actualiza su plan (ej: B谩sico 鈫 Profesional): - -```typescript -// Auto-desbloqueo de l铆mites -UPDATE constructoras.constructora_limits -SET limit_value = 15 -- Era 5 en plan B谩sico -WHERE constructora_id = $tenant_id - AND limit_key = 'max_active_projects'; - -// Activar features adicionales (si aplica) -UPDATE constructoras.constructora_feature_flags -SET is_enabled = true -WHERE constructora_id = $tenant_id - AND flag_key = 'projects.ai_schedule_optimization'; - -// Audit log -INSERT INTO audit.plan_changes ( - constructora_id, - old_plan, - new_plan, - changed_at, - changed_by -) VALUES ( - $tenant_id, - 'basico', - 'profesional', - NOW(), - $admin_user_id -); -``` - -**Efectos inmediatos:** -- 鉁 L铆mites actualizados (sin reinicio) -- 鉁 Features nuevos disponibles -- 鉁 Facturaci贸n ajustada pro-rata -- 鉁 Notificaci贸n al tenant admin - -### Soporte y Troubleshooting - -**Herramientas de soporte para equipo interno:** - -```sql --- Ver estado de m贸dulo para un tenant espec铆fico -SELECT - c.name AS constructora, - cm.is_active, - cm.activated_at, - c.plan, - cl.limit_value AS max_projects, - (SELECT COUNT(*) FROM projects.projects WHERE constructora_id = c.id) AS projects_count -FROM constructoras.constructoras c -JOIN constructoras.constructora_modules cm ON cm.constructora_id = c.id -LEFT JOIN constructoras.constructora_limits cl ON cl.constructora_id = c.id - AND cl.limit_key = 'max_active_projects' -WHERE c.subdomain = 'constructora-abc' - AND cm.module_code = 'MAI-002'; - --- Diagn贸stico de performance para un tenant -SELECT - constructora_id, - AVG(response_time_ms) AS avg_response_time, - MAX(response_time_ms) AS max_response_time, - COUNT(*) AS total_requests, - COUNT(*) FILTER (WHERE status_code >= 500) AS errors -FROM api_logs -WHERE module = 'MAI-002' - AND constructora_id = $tenant_id - AND created_at > NOW() - INTERVAL '24 hours' -GROUP BY constructora_id; -``` - ---- - -## 馃敆 Integraciones con Otras 脡picas - -| 脡pica | Relaci贸n | Campos Clave | -|-------|----------|--------------| -| MAI-001 (Fundamentos) | `constructoraId`, `userId` | Multi-tenancy, Auth | -| MAI-003 (Presupuestos) | `Project.contractAmount` | Presupuesto maestro por proyecto | -| MAI-004 (Compras) | `Project.id` | Requisiciones filtradas por proyecto | -| MAI-005 (Control de Obra) | `HousingUnit.id` | Avances f铆sicos por vivienda | -| MAI-007 (RRHH) | `TeamAssignment` | Asistencias de cuadrillas en proyecto | - ---- - -## 馃搳 M茅tricas de Progreso - -| M茅trica | Planificado | Actual | % | -|---------|-------------|--------|---| -| **RFs** | 4 | 4 | 100% 鉁 | -| **ETs** | 4 | 4 | 100% 鉁 | -| **USs** | 9 | 9 | 100% 鉁 | -| **Story Points** | 45 SP | 45 SP | 100% 鉁 | -| **Tama帽o Documentaci贸n** | ~315 KB | ~230 KB | 73% | -| **Tiempo invertido** | - | ~8 horas | - | - ---- - -## 馃帀 MAI-002: 脡PICA COMPLETA - -### 鉁 Logros Alcanzados - -**Documentaci贸n Generada:** -- 鉁 4 Requerimientos Funcionales (~104 KB) -- 鉁 4 Especificaciones T茅cnicas (~164 KB) -- 鉁 9 Historias de Usuario (~92 KB) -- 鉁 1 Resumen Ejecutivo -- **Total:** 18 documentos | ~230 KB - -**Cobertura T茅cnica:** -- 鉁 11 tablas de base de datos especificadas -- 鉁 8 entities TypeORM completas -- 鉁 12+ services con l贸gica de negocio -- 鉁 15+ controllers RESTful -- 鉁 20+ componentes React -- 鉁 Validaciones Zod completas -- 鉁 Tests unitarios especificados -- 鉁 Cron jobs para alertas -- 鉁 Sistema de eventos completo - -**Story Points Cubiertos:** -- RFs: ~16 SP (estimado) -- ETs: ~24 SP (estimado) -- USs: 45 SP (exacto) -- **Total:** 45+ SP de trabajo especificado - -### 馃摝 Entregables Listos para Implementaci贸n - -**Backend (NestJS + PostgreSQL):** -- Schema completo de 11 tablas -- Migrations especificadas -- Entities, DTOs, Services, Controllers -- Validaciones de negocio -- Event emitters -- Triggers y funciones SQL -- Row Level Security (RLS) - -**Frontend (React + TypeScript):** -- 20+ componentes especificados -- Formularios con React Hook Form + Zod -- State management (Zustand) -- API service layer -- Mockups/wireframes -- Responsive design - -**Features Completas:** -- 鉁 Multi-proyecto con multi-tenancy -- 鉁 Jerarqu铆a flexible (horizontal/vertical/mixto) -- 鉁 Prototipos con versionado -- 鉁 Gesti贸n de equipo con workload -- 鉁 Calendario de hitos con dependencias -- 鉁 Alertas autom谩ticas - ---- - -## 馃幆 Pr贸ximos Pasos Recomendados - -### Opci贸n 1: Iniciar Implementaci贸n de MAI-002 猸 RECOMENDADO -**Raz贸n:** Documentaci贸n 100% completa, equipo puede empezar desarrollo inmediato - -**Sprint Planning:** -- Sprint 3: US-PROJ-001 + US-PROJ-002 (13 SP) -- Sprint 4: US-PROJ-003 + US-PROJ-004 (14 SP) -- Sprint 5: US-PROJ-005 + US-PROJ-006 (8 SP) -- Sprint 6: US-PROJ-007 + US-PROJ-008 + US-PROJ-009 (10 SP) - -**Estimaci贸n:** 4 sprints de 2 semanas = 8 semanas - -### Opci贸n 2: Continuar con Siguiente 脡pica -**Opciones:** -- MAI-003: Presupuestos y Control de Costos (50 SP) -- MAI-004: Compras e Inventarios (50 SP) -- MAI-005: Control de Obra y Avances (45 SP) - -**Beneficio:** Cobertura completa del sistema antes de implementar - -### Opci贸n 3: Enfoque Paralelo -- Equipo A: Implementa MAI-002 -- Equipo B: Documenta MAI-003 o MAI-005 -**Beneficio:** Velocidad m谩xima, trabajo paralelo - ---- - -## 馃摑 Lecciones de MAI-002 - -### Patrones T茅cnicos Exitosos -鉁 Jerarqu铆a recursiva (Proyecto 鈫 Etapa 鈫 Manzana 鈫 Lote 鈫 Vivienda) -鉁 ENUMs para estados y tipos (type-safe) -鉁 Event emitters para notificaciones -鉁 C谩lculo autom谩tico de fechas y m茅tricas -鉁 C贸digo auto-generado secuencial por a帽o - -### Complejidad Manejada -鉁 M煤ltiples tipos de proyectos (horizontal, vertical, mixto) -鉁 Transiciones de estado validadas -鉁 Gesti贸n de equipo con workload distribuido -鉁 Calendario con dependencias de hitos - -### Reutilizaci贸n de GAMILIT -- **Estructura jer谩rquica:** ~40% similar a Cursos 鈫 M贸dulos 鈫 Lecciones -- **Team assignments:** ~50% similar a Professor assignments -- **Prototypes:** Concepto nuevo, 0% reutilizaci贸n - ---- - -**Generado:** 2025-11-17 -**Autor:** Sistema de Documentaci贸n Autom谩tica -**Pr贸xima revisi贸n:** Al completar ETs y USs pendientes diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/_MAP.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/_MAP.md deleted file mode 100644 index 9d33608c5..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/_MAP.md +++ /dev/null @@ -1,547 +0,0 @@ -# _MAP: MAI-002 - Proyectos y Estructura - -**Epica:** MAI-002 -**Nombre:** Proyectos y Estructura de Obra -**Fase:** 2 - Core Business -**Story Points:** 45 SP -**Estado:** 100% Documentado | Pendiente de Implementacion -**Sprint:** Sprint 3-6 (Semanas 3-10) -**Ultima actualizacion:** 2025-12-06 - ---- - -## Proposito - -Sistema integral de gestion de proyectos de construccion inmobiliaria que permite: -- Gestion completa de proyectos (fraccionamientos, conjuntos habitacionales, edificios verticales) -- Estructura jerarquica flexible: Proyecto 鈫 Etapa 鈫 Manzana (opcional) 鈫 Lote 鈫 Vivienda -- Catalogo de prototipos de vivienda con versionado -- Asignacion de equipo con validacion de carga de trabajo -- Calendario de obra con hitos, fases y fechas criticas -- Sistema de alertas automaticas - -**Reutilizacion GAMILIT:** 40% de componentes de infraestructura - ---- - -## Estructura de Archivos - -``` -MAI-002-proyectos-estructura/ -鈹溾攢鈹 README.md # (Pendiente) Descripcion del modulo -鈹溾攢鈹 _MAP.md # Este archivo - Mapa de navegacion -鈹溾攢鈹 RESUMEN-EPICA-MAI-002.md # Resumen ejecutivo completo -鈹 -鈹溾攢鈹 requerimientos-funcionales/ # 4 Requerimientos Funcionales (100%) -鈹 鈹溾攢鈹 RF-PROJ-001-catalogo-proyectos.md -鈹 鈹溾攢鈹 RF-PROJ-002-estructura-jerarquica-obra.md -鈹 鈹溾攢鈹 RF-PROJ-003-prototipos-vivienda.md -鈹 鈹斺攢鈹 RF-PROJ-004-asignacion-equipo-calendario.md -鈹 -鈹溾攢鈹 especificaciones/ # 4 Especificaciones Tecnicas (100%) -鈹 鈹溾攢鈹 ET-PROJ-001-implementacion-catalogo-proyectos.md -鈹 鈹溾攢鈹 ET-PROJ-002-implementacion-estructura-jerarquica.md -鈹 鈹溾攢鈹 ET-PROJ-003-implementacion-prototipos.md -鈹 鈹斺攢鈹 ET-PROJ-004-implementacion-equipo-calendario.md -鈹 -鈹溾攢鈹 historias-usuario/ # 9 Historias de Usuario (100%) -鈹 鈹溾攢鈹 US-PROJ-001-catalogo-proyectos.md # 8 SP - CRUD proyectos -鈹 鈹溾攢鈹 US-PROJ-002-transiciones-estado.md # 5 SP - Workflow estados -鈹 鈹溾攢鈹 US-PROJ-003-estructura-fraccionamiento.md # 8 SP - Wizard jerarquia -鈹 鈹溾攢鈹 US-PROJ-004-estructura-torre-vertical.md # 6 SP - Torres/Edificios -鈹 鈹溾攢鈹 US-PROJ-005-gestion-prototipos.md # 5 SP - Catalogo prototipos -鈹 鈹溾攢鈹 US-PROJ-006-asignacion-prototipos-lotes.md # 3 SP - Asignacion masiva -鈹 鈹溾攢鈹 US-PROJ-007-asignacion-equipo.md # 4 SP - Team management -鈹 鈹溾攢鈹 US-PROJ-008-calendario-hitos.md # 3 SP - Milestones -鈹 鈹斺攢鈹 US-PROJ-009-alertas-fechas-criticas.md # 3 SP - Sistema alertas -鈹 -鈹斺攢鈹 implementacion/ # Archivos de soporte tecnico - 鈹溾攢鈹 TRACEABILITY.yml # Matriz de trazabilidad completa - 鈹溾攢鈹 ET-PROJ-001-rls-policies.sql # RLS para proyectos - 鈹斺攢鈹 ET-PROJ-002-rls-policies.sql # RLS para estructura -``` - ---- - -## Contenido - -### Requerimientos Funcionales (4) - -| ID | Archivo | Titulo | US Asociadas | Estado | -|----|---------|--------|--------------|--------| -| RF-PROJ-001 | [RF-PROJ-001-catalogo-proyectos.md](./requerimientos-funcionales/RF-PROJ-001-catalogo-proyectos.md) | Catalogo de Proyectos | US-001, US-002 | Documentado | -| RF-PROJ-002 | [RF-PROJ-002-estructura-jerarquica-obra.md](./requerimientos-funcionales/RF-PROJ-002-estructura-jerarquica-obra.md) | Estructura Jerarquica de Obra | US-003, US-004 | Documentado | -| RF-PROJ-003 | [RF-PROJ-003-prototipos-vivienda.md](./requerimientos-funcionales/RF-PROJ-003-prototipos-vivienda.md) | Prototipos de Vivienda | US-005, US-006 | Documentado | -| RF-PROJ-004 | [RF-PROJ-004-asignacion-equipo-calendario.md](./requerimientos-funcionales/RF-PROJ-004-asignacion-equipo-calendario.md) | Asignacion de Equipo y Calendario | US-007, US-008, US-009 | Documentado | - -### Especificaciones Tecnicas (4) - -| ID | Archivo | Titulo | RF | Tablas | Endpoints | Estado | -|----|---------|--------|----|--------|-----------|--------| -| ET-PROJ-001 | [ET-PROJ-001-implementacion-catalogo-proyectos.md](./especificaciones/ET-PROJ-001-implementacion-catalogo-proyectos.md) | Implementacion Catalogo Proyectos | RF-PROJ-001 | 3 | 9 | Documentado | -| ET-PROJ-002 | [ET-PROJ-002-implementacion-estructura-jerarquica.md](./especificaciones/ET-PROJ-002-implementacion-estructura-jerarquica.md) | Implementacion Estructura Jerarquica | RF-PROJ-002 | 5 | 15 | Documentado | -| ET-PROJ-003 | [ET-PROJ-003-implementacion-prototipos.md](./especificaciones/ET-PROJ-003-implementacion-prototipos.md) | Implementacion Prototipos | RF-PROJ-003 | 4 | 9 | Documentado | -| ET-PROJ-004 | [ET-PROJ-004-implementacion-equipo-calendario.md](./especificaciones/ET-PROJ-004-implementacion-equipo-calendario.md) | Implementacion Equipo y Calendario | RF-PROJ-004 | 5 | 11 | Documentado | - -### Historias de Usuario (9) - -| ID | Archivo | Titulo | SP | Prioridad | Estado | -|----|---------|--------|----|-----------|--------| -| US-PROJ-001 | [US-PROJ-001-catalogo-proyectos.md](./historias-usuario/US-PROJ-001-catalogo-proyectos.md) | Catalogo de Proyectos | 8 | P0 | Documentado | -| US-PROJ-002 | [US-PROJ-002-transiciones-estado.md](./historias-usuario/US-PROJ-002-transiciones-estado.md) | Transiciones de Estado | 5 | P0 | Documentado | -| US-PROJ-003 | [US-PROJ-003-estructura-fraccionamiento.md](./historias-usuario/US-PROJ-003-estructura-fraccionamiento.md) | Estructura de Fraccionamiento | 8 | P0 | Documentado | -| US-PROJ-004 | [US-PROJ-004-estructura-torre-vertical.md](./historias-usuario/US-PROJ-004-estructura-torre-vertical.md) | Estructura de Torre Vertical | 6 | P1 | Documentado | -| US-PROJ-005 | [US-PROJ-005-gestion-prototipos.md](./historias-usuario/US-PROJ-005-gestion-prototipos.md) | Gestion de Prototipos | 5 | P1 | Documentado | -| US-PROJ-006 | [US-PROJ-006-asignacion-prototipos-lotes.md](./historias-usuario/US-PROJ-006-asignacion-prototipos-lotes.md) | Asignacion de Prototipos a Lotes | 3 | P1 | Documentado | -| US-PROJ-007 | [US-PROJ-007-asignacion-equipo.md](./historias-usuario/US-PROJ-007-asignacion-equipo.md) | Asignacion de Equipo | 4 | P1 | Documentado | -| US-PROJ-008 | [US-PROJ-008-calendario-hitos.md](./historias-usuario/US-PROJ-008-calendario-hitos.md) | Calendario de Hitos | 3 | P1 | Documentado | -| US-PROJ-009 | [US-PROJ-009-alertas-fechas-criticas.md](./historias-usuario/US-PROJ-009-alertas-fechas-criticas.md) | Alertas de Fechas Criticas | 3 | P1 | Documentado | - -**Total Story Points:** 45 SP - -### Implementacion - -Inventarios de trazabilidad: -- [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) - Matriz completa de trazabilidad -- [ET-PROJ-001-rls-policies.sql](./implementacion/ET-PROJ-001-rls-policies.sql) - RLS para proyectos -- [ET-PROJ-002-rls-policies.sql](./implementacion/ET-PROJ-002-rls-policies.sql) - RLS para estructura jerarquica - ---- - -## Flujo de Documentacion - -``` -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 FLUJO MAI-002: PROYECTOS Y ESTRUCTURA 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 - -1. CATALOGO DE PROYECTOS - RF-PROJ-001 (Requerimiento) - 鈫 - ET-PROJ-001 (Especificacion Tecnica) - 鈫 - 鈹溾攢鈫 US-PROJ-001 (Catalogo CRUD) 鈫 Backend API + Frontend UI - 鈹斺攢鈫 US-PROJ-002 (Transiciones Estado) 鈫 State Machine + Notificaciones - -2. ESTRUCTURA JERARQUICA - RF-PROJ-002 (Requerimiento) - 鈫 - ET-PROJ-002 (Especificacion Tecnica) - 鈫 - 鈹溾攢鈫 US-PROJ-003 (Fraccionamiento) 鈫 Wizard + TreeView - 鈹斺攢鈫 US-PROJ-004 (Torre Vertical) 鈫 Adaptacion para edificios - -3. PROTOTIPOS DE VIVIENDA - RF-PROJ-003 (Requerimiento) - 鈫 - ET-PROJ-003 (Especificacion Tecnica) - 鈫 - 鈹溾攢鈫 US-PROJ-005 (Gestion Prototipos) 鈫 Catalogo + Versionado - 鈹斺攢鈫 US-PROJ-006 (Asignacion a Lotes) 鈫 Asignacion masiva - -4. EQUIPO Y CALENDARIO - RF-PROJ-004 (Requerimiento) - 鈫 - ET-PROJ-004 (Especificacion Tecnica) - 鈫 - 鈹溾攢鈫 US-PROJ-007 (Asignacion Equipo) 鈫 Workload validation - 鈹溾攢鈫 US-PROJ-008 (Calendario Hitos) 鈫 Timeline + Dependencies - 鈹斺攢鈫 US-PROJ-009 (Alertas Criticas) 鈫 Cron jobs + Notificaciones - -鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 COMPONENTES TRANSVERSALES 鈹 -鈹溾攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -鈹 鈥 RLS Policies (Multi-tenancy por constructora) 鈹 -鈹 鈥 Event Emitters (Notificaciones de cambios) 鈹 -鈹 鈥 Validaciones de negocio (Estado, jerarquia, workload) 鈹 -鈹 鈥 Triggers SQL (Contadores automaticos, calculos) 鈹 -鈹 鈥 Cron Jobs (Alertas automaticas) 鈹 -鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹 -``` - ---- - -## Relacion entre Componentes - -### 1. Dependencias Jerarquicas - -``` -Project (Proyecto) - 鈹溾攢鈹 Stage (Etapa) - 鈹 鈹斺攢鈹 Block (Manzana) [OPCIONAL - solo fraccionamientos] - 鈹 鈹斺攢鈹 Lot (Lote) - 鈹 鈹斺攢鈹 HousingUnit (Vivienda) - 鈹 - 鈹溾攢鈹 HousingPrototype (Prototipos) [CATALOGO] - 鈹 鈹溾攢鈹 Versions (Versionado) - 鈹 鈹斺攢鈹 Assigned to 鈫 Lots - 鈹 - 鈹溾攢鈹 ProjectTeamAssignment (Equipo) - 鈹 鈹溾攢鈹 Director (unico) - 鈹 鈹溾攢鈹 Residents (multiples) - 鈹 鈹斺攢鈹 Engineers/Supervisors (compartidos) - 鈹 - 鈹斺攢鈹 Calendar - 鈹溾攢鈹 Milestones (Hitos con dependencias) - 鈹溾攢鈹 ConstructionPhases (Fases) - 鈹斺攢鈹 CriticalDates (Fechas criticas + Alertas) -``` - -### 2. Flujo de Estados - -**Proyecto:** -``` -Licitacion 鈫 Adjudicado 鈫 Ejecucion 鈫 Entregado 鈫 Cerrado -``` - -**Etapa:** -``` -Planeada 鈫 En Proceso 鈫 Terminada 鈫 Entregada -``` - -**Lote:** -``` -Disponible 鈫 Vendido 鈫 En Construccion 鈫 Terminado 鈫 Entregado -``` - -**Vivienda:** -``` -Disponible 鈫 En Proceso 鈫 Terminada 鈫 Entregada -``` - -### 3. Integraciones con Otros Modulos - -| Modulo | Relacion | Campos Clave | -|--------|----------|--------------| -| MAI-001 (Fundamentos) | `constructoraId`, `userId` | Multi-tenancy, Auth | -| MAI-003 (Presupuestos) | `Project.contractAmount` | Presupuesto maestro por proyecto | -| MAI-004 (Compras) | `Project.id` | Requisiciones filtradas por proyecto | -| MAI-005 (Control de Obra) | `HousingUnit.id` | Avances fisicos por vivienda | -| MAI-007 (RRHH) | `TeamAssignment` | Asistencias de cuadrillas en proyecto | - ---- - -## Quick Links - -### Documentos Principales -- [RESUMEN-EPICA-MAI-002.md](./RESUMEN-EPICA-MAI-002.md) - Resumen ejecutivo completo (830+ lineas) -- [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) - Matriz de trazabilidad (307 lineas) - -### Por Tipo de Proyecto - -**Fraccionamiento Horizontal:** -- RF-PROJ-002 (Estructura con manzanas) -- US-PROJ-003 (Wizard de fraccionamiento) - -**Edificio Vertical:** -- RF-PROJ-002 (Estructura sin manzanas) -- US-PROJ-004 (Wizard de torre vertical) - -**Conjunto Habitacional Mixto:** -- RF-PROJ-002 (Estructura flexible) -- US-PROJ-003 + US-PROJ-004 (Combinacion) - -### Por Funcionalidad - -**Gestion de Proyectos:** -- RF-PROJ-001 鈫 ET-PROJ-001 鈫 US-PROJ-001, US-PROJ-002 - -**Estructura de Obra:** -- RF-PROJ-002 鈫 ET-PROJ-002 鈫 US-PROJ-003, US-PROJ-004 - -**Prototipos:** -- RF-PROJ-003 鈫 ET-PROJ-003 鈫 US-PROJ-005, US-PROJ-006 - -**Equipo y Calendario:** -- RF-PROJ-004 鈫 ET-PROJ-004 鈫 US-PROJ-007, US-PROJ-008, US-PROJ-009 - -### Archivos SQL -- [ET-PROJ-001-rls-policies.sql](./implementacion/ET-PROJ-001-rls-policies.sql) - Row Level Security para proyectos -- [ET-PROJ-002-rls-policies.sql](./implementacion/ET-PROJ-002-rls-policies.sql) - Row Level Security para estructura - ---- - -## Modulos Afectados - -### Base de Datos -- **Schema:** `project_management` -- **Tablas:** 17 tablas principales - - Proyectos: `projects`, `project_documents`, `project_metrics` - - Estructura: `stages`, `blocks`, `lots`, `housing_units`, `unit_progress` - - Prototipos: `housing_prototypes`, `prototype_versions`, `prototype_documents`, `prototype_costs` - - Equipo: `project_team_assignments` - - Calendario: `project_milestones`, `construction_phases`, `critical_dates`, `milestone_dependencies` -- **ENUMs:** - - `ProjectType` (fraccionamiento_horizontal, conjunto_habitacional, edificio_vertical, mixto) - - `ProjectStatus` (licitacion, adjudicado, ejecucion, entregado, cerrado) - - `PrototypeCategory` (unifamiliar, departamento, duplex) - - `ProjectRole` (director, resident, engineer, supervisor, purchases_manager) - - `MilestoneType` (11 tipos: arranque, permisos, cimentacion, etc.) -- **Funciones SQL:** - - `get_user_total_workload()` - Calculo de carga de trabajo - - `get_role_workload_limit()` - Limites por rol - - Triggers para contadores automaticos - -### Backend (NestJS) -- **Modulo:** `project-management` -- **Path:** `apps/backend/src/modules/project-management/` -- **Entities:** 11 entities principales - - Project, Stage, Block, Lot, HousingUnit - - HousingPrototype, PrototypeVersion - - ProjectTeamAssignment, Milestone, CriticalDate, ConstructionPhase -- **Services:** 8+ services - - ProjectsService, StagesService, LotsService, HousingUnitsService - - PrototypesService, TeamAssignmentsService, MilestonesService, AlertsService -- **Controllers:** 6+ controllers RESTful -- **Total Endpoints:** 52 endpoints - -### Frontend (React + TypeScript) -- **Features:** `projects` -- **Path:** `apps/frontend/src/features/projects/` -- **Pages:** - - ProjectsListPage, ProjectDetailPage, CreateProjectPage - - StructurePage, PrototypesPage, CalendarPage -- **Components:** 43+ componentes - - ProjectForm, ProjectCard, ProjectStatusBadge - - StructureTreeView, BulkLotCreationForm, HousingUnitProgressCard - - PrototypeGallery, PrototypeForm - - TeamRoster, MilestoneTimeline, CriticalDatesCalendar -- **Stores:** projectStore, structureStore, prototypeStore -- **Services:** projects.api.ts - ---- - -## Metricas - -| Metrica | Valor | -|---------|-------| -| **Story Points estimados** | 45 SP | -| **Duracion estimada** | 8 semanas (4 sprints de 2 semanas) | -| **Desarrolladores requeridos** | 2 full-stack | -| **Reutilizacion GAMILIT** | 40% | -| **RF completados** | 4/4 (100%) | -| **ET completadas** | 4/4 (100%) | -| **US completadas** | 9/9 (100%) | -| **Documentacion generada** | ~230 KB | -| **Tablas DB** | 17 tablas | -| **Endpoints API** | 52 endpoints | -| **Componentes UI** | 43 componentes | -| **Validaciones de negocio** | 20+ reglas | - ---- - -## Caracteristicas Clave - -### 1. Jerarquia Flexible -- Soporta 3 estructuras: Fraccionamiento (con manzanas), Conjunto (sin manzanas), Torre vertical (niveles) -- Navegacion de arbol recursivo con expand/collapse -- Creacion masiva: hasta 500 lotes en <3 segundos -- Codigos unicos auto-generados - -### 2. Prototipos con Versionado -- Catalogo centralizado de prototipos -- Versionado automatico (v1, v2, v3...) -- Depreciacion de versiones antiguas -- Asignacion masiva a lotes -- Snapshot de version al momento de asignacion -- Herencia de caracteristicas a viviendas - -### 3. Gestion de Equipo Inteligente -- Validacion de limites de workload por rol: - - Director: 500% (5 proyectos) - - Residente: 200% (2 proyectos) - - Ingeniero: 800% (8 proyectos) -- Solo un Director principal por proyecto -- Dashboard de disponibilidad de equipo -- Historial de asignaciones - -### 4. Calendario Inteligente -- Hitos con dependencias (validacion de grafo) -- 11 tipos de hitos predefinidos -- Fases constructivas (9 fases) -- Fechas criticas con alertas automaticas -- Cron jobs para notificaciones diarias (9:00 AM) -- Sistema de notificaciones por email + in-app - -### 5. Multi-tenancy Robusto -- Row Level Security (RLS) en todas las tablas -- Aislamiento total por `constructoraId` -- Imposible ver datos de otras constructoras -- Validaciones a nivel DB + Backend - ---- - -## Configuracion SaaS Multi-tenant - -### Activacion del Modulo - -MAI-002 es un **modulo core** incluido en los 3 planes de suscripcion: - -| Plan | Modulo MAI-002 | Limites | -|------|----------------|---------| -| **Basico** | Incluido | 5 proyectos activos simultaneos | -| **Profesional** | Incluido | 15 proyectos activos simultaneos | -| **Enterprise** | Incluido | Proyectos ilimitados | - -**Activacion automatica:** Este modulo se activa durante el onboarding de un nuevo tenant (constructora). - -### Feature Flags Configurables - -- `projects.bulk_lot_creation` - Creacion masiva de hasta 500 lotes -- `projects.housing_prototypes` - Catalogo de prototipos de vivienda -- `projects.team_workload_validation` - Validacion de limites de carga por rol -- `projects.ai_schedule_optimization` - Optimizacion de calendario con IA (Beta) - ---- - -## Stack Tecnologico - -### Backend -- **Framework:** NestJS + TypeORM -- **Base de Datos:** PostgreSQL 15+ -- **Arquitectura:** Event-driven (Event Emitters) -- **Seguridad:** Row Level Security (RLS), JWT -- **Jobs:** Cron jobs para alertas automaticas - -### Frontend -- **Framework:** React + TypeScript -- **Formularios:** React Hook Form + Zod validation -- **Estado:** React Query (TanStack) + Zustand -- **UI:** Shadcn/UI components -- **Routing:** React Router v6 - -### Storage -- **Documentos:** S3-compatible (planos, permisos, renders) -- **Geoespacial:** PostGIS (coordenadas de proyectos) - Opcional - ---- - -## Patrones de Implementacion - -### 1. Jerarquia Recursiva -```typescript -// Navegacion de arbol recursivo -interface TreeNode { - id: string; - type: 'project' | 'stage' | 'block' | 'lot' | 'housing'; - children?: TreeNode[]; -} -``` - -### 2. Event-Driven Notifications -```typescript -// Event emitters para notificaciones -this.eventEmitter.emit('project.status_changed', { - projectId, - oldStatus, - newStatus, - changedBy -}); -``` - -### 3. Codigo Auto-generado -```typescript -// Patron: PROJ-{YEAR}-{SEQUENCE} -generateProjectCode(): string { - const year = new Date().getFullYear(); - const sequence = await this.getNextSequence(year); - return `PROJ-${year}-${sequence.toString().padStart(3, '0')}`; -} -``` - -### 4. Validacion de Estado -```typescript -// Maquina de estados para transiciones -const validTransitions = { - licitacion: ['adjudicado'], - adjudicado: ['ejecucion'], - ejecucion: ['entregado'], - entregado: ['cerrado'] -}; -``` - -### 5. Row Level Security (RLS) -```sql --- Politica RLS para multi-tenancy -CREATE POLICY projects_select_policy ON project_management.projects -FOR SELECT -USING (constructora_id = current_setting('app.current_constructora_id')::UUID); -``` - ---- - -## Plan de Implementacion - -### Sprint 3 (Semana 3-4) - 13 SP -- US-PROJ-001: Catalogo de Proyectos (8 SP) -- US-PROJ-002: Transiciones de Estado (5 SP) - -**Entregables:** -- CRUD completo de proyectos -- Workflow de estados con validaciones -- RLS policies implementadas - -### Sprint 4 (Semana 5-6) - 14 SP -- US-PROJ-003: Estructura de Fraccionamiento (8 SP) -- US-PROJ-004: Estructura de Torre Vertical (6 SP) - -**Entregables:** -- Wizard de creacion de estructura -- TreeView jerarquico -- Creacion masiva de lotes - -### Sprint 5 (Semana 7-8) - 8 SP -- US-PROJ-005: Gestion de Prototipos (5 SP) -- US-PROJ-006: Asignacion de Prototipos a Lotes (3 SP) - -**Entregables:** -- Catalogo de prototipos con versionado -- Asignacion masiva a lotes -- Herencia de caracteristicas - -### Sprint 6 (Semana 9-10) - 10 SP -- US-PROJ-007: Asignacion de Equipo (4 SP) -- US-PROJ-008: Calendario de Hitos (3 SP) -- US-PROJ-009: Alertas de Fechas Criticas (3 SP) - -**Entregables:** -- Gestion de equipo con workload -- Timeline de hitos -- Sistema de alertas automaticas - ---- - -## Siguientes Pasos - -### Opcion 1: Iniciar Implementacion de MAI-002 (RECOMENDADO) -**Razon:** Documentacion 100% completa, equipo puede empezar desarrollo inmediato - -**Estimacion:** 4 sprints de 2 semanas = 8 semanas - -### Opcion 2: Continuar con Siguiente Epica -**Opciones:** -- MAI-003: Presupuestos y Control de Costos (50 SP) -- MAI-004: Compras e Inventarios (50 SP) -- MAI-005: Control de Obra y Avances (45 SP) - -**Beneficio:** Cobertura completa del sistema antes de implementar - -### Opcion 3: Enfoque Paralelo -- Equipo A: Implementa MAI-002 -- Equipo B: Documenta MAI-003 o MAI-005 - -**Beneficio:** Velocidad maxima, trabajo paralelo - ---- - -## Referencias - -- **Resumen Ejecutivo:** [RESUMEN-EPICA-MAI-002.md](./RESUMEN-EPICA-MAI-002.md) -- **Trazabilidad:** [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) -- **Proyecto GAMILIT:** Reutilizacion de componentes base de infraestructura - ---- - -**Generado:** 2025-12-06 -**Mantenedores:** @tech-lead @backend-team @frontend-team @database-team -**Estado:** Documentado (Pendiente de Implementacion) diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-backend.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-backend.md deleted file mode 100644 index 18e91f0f4..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-backend.md +++ /dev/null @@ -1,2880 +0,0 @@ -# ET-PROJ-001-BACKEND: Especificaci贸n T茅cnica de Backend - Cat谩logo de Proyectos - -**Epic:** MAI-002 - Proyectos y Estructura de Obra -**RF:** RF-PROJ-001 -**Tipo:** Especificaci贸n T茅cnica Backend -**Prioridad:** Cr铆tica (P0) -**Estado:** En Implementaci贸n -**脷ltima actualizaci贸n:** 2025-12-06 - ---- - -## Stack Tecnol贸gico - -- **Framework:** NestJS 10+ -- **ORM:** TypeORM 0.3.17 -- **Base de datos:** PostgreSQL 15+ -- **Validaciones:** class-validator 0.14+ -- **Transformaciones:** class-transformer 0.5+ -- **Documentaci贸n:** @nestjs/swagger 7+ -- **Eventos:** @nestjs/event-emitter 2+ - ---- - -## 1. Arquitectura del M贸dulo - -### 1.1 Estructura de Directorios - -``` -apps/backend/src/modules/projects/ -鈹溾攢鈹 entities/ -鈹 鈹溾攢鈹 project.entity.ts -鈹 鈹溾攢鈹 stage.entity.ts -鈹 鈹溾攢鈹 project-team-assignment.entity.ts -鈹 鈹斺攢鈹 project-document.entity.ts -鈹溾攢鈹 dto/ -鈹 鈹溾攢鈹 create-project.dto.ts -鈹 鈹溾攢鈹 update-project.dto.ts -鈹 鈹溾攢鈹 change-status.dto.ts -鈹 鈹溾攢鈹 filter-project.dto.ts -鈹 鈹斺攢鈹 project-response.dto.ts -鈹溾攢鈹 controllers/ -鈹 鈹斺攢鈹 projects.controller.ts -鈹溾攢鈹 services/ -鈹 鈹溾攢鈹 projects.service.ts -鈹 鈹斺攢鈹 project-metrics.service.ts -鈹溾攢鈹 guards/ -鈹 鈹斺攢鈹 project-access.guard.ts -鈹溾攢鈹 decorators/ -鈹 鈹斺攢鈹 constructora.decorator.ts -鈹溾攢鈹 events/ -鈹 鈹斺攢鈹 project.events.ts -鈹溾攢鈹 listeners/ -鈹 鈹斺攢鈹 project-status.listener.ts -鈹溾攢鈹 projects.module.ts -鈹斺攢鈹 __tests__/ - 鈹溾攢鈹 projects.service.spec.ts - 鈹斺攢鈹 projects.controller.spec.ts -``` - ---- - -## 2. Entities TypeORM - -### 2.1 Project Entity - -**Archivo:** `entities/project.entity.ts` - -```typescript -import { - Entity, - Column, - PrimaryGeneratedColumn, - ManyToOne, - OneToMany, - JoinColumn, - CreateDateColumn, - UpdateDateColumn, - Index, - Check, -} from 'typeorm'; -import { Stage } from './stage.entity'; -import { ProjectTeamAssignment } from './project-team-assignment.entity'; -import { ProjectDocument } from './project-document.entity'; - -/** - * Enumeraci贸n de tipos de proyecto inmobiliario - */ -export enum ProjectType { - FRACCIONAMIENTO_HORIZONTAL = 'fraccionamiento_horizontal', - CONJUNTO_HABITACIONAL = 'conjunto_habitacional', - EDIFICIO_VERTICAL = 'edificio_vertical', - MIXTO = 'mixto', -} - -/** - * Enumeraci贸n de estados del ciclo de vida del proyecto - * Flujo: licitacion 鈫 adjudicado 鈫 ejecucion 鈫 entregado 鈫 cerrado - */ -export enum ProjectStatus { - LICITACION = 'licitacion', - ADJUDICADO = 'adjudicado', - EJECUCION = 'ejecucion', - ENTREGADO = 'entregado', - CERRADO = 'cerrado', -} - -/** - * Enumeraci贸n de tipos de cliente - */ -export enum ClientType { - PUBLICO = 'publico', - PRIVADO = 'privado', - MIXTO = 'mixto', -} - -/** - * Enumeraci贸n de tipos de contrato - */ -export enum ContractType { - LLAVE_EN_MANO = 'llave_en_mano', - PRECIO_ALZADO = 'precio_alzado', - ADMINISTRACION = 'administracion', - MIXTO = 'mixto', -} - -/** - * Entidad Project - Representa un proyecto de construcci贸n inmobiliaria - * - * Un proyecto puede ser: - * - Fraccionamiento Horizontal: Viviendas unifamiliares en lotes - * - Conjunto Habitacional: Viviendas adosadas o d煤plex - * - Edificio Vertical: Torre multifamiliar con departamentos - * - Mixto: Combinaci贸n de tipos - * - * @schema projects - */ -@Entity('projects', { schema: 'projects' }) -@Index('idx_projects_constructora_status', ['constructoraId', 'status']) -@Index('idx_projects_code', ['projectCode']) -@Index('idx_projects_dates', ['contractStartDate', 'scheduledEndDate']) -@Check('"contract_amount" > 0') -@Check('"total_area" > 0') -@Check('"buildable_area" > 0') -@Check('"buildable_area" <= "total_area"') -export class Project { - @PrimaryGeneratedColumn('uuid') - id: string; - - /** - * C贸digo 煤nico del proyecto - * Formato: PROJ-{YEAR}-{SEQUENCE} - * Ejemplo: PROJ-2025-001 - */ - @Column({ - type: 'varchar', - length: 20, - unique: true, - name: 'project_code', - }) - projectCode: string; - - /** - * Multi-tenant discriminator (tenant = constructora) - * Used for Row-Level Security (RLS) to isolate data between constructoras - * See: docs/00-overview/GLOSARIO.md for terminology clarification - */ - @Column({ - type: 'uuid', - name: 'constructora_id', - }) - constructoraId: string; - - // ==================== INFORMACI脫N B脕SICA ==================== - - @Column({ - type: 'varchar', - length: 200, - }) - name: string; - - @Column({ - type: 'text', - nullable: true, - }) - description: string; - - @Column({ - type: 'enum', - enum: ProjectType, - name: 'project_type', - }) - projectType: ProjectType; - - @Column({ - type: 'enum', - enum: ProjectStatus, - default: ProjectStatus.LICITACION, - }) - status: ProjectStatus; - - // ==================== DATOS DEL CLIENTE ==================== - - @Column({ - type: 'enum', - enum: ClientType, - name: 'client_type', - }) - clientType: ClientType; - - @Column({ - type: 'varchar', - length: 200, - name: 'client_name', - }) - clientName: string; - - @Column({ - type: 'varchar', - length: 13, - name: 'client_rfc', - }) - clientRFC: string; - - @Column({ - type: 'varchar', - length: 100, - nullable: true, - name: 'client_contact_name', - }) - clientContactName: string; - - @Column({ - type: 'varchar', - length: 100, - nullable: true, - name: 'client_contact_email', - }) - clientContactEmail: string; - - @Column({ - type: 'varchar', - length: 20, - nullable: true, - name: 'client_contact_phone', - }) - clientContactPhone: string; - - // ==================== INFORMACI脫N CONTRACTUAL ==================== - - @Column({ - type: 'enum', - enum: ContractType, - name: 'contract_type', - }) - contractType: ContractType; - - @Column({ - type: 'decimal', - precision: 15, - scale: 2, - name: 'contract_amount', - }) - contractAmount: number; - - // ==================== UBICACI脫N ==================== - - @Column({ type: 'text' }) - address: string; - - @Column({ - type: 'varchar', - length: 100, - }) - state: string; - - @Column({ - type: 'varchar', - length: 100, - }) - municipality: string; - - @Column({ - type: 'varchar', - length: 5, - name: 'postal_code', - }) - postalCode: string; - - @Column({ - type: 'decimal', - precision: 10, - scale: 6, - nullable: true, - }) - latitude: number; - - @Column({ - type: 'decimal', - precision: 10, - scale: 6, - nullable: true, - }) - longitude: number; - - /** - * Superficie total del terreno en m虏 - */ - @Column({ - type: 'decimal', - precision: 12, - scale: 2, - name: 'total_area', - }) - totalArea: number; - - /** - * Superficie construible en m虏 - * Debe ser <= totalArea - */ - @Column({ - type: 'decimal', - precision: 12, - scale: 2, - name: 'buildable_area', - }) - buildableArea: number; - - // ==================== FECHAS ==================== - - @Column({ - type: 'date', - nullable: true, - name: 'bidding_date', - }) - biddingDate: Date; - - @Column({ - type: 'date', - nullable: true, - name: 'award_date', - }) - awardDate: Date; - - @Column({ - type: 'date', - name: 'contract_start_date', - }) - contractStartDate: Date; - - @Column({ - type: 'date', - nullable: true, - name: 'actual_start_date', - }) - actualStartDate: Date; - - /** - * Duraci贸n contractual en meses - */ - @Column({ - type: 'integer', - name: 'contract_duration', - }) - contractDuration: number; - - /** - * Fecha calculada: contractStartDate + contractDuration - */ - @Column({ - type: 'date', - name: 'scheduled_end_date', - }) - scheduledEndDate: Date; - - @Column({ - type: 'date', - nullable: true, - name: 'actual_end_date', - }) - actualEndDate: Date; - - @Column({ - type: 'date', - nullable: true, - name: 'delivery_date', - }) - deliveryDate: Date; - - @Column({ - type: 'date', - nullable: true, - name: 'closure_date', - }) - closureDate: Date; - - // ==================== INFORMACI脫N LEGAL ==================== - - @Column({ - type: 'varchar', - length: 50, - nullable: true, - name: 'construction_license_number', - }) - constructionLicenseNumber: string; - - @Column({ - type: 'date', - nullable: true, - name: 'license_issue_date', - }) - licenseIssueDate: Date; - - @Column({ - type: 'date', - nullable: true, - name: 'license_expiration_date', - }) - licenseExpirationDate: Date; - - @Column({ - type: 'varchar', - length: 50, - nullable: true, - name: 'environmental_impact_number', - }) - environmentalImpactNumber: string; - - @Column({ - type: 'varchar', - length: 20, - nullable: true, - name: 'land_use_approved', - }) - landUseApproved: string; - - @Column({ - type: 'varchar', - length: 50, - nullable: true, - name: 'approved_plan_number', - }) - approvedPlanNumber: string; - - @Column({ - type: 'varchar', - length: 50, - nullable: true, - name: 'infonavit_number', - }) - infonavitNumber: string; - - @Column({ - type: 'varchar', - length: 50, - nullable: true, - name: 'fovissste_number', - }) - fovisssteNumber: string; - - // ==================== M脡TRICAS CALCULADAS ==================== - - @Column({ - type: 'integer', - default: 0, - name: 'total_housing_units', - }) - totalHousingUnits: number; - - @Column({ - type: 'integer', - default: 0, - name: 'delivered_housing_units', - }) - deliveredHousingUnits: number; - - /** - * Porcentaje de avance f铆sico (0-100) - * Calculado a partir del promedio de avances de stages - */ - @Column({ - type: 'decimal', - precision: 5, - scale: 2, - default: 0, - name: 'physical_progress', - }) - physicalProgress: number; - - /** - * Costo ejercido acumulado - */ - @Column({ - type: 'decimal', - precision: 15, - scale: 2, - default: 0, - name: 'exercised_cost', - }) - exercisedCost: number; - - /** - * Desviaci贸n presupuestal en porcentaje - * Positivo = sobre presupuesto - * Negativo = bajo presupuesto - */ - @Column({ - type: 'decimal', - precision: 5, - scale: 2, - default: 0, - name: 'budget_deviation', - }) - budgetDeviation: number; - - // ==================== METADATA ==================== - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; - - @Column({ - type: 'uuid', - name: 'created_by', - }) - createdBy: string; - - @Column({ - type: 'uuid', - nullable: true, - name: 'updated_by', - }) - updatedBy: string; - - // ==================== RELACIONES ==================== - - @OneToMany(() => Stage, (stage) => stage.project, { - cascade: true, - }) - stages: Stage[]; - - @OneToMany(() => ProjectTeamAssignment, (assignment) => assignment.project, { - cascade: true, - }) - teamAssignments: ProjectTeamAssignment[]; - - @OneToMany(() => ProjectDocument, (doc) => doc.project, { - cascade: true, - }) - documents: ProjectDocument[]; -} -``` - -### 2.2 Stage Entity (Entidad relacionada) - -**Archivo:** `entities/stage.entity.ts` - -```typescript -import { - Entity, - Column, - PrimaryGeneratedColumn, - ManyToOne, - JoinColumn, - CreateDateColumn, - UpdateDateColumn, - Index, -} from 'typeorm'; -import { Project } from './project.entity'; - -/** - * Entidad Stage - Representa una etapa constructiva dentro de un proyecto - * Ejemplo: Etapa 1, Etapa 2, etc. - */ -@Entity('stages', { schema: 'projects' }) -@Index('idx_stages_project', ['projectId']) -export class Stage { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'uuid', name: 'project_id' }) - projectId: string; - - @Column({ type: 'varchar', length: 100 }) - name: string; - - @Column({ type: 'text', nullable: true }) - description: string; - - @Column({ type: 'integer' }) - order: number; - - @Column({ type: 'decimal', precision: 5, scale: 2, default: 0, name: 'physical_progress' }) - physicalProgress: number; - - @Column({ type: 'date', nullable: true, name: 'start_date' }) - startDate: Date; - - @Column({ type: 'date', nullable: true, name: 'end_date' }) - endDate: Date; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; - - @ManyToOne(() => Project, (project) => project.stages, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'project_id' }) - project: Project; -} -``` - ---- - -## 3. DTOs (Data Transfer Objects) - -### 3.1 CreateProjectDto - -**Archivo:** `dto/create-project.dto.ts` - -```typescript -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { - IsString, - IsEnum, - IsNotEmpty, - MinLength, - MaxLength, - IsNumber, - Min, - IsDateString, - IsOptional, - IsEmail, - Matches, - IsInt, - ValidateIf, - IsDecimal, -} from 'class-validator'; -import { - ProjectType, - ClientType, - ContractType, -} from '../entities/project.entity'; - -export class CreateProjectDto { - // ==================== INFORMACI脫N B脕SICA ==================== - - @ApiProperty({ - description: 'Nombre del proyecto', - example: 'Fraccionamiento Villas del Sol', - minLength: 3, - maxLength: 200, - }) - @IsString() - @IsNotEmpty() - @MinLength(3, { message: 'El nombre debe tener al menos 3 caracteres' }) - @MaxLength(200, { message: 'El nombre no puede exceder 200 caracteres' }) - name: string; - - @ApiPropertyOptional({ - description: 'Descripci贸n detallada del proyecto', - example: 'Desarrollo de 250 viviendas de inter茅s social en 15 hect谩reas', - }) - @IsString() - @IsOptional() - description?: string; - - @ApiProperty({ - description: 'Tipo de proyecto inmobiliario', - enum: ProjectType, - example: ProjectType.FRACCIONAMIENTO_HORIZONTAL, - }) - @IsEnum(ProjectType, { - message: 'Tipo de proyecto inv谩lido. Valores permitidos: fraccionamiento_horizontal, conjunto_habitacional, edificio_vertical, mixto', - }) - projectType: ProjectType; - - // ==================== DATOS DEL CLIENTE ==================== - - @ApiProperty({ - description: 'Tipo de cliente', - enum: ClientType, - example: ClientType.PUBLICO, - }) - @IsEnum(ClientType, { - message: 'Tipo de cliente inv谩lido. Valores permitidos: publico, privado, mixto', - }) - clientType: ClientType; - - @ApiProperty({ - description: 'Nombre o raz贸n social del cliente', - example: 'INFONAVIT Jalisco', - minLength: 3, - maxLength: 200, - }) - @IsString() - @IsNotEmpty() - @MinLength(3) - @MaxLength(200) - clientName: string; - - @ApiProperty({ - description: 'RFC del cliente (12 o 13 caracteres)', - example: 'INF850101ABC', - minLength: 12, - maxLength: 13, - }) - @IsString() - @IsNotEmpty() - @Matches(/^[A-Z脩&]{3,4}\d{6}[A-Z0-9]{3}$/, { - message: 'RFC inv谩lido. Formato esperado: 3-4 letras + 6 d铆gitos + 3 caracteres alfanum茅ricos', - }) - clientRFC: string; - - @ApiPropertyOptional({ - description: 'Nombre del contacto principal del cliente', - example: 'Ing. Roberto Mart铆nez', - }) - @IsString() - @IsOptional() - @MaxLength(100) - clientContactName?: string; - - @ApiPropertyOptional({ - description: 'Email del contacto del cliente', - example: 'rmartinez@infonavit.gob.mx', - }) - @IsEmail({}, { message: 'Email inv谩lido' }) - @IsOptional() - clientContactEmail?: string; - - @ApiPropertyOptional({ - description: 'Tel茅fono del contacto del cliente', - example: '+52 33 1234 5678', - }) - @IsString() - @IsOptional() - @MaxLength(20) - clientContactPhone?: string; - - // ==================== INFORMACI脫N CONTRACTUAL ==================== - - @ApiProperty({ - description: 'Tipo de contrato', - enum: ContractType, - example: ContractType.LLAVE_EN_MANO, - }) - @IsEnum(ContractType, { - message: 'Tipo de contrato inv谩lido. Valores permitidos: llave_en_mano, precio_alzado, administracion, mixto', - }) - contractType: ContractType; - - @ApiProperty({ - description: 'Monto contratado en MXN', - example: 125000000.00, - minimum: 0.01, - }) - @IsNumber({ maxDecimalPlaces: 2 }, { message: 'El monto debe tener m谩ximo 2 decimales' }) - @Min(0.01, { message: 'El monto contratado debe ser mayor a 0' }) - contractAmount: number; - - @ApiProperty({ - description: 'Fecha de inicio contractual (ISO 8601)', - example: '2025-06-01', - }) - @IsDateString({}, { message: 'Fecha de inicio inv谩lida. Formato esperado: YYYY-MM-DD' }) - contractStartDate: string; - - @ApiProperty({ - description: 'Duraci贸n del contrato en meses', - example: 24, - minimum: 1, - }) - @IsInt({ message: 'La duraci贸n debe ser un n煤mero entero' }) - @Min(1, { message: 'La duraci贸n debe ser al menos 1 mes' }) - contractDuration: number; - - @ApiPropertyOptional({ - description: 'Fecha de licitaci贸n (ISO 8601)', - example: '2025-03-15', - }) - @IsDateString({}, { message: 'Fecha de licitaci贸n inv谩lida' }) - @IsOptional() - biddingDate?: string; - - @ApiPropertyOptional({ - description: 'Fecha de adjudicaci贸n (ISO 8601)', - example: '2025-04-30', - }) - @IsDateString({}, { message: 'Fecha de adjudicaci贸n inv谩lida' }) - @IsOptional() - awardDate?: string; - - // ==================== UBICACI脫N ==================== - - @ApiProperty({ - description: 'Direcci贸n completa del proyecto', - example: 'Carretera Federal 200 Km 45', - minLength: 10, - }) - @IsString() - @IsNotEmpty() - @MinLength(10, { message: 'La direcci贸n debe tener al menos 10 caracteres' }) - address: string; - - @ApiProperty({ - description: 'Estado de la Rep煤blica Mexicana', - example: 'Jalisco', - }) - @IsString() - @IsNotEmpty() - @MaxLength(100) - state: string; - - @ApiProperty({ - description: 'Municipio', - example: 'Zapopan', - }) - @IsString() - @IsNotEmpty() - @MaxLength(100) - municipality: string; - - @ApiProperty({ - description: 'C贸digo postal (5 d铆gitos)', - example: '45100', - }) - @IsString() - @IsNotEmpty() - @Matches(/^\d{5}$/, { message: 'El c贸digo postal debe tener 5 d铆gitos' }) - postalCode: string; - - @ApiPropertyOptional({ - description: 'Latitud GPS (grados decimales)', - example: 20.6736, - minimum: -90, - maximum: 90, - }) - @IsNumber() - @Min(-90) - @Min(90) - @IsOptional() - latitude?: number; - - @ApiPropertyOptional({ - description: 'Longitud GPS (grados decimales)', - example: -103.3927, - minimum: -180, - maximum: 180, - }) - @IsNumber() - @Min(-180) - @Min(180) - @IsOptional() - longitude?: number; - - @ApiProperty({ - description: 'Superficie total del terreno en m虏', - example: 150000.00, - minimum: 0.01, - }) - @IsNumber({ maxDecimalPlaces: 2 }) - @Min(0.01, { message: 'La superficie total debe ser mayor a 0' }) - totalArea: number; - - @ApiProperty({ - description: 'Superficie construible en m虏', - example: 120000.00, - minimum: 0.01, - }) - @IsNumber({ maxDecimalPlaces: 2 }) - @Min(0.01, { message: 'La superficie construible debe ser mayor a 0' }) - buildableArea: number; - - // ==================== INFORMACI脫N LEGAL ==================== - - @ApiPropertyOptional({ - description: 'N煤mero de licencia de construcci贸n', - example: 'LIC-2024-ZPN-0456', - }) - @IsString() - @IsOptional() - @MaxLength(50) - constructionLicenseNumber?: string; - - @ApiPropertyOptional({ - description: 'Fecha de emisi贸n de la licencia', - example: '2025-04-15', - }) - @IsDateString() - @IsOptional() - licenseIssueDate?: string; - - @ApiPropertyOptional({ - description: 'Fecha de vencimiento de la licencia', - example: '2027-04-14', - }) - @IsDateString() - @IsOptional() - licenseExpirationDate?: string; - - @ApiPropertyOptional({ - description: 'N煤mero de manifestaci贸n de impacto ambiental', - example: 'MIA-2024-045', - }) - @IsString() - @IsOptional() - @MaxLength(50) - environmentalImpactNumber?: string; - - @ApiPropertyOptional({ - description: 'Uso de suelo aprobado', - example: 'H4', - }) - @IsString() - @IsOptional() - @MaxLength(20) - landUseApproved?: string; - - @ApiPropertyOptional({ - description: 'N煤mero de plano autorizado', - example: 'PLANO-ZPN-2024-145', - }) - @IsString() - @IsOptional() - @MaxLength(50) - approvedPlanNumber?: string; - - @ApiPropertyOptional({ - description: 'N煤mero de registro INFONAVIT', - example: 'INF-2024-JL-0123', - }) - @IsString() - @IsOptional() - @MaxLength(50) - infonavitNumber?: string; - - @ApiPropertyOptional({ - description: 'N煤mero de registro FOVISSSTE', - example: 'FOV-2024-JL-0089', - }) - @IsString() - @IsOptional() - @MaxLength(50) - fovisssteNumber?: string; -} -``` - -### 3.2 UpdateProjectDto - -**Archivo:** `dto/update-project.dto.ts` - -```typescript -import { ApiPropertyOptional, PartialType, OmitType } from '@nestjs/swagger'; -import { CreateProjectDto } from './create-project.dto'; -import { IsEnum, IsOptional } from 'class-validator'; -import { ProjectStatus } from '../entities/project.entity'; - -/** - * DTO para actualizaci贸n de proyecto - * Todos los campos son opcionales excepto el ID (proporcionado en la ruta) - * El campo 'status' se maneja por separado en ChangeStatusDto - */ -export class UpdateProjectDto extends PartialType( - OmitType(CreateProjectDto, [] as const), -) { - @ApiPropertyOptional({ - description: 'Estado del proyecto (usar endpoint /change-status para transiciones controladas)', - enum: ProjectStatus, - example: ProjectStatus.EJECUCION, - }) - @IsEnum(ProjectStatus) - @IsOptional() - status?: ProjectStatus; -} -``` - -### 3.3 ChangeStatusDto - -**Archivo:** `dto/change-status.dto.ts` - -```typescript -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsDateString, IsOptional } from 'class-validator'; -import { ProjectStatus } from '../entities/project.entity'; - -/** - * DTO para cambio de estado del proyecto con validaciones de transici贸n - */ -export class ChangeStatusDto { - @ApiProperty({ - description: 'Nuevo estado del proyecto', - enum: ProjectStatus, - example: ProjectStatus.EJECUCION, - }) - @IsEnum(ProjectStatus, { - message: 'Estado inv谩lido. Valores permitidos: licitacion, adjudicado, ejecucion, entregado, cerrado', - }) - @IsNotEmpty() - newStatus: ProjectStatus; - - @ApiProperty({ - description: 'Fecha efectiva del cambio de estado (ISO 8601). Si no se proporciona, se usa la fecha actual', - example: '2025-06-15', - required: false, - }) - @IsDateString() - @IsOptional() - effectiveDate?: string; -} -``` - -### 3.4 FilterProjectDto - -**Archivo:** `dto/filter-project.dto.ts` - -```typescript -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsEnum, IsOptional, IsString, IsInt, Min, Max } from 'class-validator'; -import { Type } from 'class-transformer'; -import { ProjectStatus, ProjectType } from '../entities/project.entity'; - -/** - * DTO para filtrado y paginaci贸n de proyectos - */ -export class FilterProjectDto { - @ApiPropertyOptional({ - description: 'Filtrar por estado del proyecto', - enum: ProjectStatus, - example: ProjectStatus.EJECUCION, - }) - @IsEnum(ProjectStatus) - @IsOptional() - status?: ProjectStatus; - - @ApiPropertyOptional({ - description: 'Filtrar por tipo de proyecto', - enum: ProjectType, - example: ProjectType.FRACCIONAMIENTO_HORIZONTAL, - }) - @IsEnum(ProjectType) - @IsOptional() - projectType?: ProjectType; - - @ApiPropertyOptional({ - description: 'Buscar por nombre o c贸digo de proyecto (b煤squeda parcial)', - example: 'Villas', - }) - @IsString() - @IsOptional() - search?: string; - - @ApiPropertyOptional({ - description: 'N煤mero de p谩gina (inicia en 1)', - example: 1, - minimum: 1, - default: 1, - }) - @Type(() => Number) - @IsInt() - @Min(1) - @IsOptional() - page?: number = 1; - - @ApiPropertyOptional({ - description: 'Cantidad de registros por p谩gina', - example: 20, - minimum: 1, - maximum: 100, - default: 20, - }) - @Type(() => Number) - @IsInt() - @Min(1) - @Max(100) - @IsOptional() - limit?: number = 20; -} -``` - -### 3.5 ProjectResponseDto - -**Archivo:** `dto/project-response.dto.ts` - -```typescript -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Expose, Type } from 'class-transformer'; -import { ProjectStatus, ProjectType, ClientType, ContractType } from '../entities/project.entity'; - -/** - * DTO de respuesta para proyecto con datos serializados - */ -export class ProjectResponseDto { - @ApiProperty({ example: 'uuid-generated' }) - @Expose() - id: string; - - @ApiProperty({ example: 'PROJ-2025-001' }) - @Expose() - projectCode: string; - - @ApiProperty({ example: 'Fraccionamiento Villas del Sol' }) - @Expose() - name: string; - - @ApiPropertyOptional({ example: 'Desarrollo de 250 viviendas de inter茅s social' }) - @Expose() - description?: string; - - @ApiProperty({ enum: ProjectType }) - @Expose() - projectType: ProjectType; - - @ApiProperty({ enum: ProjectStatus }) - @Expose() - status: ProjectStatus; - - @ApiProperty({ enum: ClientType }) - @Expose() - clientType: ClientType; - - @ApiProperty({ example: 'INFONAVIT Jalisco' }) - @Expose() - clientName: string; - - @ApiProperty({ enum: ContractType }) - @Expose() - contractType: ContractType; - - @ApiProperty({ example: 125000000.00 }) - @Expose() - contractAmount: number; - - @ApiProperty({ example: 'Zapopan, Jalisco' }) - @Expose() - get location(): string { - return `${this['municipality']}, ${this['state']}`; - } - - @ApiProperty({ example: '2025-06-01' }) - @Expose() - contractStartDate: Date; - - @ApiProperty({ example: '2027-06-01' }) - @Expose() - scheduledEndDate: Date; - - @ApiProperty({ example: 78.5 }) - @Expose() - physicalProgress: number; - - @ApiProperty({ example: 250 }) - @Expose() - totalHousingUnits: number; - - @ApiProperty({ example: 187 }) - @Expose() - deliveredHousingUnits: number; - - @ApiProperty() - @Expose() - @Type(() => Date) - createdAt: Date; - - @ApiProperty() - @Expose() - @Type(() => Date) - updatedAt: Date; -} -``` - ---- - -## 4. Services - -### 4.1 ProjectsService - -**Archivo:** `services/projects.service.ts` - -```typescript -import { - Injectable, - BadRequestException, - NotFoundException, - ConflictException, -} from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, FindOptionsWhere } from 'typeorm'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { Project, ProjectStatus, ProjectType } from '../entities/project.entity'; -import { CreateProjectDto } from '../dto/create-project.dto'; -import { UpdateProjectDto } from '../dto/update-project.dto'; -import { FilterProjectDto } from '../dto/filter-project.dto'; -import { ChangeStatusDto } from '../dto/change-status.dto'; - -/** - * Servicio de gesti贸n de proyectos - * - * Responsabilidades: - * - CRUD de proyectos con multi-tenancy - * - Generaci贸n de c贸digos 煤nicos de proyecto - * - Validaci贸n de transiciones de estado - * - C谩lculo de fechas programadas - * - Emisi贸n de eventos de dominio - */ -@Injectable() -export class ProjectsService { - constructor( - @InjectRepository(Project) - private readonly projectRepo: Repository, - private readonly eventEmitter: EventEmitter2, - ) {} - - /** - * Crear nuevo proyecto - * - * @param dto - Datos del proyecto - * @param constructoraId - ID de la constructora (multi-tenant) - * @param userId - ID del usuario que crea el proyecto - * @returns Proyecto creado con c贸digo generado - * - * @throws BadRequestException si buildableArea > totalArea - */ - async create( - dto: CreateProjectDto, - constructoraId: string, - userId: string, - ): Promise { - // Validar que superficie construible no exceda superficie total - if (dto.buildableArea > dto.totalArea) { - throw new BadRequestException( - 'La superficie construible no puede ser mayor a la superficie total', - ); - } - - // Generar c贸digo 煤nico de proyecto - const projectCode = await this.generateProjectCode(constructoraId); - - // Calcular fecha de terminaci贸n programada - const scheduledEndDate = this.calculateScheduledEndDate( - new Date(dto.contractStartDate), - dto.contractDuration, - ); - - // Crear entidad - const project = this.projectRepo.create({ - ...dto, - projectCode, - constructoraId, - scheduledEndDate, - status: dto.biddingDate ? ProjectStatus.LICITACION : ProjectStatus.ADJUDICADO, - createdBy: userId, - }); - - // Persistir - const saved = await this.projectRepo.save(project); - - // Emitir evento de dominio - this.eventEmitter.emit('project.created', { - projectId: saved.id, - constructoraId: saved.constructoraId, - projectCode: saved.projectCode, - createdBy: userId, - }); - - return saved; - } - - /** - * Listar proyectos con filtros y paginaci贸n - * - * @param constructoraId - ID de la constructora (RLS) - * @param filters - Filtros de b煤squeda y paginaci贸n - * @returns Lista paginada de proyectos - */ - async findAll( - constructoraId: string, - filters: FilterProjectDto, - ): Promise<{ - items: Project[]; - meta: { - page: number; - limit: number; - totalItems: number; - totalPages: number; - hasNextPage: boolean; - hasPreviousPage: boolean; - }; - }> { - const page = filters.page || 1; - const limit = filters.limit || 20; - const skip = (page - 1) * limit; - - // Construir query - const query = this.projectRepo - .createQueryBuilder('project') - .where('project.constructoraId = :constructoraId', { constructoraId }) - .orderBy('project.createdAt', 'DESC'); - - // Aplicar filtros opcionales - if (filters.status) { - query.andWhere('project.status = :status', { status: filters.status }); - } - - if (filters.projectType) { - query.andWhere('project.projectType = :projectType', { - projectType: filters.projectType, - }); - } - - if (filters.search) { - query.andWhere( - '(project.name ILIKE :search OR project.projectCode ILIKE :search)', - { search: `%${filters.search}%` }, - ); - } - - // Ejecutar con paginaci贸n - const [items, totalItems] = await query - .skip(skip) - .take(limit) - .getManyAndCount(); - - const totalPages = Math.ceil(totalItems / limit); - - return { - items, - meta: { - page, - limit, - totalItems, - totalPages, - hasNextPage: page < totalPages, - hasPreviousPage: page > 1, - }, - }; - } - - /** - * Obtener proyecto por ID con verificaci贸n de tenant - * - * @param id - UUID del proyecto - * @param constructoraId - ID de la constructora (RLS) - * @param relations - Relaciones a cargar - * @returns Proyecto encontrado - * - * @throws NotFoundException si el proyecto no existe o no pertenece a la constructora - */ - async findOne( - id: string, - constructoraId: string, - relations: string[] = [], - ): Promise { - const where: FindOptionsWhere = { id, constructoraId }; - - const project = await this.projectRepo.findOne({ - where, - relations, - }); - - if (!project) { - throw new NotFoundException( - `Proyecto con ID ${id} no encontrado o no pertenece a la constructora`, - ); - } - - return project; - } - - /** - * Actualizar proyecto - * - * @param id - UUID del proyecto - * @param dto - Datos a actualizar - * @param constructoraId - ID de la constructora (RLS) - * @param userId - ID del usuario que actualiza - * @returns Proyecto actualizado - * - * @throws NotFoundException si el proyecto no existe - * @throws BadRequestException si la transici贸n de estado es inv谩lida - */ - async update( - id: string, - dto: UpdateProjectDto, - constructoraId: string, - userId: string, - ): Promise { - const project = await this.findOne(id, constructoraId); - - // Validar transici贸n de estado si se est谩 cambiando - if (dto.status && dto.status !== project.status) { - this.validateStatusTransition(project.status, dto.status); - } - - // Validar superficie construible - const newBuildableArea = dto.buildableArea ?? project.buildableArea; - const newTotalArea = dto.totalArea ?? project.totalArea; - if (newBuildableArea > newTotalArea) { - throw new BadRequestException( - 'La superficie construible no puede ser mayor a la superficie total', - ); - } - - // Recalcular fecha programada si cambi贸 duraci贸n o fecha de inicio - if (dto.contractStartDate || dto.contractDuration) { - const startDate = dto.contractStartDate - ? new Date(dto.contractStartDate) - : project.contractStartDate; - const duration = dto.contractDuration ?? project.contractDuration; - project.scheduledEndDate = this.calculateScheduledEndDate(startDate, duration); - } - - // Aplicar cambios - const oldStatus = project.status; - Object.assign(project, dto); - project.updatedBy = userId; - - // Persistir - const updated = await this.projectRepo.save(project); - - // Emitir evento si cambi贸 de estado - if (dto.status && dto.status !== oldStatus) { - this.eventEmitter.emit('project.status.changed', { - projectId: updated.id, - constructoraId: updated.constructoraId, - oldStatus, - newStatus: dto.status, - changedBy: userId, - }); - } - - return updated; - } - - /** - * Cambiar estado del proyecto con validaciones y actualizaci贸n de fechas - * - * @param id - UUID del proyecto - * @param dto - DTO con nuevo estado y fecha efectiva - * @param constructoraId - ID de la constructora (RLS) - * @param userId - ID del usuario que realiza el cambio - * @returns Proyecto con estado actualizado - * - * @throws BadRequestException si la transici贸n es inv谩lida - */ - async changeStatus( - id: string, - dto: ChangeStatusDto, - constructoraId: string, - userId: string, - ): Promise { - const project = await this.findOne(id, constructoraId); - - // Validar transici贸n - this.validateStatusTransition(project.status, dto.newStatus); - - const oldStatus = project.status; - project.status = dto.newStatus; - project.updatedBy = userId; - - // Actualizar fechas seg煤n el nuevo estado - const effectiveDate = dto.effectiveDate ? new Date(dto.effectiveDate) : new Date(); - - switch (dto.newStatus) { - case ProjectStatus.ADJUDICADO: - if (!project.awardDate) { - project.awardDate = effectiveDate; - } - break; - - case ProjectStatus.EJECUCION: - if (!project.actualStartDate) { - project.actualStartDate = effectiveDate; - } - break; - - case ProjectStatus.ENTREGADO: - if (!project.actualEndDate) { - project.actualEndDate = effectiveDate; - } - if (!project.deliveryDate) { - project.deliveryDate = effectiveDate; - } - break; - - case ProjectStatus.CERRADO: - if (!project.closureDate) { - project.closureDate = effectiveDate; - } - break; - } - - // Persistir - const updated = await this.projectRepo.save(project); - - // Emitir evento - this.eventEmitter.emit('project.status.changed', { - projectId: updated.id, - constructoraId: updated.constructoraId, - oldStatus, - newStatus: dto.newStatus, - effectiveDate, - changedBy: userId, - }); - - return updated; - } - - /** - * Eliminar proyecto (soft delete) - * Solo permitido si el proyecto est谩 en estado LICITACION - * - * @param id - UUID del proyecto - * @param constructoraId - ID de la constructora (RLS) - * @throws BadRequestException si el proyecto no est谩 en LICITACION - */ - async remove(id: string, constructoraId: string): Promise { - const project = await this.findOne(id, constructoraId); - - if (project.status !== ProjectStatus.LICITACION) { - throw new BadRequestException( - 'Solo se pueden eliminar proyectos en estado de licitaci贸n', - ); - } - - await this.projectRepo.remove(project); - - this.eventEmitter.emit('project.deleted', { - projectId: id, - constructoraId, - }); - } - - // ==================== M脡TODOS PRIVADOS ==================== - - /** - * Generar c贸digo 煤nico de proyecto - * Formato: PROJ-{YEAR}-{SEQUENCE} - * Ejemplo: PROJ-2025-001 - * - * @param constructoraId - ID de la constructora - * @returns C贸digo generado - */ - private async generateProjectCode(constructoraId: string): Promise { - const year = new Date().getFullYear(); - const prefix = `PROJ-${year}-`; - - // Obtener 煤ltimo c贸digo del a帽o para esta constructora - const lastProject = await this.projectRepo - .createQueryBuilder('project') - .where('project.constructoraId = :constructoraId', { constructoraId }) - .andWhere('project.projectCode LIKE :prefix', { prefix: `${prefix}%` }) - .orderBy('project.projectCode', 'DESC') - .getOne(); - - let sequence = 1; - if (lastProject) { - const lastSequence = parseInt(lastProject.projectCode.split('-').pop() || '0'); - sequence = lastSequence + 1; - } - - return `${prefix}${sequence.toString().padStart(3, '0')}`; - } - - /** - * Calcular fecha de terminaci贸n programada - * - * @param startDate - Fecha de inicio - * @param durationMonths - Duraci贸n en meses - * @returns Fecha calculada - */ - private calculateScheduledEndDate(startDate: Date, durationMonths: number): Date { - const endDate = new Date(startDate); - endDate.setMonth(endDate.getMonth() + durationMonths); - return endDate; - } - - /** - * Validar transici贸n de estado seg煤n reglas de negocio - * - * Flujo v谩lido: - * LICITACION 鈫 ADJUDICADO 鈫 EJECUCION 鈫 ENTREGADO 鈫 CERRADO - * - * @param currentStatus - Estado actual - * @param newStatus - Nuevo estado - * @throws BadRequestException si la transici贸n no es v谩lida - */ - private validateStatusTransition( - currentStatus: ProjectStatus, - newStatus: ProjectStatus, - ): void { - const validTransitions: Record = { - [ProjectStatus.LICITACION]: [ProjectStatus.ADJUDICADO], - [ProjectStatus.ADJUDICADO]: [ProjectStatus.EJECUCION], - [ProjectStatus.EJECUCION]: [ProjectStatus.ENTREGADO], - [ProjectStatus.ENTREGADO]: [ProjectStatus.CERRADO], - [ProjectStatus.CERRADO]: [], // Estado final, no hay transiciones - }; - - const allowedTransitions = validTransitions[currentStatus]; - - if (!allowedTransitions.includes(newStatus)) { - throw new BadRequestException( - `No se puede cambiar de estado "${currentStatus}" a "${newStatus}". ` + - `Transiciones permitidas: ${allowedTransitions.join(', ') || 'ninguna (estado final)'}`, - ); - } - } -} -``` - -### 4.2 ProjectMetricsService - -**Archivo:** `services/project-metrics.service.ts` - -```typescript -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Project } from '../entities/project.entity'; - -/** - * Servicio de c谩lculo de m茅tricas del proyecto - */ -@Injectable() -export class ProjectMetricsService { - constructor( - @InjectRepository(Project) - private readonly projectRepo: Repository, - ) {} - - /** - * Calcular todas las m茅tricas del proyecto - * - * @param project - Proyecto a analizar - * @returns Objeto con m茅tricas f铆sicas, financieras y temporales - */ - async calculateMetrics(project: Project): Promise<{ - physical: { - progress: number; - totalUnits: number; - delivered: number; - pending: number; - deliveryRate: number; - }; - financial: { - budget: number; - exercised: number; - available: number; - progress: number; - deviation: number; - deviationStatus: 'green' | 'yellow' | 'red'; - }; - temporal: { - contractDuration: number; - elapsedMonths: number; - remainingMonths: number; - scheduledEnd: Date; - actualEnd: Date | null; - deviation: number; - deviationStatus: 'green' | 'yellow' | 'red'; - }; - }> { - // Calcular avance f铆sico (promedio de stages) - const physicalProgress = await this.calculatePhysicalProgress(project.id); - - // Calcular avance financiero - const financialProgress = (project.exercisedCost / project.contractAmount) * 100; - - // Calcular desviaci贸n presupuestal - const budgetDeviation = this.calculateBudgetDeviation( - project.contractAmount, - project.exercisedCost, - ); - - // Calcular desviaci贸n temporal - const temporalDeviation = this.calculateTemporalDeviation(project, physicalProgress); - - // Actualizar m茅tricas en BD - project.physicalProgress = physicalProgress; - project.budgetDeviation = budgetDeviation; - await this.projectRepo.save(project); - - return { - physical: { - progress: physicalProgress, - totalUnits: project.totalHousingUnits, - delivered: project.deliveredHousingUnits, - pending: project.totalHousingUnits - project.deliveredHousingUnits, - deliveryRate: project.totalHousingUnits > 0 - ? (project.deliveredHousingUnits / project.totalHousingUnits) * 100 - : 0, - }, - financial: { - budget: project.contractAmount, - exercised: project.exercisedCost, - available: project.contractAmount - project.exercisedCost, - progress: financialProgress, - deviation: budgetDeviation, - deviationStatus: this.getDeviationStatus(budgetDeviation), - }, - temporal: { - contractDuration: project.contractDuration, - elapsedMonths: this.calculateElapsedMonths(project), - remainingMonths: this.calculateRemainingMonths(project), - scheduledEnd: project.scheduledEndDate, - actualEnd: project.actualEndDate, - deviation: temporalDeviation, - deviationStatus: this.getDeviationStatus(temporalDeviation), - }, - }; - } - - /** - * Calcular avance f铆sico promedio del proyecto - * Basado en el promedio de avances de todas las etapas - */ - private async calculatePhysicalProgress(projectId: string): Promise { - const result = await this.projectRepo.query( - ` - SELECT COALESCE(AVG(s.physical_progress), 0) as avg_progress - FROM projects.stages s - WHERE s.project_id = $1 - `, - [projectId], - ); - - return parseFloat(result[0]?.avg_progress || '0'); - } - - /** - * Calcular desviaci贸n presupuestal en porcentaje - */ - private calculateBudgetDeviation(budget: number, exercised: number): number { - if (budget === 0) return 0; - return ((exercised - budget) / budget) * 100; - } - - /** - * Calcular desviaci贸n temporal comparando avance real vs programado - */ - private calculateTemporalDeviation( - project: Project, - physicalProgress: number, - ): number { - const now = new Date(); - const totalDays = - (project.scheduledEndDate.getTime() - project.contractStartDate.getTime()) / - (1000 * 60 * 60 * 24); - const elapsedDays = - (now.getTime() - project.contractStartDate.getTime()) / (1000 * 60 * 60 * 24); - - const expectedProgress = (elapsedDays / totalDays) * 100; - return physicalProgress - expectedProgress; // Positivo = adelantado, Negativo = atrasado - } - - /** - * Calcular meses transcurridos desde el inicio - */ - private calculateElapsedMonths(project: Project): number { - const now = new Date(); - const start = project.actualStartDate || project.contractStartDate; - const months = - (now.getFullYear() - start.getFullYear()) * 12 + - (now.getMonth() - start.getMonth()); - return Math.max(0, months); - } - - /** - * Calcular meses restantes hasta la fecha programada - */ - private calculateRemainingMonths(project: Project): number { - const now = new Date(); - const months = - (project.scheduledEndDate.getFullYear() - now.getFullYear()) * 12 + - (project.scheduledEndDate.getMonth() - now.getMonth()); - return Math.max(0, months); - } - - /** - * Determinar status de sem谩foro seg煤n desviaci贸n - * Verde: 卤5% - * Amarillo: 卤5% a 卤15% - * Rojo: > 卤15% - */ - private getDeviationStatus(deviation: number): 'green' | 'yellow' | 'red' { - const abs = Math.abs(deviation); - if (abs <= 5) return 'green'; - if (abs <= 15) return 'yellow'; - return 'red'; - } -} -``` - ---- - -## 5. Controllers - -### 5.1 ProjectsController - -**Archivo:** `controllers/projects.controller.ts` - -```typescript -import { - Controller, - Get, - Post, - Patch, - Delete, - Param, - Body, - Query, - UseGuards, - Request, - HttpCode, - HttpStatus, - ParseUUIDPipe, -} from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBearerAuth, - ApiParam, - ApiQuery, -} from '@nestjs/swagger'; -import { ProjectsService } from '../services/projects.service'; -import { ProjectMetricsService } from '../services/project-metrics.service'; -import { CreateProjectDto } from '../dto/create-project.dto'; -import { UpdateProjectDto } from '../dto/update-project.dto'; -import { FilterProjectDto } from '../dto/filter-project.dto'; -import { ChangeStatusDto } from '../dto/change-status.dto'; -import { ProjectResponseDto } from '../dto/project-response.dto'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; -import { RolesGuard } from '../../auth/guards/roles.guard'; -import { Roles } from '../../auth/decorators/roles.decorator'; -import { Constructora } from '../decorators/constructora.decorator'; - -/** - * Controlador de proyectos - * - * Endpoints: - * - POST /projects - Crear proyecto - * - GET /projects - Listar proyectos con filtros - * - GET /projects/:id - Obtener proyecto por ID - * - PATCH /projects/:id - Actualizar proyecto - * - DELETE /projects/:id - Eliminar proyecto (solo LICITACION) - * - POST /projects/:id/status - Cambiar estado del proyecto - * - GET /projects/:id/metrics - Obtener m茅tricas del proyecto - */ -@ApiTags('Projects') -@ApiBearerAuth() -@Controller('projects') -@UseGuards(JwtAuthGuard, RolesGuard) -export class ProjectsController { - constructor( - private readonly projectsService: ProjectsService, - private readonly metricsService: ProjectMetricsService, - ) {} - - /** - * Crear nuevo proyecto - * - * Roles permitidos: director, engineer - */ - @Post() - @Roles('director', 'engineer') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ - summary: 'Crear nuevo proyecto', - description: 'Crea un nuevo proyecto de construcci贸n con c贸digo autogenerado. ' + - 'El proyecto inicia en estado LICITACION si tiene biddingDate, ' + - 'o ADJUDICADO si no la tiene.', - }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: 'Proyecto creado exitosamente', - type: ProjectResponseDto, - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Datos inv谩lidos (validaci贸n fall贸)', - }) - @ApiResponse({ - status: HttpStatus.UNAUTHORIZED, - description: 'No autenticado', - }) - @ApiResponse({ - status: HttpStatus.FORBIDDEN, - description: 'No autorizado (rol insuficiente)', - }) - async create( - @Body() dto: CreateProjectDto, - @Constructora() constructoraId: string, - @Request() req: any, - ): Promise { - const project = await this.projectsService.create( - dto, - constructoraId, - req.user.userId, - ); - return project; - } - - /** - * Listar proyectos con filtros y paginaci贸n - * - * Roles permitidos: todos - */ - @Get() - @Roles('director', 'engineer', 'resident', 'purchases', 'finance', 'hr') - @ApiOperation({ - summary: 'Listar proyectos', - description: 'Lista todos los proyectos de la constructora con filtros opcionales ' + - 'y paginaci贸n. Soporta b煤squeda por nombre/c贸digo.', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Lista de proyectos obtenida exitosamente', - type: [ProjectResponseDto], - }) - async findAll( - @Constructora() constructoraId: string, - @Query() filters: FilterProjectDto, - ) { - return this.projectsService.findAll(constructoraId, filters); - } - - /** - * Obtener proyecto por ID - * - * Roles permitidos: todos - */ - @Get(':id') - @Roles('director', 'engineer', 'resident', 'purchases', 'finance', 'hr') - @ApiOperation({ - summary: 'Obtener proyecto por ID', - description: 'Obtiene los detalles completos de un proyecto incluyendo ' + - 'relaciones (stages, teamAssignments, documents)', - }) - @ApiParam({ - name: 'id', - description: 'UUID del proyecto', - example: '123e4567-e89b-12d3-a456-426614174000', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Proyecto encontrado', - type: ProjectResponseDto, - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Proyecto no encontrado o no pertenece a la constructora', - }) - async findOne( - @Param('id', ParseUUIDPipe) id: string, - @Constructora() constructoraId: string, - ): Promise { - return this.projectsService.findOne(id, constructoraId, [ - 'stages', - 'teamAssignments', - 'documents', - ]); - } - - /** - * Actualizar proyecto - * - * Roles permitidos: director, engineer - */ - @Patch(':id') - @Roles('director', 'engineer') - @ApiOperation({ - summary: 'Actualizar proyecto', - description: 'Actualiza los datos del proyecto. Para cambiar el estado, ' + - 'usar el endpoint POST /projects/:id/status', - }) - @ApiParam({ - name: 'id', - description: 'UUID del proyecto', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Proyecto actualizado exitosamente', - type: ProjectResponseDto, - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Datos inv谩lidos', - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Proyecto no encontrado', - }) - async update( - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: UpdateProjectDto, - @Constructora() constructoraId: string, - @Request() req: any, - ): Promise { - return this.projectsService.update( - id, - dto, - constructoraId, - req.user.userId, - ); - } - - /** - * Eliminar proyecto - * Solo permitido si est谩 en estado LICITACION - * - * Roles permitidos: director - */ - @Delete(':id') - @Roles('director') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ - summary: 'Eliminar proyecto', - description: 'Elimina un proyecto. Solo permitido si el proyecto est谩 en estado LICITACION.', - }) - @ApiParam({ - name: 'id', - description: 'UUID del proyecto', - }) - @ApiResponse({ - status: HttpStatus.NO_CONTENT, - description: 'Proyecto eliminado exitosamente', - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'No se puede eliminar el proyecto (estado diferente a LICITACION)', - }) - @ApiResponse({ - status: HttpStatus.NOT_FOUND, - description: 'Proyecto no encontrado', - }) - async remove( - @Param('id', ParseUUIDPipe) id: string, - @Constructora() constructoraId: string, - ): Promise { - await this.projectsService.remove(id, constructoraId); - } - - /** - * Cambiar estado del proyecto con validaciones - * - * Roles permitidos: director, engineer, resident - */ - @Post(':id/status') - @Roles('director', 'engineer', 'resident') - @ApiOperation({ - summary: 'Cambiar estado del proyecto', - description: 'Cambia el estado del proyecto validando transiciones permitidas. ' + - 'Actualiza autom谩ticamente las fechas correspondientes seg煤n el nuevo estado.', - }) - @ApiParam({ - name: 'id', - description: 'UUID del proyecto', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Estado cambiado exitosamente', - type: ProjectResponseDto, - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Transici贸n de estado inv谩lida', - }) - async changeStatus( - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: ChangeStatusDto, - @Constructora() constructoraId: string, - @Request() req: any, - ): Promise { - return this.projectsService.changeStatus( - id, - dto, - constructoraId, - req.user.userId, - ); - } - - /** - * Obtener m茅tricas del proyecto - * - * Roles permitidos: todos - */ - @Get(':id/metrics') - @Roles('director', 'engineer', 'resident', 'purchases', 'finance', 'hr') - @ApiOperation({ - summary: 'Obtener m茅tricas del proyecto', - description: 'Calcula y retorna m茅tricas f铆sicas, financieras y temporales ' + - 'del proyecto en tiempo real.', - }) - @ApiParam({ - name: 'id', - description: 'UUID del proyecto', - }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'M茅tricas calculadas exitosamente', - schema: { - example: { - physical: { - progress: 78.5, - totalUnits: 250, - delivered: 187, - pending: 63, - deliveryRate: 74.8, - }, - financial: { - budget: 125000000, - exercised: 97125000, - available: 27875000, - progress: 77.7, - deviation: 2.5, - deviationStatus: 'yellow', - }, - temporal: { - contractDuration: 24, - elapsedMonths: 18, - remainingMonths: 6, - scheduledEnd: '2026-05-15', - actualEnd: null, - deviation: -1.5, - deviationStatus: 'green', - }, - }, - }, - }) - async getMetrics( - @Param('id', ParseUUIDPipe) id: string, - @Constructora() constructoraId: string, - ) { - const project = await this.projectsService.findOne(id, constructoraId); - return this.metricsService.calculateMetrics(project); - } -} -``` - ---- - -## 6. Guards y Decoradores - -### 6.1 Decorador Custom: Constructora - -**Archivo:** `decorators/constructora.decorator.ts` - -```typescript -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; - -/** - * Decorador custom para extraer el constructoraId del usuario autenticado - * Uso: @Constructora() constructoraId: string - */ -export const Constructora = createParamDecorator( - (data: unknown, ctx: ExecutionContext): string => { - const request = ctx.switchToHttp().getRequest(); - return request.user?.constructoraId; - }, -); -``` - -### 6.2 Guard: ProjectAccessGuard - -**Archivo:** `guards/project-access.guard.ts` - -```typescript -import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; -import { ProjectsService } from '../services/projects.service'; - -/** - * Guard para verificar que el usuario tenga acceso al proyecto - * Valida que el proyecto pertenezca a la constructora del usuario - */ -@Injectable() -export class ProjectAccessGuard implements CanActivate { - constructor(private readonly projectsService: ProjectsService) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const projectId = request.params.id; - const constructoraId = request.user?.constructoraId; - - if (!projectId || !constructoraId) { - throw new ForbiddenException('Acceso denegado'); - } - - try { - await this.projectsService.findOne(projectId, constructoraId); - return true; - } catch (error) { - throw new ForbiddenException('No tienes acceso a este proyecto'); - } - } -} -``` - ---- - -## 7. Event Listeners - -### 7.1 ProjectStatusListener - -**Archivo:** `listeners/project-status.listener.ts` - -```typescript -import { Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; - -/** - * Listener de eventos de cambio de estado de proyecto - */ -@Injectable() -export class ProjectStatusListener { - private readonly logger = new Logger(ProjectStatusListener.name); - - @OnEvent('project.created') - handleProjectCreated(payload: any) { - this.logger.log( - `Proyecto creado: ${payload.projectCode} (ID: ${payload.projectId}) ` + - `por usuario ${payload.createdBy}`, - ); - // Aqu铆 se pueden agregar acciones adicionales: - // - Enviar email de notificaci贸n - // - Crear registros de auditor铆a - // - Inicializar configuraciones por defecto - } - - @OnEvent('project.status.changed') - handleStatusChanged(payload: any) { - this.logger.log( - `Proyecto ${payload.projectId} cambi贸 de estado: ` + - `${payload.oldStatus} 鈫 ${payload.newStatus}`, - ); - // Acciones seg煤n el nuevo estado: - // - EJECUCION: Notificar al equipo de obra - // - ENTREGADO: Generar reporte de cierre - // - CERRADO: Archivar documentos - } - - @OnEvent('project.deleted') - handleProjectDeleted(payload: any) { - this.logger.warn(`Proyecto eliminado: ${payload.projectId}`); - // Limpiar datos relacionados si es necesario - } -} -``` - ---- - -## 8. Module Configuration - -### 8.1 ProjectsModule - -**Archivo:** `projects.module.ts` - -```typescript -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ProjectsController } from './controllers/projects.controller'; -import { ProjectsService } from './services/projects.service'; -import { ProjectMetricsService } from './services/project-metrics.service'; -import { Project } from './entities/project.entity'; -import { Stage } from './entities/stage.entity'; -import { ProjectTeamAssignment } from './entities/project-team-assignment.entity'; -import { ProjectDocument } from './entities/project-document.entity'; -import { ProjectStatusListener } from './listeners/project-status.listener'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([ - Project, - Stage, - ProjectTeamAssignment, - ProjectDocument, - ]), - ], - controllers: [ProjectsController], - providers: [ - ProjectsService, - ProjectMetricsService, - ProjectStatusListener, - ], - exports: [ProjectsService, ProjectMetricsService], -}) -export class ProjectsModule {} -``` - ---- - -## 9. Migraciones TypeORM - -### 9.1 Migraci贸n: CreateProjectsTable - -**Archivo:** `migrations/1701234567890-CreateProjectsTable.ts` - -```typescript -import { MigrationInterface, QueryRunner, Table, TableIndex, TableCheck } from 'typeorm'; - -export class CreateProjectsTable1701234567890 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - // Crear schema si no existe - await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS projects`); - - // Crear tabla projects - await queryRunner.createTable( - new Table({ - name: 'projects', - schema: 'projects', - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'uuid_generate_v4()', - }, - { - name: 'project_code', - type: 'varchar', - length: '20', - isUnique: true, - }, - { - name: 'constructora_id', - type: 'uuid', - }, - { - name: 'name', - type: 'varchar', - length: '200', - }, - { - name: 'description', - type: 'text', - isNullable: true, - }, - { - name: 'project_type', - type: 'enum', - enum: ['fraccionamiento_horizontal', 'conjunto_habitacional', 'edificio_vertical', 'mixto'], - }, - { - name: 'status', - type: 'enum', - enum: ['licitacion', 'adjudicado', 'ejecucion', 'entregado', 'cerrado'], - default: "'licitacion'", - }, - { - name: 'client_type', - type: 'enum', - enum: ['publico', 'privado', 'mixto'], - }, - { - name: 'client_name', - type: 'varchar', - length: '200', - }, - { - name: 'client_rfc', - type: 'varchar', - length: '13', - }, - { - name: 'client_contact_name', - type: 'varchar', - length: '100', - isNullable: true, - }, - { - name: 'client_contact_email', - type: 'varchar', - length: '100', - isNullable: true, - }, - { - name: 'client_contact_phone', - type: 'varchar', - length: '20', - isNullable: true, - }, - { - name: 'contract_type', - type: 'enum', - enum: ['llave_en_mano', 'precio_alzado', 'administracion', 'mixto'], - }, - { - name: 'contract_amount', - type: 'decimal', - precision: 15, - scale: 2, - }, - { - name: 'address', - type: 'text', - }, - { - name: 'state', - type: 'varchar', - length: '100', - }, - { - name: 'municipality', - type: 'varchar', - length: '100', - }, - { - name: 'postal_code', - type: 'varchar', - length: '5', - }, - { - name: 'latitude', - type: 'decimal', - precision: 10, - scale: 6, - isNullable: true, - }, - { - name: 'longitude', - type: 'decimal', - precision: 10, - scale: 6, - isNullable: true, - }, - { - name: 'total_area', - type: 'decimal', - precision: 12, - scale: 2, - }, - { - name: 'buildable_area', - type: 'decimal', - precision: 12, - scale: 2, - }, - { - name: 'bidding_date', - type: 'date', - isNullable: true, - }, - { - name: 'award_date', - type: 'date', - isNullable: true, - }, - { - name: 'contract_start_date', - type: 'date', - }, - { - name: 'actual_start_date', - type: 'date', - isNullable: true, - }, - { - name: 'contract_duration', - type: 'integer', - }, - { - name: 'scheduled_end_date', - type: 'date', - }, - { - name: 'actual_end_date', - type: 'date', - isNullable: true, - }, - { - name: 'delivery_date', - type: 'date', - isNullable: true, - }, - { - name: 'closure_date', - type: 'date', - isNullable: true, - }, - { - name: 'construction_license_number', - type: 'varchar', - length: '50', - isNullable: true, - }, - { - name: 'license_issue_date', - type: 'date', - isNullable: true, - }, - { - name: 'license_expiration_date', - type: 'date', - isNullable: true, - }, - { - name: 'environmental_impact_number', - type: 'varchar', - length: '50', - isNullable: true, - }, - { - name: 'land_use_approved', - type: 'varchar', - length: '20', - isNullable: true, - }, - { - name: 'approved_plan_number', - type: 'varchar', - length: '50', - isNullable: true, - }, - { - name: 'infonavit_number', - type: 'varchar', - length: '50', - isNullable: true, - }, - { - name: 'fovissste_number', - type: 'varchar', - length: '50', - isNullable: true, - }, - { - name: 'total_housing_units', - type: 'integer', - default: 0, - }, - { - name: 'delivered_housing_units', - type: 'integer', - default: 0, - }, - { - name: 'physical_progress', - type: 'decimal', - precision: 5, - scale: 2, - default: 0, - }, - { - name: 'exercised_cost', - type: 'decimal', - precision: 15, - scale: 2, - default: 0, - }, - { - name: 'budget_deviation', - type: 'decimal', - precision: 5, - scale: 2, - default: 0, - }, - { - name: 'created_at', - type: 'timestamp', - default: 'CURRENT_TIMESTAMP', - }, - { - name: 'updated_at', - type: 'timestamp', - default: 'CURRENT_TIMESTAMP', - onUpdate: 'CURRENT_TIMESTAMP', - }, - { - name: 'created_by', - type: 'uuid', - }, - { - name: 'updated_by', - type: 'uuid', - isNullable: true, - }, - ], - }), - true, - ); - - // Crear 铆ndices - await queryRunner.createIndex( - 'projects.projects', - new TableIndex({ - name: 'idx_projects_constructora_status', - columnNames: ['constructora_id', 'status'], - }), - ); - - await queryRunner.createIndex( - 'projects.projects', - new TableIndex({ - name: 'idx_projects_code', - columnNames: ['project_code'], - }), - ); - - await queryRunner.createIndex( - 'projects.projects', - new TableIndex({ - name: 'idx_projects_dates', - columnNames: ['contract_start_date', 'scheduled_end_date'], - }), - ); - - // Crear constraints - await queryRunner.createCheckConstraint( - 'projects.projects', - new TableCheck({ - name: 'chk_contract_amount_positive', - expression: 'contract_amount > 0', - }), - ); - - await queryRunner.createCheckConstraint( - 'projects.projects', - new TableCheck({ - name: 'chk_total_area_positive', - expression: 'total_area > 0', - }), - ); - - await queryRunner.createCheckConstraint( - 'projects.projects', - new TableCheck({ - name: 'chk_buildable_area_valid', - expression: 'buildable_area > 0 AND buildable_area <= total_area', - }), - ); - - // Crear policy de Row Level Security (RLS) - await queryRunner.query(` - ALTER TABLE projects.projects ENABLE ROW LEVEL SECURITY; - `); - - await queryRunner.query(` - CREATE POLICY project_isolation ON projects.projects - USING (constructora_id = current_setting('app.current_constructora_id')::uuid); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('projects.projects', true); - await queryRunner.query(`DROP SCHEMA IF EXISTS projects CASCADE`); - } -} -``` - ---- - -## 10. Tests Unitarios - -### 10.1 ProjectsService Tests - -**Archivo:** `__tests__/projects.service.spec.ts` - -```typescript -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { ProjectsService } from '../services/projects.service'; -import { Project, ProjectStatus } from '../entities/project.entity'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; - -describe('ProjectsService', () => { - let service: ProjectsService; - let repository: Repository; - let eventEmitter: EventEmitter2; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ProjectsService, - { - provide: getRepositoryToken(Project), - useClass: Repository, - }, - { - provide: EventEmitter2, - useValue: { - emit: jest.fn(), - }, - }, - ], - }).compile(); - - service = module.get(ProjectsService); - repository = module.get>(getRepositoryToken(Project)); - eventEmitter = module.get(EventEmitter2); - }); - - describe('generateProjectCode', () => { - it('should generate unique project code with format PROJ-YYYY-XXX', async () => { - jest.spyOn(repository, 'createQueryBuilder').mockReturnValue({ - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - getOne: jest.fn().mockResolvedValue(null), - } as any); - - const code = await service['generateProjectCode']('constructora-uuid'); - - expect(code).toMatch(/^PROJ-\d{4}-\d{3}$/); - expect(code).toContain(new Date().getFullYear().toString()); - }); - - it('should increment sequence when existing project found', async () => { - const lastProject = { projectCode: 'PROJ-2025-005' } as Project; - - jest.spyOn(repository, 'createQueryBuilder').mockReturnValue({ - where: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - getOne: jest.fn().mockResolvedValue(lastProject), - } as any); - - const code = await service['generateProjectCode']('constructora-uuid'); - - expect(code).toBe('PROJ-2025-006'); - }); - }); - - describe('validateStatusTransition', () => { - it('should allow valid transition: LICITACION 鈫 ADJUDICADO', () => { - expect(() => - service['validateStatusTransition']( - ProjectStatus.LICITACION, - ProjectStatus.ADJUDICADO, - ), - ).not.toThrow(); - }); - - it('should reject invalid transition: LICITACION 鈫 EJECUCION', () => { - expect(() => - service['validateStatusTransition']( - ProjectStatus.LICITACION, - ProjectStatus.EJECUCION, - ), - ).toThrow(BadRequestException); - }); - - it('should reject transition from CERRADO', () => { - expect(() => - service['validateStatusTransition']( - ProjectStatus.CERRADO, - ProjectStatus.EJECUCION, - ), - ).toThrow(BadRequestException); - }); - }); - - describe('calculateScheduledEndDate', () => { - it('should calculate end date correctly', () => { - const startDate = new Date('2025-06-01'); - const duration = 24; // meses - - const endDate = service['calculateScheduledEndDate'](startDate, duration); - - expect(endDate).toEqual(new Date('2027-06-01')); - }); - }); - - describe('create', () => { - it('should create project with auto-generated code', async () => { - const createDto = { - name: 'Test Project', - contractStartDate: '2025-06-01', - contractDuration: 24, - totalArea: 100000, - buildableArea: 80000, - } as any; - - const savedProject = { - id: 'uuid', - projectCode: 'PROJ-2025-001', - ...createDto, - } as Project; - - jest.spyOn(service as any, 'generateProjectCode').mockResolvedValue('PROJ-2025-001'); - jest.spyOn(repository, 'create').mockReturnValue(savedProject); - jest.spyOn(repository, 'save').mockResolvedValue(savedProject); - - const result = await service.create(createDto, 'constructora-id', 'user-id'); - - expect(result.projectCode).toBe('PROJ-2025-001'); - expect(eventEmitter.emit).toHaveBeenCalledWith('project.created', expect.any(Object)); - }); - - it('should reject if buildableArea > totalArea', async () => { - const createDto = { - totalArea: 100000, - buildableArea: 150000, - } as any; - - await expect( - service.create(createDto, 'constructora-id', 'user-id'), - ).rejects.toThrow(BadRequestException); - }); - }); -}); -``` - ---- - -## 11. Configuraci贸n de Swagger - -### 11.1 Swagger Tags y Metadata - -```typescript -// main.ts -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; - -const config = new DocumentBuilder() - .setTitle('ERP Construcci贸n - API') - .setDescription('API REST para gesti贸n de proyectos de construcci贸n inmobiliaria') - .setVersion('1.0') - .addTag('Projects', 'Gesti贸n de proyectos de construcci贸n') - .addBearerAuth() - .build(); - -const document = SwaggerModule.createDocument(app, config); -SwaggerModule.setup('api/docs', app, document); -``` - ---- - -## 12. Validaciones Custom - -### 12.1 Validador: IsAfter - -**Archivo:** `validators/is-after.validator.ts` - -```typescript -import { - registerDecorator, - ValidationOptions, - ValidationArguments, -} from 'class-validator'; - -/** - * Decorador personalizado para validar que una fecha sea posterior a otra - */ -export function IsAfter(property: string, validationOptions?: ValidationOptions) { - return function (object: Object, propertyName: string) { - registerDecorator({ - name: 'isAfter', - target: object.constructor, - propertyName: propertyName, - constraints: [property], - options: validationOptions, - validator: { - validate(value: any, args: ValidationArguments) { - const [relatedPropertyName] = args.constraints; - const relatedValue = (args.object as any)[relatedPropertyName]; - return ( - typeof value === 'string' && - typeof relatedValue === 'string' && - new Date(value) > new Date(relatedValue) - ); - }, - defaultMessage(args: ValidationArguments) { - const [relatedPropertyName] = args.constraints; - return `${propertyName} debe ser posterior a ${relatedPropertyName}`; - }, - }, - }); - }; -} -``` - ---- - -**Fecha de creaci贸n:** 2025-12-06 -**Versi贸n:** 1.0 -**Autor:** Claude Opus 4.5 diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-database.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-database.md deleted file mode 100644 index 66f644b54..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-database.md +++ /dev/null @@ -1,2561 +0,0 @@ -# DDL-SPEC: Schema project_management - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **Schema** | project_management | -| **Modulo** | MAI-002 | -| **Version** | 1.0 | -| **Estado** | En Diseno | -| **Autor** | Requirements-Analyst | -| **Fecha** | 2025-12-06 | - ---- - -## Descripcion General - -El schema `project_management` contiene todas las tablas para la gestion completa de proyectos de construccion, incluyendo la jerarquia de 5 niveles (Proyecto 鈫 Etapa 鈫 Manzana 鈫 Lote 鈫 Vivienda), prototipos de vivienda con versionado, asignacion de equipo con validacion de workload, calendario de hitos y fechas criticas. - -### Alcance - -- Catalogo de proyectos con 4 tipos y 5 estados del ciclo de vida -- Estructura jerarquica flexible (fraccionamiento, conjunto, torre, mixto) -- Prototipos de vivienda con versionado automatico -- Asignacion de equipo con validacion de limites por rol -- Calendario de hitos con dependencias y alertas automaticas -- Fases constructivas y avances fisicos ponderados -- Documentos y permisos legales por proyecto - -### RF Cubiertos - -| RF | Titulo | Tablas | -|----|--------|--------| -| RF-PROJ-001 | Catalogo de Proyectos | projects | -| RF-PROJ-002 | Estructura Jerarquica de Obra | stages, blocks, lots, housing_units | -| RF-PROJ-003 | Prototipos de Vivienda | housing_prototypes, prototype_versions | -| RF-PROJ-004 | Asignacion de Equipo y Calendario | project_team_assignments, project_milestones, critical_dates, construction_phases | - ---- - -## Diagrama Entidad-Relacion - -```mermaid -erDiagram - %% Jerarquia Principal - projects ||--o{ stages : "tiene" - projects ||--o{ housing_prototypes : "define" - projects ||--o{ project_team_assignments : "asigna" - projects ||--o{ project_milestones : "planifica" - projects ||--o{ critical_dates : "registra" - projects ||--o{ project_documents : "almacena" - - stages ||--o{ blocks : "contiene" - stages ||--o{ lots : "contiene_directos" - - blocks ||--o{ lots : "agrupa" - - lots ||--o{ housing_units : "construye" - - %% Prototipos - housing_prototypes ||--o{ prototype_versions : "versiones" - housing_prototypes ||--o{ housing_units : "asignado_a" - - %% Fases Constructivas - housing_units ||--o{ construction_phases : "progreso" - - %% Relaciones con Core - projects }o--|| tenants : "pertenece" - projects }o--|| contacts : "cliente" - housing_units }o--|| contacts : "derechohabiente" - - projects { - uuid id PK - uuid tenant_id FK - varchar project_code UK - varchar name - enum project_type - enum project_status - enum client_type - varchar client_name - decimal contract_amount - text address - date contract_start_date - date scheduled_end_date - int total_housing_units - decimal physical_progress - boolean is_active - } - - stages { - uuid id PK - uuid tenant_id FK - uuid project_id FK - varchar code - varchar name - enum stage_status - int stage_number - date planned_start_date - date planned_end_date - decimal progress - boolean is_active - } - - blocks { - uuid id PK - uuid tenant_id FK - uuid stage_id FK - varchar code - varchar name - varchar block_type - int total_lots - decimal total_area - boolean is_active - } - - lots { - uuid id PK - uuid tenant_id FK - uuid project_id FK - uuid stage_id FK - uuid block_id FK - varchar lot_number - enum lot_status - decimal lot_area - decimal front_meters - decimal depth_meters - decimal cadastral_value - decimal sale_price - uuid owner_id FK - date sale_date - boolean is_active - } - - housing_units { - uuid id PK - uuid tenant_id FK - uuid project_id FK - uuid lot_id FK - uuid prototype_id FK - varchar unit_code - enum unit_type - enum unit_status - decimal construction_area - decimal total_cost - decimal physical_progress - date construction_start_date - date estimated_delivery_date - jsonb prototype_snapshot - boolean is_active - } - - housing_prototypes { - uuid id PK - uuid tenant_id FK - uuid project_id FK - varchar code UK - varchar name - enum category - enum segment - int current_version - decimal construction_area - decimal land_area - int bedrooms - int bathrooms - decimal estimated_cost - boolean is_active - } - - prototype_versions { - uuid id PK - uuid tenant_id FK - uuid prototype_id FK - int version_number - varchar change_description - jsonb specifications - decimal estimated_cost - timestamptz created_at - uuid created_by FK - } - - project_team_assignments { - uuid id PK - uuid tenant_id FK - uuid project_id FK - uuid user_id FK - enum role_type - boolean is_primary - int workload_percentage - date assignment_start_date - date assignment_end_date - boolean is_active - } - - project_milestones { - uuid id PK - uuid tenant_id FK - uuid project_id FK - enum milestone_type - varchar name - date planned_date - date actual_date - enum status - jsonb dependencies - boolean is_active - } - - critical_dates { - uuid id PK - uuid tenant_id FK - uuid project_id FK - enum date_type - varchar name - date date_value - int alert_days_before - boolean alert_sent - boolean is_active - } - - construction_phases { - uuid id PK - uuid tenant_id FK - uuid housing_unit_id FK - enum phase_name - decimal weight_percentage - decimal progress_percentage - date start_date - date end_date - enum status - boolean is_active - } - - project_documents { - uuid id PK - uuid tenant_id FK - uuid project_id FK - enum document_type - varchar name - varchar file_path - date issue_date - date expiration_date - boolean is_active - } -``` - ---- - -## ENUMs - -### 1. project_type - -Tipos de proyectos de construccion. - -```sql -CREATE TYPE project_management.project_type AS ENUM ( - 'fraccionamiento_horizontal', - 'conjunto_habitacional', - 'edificio_vertical', - 'mixto' -); -``` - -**Descripcion:** -- `fraccionamiento_horizontal`: Proyecto con etapas, manzanas y lotes -- `conjunto_habitacional`: Proyecto sin manzanas (lotes directos) -- `edificio_vertical`: Torres con niveles y departamentos -- `mixto`: Combinacion de estructuras anteriores - ---- - -### 2. project_status - -Estados del ciclo de vida del proyecto. - -```sql -CREATE TYPE project_management.project_status AS ENUM ( - 'licitacion', - 'adjudicado', - 'ejecucion', - 'entregado', - 'cerrado' -); -``` - -**Descripcion:** -- `licitacion`: Proyecto en proceso de licitacion -- `adjudicado`: Proyecto ganado pero no iniciado -- `ejecucion`: Proyecto en construccion activa -- `entregado`: Proyecto terminado y entregado al cliente -- `cerrado`: Proyecto cerrado administrativamente - ---- - -### 3. client_type - -Tipo de cliente del proyecto. - -```sql -CREATE TYPE project_management.client_type AS ENUM ( - 'publico', - 'privado', - 'mixto' -); -``` - ---- - -### 4. contract_type - -Tipo de contrato del proyecto. - -```sql -CREATE TYPE project_management.contract_type AS ENUM ( - 'llave_en_mano', - 'precio_alzado', - 'administracion', - 'mixto' -); -``` - ---- - -### 5. stage_status - -Estado de la etapa del proyecto. - -```sql -CREATE TYPE project_management.stage_status AS ENUM ( - 'pending', - 'in_progress', - 'completed', - 'cancelled' -); -``` - ---- - -### 6. lot_status - -Estado del lote/terreno. - -```sql -CREATE TYPE project_management.lot_status AS ENUM ( - 'available', - 'reserved', - 'sold', - 'in_construction', - 'completed', - 'delivered', - 'cancelled' -); -``` - -**Descripcion:** -- `available`: Lote disponible para venta -- `reserved`: Lote reservado temporalmente -- `sold`: Lote vendido -- `in_construction`: Vivienda en construccion -- `completed`: Vivienda terminada -- `delivered`: Vivienda entregada al propietario -- `cancelled`: Lote cancelado - ---- - -### 7. unit_type - -Tipo de unidad habitacional. - -```sql -CREATE TYPE project_management.unit_type AS ENUM ( - 'casa_unifamiliar', - 'departamento', - 'duplex', - 'triplex', - 'local_comercial', - 'bodega' -); -``` - ---- - -### 8. unit_status - -Estado de la unidad habitacional. - -```sql -CREATE TYPE project_management.unit_status AS ENUM ( - 'planned', - 'in_construction', - 'completed', - 'delivered', - 'cancelled' -); -``` - ---- - -### 9. prototype_category - -Categoria del prototipo de vivienda. - -```sql -CREATE TYPE project_management.prototype_category AS ENUM ( - 'casa_unifamiliar', - 'departamento', - 'duplex', - 'triplex', - 'local_comercial' -); -``` - ---- - -### 10. prototype_segment - -Segmento de mercado del prototipo. - -```sql -CREATE TYPE project_management.prototype_segment AS ENUM ( - 'interes_social', - 'interes_medio', - 'residencial_medio', - 'residencial_alto', - 'premium' -); -``` - ---- - -### 11. team_role_type - -Roles del equipo de proyecto. - -```sql -CREATE TYPE project_management.team_role_type AS ENUM ( - 'director', - 'residente', - 'ingeniero_civil', - 'ingeniero_electrico', - 'ingeniero_hidraulico', - 'supervisor', - 'gerente_compras', - 'coordinador_calidad', - 'coordinador_seguridad' -); -``` - ---- - -### 12. milestone_type - -Tipos de hitos del proyecto. - -```sql -CREATE TYPE project_management.milestone_type AS ENUM ( - 'arranque', - 'preliminares', - 'cimentacion', - 'estructura', - 'albanileria', - 'instalaciones', - 'acabados', - 'exteriores', - 'urbanizacion', - 'entrega_parcial', - 'cierre_administrativo' -); -``` - ---- - -### 13. critical_date_type - -Tipos de fechas criticas. - -```sql -CREATE TYPE project_management.critical_date_type AS ENUM ( - 'contractual', - 'regulatory', - 'financial', - 'milestone', - 'other' -); -``` - ---- - -### 14. construction_phase_name - -Fases constructivas para avance fisico. - -```sql -CREATE TYPE project_management.construction_phase_name AS ENUM ( - 'preliminares', - 'cimentacion', - 'estructura', - 'muros_albanileria', - 'instalaciones_hidraulicas', - 'instalaciones_electricas', - 'instalaciones_sanitarias', - 'acabados_interiores', - 'acabados_exteriores' -); -``` - ---- - -### 15. construction_phase_status - -Estado de la fase constructiva. - -```sql -CREATE TYPE project_management.construction_phase_status AS ENUM ( - 'not_started', - 'in_progress', - 'completed', - 'on_hold' -); -``` - ---- - -### 16. document_type - -Tipos de documentos del proyecto. - -```sql -CREATE TYPE project_management.document_type AS ENUM ( - 'construction_license', - 'environmental_impact', - 'land_use_permit', - 'approved_plan', - 'infonavit_certification', - 'fovissste_certification', - 'contract', - 'other' -); -``` - ---- - -## Tablas Principales - -### 1. projects - -Catalogo principal de proyectos de construccion. - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: projects --- Descripcion: Catalogo principal de proyectos de construccion --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.projects ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Codigo auto-generado - project_code VARCHAR(20) NOT NULL UNIQUE, - - -- Informacion basica - name VARCHAR(200) NOT NULL, - description TEXT, - project_type project_management.project_type NOT NULL, - status project_management.project_status NOT NULL DEFAULT 'licitacion', - - -- Cliente - client_type project_management.client_type NOT NULL, - client_id UUID REFERENCES core_catalogs.contacts(id), - client_name VARCHAR(200) NOT NULL, - client_rfc VARCHAR(13), - client_contact_name VARCHAR(100), - client_contact_email VARCHAR(100), - client_contact_phone VARCHAR(20), - - -- Contrato - contract_type project_management.contract_type NOT NULL, - contract_number VARCHAR(50), - contract_amount DECIMAL(15,2) NOT NULL, - contract_currency_id UUID REFERENCES core_catalogs.currencies(id), - - -- Ubicacion - address TEXT NOT NULL, - state VARCHAR(100) NOT NULL, - municipality VARCHAR(100) NOT NULL, - postal_code VARCHAR(5), - country_id UUID REFERENCES core_catalogs.countries(id), - state_id UUID REFERENCES core_catalogs.states(id), - latitude DECIMAL(10,6), - longitude DECIMAL(10,6), - total_area DECIMAL(12,2) NOT NULL, -- m虏 - buildable_area DECIMAL(12,2) NOT NULL, -- m虏 - - -- Fechas - bidding_date DATE, - award_date DATE, - contract_start_date DATE NOT NULL, - actual_start_date DATE, - contract_duration INTEGER NOT NULL, -- meses - scheduled_end_date DATE NOT NULL, - actual_end_date DATE, - delivery_date DATE, - closure_date DATE, - - -- Informacion legal - construction_license_number VARCHAR(50), - license_issue_date DATE, - license_expiration_date DATE, - environmental_impact_number VARCHAR(50), - land_use_approved VARCHAR(20), - approved_plan_number VARCHAR(50), - infonavit_number VARCHAR(50), - fovissste_number VARCHAR(50), - - -- Metricas calculadas (triggers) - total_housing_units INTEGER NOT NULL DEFAULT 0, - delivered_housing_units INTEGER NOT NULL DEFAULT 0, - physical_progress DECIMAL(5,2) NOT NULL DEFAULT 0, -- % - financial_progress DECIMAL(5,2) NOT NULL DEFAULT 0, -- % - - -- Configuracion - settings JSONB DEFAULT '{}', - metadata JSONB DEFAULT '{}', - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT chk_projects_total_area CHECK (total_area > 0), - CONSTRAINT chk_projects_buildable_area CHECK (buildable_area > 0 AND buildable_area <= total_area), - CONSTRAINT chk_projects_contract_amount CHECK (contract_amount > 0), - CONSTRAINT chk_projects_duration CHECK (contract_duration > 0), - CONSTRAINT chk_projects_progress CHECK ( - physical_progress >= 0 AND physical_progress <= 100 AND - financial_progress >= 0 AND financial_progress <= 100 - ), - CONSTRAINT chk_projects_dates CHECK (scheduled_end_date > contract_start_date), - CONSTRAINT chk_projects_actual_dates CHECK ( - actual_end_date IS NULL OR actual_end_date >= actual_start_date - ) -); - --- Comentarios -COMMENT ON TABLE project_management.projects IS 'Catalogo principal de proyectos de construccion'; -COMMENT ON COLUMN project_management.projects.tenant_id IS 'ID de la constructora (multi-tenancy)'; -COMMENT ON COLUMN project_management.projects.project_code IS 'Codigo auto-generado: PROJ-2025-001'; -COMMENT ON COLUMN project_management.projects.project_type IS 'Tipo: fraccionamiento_horizontal, conjunto_habitacional, edificio_vertical, mixto'; -COMMENT ON COLUMN project_management.projects.status IS 'Estado: licitacion, adjudicado, ejecucion, entregado, cerrado'; -COMMENT ON COLUMN project_management.projects.total_housing_units IS 'Total de viviendas (calculado por trigger)'; -COMMENT ON COLUMN project_management.projects.physical_progress IS 'Avance fisico en porcentaje (calculado por trigger)'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: projects --- ============================================================================ - --- Indice obligatorio para RLS -CREATE INDEX idx_projects_tenant_id ON project_management.projects(tenant_id); - --- Indice para codigo unico -CREATE UNIQUE INDEX idx_projects_code ON project_management.projects(project_code); - --- Indices para busqueda y filtros -CREATE INDEX idx_projects_status ON project_management.projects(status) WHERE is_active = true; -CREATE INDEX idx_projects_type ON project_management.projects(project_type) WHERE is_active = true; -CREATE INDEX idx_projects_client_id ON project_management.projects(client_id); -CREATE INDEX idx_projects_created_at ON project_management.projects(created_at DESC); - --- Indice para busqueda full-text -CREATE INDEX idx_projects_search ON project_management.projects - USING gin(to_tsvector('spanish', - name || ' ' || - COALESCE(project_code, '') || ' ' || - COALESCE(client_name, '') || ' ' || - COALESCE(description, '') - )); - --- Indice para ubicacion geografica -CREATE INDEX idx_projects_location ON project_management.projects(state, municipality); - --- Indice compuesto para dashboard -CREATE INDEX idx_projects_tenant_status_active - ON project_management.projects(tenant_id, status, is_active); -``` - ---- - -### 2. stages - -Etapas del proyecto (1er nivel de jerarquia). - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: stages --- Descripcion: Etapas del proyecto (1er nivel de jerarquia) --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.stages ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion con proyecto - project_id UUID NOT NULL REFERENCES project_management.projects(id) ON DELETE CASCADE, - - -- Informacion basica - code VARCHAR(20) NOT NULL, - name VARCHAR(200) NOT NULL, - description TEXT, - status project_management.stage_status NOT NULL DEFAULT 'pending', - stage_number INTEGER NOT NULL, - - -- Fechas planificadas - planned_start_date DATE NOT NULL, - planned_end_date DATE NOT NULL, - actual_start_date DATE, - actual_end_date DATE, - - -- Metricas - total_lots INTEGER NOT NULL DEFAULT 0, - total_housing_units INTEGER NOT NULL DEFAULT 0, - progress DECIMAL(5,2) NOT NULL DEFAULT 0, -- % - - -- Configuracion - metadata JSONB DEFAULT '{}', - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT uq_stages_project_code UNIQUE (project_id, code), - CONSTRAINT uq_stages_project_number UNIQUE (project_id, stage_number), - CONSTRAINT chk_stages_dates CHECK (planned_end_date > planned_start_date), - CONSTRAINT chk_stages_actual_dates CHECK ( - actual_end_date IS NULL OR actual_end_date >= actual_start_date - ), - CONSTRAINT chk_stages_progress CHECK (progress >= 0 AND progress <= 100), - CONSTRAINT chk_stages_stage_number CHECK (stage_number > 0) -); - --- Comentarios -COMMENT ON TABLE project_management.stages IS 'Etapas del proyecto (1er nivel de jerarquia)'; -COMMENT ON COLUMN project_management.stages.stage_number IS 'Numero secuencial de la etapa dentro del proyecto'; -COMMENT ON COLUMN project_management.stages.progress IS 'Avance fisico de la etapa en porcentaje'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: stages --- ============================================================================ - -CREATE INDEX idx_stages_tenant_id ON project_management.stages(tenant_id); -CREATE INDEX idx_stages_project_id ON project_management.stages(project_id); -CREATE INDEX idx_stages_status ON project_management.stages(status) WHERE is_active = true; -CREATE INDEX idx_stages_project_number ON project_management.stages(project_id, stage_number); -``` - ---- - -### 3. blocks - -Manzanas del proyecto (2do nivel de jerarquia, solo para fraccionamientos). - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: blocks --- Descripcion: Manzanas (2do nivel jerarquia, solo fraccionamientos) --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.blocks ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion con etapa - stage_id UUID NOT NULL REFERENCES project_management.stages(id) ON DELETE CASCADE, - - -- Informacion basica - code VARCHAR(20) NOT NULL, - name VARCHAR(200) NOT NULL, - description TEXT, - block_type VARCHAR(50), -- comercial, residencial, mixto - - -- Metricas - total_lots INTEGER NOT NULL DEFAULT 0, - total_area DECIMAL(12,2), -- m虏 - - -- Configuracion - metadata JSONB DEFAULT '{}', - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT uq_blocks_stage_code UNIQUE (stage_id, code), - CONSTRAINT chk_blocks_total_area CHECK (total_area IS NULL OR total_area > 0) -); - --- Comentarios -COMMENT ON TABLE project_management.blocks IS 'Manzanas del proyecto (solo para fraccionamientos horizontales)'; -COMMENT ON COLUMN project_management.blocks.block_type IS 'Tipo de manzana: comercial, residencial, mixto'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: blocks --- ============================================================================ - -CREATE INDEX idx_blocks_tenant_id ON project_management.blocks(tenant_id); -CREATE INDEX idx_blocks_stage_id ON project_management.blocks(stage_id); -CREATE INDEX idx_blocks_stage_code ON project_management.blocks(stage_id, code); -``` - ---- - -### 4. lots - -Lotes/terrenos individuales (3er nivel de jerarquia). - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: lots --- Descripcion: Lotes/terrenos individuales (3er nivel jerarquia) --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.lots ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion jerarquica (project_id siempre, block_id opcional) - project_id UUID NOT NULL REFERENCES project_management.projects(id) ON DELETE CASCADE, - stage_id UUID NOT NULL REFERENCES project_management.stages(id) ON DELETE CASCADE, - block_id UUID REFERENCES project_management.blocks(id) ON DELETE CASCADE, - - -- Identificacion del lote - lot_number VARCHAR(20) NOT NULL, - cadastral_key VARCHAR(50), - - -- Estado - status project_management.lot_status NOT NULL DEFAULT 'available', - - -- Dimensiones - lot_area DECIMAL(12,2) NOT NULL, -- m虏 - front_meters DECIMAL(8,2), - depth_meters DECIMAL(8,2), - - -- Valores - cadastral_value DECIMAL(15,2), - base_price DECIMAL(15,2), - sale_price DECIMAL(15,2), - - -- Venta - owner_id UUID REFERENCES core_catalogs.contacts(id), - sale_date DATE, - sale_notes TEXT, - - -- Configuracion - metadata JSONB DEFAULT '{}', - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT uq_lots_project_stage_number UNIQUE (project_id, stage_id, lot_number), - CONSTRAINT chk_lots_area CHECK (lot_area > 0), - CONSTRAINT chk_lots_dimensions CHECK ( - (front_meters IS NULL AND depth_meters IS NULL) OR - (front_meters > 0 AND depth_meters > 0) - ), - CONSTRAINT chk_lots_prices CHECK ( - (cadastral_value IS NULL OR cadastral_value > 0) AND - (base_price IS NULL OR base_price > 0) AND - (sale_price IS NULL OR sale_price > 0) - ), - CONSTRAINT chk_lots_sold_has_owner CHECK ( - status != 'sold' OR owner_id IS NOT NULL - ) -); - --- Comentarios -COMMENT ON TABLE project_management.lots IS 'Lotes/terrenos individuales donde se construiran viviendas'; -COMMENT ON COLUMN project_management.lots.status IS 'Estado: available, reserved, sold, in_construction, completed, delivered'; -COMMENT ON COLUMN project_management.lots.block_id IS 'FK a blocks (NULL para conjuntos sin manzanas)'; -COMMENT ON COLUMN project_management.lots.owner_id IS 'FK a contacts (derechohabiente/comprador)'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: lots --- ============================================================================ - -CREATE INDEX idx_lots_tenant_id ON project_management.lots(tenant_id); -CREATE INDEX idx_lots_project_id ON project_management.lots(project_id); -CREATE INDEX idx_lots_stage_id ON project_management.lots(stage_id); -CREATE INDEX idx_lots_block_id ON project_management.lots(block_id); -CREATE INDEX idx_lots_owner_id ON project_management.lots(owner_id); -CREATE INDEX idx_lots_status ON project_management.lots(status) WHERE is_active = true; -CREATE INDEX idx_lots_project_stage_number ON project_management.lots(project_id, stage_id, lot_number); -``` - ---- - -### 5. housing_units - -Viviendas construidas en los lotes (4to nivel de jerarquia). - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: housing_units --- Descripcion: Viviendas construidas en los lotes (4to nivel jerarquia) --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.housing_units ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion jerarquica - project_id UUID NOT NULL REFERENCES project_management.projects(id) ON DELETE CASCADE, - lot_id UUID NOT NULL REFERENCES project_management.lots(id) ON DELETE CASCADE, - prototype_id UUID REFERENCES project_management.housing_prototypes(id), - - -- Identificacion - unit_code VARCHAR(50) NOT NULL, - unit_type project_management.unit_type NOT NULL, - status project_management.unit_status NOT NULL DEFAULT 'planned', - - -- Caracteristicas (heredadas del prototipo al momento de asignacion) - construction_area DECIMAL(10,2) NOT NULL, -- m虏 - land_area DECIMAL(10,2), - bedrooms INTEGER, - bathrooms DECIMAL(3,1), - parking_spaces INTEGER, - levels INTEGER, - - -- Costos - estimated_cost DECIMAL(15,2), - actual_cost DECIMAL(15,2), - total_cost DECIMAL(15,2), - - -- Avance - physical_progress DECIMAL(5,2) NOT NULL DEFAULT 0, -- % - - -- Fechas - construction_start_date DATE, - estimated_delivery_date DATE, - actual_delivery_date DATE, - - -- Snapshot del prototipo (JSONB) - prototype_snapshot JSONB, - - -- Configuracion - metadata JSONB DEFAULT '{}', - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT uq_housing_units_project_code UNIQUE (project_id, unit_code), - CONSTRAINT chk_housing_units_area CHECK (construction_area > 0), - CONSTRAINT chk_housing_units_rooms CHECK ( - bedrooms IS NULL OR bedrooms > 0 - ), - CONSTRAINT chk_housing_units_bathrooms CHECK ( - bathrooms IS NULL OR bathrooms > 0 - ), - CONSTRAINT chk_housing_units_costs CHECK ( - (estimated_cost IS NULL OR estimated_cost > 0) AND - (actual_cost IS NULL OR actual_cost >= 0) AND - (total_cost IS NULL OR total_cost >= 0) - ), - CONSTRAINT chk_housing_units_progress CHECK ( - physical_progress >= 0 AND physical_progress <= 100 - ), - CONSTRAINT chk_housing_units_dates CHECK ( - actual_delivery_date IS NULL OR - construction_start_date IS NULL OR - actual_delivery_date >= construction_start_date - ) -); - --- Comentarios -COMMENT ON TABLE project_management.housing_units IS 'Viviendas construidas en los lotes'; -COMMENT ON COLUMN project_management.housing_units.prototype_id IS 'FK al prototipo asignado (puede ser NULL si es custom)'; -COMMENT ON COLUMN project_management.housing_units.prototype_snapshot IS 'Snapshot JSON del prototipo al momento de asignacion'; -COMMENT ON COLUMN project_management.housing_units.physical_progress IS 'Avance fisico calculado desde construction_phases'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: housing_units --- ============================================================================ - -CREATE INDEX idx_housing_units_tenant_id ON project_management.housing_units(tenant_id); -CREATE INDEX idx_housing_units_project_id ON project_management.housing_units(project_id); -CREATE INDEX idx_housing_units_lot_id ON project_management.housing_units(lot_id); -CREATE INDEX idx_housing_units_prototype_id ON project_management.housing_units(prototype_id); -CREATE INDEX idx_housing_units_status ON project_management.housing_units(status) WHERE is_active = true; -CREATE INDEX idx_housing_units_project_code ON project_management.housing_units(project_id, unit_code); -``` - ---- - -### 6. housing_prototypes - -Catalogo de prototipos de vivienda. - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: housing_prototypes --- Descripcion: Catalogo de prototipos de vivienda con versionado --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.housing_prototypes ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion con proyecto (opcional, puede ser catalogo general) - project_id UUID REFERENCES project_management.projects(id), - - -- Identificacion - code VARCHAR(50) NOT NULL, - name VARCHAR(200) NOT NULL, - description TEXT, - - -- Clasificacion - category project_management.prototype_category NOT NULL, - segment project_management.prototype_segment NOT NULL, - - -- Versionado - current_version INTEGER NOT NULL DEFAULT 1, - - -- Caracteristicas basicas - construction_area DECIMAL(10,2) NOT NULL, -- m虏 - land_area DECIMAL(10,2), - sellable_area DECIMAL(10,2), - bedrooms INTEGER NOT NULL, - bathrooms DECIMAL(3,1) NOT NULL, - half_bathrooms INTEGER DEFAULT 0, - parking_spaces INTEGER DEFAULT 0, - levels INTEGER DEFAULT 1, - - -- Distribucion - has_kitchen BOOLEAN DEFAULT true, - has_dining_room BOOLEAN DEFAULT true, - has_living_room BOOLEAN DEFAULT true, - has_laundry_area BOOLEAN DEFAULT false, - has_patio BOOLEAN DEFAULT false, - has_garden BOOLEAN DEFAULT false, - has_balcony BOOLEAN DEFAULT false, - has_roof_garden BOOLEAN DEFAULT false, - - -- Acabados - floor_type VARCHAR(50), - wall_finish VARCHAR(50), - kitchen_finish VARCHAR(50), - bathroom_finish VARCHAR(50), - - -- Costos estimados - estimated_cost DECIMAL(15,2), - cost_per_sqm DECIMAL(10,2), - - -- Archivos - floor_plan_url TEXT, - facade_image_url TEXT, - render_images JSONB DEFAULT '[]', - - -- Especificaciones tecnicas (JSONB) - specifications JSONB DEFAULT '{}', - - -- Configuracion - metadata JSONB DEFAULT '{}', - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT uq_prototypes_tenant_code UNIQUE (tenant_id, code), - CONSTRAINT chk_prototypes_areas CHECK ( - construction_area > 0 AND - (land_area IS NULL OR land_area > 0) AND - (sellable_area IS NULL OR sellable_area > 0) - ), - CONSTRAINT chk_prototypes_rooms CHECK ( - bedrooms > 0 AND - bathrooms > 0 AND - half_bathrooms >= 0 AND - parking_spaces >= 0 AND - levels > 0 - ), - CONSTRAINT chk_prototypes_costs CHECK ( - (estimated_cost IS NULL OR estimated_cost > 0) AND - (cost_per_sqm IS NULL OR cost_per_sqm > 0) - ), - CONSTRAINT chk_prototypes_version CHECK (current_version > 0) -); - --- Comentarios -COMMENT ON TABLE project_management.housing_prototypes IS 'Catalogo de prototipos de vivienda con versionado automatico'; -COMMENT ON COLUMN project_management.housing_prototypes.project_id IS 'FK a proyecto (NULL = prototipo general del catalogo)'; -COMMENT ON COLUMN project_management.housing_prototypes.current_version IS 'Version actual del prototipo'; -COMMENT ON COLUMN project_management.housing_prototypes.specifications IS 'Especificaciones tecnicas en formato JSON'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: housing_prototypes --- ============================================================================ - -CREATE INDEX idx_prototypes_tenant_id ON project_management.housing_prototypes(tenant_id); -CREATE INDEX idx_prototypes_project_id ON project_management.housing_prototypes(project_id); -CREATE INDEX idx_prototypes_category ON project_management.housing_prototypes(category) WHERE is_active = true; -CREATE INDEX idx_prototypes_segment ON project_management.housing_prototypes(segment) WHERE is_active = true; -CREATE INDEX idx_prototypes_tenant_code ON project_management.housing_prototypes(tenant_id, code); - --- Indice para busqueda full-text -CREATE INDEX idx_prototypes_search ON project_management.housing_prototypes - USING gin(to_tsvector('spanish', - name || ' ' || - COALESCE(code, '') || ' ' || - COALESCE(description, '') - )); -``` - ---- - -### 7. prototype_versions - -Historial de versiones de prototipos. - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: prototype_versions --- Descripcion: Historial de versiones de prototipos --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.prototype_versions ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion con prototipo - prototype_id UUID NOT NULL REFERENCES project_management.housing_prototypes(id) ON DELETE CASCADE, - - -- Version - version_number INTEGER NOT NULL, - change_description TEXT NOT NULL, - - -- Snapshot completo de las especificaciones - specifications JSONB NOT NULL, - - -- Costos en esta version - estimated_cost DECIMAL(15,2), - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT uq_prototype_versions_prototype_version - UNIQUE (prototype_id, version_number), - CONSTRAINT chk_prototype_versions_number CHECK (version_number > 0) -); - --- Comentarios -COMMENT ON TABLE project_management.prototype_versions IS 'Historial de versiones de prototipos de vivienda'; -COMMENT ON COLUMN project_management.prototype_versions.specifications IS 'Snapshot completo del prototipo en esta version'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: prototype_versions --- ============================================================================ - -CREATE INDEX idx_prototype_versions_tenant_id ON project_management.prototype_versions(tenant_id); -CREATE INDEX idx_prototype_versions_prototype_id ON project_management.prototype_versions(prototype_id); -CREATE INDEX idx_prototype_versions_created_at ON project_management.prototype_versions(created_at DESC); -``` - ---- - -### 8. project_team_assignments - -Asignacion de equipo de trabajo al proyecto. - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: project_team_assignments --- Descripcion: Asignacion de equipo de trabajo al proyecto --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.project_team_assignments ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relaciones - project_id UUID NOT NULL REFERENCES project_management.projects(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES core_users.users(id) ON DELETE CASCADE, - - -- Rol en el proyecto - role_type project_management.team_role_type NOT NULL, - is_primary BOOLEAN NOT NULL DEFAULT false, - - -- Carga de trabajo - workload_percentage INTEGER NOT NULL DEFAULT 100, - - -- Vigencia - assignment_start_date DATE NOT NULL, - assignment_end_date DATE, - - -- Notas - notes TEXT, - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT uq_team_assignments_project_user_role - UNIQUE (project_id, user_id, role_type), - CONSTRAINT chk_team_assignments_workload - CHECK (workload_percentage > 0 AND workload_percentage <= 100), - CONSTRAINT chk_team_assignments_dates - CHECK (assignment_end_date IS NULL OR assignment_end_date > assignment_start_date) -); - --- Comentarios -COMMENT ON TABLE project_management.project_team_assignments IS 'Asignacion de equipo de trabajo a proyectos con validacion de workload'; -COMMENT ON COLUMN project_management.project_team_assignments.is_primary IS 'Indica si es el responsable principal (ej: Director principal)'; -COMMENT ON COLUMN project_management.project_team_assignments.workload_percentage IS 'Porcentaje de tiempo dedicado al proyecto (1-100)'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: project_team_assignments --- ============================================================================ - -CREATE INDEX idx_team_assignments_tenant_id ON project_management.project_team_assignments(tenant_id); -CREATE INDEX idx_team_assignments_project_id ON project_management.project_team_assignments(project_id); -CREATE INDEX idx_team_assignments_user_id ON project_management.project_team_assignments(user_id); -CREATE INDEX idx_team_assignments_role ON project_management.project_team_assignments(role_type); -CREATE INDEX idx_team_assignments_active ON project_management.project_team_assignments(is_active, assignment_end_date); -``` - ---- - -### 9. project_milestones - -Hitos del proyecto con dependencias. - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: project_milestones --- Descripcion: Hitos del proyecto con dependencias y estado --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.project_milestones ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion con proyecto - project_id UUID NOT NULL REFERENCES project_management.projects(id) ON DELETE CASCADE, - - -- Tipo de hito - milestone_type project_management.milestone_type NOT NULL, - - -- Informacion - name VARCHAR(200) NOT NULL, - description TEXT, - - -- Fechas - planned_date DATE NOT NULL, - actual_date DATE, - - -- Estado - status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, in_progress, completed, delayed - - -- Dependencias (array de IDs) - dependencies JSONB DEFAULT '[]', - - -- Responsable - responsible_user_id UUID REFERENCES core_users.users(id), - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT chk_milestones_status - CHECK (status IN ('pending', 'in_progress', 'completed', 'delayed', 'cancelled')) -); - --- Comentarios -COMMENT ON TABLE project_management.project_milestones IS 'Hitos del proyecto con dependencias y seguimiento'; -COMMENT ON COLUMN project_management.project_milestones.dependencies IS 'Array JSON de IDs de hitos prerequisitos'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: project_milestones --- ============================================================================ - -CREATE INDEX idx_milestones_tenant_id ON project_management.project_milestones(tenant_id); -CREATE INDEX idx_milestones_project_id ON project_management.project_milestones(project_id); -CREATE INDEX idx_milestones_type ON project_management.project_milestones(milestone_type); -CREATE INDEX idx_milestones_status ON project_management.project_milestones(status) WHERE is_active = true; -CREATE INDEX idx_milestones_planned_date ON project_management.project_milestones(planned_date); -CREATE INDEX idx_milestones_responsible ON project_management.project_milestones(responsible_user_id); -``` - ---- - -### 10. critical_dates - -Fechas criticas con alertas automaticas. - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: critical_dates --- Descripcion: Fechas criticas del proyecto con alertas automaticas --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.critical_dates ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion con proyecto - project_id UUID NOT NULL REFERENCES project_management.projects(id) ON DELETE CASCADE, - - -- Tipo de fecha - date_type project_management.critical_date_type NOT NULL, - - -- Informacion - name VARCHAR(200) NOT NULL, - description TEXT, - - -- Fecha - date_value DATE NOT NULL, - - -- Alertas - alert_days_before INTEGER NOT NULL DEFAULT 7, - alert_sent BOOLEAN NOT NULL DEFAULT false, - alert_sent_at TIMESTAMPTZ, - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT chk_critical_dates_alert_days - CHECK (alert_days_before >= 0 AND alert_days_before <= 365) -); - --- Comentarios -COMMENT ON TABLE project_management.critical_dates IS 'Fechas criticas con sistema de alertas automaticas'; -COMMENT ON COLUMN project_management.critical_dates.alert_days_before IS 'Dias antes de la fecha para enviar alerta'; -COMMENT ON COLUMN project_management.critical_dates.alert_sent IS 'Indica si ya se envio la alerta'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: critical_dates --- ============================================================================ - -CREATE INDEX idx_critical_dates_tenant_id ON project_management.critical_dates(tenant_id); -CREATE INDEX idx_critical_dates_project_id ON project_management.critical_dates(project_id); -CREATE INDEX idx_critical_dates_type ON project_management.critical_dates(date_type); -CREATE INDEX idx_critical_dates_date ON project_management.critical_dates(date_value); -CREATE INDEX idx_critical_dates_alerts ON project_management.critical_dates(alert_sent, date_value) - WHERE is_active = true; -``` - ---- - -### 11. construction_phases - -Fases constructivas para avance fisico detallado. - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: construction_phases --- Descripcion: Fases constructivas para calculo de avance fisico --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.construction_phases ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion con vivienda - housing_unit_id UUID NOT NULL REFERENCES project_management.housing_units(id) ON DELETE CASCADE, - - -- Fase constructiva - phase_name project_management.construction_phase_name NOT NULL, - weight_percentage DECIMAL(5,2) NOT NULL, -- % de peso en avance total - progress_percentage DECIMAL(5,2) NOT NULL DEFAULT 0, -- % de avance de esta fase - - -- Fechas - start_date DATE, - end_date DATE, - - -- Estado - status project_management.construction_phase_status NOT NULL DEFAULT 'not_started', - - -- Notas - notes TEXT, - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT uq_construction_phases_unit_phase - UNIQUE (housing_unit_id, phase_name), - CONSTRAINT chk_construction_phases_weight - CHECK (weight_percentage >= 0 AND weight_percentage <= 100), - CONSTRAINT chk_construction_phases_progress - CHECK (progress_percentage >= 0 AND progress_percentage <= 100), - CONSTRAINT chk_construction_phases_dates - CHECK (end_date IS NULL OR start_date IS NULL OR end_date >= start_date) -); - --- Comentarios -COMMENT ON TABLE project_management.construction_phases IS 'Fases constructivas para calculo de avance fisico ponderado'; -COMMENT ON COLUMN project_management.construction_phases.weight_percentage IS 'Peso de la fase en el avance total (suma debe ser 100%)'; -COMMENT ON COLUMN project_management.construction_phases.progress_percentage IS 'Avance de esta fase especifica'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: construction_phases --- ============================================================================ - -CREATE INDEX idx_construction_phases_tenant_id ON project_management.construction_phases(tenant_id); -CREATE INDEX idx_construction_phases_unit_id ON project_management.construction_phases(housing_unit_id); -CREATE INDEX idx_construction_phases_status ON project_management.construction_phases(status) WHERE is_active = true; -``` - ---- - -### 12. project_documents - -Documentos y permisos del proyecto. - -#### DDL Completo - -```sql --- ============================================================================ --- Schema: project_management --- Tabla: project_documents --- Descripcion: Documentos legales y administrativos del proyecto --- Modulo: MAI-002 --- ============================================================================ - -CREATE TABLE IF NOT EXISTS project_management.project_documents ( - -- Primary Key - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Multi-tenant - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, - - -- Relacion con proyecto - project_id UUID NOT NULL REFERENCES project_management.projects(id) ON DELETE CASCADE, - - -- Tipo de documento - document_type project_management.document_type NOT NULL, - - -- Informacion - name VARCHAR(200) NOT NULL, - description TEXT, - document_number VARCHAR(50), - - -- Archivo - file_path TEXT, - file_size BIGINT, - file_mime_type VARCHAR(100), - - -- Fechas - issue_date DATE, - expiration_date DATE, - - -- Auditoria - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_by UUID REFERENCES core_users.users(id), - updated_by UUID REFERENCES core_users.users(id), - - -- Soft delete - is_active BOOLEAN NOT NULL DEFAULT true, - deleted_at TIMESTAMPTZ, - deleted_by UUID REFERENCES core_users.users(id), - - -- Constraints - CONSTRAINT chk_project_documents_file_size - CHECK (file_size IS NULL OR file_size > 0), - CONSTRAINT chk_project_documents_dates - CHECK (expiration_date IS NULL OR issue_date IS NULL OR expiration_date >= issue_date) -); - --- Comentarios -COMMENT ON TABLE project_management.project_documents IS 'Documentos legales y administrativos del proyecto'; -COMMENT ON COLUMN project_management.project_documents.document_type IS 'Tipo: construction_license, environmental_impact, contract, etc.'; -``` - -#### Indices - -```sql --- ============================================================================ --- INDICES: project_documents --- ============================================================================ - -CREATE INDEX idx_project_documents_tenant_id ON project_management.project_documents(tenant_id); -CREATE INDEX idx_project_documents_project_id ON project_management.project_documents(project_id); -CREATE INDEX idx_project_documents_type ON project_management.project_documents(document_type); -CREATE INDEX idx_project_documents_expiration ON project_management.project_documents(expiration_date) - WHERE is_active = true AND expiration_date IS NOT NULL; -``` - ---- - -## Row Level Security (RLS) - -### Habilitar RLS en Todas las Tablas - -```sql --- ============================================================================ --- HABILITAR RLS --- ============================================================================ - -ALTER TABLE project_management.projects ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.stages ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.blocks ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.lots ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.housing_units ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.housing_prototypes ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.prototype_versions ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.project_team_assignments ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.project_milestones ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.critical_dates ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.construction_phases ENABLE ROW LEVEL SECURITY; -ALTER TABLE project_management.project_documents ENABLE ROW LEVEL SECURITY; -``` - -### Politicas RLS por Tabla - -```sql --- ============================================================================ --- RLS POLICIES: projects --- ============================================================================ - -CREATE POLICY tenant_isolation_select_projects ON project_management.projects - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_projects ON project_management.projects - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_projects ON project_management.projects - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_projects ON project_management.projects - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: stages --- ============================================================================ - -CREATE POLICY tenant_isolation_select_stages ON project_management.stages - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_stages ON project_management.stages - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_stages ON project_management.stages - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_stages ON project_management.stages - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: blocks --- ============================================================================ - -CREATE POLICY tenant_isolation_select_blocks ON project_management.blocks - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_blocks ON project_management.blocks - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_blocks ON project_management.blocks - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_blocks ON project_management.blocks - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: lots --- ============================================================================ - -CREATE POLICY tenant_isolation_select_lots ON project_management.lots - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_lots ON project_management.lots - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_lots ON project_management.lots - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_lots ON project_management.lots - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: housing_units --- ============================================================================ - -CREATE POLICY tenant_isolation_select_housing_units ON project_management.housing_units - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_housing_units ON project_management.housing_units - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_housing_units ON project_management.housing_units - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_housing_units ON project_management.housing_units - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: housing_prototypes --- ============================================================================ - -CREATE POLICY tenant_isolation_select_prototypes ON project_management.housing_prototypes - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_prototypes ON project_management.housing_prototypes - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_prototypes ON project_management.housing_prototypes - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_prototypes ON project_management.housing_prototypes - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: prototype_versions --- ============================================================================ - -CREATE POLICY tenant_isolation_select_prototype_versions ON project_management.prototype_versions - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_prototype_versions ON project_management.prototype_versions - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_prototype_versions ON project_management.prototype_versions - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_prototype_versions ON project_management.prototype_versions - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: project_team_assignments --- ============================================================================ - -CREATE POLICY tenant_isolation_select_team_assignments ON project_management.project_team_assignments - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_team_assignments ON project_management.project_team_assignments - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_team_assignments ON project_management.project_team_assignments - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_team_assignments ON project_management.project_team_assignments - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: project_milestones --- ============================================================================ - -CREATE POLICY tenant_isolation_select_milestones ON project_management.project_milestones - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_milestones ON project_management.project_milestones - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_milestones ON project_management.project_milestones - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_milestones ON project_management.project_milestones - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: critical_dates --- ============================================================================ - -CREATE POLICY tenant_isolation_select_critical_dates ON project_management.critical_dates - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_critical_dates ON project_management.critical_dates - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_critical_dates ON project_management.critical_dates - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_critical_dates ON project_management.critical_dates - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: construction_phases --- ============================================================================ - -CREATE POLICY tenant_isolation_select_construction_phases ON project_management.construction_phases - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_construction_phases ON project_management.construction_phases - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_construction_phases ON project_management.construction_phases - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_construction_phases ON project_management.construction_phases - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - --- ============================================================================ --- RLS POLICIES: project_documents --- ============================================================================ - -CREATE POLICY tenant_isolation_select_project_documents ON project_management.project_documents - FOR SELECT - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_insert_project_documents ON project_management.project_documents - FOR INSERT - WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_update_project_documents ON project_management.project_documents - FOR UPDATE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); - -CREATE POLICY tenant_isolation_delete_project_documents ON project_management.project_documents - FOR DELETE - USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -``` - ---- - -## Funciones SQL - -### 1. Actualizar updated_at - -```sql --- ============================================================================ --- FUNCION: set_updated_at --- Descripcion: Actualiza automaticamente el timestamp updated_at --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.set_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; -``` - ---- - -### 2. Generar Codigo de Proyecto - -```sql --- ============================================================================ --- FUNCION: generate_project_code --- Descripcion: Genera codigo auto-incremental por tenant y a帽o (PROJ-2025-001) --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.generate_project_code(p_tenant_id UUID) -RETURNS VARCHAR AS $$ -DECLARE - v_year INTEGER; - v_sequence INTEGER; - v_code VARCHAR(20); -BEGIN - v_year := EXTRACT(YEAR FROM CURRENT_DATE); - - -- Obtener siguiente numero secuencial del a帽o actual - SELECT COALESCE(MAX( - CAST( - SUBSTRING(project_code FROM '\d+$') AS INTEGER - ) - ), 0) + 1 - INTO v_sequence - FROM project_management.projects - WHERE tenant_id = p_tenant_id - AND project_code LIKE 'PROJ-' || v_year || '-%'; - - -- Generar codigo: PROJ-2025-001 - v_code := 'PROJ-' || v_year || '-' || LPAD(v_sequence::TEXT, 3, '0'); - - RETURN v_code; -END; -$$ LANGUAGE plpgsql; -``` - ---- - -### 3. Calcular Avance Fisico de Proyecto - -```sql --- ============================================================================ --- FUNCION: calculate_project_physical_progress --- Descripcion: Calcula avance fisico del proyecto desde housing_units --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.calculate_project_physical_progress(p_project_id UUID) -RETURNS DECIMAL AS $$ -DECLARE - v_progress DECIMAL(5,2); -BEGIN - SELECT COALESCE(AVG(physical_progress), 0) - INTO v_progress - FROM project_management.housing_units - WHERE project_id = p_project_id - AND is_active = true; - - RETURN ROUND(v_progress, 2); -END; -$$ LANGUAGE plpgsql STABLE; -``` - ---- - -### 4. Calcular Avance Fisico de Vivienda - -```sql --- ============================================================================ --- FUNCION: calculate_housing_unit_progress --- Descripcion: Calcula avance ponderado desde construction_phases --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.calculate_housing_unit_progress(p_housing_unit_id UUID) -RETURNS DECIMAL AS $$ -DECLARE - v_progress DECIMAL(5,2); -BEGIN - SELECT COALESCE(SUM( - (weight_percentage / 100.0) * (progress_percentage / 100.0) - ) * 100, 0) - INTO v_progress - FROM project_management.construction_phases - WHERE housing_unit_id = p_housing_unit_id - AND is_active = true; - - RETURN ROUND(v_progress, 2); -END; -$$ LANGUAGE plpgsql STABLE; -``` - ---- - -### 5. Validar Workload del Usuario - -```sql --- ============================================================================ --- FUNCION: validate_user_workload --- Descripcion: Valida que el usuario no exceda limite de workload por rol --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.validate_user_workload( - p_user_id UUID, - p_role_type project_management.team_role_type, - p_new_workload INTEGER, - p_exclude_assignment_id UUID DEFAULT NULL -) -RETURNS BOOLEAN AS $$ -DECLARE - v_current_workload INTEGER; - v_max_workload INTEGER; -BEGIN - -- Limites por rol - v_max_workload := CASE p_role_type - WHEN 'director' THEN 500 - WHEN 'residente' THEN 200 - WHEN 'ingeniero_civil' THEN 800 - WHEN 'ingeniero_electrico' THEN 800 - WHEN 'ingeniero_hidraulico' THEN 800 - WHEN 'supervisor' THEN 300 - WHEN 'gerente_compras' THEN 400 - WHEN 'coordinador_calidad' THEN 500 - WHEN 'coordinador_seguridad' THEN 500 - ELSE 100 - END; - - -- Calcular workload actual - SELECT COALESCE(SUM(workload_percentage), 0) - INTO v_current_workload - FROM project_management.project_team_assignments - WHERE user_id = p_user_id - AND role_type = p_role_type - AND is_active = true - AND (assignment_end_date IS NULL OR assignment_end_date >= CURRENT_DATE) - AND (p_exclude_assignment_id IS NULL OR id != p_exclude_assignment_id); - - -- Validar que no exceda el limite - RETURN (v_current_workload + p_new_workload) <= v_max_workload; -END; -$$ LANGUAGE plpgsql STABLE; -``` - ---- - -### 6. Snapshot de Prototipo - -```sql --- ============================================================================ --- FUNCION: create_prototype_snapshot --- Descripcion: Crea snapshot JSON del prototipo para housing_unit --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.create_prototype_snapshot(p_prototype_id UUID) -RETURNS JSONB AS $$ -DECLARE - v_snapshot JSONB; -BEGIN - SELECT jsonb_build_object( - 'prototype_id', id, - 'code', code, - 'name', name, - 'category', category, - 'segment', segment, - 'version', current_version, - 'construction_area', construction_area, - 'land_area', land_area, - 'bedrooms', bedrooms, - 'bathrooms', bathrooms, - 'specifications', specifications, - 'estimated_cost', estimated_cost, - 'snapshot_date', NOW() - ) - INTO v_snapshot - FROM project_management.housing_prototypes - WHERE id = p_prototype_id; - - RETURN v_snapshot; -END; -$$ LANGUAGE plpgsql STABLE; -``` - ---- - -## Triggers - -### 1. Auto-generar Codigo de Proyecto - -```sql --- ============================================================================ --- TRIGGER: Auto-generar project_code --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.trg_generate_project_code() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.project_code IS NULL OR NEW.project_code = '' THEN - NEW.project_code := project_management.generate_project_code(NEW.tenant_id); - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_projects_generate_code - BEFORE INSERT ON project_management.projects - FOR EACH ROW - EXECUTE FUNCTION project_management.trg_generate_project_code(); -``` - ---- - -### 2. Actualizar updated_at - -```sql --- ============================================================================ --- TRIGGERS: Actualizar updated_at en todas las tablas --- ============================================================================ - -CREATE TRIGGER trg_projects_update_timestamp - BEFORE UPDATE ON project_management.projects - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_stages_update_timestamp - BEFORE UPDATE ON project_management.stages - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_blocks_update_timestamp - BEFORE UPDATE ON project_management.blocks - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_lots_update_timestamp - BEFORE UPDATE ON project_management.lots - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_housing_units_update_timestamp - BEFORE UPDATE ON project_management.housing_units - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_prototypes_update_timestamp - BEFORE UPDATE ON project_management.housing_prototypes - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_team_assignments_update_timestamp - BEFORE UPDATE ON project_management.project_team_assignments - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_milestones_update_timestamp - BEFORE UPDATE ON project_management.project_milestones - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_critical_dates_update_timestamp - BEFORE UPDATE ON project_management.critical_dates - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_construction_phases_update_timestamp - BEFORE UPDATE ON project_management.construction_phases - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); - -CREATE TRIGGER trg_project_documents_update_timestamp - BEFORE UPDATE ON project_management.project_documents - FOR EACH ROW - EXECUTE FUNCTION project_management.set_updated_at(); -``` - ---- - -### 3. Validar Workload en Asignacion - -```sql --- ============================================================================ --- TRIGGER: Validar workload antes de asignar --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.trg_validate_team_assignment_workload() -RETURNS TRIGGER AS $$ -BEGIN - IF NOT project_management.validate_user_workload( - NEW.user_id, - NEW.role_type, - NEW.workload_percentage, - NEW.id - ) THEN - RAISE EXCEPTION 'User workload exceeds limit for role %', NEW.role_type; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_team_assignments_validate_workload - BEFORE INSERT OR UPDATE ON project_management.project_team_assignments - FOR EACH ROW - EXECUTE FUNCTION project_management.trg_validate_team_assignment_workload(); -``` - ---- - -### 4. Actualizar Metricas de Proyecto - -```sql --- ============================================================================ --- TRIGGER: Actualizar metricas del proyecto --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.trg_update_project_metrics() -RETURNS TRIGGER AS $$ -DECLARE - v_project_id UUID; -BEGIN - -- Obtener project_id segun la tabla - IF TG_TABLE_NAME = 'housing_units' THEN - v_project_id := COALESCE(NEW.project_id, OLD.project_id); - ELSIF TG_TABLE_NAME = 'lots' THEN - v_project_id := COALESCE(NEW.project_id, OLD.project_id); - END IF; - - -- Actualizar total_housing_units - UPDATE project_management.projects - SET total_housing_units = ( - SELECT COUNT(*) - FROM project_management.housing_units - WHERE project_id = v_project_id - AND is_active = true - ), - physical_progress = project_management.calculate_project_physical_progress(v_project_id) - WHERE id = v_project_id; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_housing_units_update_project_metrics - AFTER INSERT OR UPDATE OR DELETE ON project_management.housing_units - FOR EACH ROW - EXECUTE FUNCTION project_management.trg_update_project_metrics(); -``` - ---- - -### 5. Actualizar Avance de Vivienda desde Fases - -```sql --- ============================================================================ --- TRIGGER: Actualizar avance de vivienda desde construction_phases --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.trg_update_housing_unit_progress() -RETURNS TRIGGER AS $$ -DECLARE - v_housing_unit_id UUID; -BEGIN - v_housing_unit_id := COALESCE(NEW.housing_unit_id, OLD.housing_unit_id); - - UPDATE project_management.housing_units - SET physical_progress = project_management.calculate_housing_unit_progress(v_housing_unit_id) - WHERE id = v_housing_unit_id; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_construction_phases_update_progress - AFTER INSERT OR UPDATE OR DELETE ON project_management.construction_phases - FOR EACH ROW - EXECUTE FUNCTION project_management.trg_update_housing_unit_progress(); -``` - ---- - -### 6. Crear Version de Prototipo - -```sql --- ============================================================================ --- TRIGGER: Crear version automatica al actualizar prototipo --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.trg_create_prototype_version() -RETURNS TRIGGER AS $$ -BEGIN - -- Solo si cambio algo relevante - IF OLD.construction_area IS DISTINCT FROM NEW.construction_area OR - OLD.bedrooms IS DISTINCT FROM NEW.bedrooms OR - OLD.estimated_cost IS DISTINCT FROM NEW.estimated_cost OR - OLD.specifications IS DISTINCT FROM NEW.specifications THEN - - -- Incrementar version - NEW.current_version := OLD.current_version + 1; - - -- Crear registro de version - INSERT INTO project_management.prototype_versions ( - tenant_id, - prototype_id, - version_number, - change_description, - specifications, - estimated_cost, - created_by - ) VALUES ( - NEW.tenant_id, - NEW.id, - NEW.current_version, - 'Auto-generated version', - jsonb_build_object( - 'construction_area', NEW.construction_area, - 'bedrooms', NEW.bedrooms, - 'bathrooms', NEW.bathrooms, - 'specifications', NEW.specifications - ), - NEW.estimated_cost, - NEW.updated_by - ); - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_prototypes_create_version - BEFORE UPDATE ON project_management.housing_prototypes - FOR EACH ROW - EXECUTE FUNCTION project_management.trg_create_prototype_version(); -``` - ---- - -### 7. Crear Snapshot al Asignar Prototipo - -```sql --- ============================================================================ --- TRIGGER: Crear snapshot del prototipo al asignar a housing_unit --- ============================================================================ - -CREATE OR REPLACE FUNCTION project_management.trg_create_housing_unit_snapshot() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.prototype_id IS NOT NULL AND NEW.prototype_snapshot IS NULL THEN - NEW.prototype_snapshot := project_management.create_prototype_snapshot(NEW.prototype_id); - - -- Heredar caracteristicas del prototipo - SELECT - construction_area, - land_area, - bedrooms, - bathrooms, - estimated_cost - INTO - NEW.construction_area, - NEW.land_area, - NEW.bedrooms, - NEW.bathrooms, - NEW.estimated_cost - FROM project_management.housing_prototypes - WHERE id = NEW.prototype_id; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER trg_housing_units_create_snapshot - BEFORE INSERT ON project_management.housing_units - FOR EACH ROW - EXECUTE FUNCTION project_management.trg_create_housing_unit_snapshot(); -``` - ---- - -## Seed Data - -### Prototipos de Ejemplo - -```sql --- ============================================================================ --- SEED: Prototipos de ejemplo por tenant --- ============================================================================ - --- Nota: Estos seeds se ejecutan durante el onboarding del tenant --- El tenant_id se sustituye en tiempo de ejecucion - -INSERT INTO project_management.housing_prototypes ( - tenant_id, - code, - name, - category, - segment, - construction_area, - land_area, - bedrooms, - bathrooms, - parking_spaces, - estimated_cost -) VALUES -( - '{{tenant_id}}', - 'CASA-A-001', - 'Casa Tipo A - Interes Social', - 'casa_unifamiliar', - 'interes_social', - 50.00, - 90.00, - 2, - 1.0, - 1, - 450000.00 -), -( - '{{tenant_id}}', - 'CASA-B-001', - 'Casa Tipo B - Interes Medio', - 'casa_unifamiliar', - 'interes_medio', - 75.00, - 120.00, - 3, - 2.0, - 2, - 850000.00 -), -( - '{{tenant_id}}', - 'DEPTO-A-001', - 'Departamento Tipo A', - 'departamento', - 'interes_medio', - 60.00, - NULL, - 2, - 1.0, - 1, - 650000.00 -); -``` - ---- - -## Consideraciones de Performance - -| Tabla | Volumen Esperado | Estrategia | -|-------|------------------|------------| -| projects | Medio (~100-500/tenant) | Indices compuestos, cache | -| stages | Medio (~3-10 por proyecto) | Indices en project_id | -| blocks | Medio (~5-20 por etapa) | Indices en stage_id | -| lots | Alto (~100-5000 por proyecto) | Particionamiento futuro, bulk operations | -| housing_units | Alto (~100-5000 por proyecto) | Indices GIN, paginacion | -| housing_prototypes | Bajo (~10-50/tenant) | Cache agresivo | -| project_team_assignments | Medio (~5-15 por proyecto) | Indices compuestos | -| construction_phases | Alto (~9 por vivienda) | Indices en housing_unit_id | - -### Optimizaciones Recomendadas - -1. **Bulk Operations:** Crear lotes en lote (hasta 500) usando `INSERT ... SELECT` -2. **Materialized Views:** Para dashboards con metricas agregadas -3. **Particionamiento:** Considerar particionar `lots` y `housing_units` por proyecto en instalaciones grandes -4. **Cache:** Cachear prototipos, ENUMs y metadatos del proyecto -5. **Indices Parciales:** Usar `WHERE is_active = true` en indices de busqueda - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-06 | Requirements-Analyst | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| DBA | - | - | [ ] | -| Tech Lead | - | - | [ ] | -| Architect | - | - | [ ] | diff --git a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-frontend.md b/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-frontend.md deleted file mode 100644 index a1ddc0e20..000000000 --- a/projects/erp-construccion/docs/02-definicion-modulos/MAI-002-proyectos-estructura/especificaciones/ET-PROJ-001-frontend.md +++ /dev/null @@ -1,2382 +0,0 @@ -# ET-PROJ-001-FRONTEND: Especificaci贸n Frontend - Cat谩logo de Proyectos - -## Identificaci贸n - -| Campo | Valor | -|-------|-------| -| **ID** | ET-PROJ-001-FRONTEND | -| **M贸dulo** | MAI-002 Proyectos y Estructura | -| **RF Base** | RF-PROJ-001 Cat谩logo de Proyectos | -| **Versi贸n** | 1.0 | -| **Estado** | En Dise帽o | -| **Framework** | React 18 + TypeScript | -| **UI Library** | shadcn/ui + Tailwind CSS | -| **State** | Zustand | -| **Forms** | React Hook Form + Zod | -| **Tables** | TanStack Table v8 | -| **Autor** | Requirements-Analyst | -| **Fecha** | 2025-12-06 | - ---- - -## Descripci贸n General - -Especificaci贸n t茅cnica del m贸dulo frontend para el Cat谩logo de Proyectos de Construcci贸n. Incluye p谩ginas, componentes, stores, hooks y servicios para la gesti贸n completa de proyectos inmobiliarios con soporte para fraccionamientos horizontales, conjuntos habitacionales, edificios verticales y proyectos mixtos. - -### Caracter铆sticas Principales - -- CRUD completo de proyectos de construcci贸n -- 4 tipos de proyectos (Fraccionamiento, Conjunto, Torre, Mixto) -- Gesti贸n de ciclo de vida con 5 estados -- Filtros avanzados (tipo, estado, cliente, ubicaci贸n) -- Dashboard con m茅tricas f铆sicas y financieras -- Mapas interactivos con geolocalizaci贸n -- Timeline de hitos y fechas cr铆ticas -- Exportaci贸n de datos (PDF, Excel) - ---- - -## Estructura de Archivos - -``` -apps/frontend/src/modules/projects/ -鈹溾攢鈹 index.ts -鈹溾攢鈹 routes.tsx -鈹溾攢鈹 pages/ -鈹 鈹溾攢鈹 ProjectsListPage.tsx -鈹 鈹溾攢鈹 ProjectDetailPage.tsx -鈹 鈹溾攢鈹 ProjectFormPage.tsx -鈹 鈹溾攢鈹 ProjectDashboardPage.tsx -鈹 鈹斺攢鈹 ProjectMapPage.tsx -鈹溾攢鈹 components/ -鈹 鈹溾攢鈹 projects/ -鈹 鈹 鈹溾攢鈹 ProjectList.tsx -鈹 鈹 鈹溾攢鈹 ProjectCard.tsx -鈹 鈹 鈹溾攢鈹 ProjectForm.tsx -鈹 鈹 鈹溾攢鈹 ProjectDetail.tsx -鈹 鈹 鈹溾攢鈹 ProjectFilters.tsx -鈹 鈹 鈹溾攢鈹 ProjectStatusBadge.tsx -鈹 鈹 鈹溾攢鈹 ProjectTypeBadge.tsx -鈹 鈹 鈹溾攢鈹 ProjectMetrics.tsx -鈹 鈹 鈹溾攢鈹 ProjectTimeline.tsx -鈹 鈹 鈹溾攢鈹 ProjectLocation.tsx -鈹 鈹 鈹溾攢鈹 ProjectClientInfo.tsx -鈹 鈹 鈹溾攢鈹 ProjectLegalInfo.tsx -鈹 鈹 鈹斺攢鈹 ProjectStateTransition.tsx -鈹 鈹斺攢鈹 shared/ -鈹 鈹溾攢鈹 Map.tsx -鈹 鈹溾攢鈹 DateRangePicker.tsx -鈹 鈹斺攢鈹 MetricCard.tsx -鈹溾攢鈹 stores/ -鈹 鈹斺攢鈹 projects.store.ts -鈹溾攢鈹 hooks/ -鈹 鈹溾攢鈹 useProjects.ts -鈹 鈹溾攢鈹 useProjectMetrics.ts -鈹 鈹斺攢鈹 useProjectStates.ts -鈹溾攢鈹 services/ -鈹 鈹斺攢鈹 projects.service.ts -鈹斺攢鈹 types/ - 鈹斺攢鈹 project.types.ts -``` - ---- - -## Types (TypeScript) - -### Tipos y Enums - -```typescript -// types/project.types.ts - -export enum ProjectType { - FRACCIONAMIENTO_HORIZONTAL = 'fraccionamiento_horizontal', - CONJUNTO_HABITACIONAL = 'conjunto_habitacional', - EDIFICIO_VERTICAL = 'edificio_vertical', - MIXTO = 'mixto', -} - -export enum ProjectStatus { - LICITACION = 'licitacion', - ADJUDICADO = 'adjudicado', - EJECUCION = 'ejecucion', - ENTREGADO = 'entregado', - CERRADO = 'cerrado', -} - -export enum ClientType { - PUBLICO = 'publico', - PRIVADO = 'privado', - MIXTO = 'mixto', -} - -export enum ContractType { - LLAVE_EN_MANO = 'llave_en_mano', - PRECIO_ALZADO = 'precio_alzado', - ADMINISTRACION = 'administracion', - MIXTO = 'mixto', -} - -export const ProjectTypeLabels: Record = { - [ProjectType.FRACCIONAMIENTO_HORIZONTAL]: 'Fraccionamiento Horizontal', - [ProjectType.CONJUNTO_HABITACIONAL]: 'Conjunto Habitacional', - [ProjectType.EDIFICIO_VERTICAL]: 'Edificio Vertical', - [ProjectType.MIXTO]: 'Proyecto Mixto', -}; - -export const ProjectStatusLabels: Record = { - [ProjectStatus.LICITACION]: 'Licitaci贸n', - [ProjectStatus.ADJUDICADO]: 'Adjudicado', - [ProjectStatus.EJECUCION]: 'Ejecuci贸n', - [ProjectStatus.ENTREGADO]: 'Entregado', - [ProjectStatus.CERRADO]: 'Cerrado', -}; - -export const ClientTypeLabels: Record = { - [ClientType.PUBLICO]: 'P煤blico', - [ClientType.PRIVADO]: 'Privado', - [ClientType.MIXTO]: 'Mixto', -}; - -export const ContractTypeLabels: Record = { - [ContractType.LLAVE_EN_MANO]: 'Llave en Mano', - [ContractType.PRECIO_ALZADO]: 'Precio Alzado', - [ContractType.ADMINISTRACION]: 'Administraci贸n', - [ContractType.MIXTO]: 'Mixto', -}; -``` - -### Interfaces Principales - -```typescript -// types/project.types.ts (continuaci贸n) - -export interface Project { - id: string; - projectCode: string; // PROJ-2025-001 - constructoraId: string; - - // Informaci贸n b谩sica - name: string; - description?: string; - projectType: ProjectType; - status: ProjectStatus; - - // Cliente - clientType: ClientType; - clientName: string; - clientRFC: string; - clientContactName?: string; - clientContactEmail?: string; - clientContactPhone?: string; - contractType: ContractType; - contractAmount: number; - - // Ubicaci贸n - address: string; - state: string; - municipality: string; - postalCode: string; - latitude?: number; - longitude?: number; - totalArea: number; // m虏 - buildableArea: number; // m虏 - - // Fechas - biddingDate?: Date; - awardDate?: Date; - contractStartDate: Date; - actualStartDate?: Date; - contractualDeadlineMonths: number; - plannedEndDate: Date; - actualEndDate?: Date; - deliveryDate?: Date; - closureDate?: Date; - - // Informaci贸n legal - buildingLicense?: string; - licenseIssueDate?: Date; - licenseExpiryDate?: Date; - environmentalImpact?: string; - landUseApproval?: string; - approvedPlanNumber?: string; - infonavitNumber?: string; - fovisssteNumber?: string; - - // M茅tricas (calculadas) - totalUnits?: number; - deliveredUnits?: number; - unitsInProgress?: number; - totalBuiltArea?: number; - physicalProgress?: number; // % - budgetedAmount?: number; - executedAmount?: number; - financialProgress?: number; // % - daysElapsed?: number; - daysRemaining?: number; - scheduleProgress?: number; // % - - // Metadata - isActive: boolean; - createdAt: string; - updatedAt: string; - createdBy?: string; - updatedBy?: string; -} - -export interface CreateProjectDto { - name: string; - description?: string; - projectType: ProjectType; - - // Cliente - clientType: ClientType; - clientName: string; - clientRFC: string; - clientContactName?: string; - clientContactEmail?: string; - clientContactPhone?: string; - contractType: ContractType; - contractAmount: number; - - // Ubicaci贸n - address: string; - state: string; - municipality: string; - postalCode: string; - latitude?: number; - longitude?: number; - totalArea: number; - buildableArea: number; - - // Fechas - biddingDate?: string; - awardDate?: string; - contractStartDate: string; - actualStartDate?: string; - contractualDeadlineMonths: number; - - // Informaci贸n legal - buildingLicense?: string; - licenseIssueDate?: string; - licenseExpiryDate?: string; - environmentalImpact?: string; - landUseApproval?: string; - approvedPlanNumber?: string; - infonavitNumber?: string; - fovisssteNumber?: string; -} - -export interface UpdateProjectDto extends Partial {} - -export interface ProjectFilters { - search?: string; - projectType?: ProjectType[]; - status?: ProjectStatus[]; - clientType?: ClientType[]; - state?: string[]; - municipality?: string[]; - dateRangeStart?: string; - dateRangeEnd?: string; - minAmount?: number; - maxAmount?: number; - hasLicense?: boolean; - page?: number; - limit?: number; - sortBy?: string; - sortOrder?: 'asc' | 'desc'; -} - -export interface ProjectMetrics { - totalProjects: number; - activeProjects: number; - projectsByType: Record; - projectsByStatus: Record; - totalContractValue: number; - totalUnits: number; - avgPhysicalProgress: number; - avgFinancialProgress: number; -} - -export interface StateTransition { - fromState: ProjectStatus; - toState: ProjectStatus; - allowedTransitions: ProjectStatus[]; - requiresApproval: boolean; - validationRules: string[]; -} -``` - ---- - -## Store (Zustand) - -### Projects Store - -```typescript -// stores/projects.store.ts -import { create } from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { Project, ProjectFilters, CreateProjectDto, UpdateProjectDto, ProjectMetrics } from '../types/project.types'; -import { projectsService } from '../services/projects.service'; -import { PaginatedResult } from '@core/types/pagination'; - -interface ProjectsState { - // Data - projects: Project[]; - selectedProject: Project | null; - metrics: ProjectMetrics | null; - total: number; - page: number; - limit: number; - - // UI State - isLoading: boolean; - isSaving: boolean; - error: string | null; - filters: ProjectFilters; - - // Actions - Fetching - fetchProjects: (filters?: ProjectFilters) => Promise; - fetchProject: (id: string) => Promise; - fetchMetrics: () => Promise; - - // Actions - CRUD - createProject: (dto: CreateProjectDto) => Promise; - updateProject: (id: string, dto: UpdateProjectDto) => Promise; - deleteProject: (id: string) => Promise; - - // Actions - State Transitions - transitionProjectState: (id: string, toState: ProjectStatus) => Promise; - - // Actions - Filters - setFilters: (filters: Partial) => void; - resetFilters: () => void; - - // Actions - UI - setSelectedProject: (project: Project | null) => void; - clearError: () => void; -} - -const defaultFilters: ProjectFilters = { - page: 1, - limit: 20, - search: '', - sortBy: 'createdAt', - sortOrder: 'desc', -}; - -export const useProjectsStore = create()( - devtools( - persist( - (set, get) => ({ - // Initial state - projects: [], - selectedProject: null, - metrics: null, - total: 0, - page: 1, - limit: 20, - isLoading: false, - isSaving: false, - error: null, - filters: defaultFilters, - - // Fetching actions - fetchProjects: async (filters?: ProjectFilters) => { - set({ isLoading: true, error: null }); - try { - const mergedFilters = { ...get().filters, ...filters }; - const result = await projectsService.getAll(mergedFilters); - set({ - projects: result.data, - total: result.meta.total, - page: result.meta.page, - limit: result.meta.limit, - isLoading: false, - filters: mergedFilters, - }); - } catch (error: any) { - set({ error: error.message, isLoading: false }); - } - }, - - fetchProject: async (id: string) => { - set({ isLoading: true, error: null }); - try { - const project = await projectsService.getById(id); - set({ selectedProject: project, isLoading: false }); - } catch (error: any) { - set({ error: error.message, isLoading: false }); - } - }, - - fetchMetrics: async () => { - set({ isLoading: true, error: null }); - try { - const metrics = await projectsService.getMetrics(); - set({ metrics, isLoading: false }); - } catch (error: any) { - set({ error: error.message, isLoading: false }); - } - }, - - // CRUD actions - createProject: async (dto: CreateProjectDto) => { - set({ isSaving: true, error: null }); - try { - const project = await projectsService.create(dto); - set((state) => ({ - projects: [project, ...state.projects], - isSaving: false, - })); - return project; - } catch (error: any) { - set({ error: error.message, isSaving: false }); - throw error; - } - }, - - updateProject: async (id: string, dto: UpdateProjectDto) => { - set({ isSaving: true, error: null }); - try { - const project = await projectsService.update(id, dto); - set((state) => ({ - projects: state.projects.map((p) => (p.id === id ? project : p)), - selectedProject: state.selectedProject?.id === id ? project : state.selectedProject, - isSaving: false, - })); - return project; - } catch (error: any) { - set({ error: error.message, isSaving: false }); - throw error; - } - }, - - deleteProject: async (id: string) => { - set({ isSaving: true, error: null }); - try { - await projectsService.delete(id); - set((state) => ({ - projects: state.projects.filter((p) => p.id !== id), - selectedProject: state.selectedProject?.id === id ? null : state.selectedProject, - isSaving: false, - })); - } catch (error: any) { - set({ error: error.message, isSaving: false }); - throw error; - } - }, - - // State transitions - transitionProjectState: async (id: string, toState: ProjectStatus) => { - set({ isSaving: true, error: null }); - try { - const project = await projectsService.transitionState(id, toState); - set((state) => ({ - projects: state.projects.map((p) => (p.id === id ? project : p)), - selectedProject: state.selectedProject?.id === id ? project : state.selectedProject, - isSaving: false, - })); - return project; - } catch (error: any) { - set({ error: error.message, isSaving: false }); - throw error; - } - }, - - // Filter actions - setFilters: (filters: Partial) => { - set((state) => ({ - filters: { ...state.filters, ...filters, page: 1 }, - })); - }, - - resetFilters: () => { - set({ filters: defaultFilters }); - }, - - // UI actions - setSelectedProject: (project: Project | null) => { - set({ selectedProject: project }); - }, - - clearError: () => { - set({ error: null }); - }, - }), - { - name: 'projects-store', - partialize: (state) => ({ - filters: state.filters, - }), - } - ), - { name: 'projects-store' } - ) -); -``` - ---- - -## Services (API) - -### Projects Service - -```typescript -// services/projects.service.ts -import axios from '@core/lib/axios'; -import { Project, ProjectFilters, CreateProjectDto, UpdateProjectDto, ProjectMetrics, ProjectStatus } from '../types/project.types'; -import { PaginatedResult } from '@core/types/pagination'; - -const BASE_URL = '/api/v1/projects'; - -export const projectsService = { - /** - * Get all projects with filters and pagination - */ - async getAll(filters: ProjectFilters): Promise> { - const params = new URLSearchParams(); - - if (filters.search) params.append('search', filters.search); - if (filters.projectType?.length) params.append('projectType', filters.projectType.join(',')); - if (filters.status?.length) params.append('status', filters.status.join(',')); - if (filters.clientType?.length) params.append('clientType', filters.clientType.join(',')); - if (filters.state?.length) params.append('state', filters.state.join(',')); - if (filters.municipality?.length) params.append('municipality', filters.municipality.join(',')); - if (filters.dateRangeStart) params.append('dateRangeStart', filters.dateRangeStart); - if (filters.dateRangeEnd) params.append('dateRangeEnd', filters.dateRangeEnd); - if (filters.minAmount !== undefined) params.append('minAmount', filters.minAmount.toString()); - if (filters.maxAmount !== undefined) params.append('maxAmount', filters.maxAmount.toString()); - if (filters.hasLicense !== undefined) params.append('hasLicense', filters.hasLicense.toString()); - if (filters.page) params.append('page', filters.page.toString()); - if (filters.limit) params.append('limit', filters.limit.toString()); - if (filters.sortBy) params.append('sortBy', filters.sortBy); - if (filters.sortOrder) params.append('sortOrder', filters.sortOrder); - - const { data } = await axios.get>(`${BASE_URL}?${params.toString()}`); - return data; - }, - - /** - * Get project by ID - */ - async getById(id: string): Promise { - const { data } = await axios.get(`${BASE_URL}/${id}`); - return data; - }, - - /** - * Create new project - */ - async create(dto: CreateProjectDto): Promise { - const { data } = await axios.post(BASE_URL, dto); - return data; - }, - - /** - * Update project - */ - async update(id: string, dto: UpdateProjectDto): Promise { - const { data } = await axios.patch(`${BASE_URL}/${id}`, dto); - return data; - }, - - /** - * Delete project - */ - async delete(id: string): Promise { - await axios.delete(`${BASE_URL}/${id}`); - }, - - /** - * Transition project state - */ - async transitionState(id: string, toState: ProjectStatus): Promise { - const { data } = await axios.post(`${BASE_URL}/${id}/transition`, { toState }); - return data; - }, - - /** - * Get project metrics - */ - async getMetrics(): Promise { - const { data } = await axios.get(`${BASE_URL}/metrics`); - return data; - }, - - /** - * Export projects to Excel - */ - async exportToExcel(filters: ProjectFilters): Promise { - const params = new URLSearchParams(); - if (filters.search) params.append('search', filters.search); - if (filters.projectType?.length) params.append('projectType', filters.projectType.join(',')); - if (filters.status?.length) params.append('status', filters.status.join(',')); - - const { data } = await axios.get(`${BASE_URL}/export/excel?${params.toString()}`, { - responseType: 'blob', - }); - return data; - }, - - /** - * Export project to PDF - */ - async exportToPdf(id: string): Promise { - const { data } = await axios.get(`${BASE_URL}/${id}/export/pdf`, { - responseType: 'blob', - }); - return data; - }, -}; -``` - ---- - -## Validation (Zod) - -### Project Schema - -```typescript -// types/project.validation.ts -import { z } from 'zod'; -import { ProjectType, ProjectStatus, ClientType, ContractType } from './project.types'; - -export const projectFormSchema = z.object({ - // Informaci贸n b谩sica - name: z - .string() - .min(1, 'Nombre requerido') - .max(200, 'M谩ximo 200 caracteres'), - description: z - .string() - .max(1000, 'M谩ximo 1000 caracteres') - .optional() - .or(z.literal('')), - projectType: z.nativeEnum(ProjectType, { - required_error: 'Tipo de proyecto requerido', - }), - - // Cliente - clientType: z.nativeEnum(ClientType, { - required_error: 'Tipo de cliente requerido', - }), - clientName: z - .string() - .min(1, 'Nombre del cliente requerido') - .max(200, 'M谩ximo 200 caracteres'), - clientRFC: z - .string() - .min(12, 'RFC inv谩lido (m铆nimo 12 caracteres)') - .max(13, 'RFC inv谩lido (m谩ximo 13 caracteres)') - .regex(/^[A-Z脩&]{3,4}\d{6}[A-Z0-9]{3}$/, 'Formato de RFC inv谩lido'), - clientContactName: z - .string() - .max(100, 'M谩ximo 100 caracteres') - .optional() - .or(z.literal('')), - clientContactEmail: z - .string() - .email('Email inv谩lido') - .optional() - .or(z.literal('')), - clientContactPhone: z - .string() - .max(20, 'M谩ximo 20 caracteres') - .optional() - .or(z.literal('')), - contractType: z.nativeEnum(ContractType, { - required_error: 'Tipo de contrato requerido', - }), - contractAmount: z - .number({ - required_error: 'Monto contratado requerido', - invalid_type_error: 'Debe ser un n煤mero', - }) - .positive('Debe ser mayor a 0') - .max(999999999999.99, 'Monto demasiado grande'), - - // Ubicaci贸n - address: z - .string() - .min(1, 'Direcci贸n requerida') - .max(500, 'M谩ximo 500 caracteres'), - state: z - .string() - .min(1, 'Estado requerido') - .max(100, 'M谩ximo 100 caracteres'), - municipality: z - .string() - .min(1, 'Municipio requerido') - .max(100, 'M谩ximo 100 caracteres'), - postalCode: z - .string() - .length(5, 'C贸digo postal debe tener 5 d铆gitos') - .regex(/^\d{5}$/, 'C贸digo postal inv谩lido'), - latitude: z - .number() - .min(-90, 'Latitud inv谩lida') - .max(90, 'Latitud inv谩lida') - .optional() - .nullable(), - longitude: z - .number() - .min(-180, 'Longitud inv谩lida') - .max(180, 'Longitud inv谩lida') - .optional() - .nullable(), - totalArea: z - .number({ - required_error: '脕rea total requerida', - }) - .positive('Debe ser mayor a 0'), - buildableArea: z - .number({ - required_error: '脕rea construible requerida', - }) - .positive('Debe ser mayor a 0'), - - // Fechas - biddingDate: z.string().optional().or(z.literal('')), - awardDate: z.string().optional().or(z.literal('')), - contractStartDate: z.string().min(1, 'Fecha de inicio contractual requerida'), - actualStartDate: z.string().optional().or(z.literal('')), - contractualDeadlineMonths: z - .number({ - required_error: 'Plazo contractual requerido', - }) - .int('Debe ser un n煤mero entero') - .positive('Debe ser mayor a 0') - .max(120, 'M谩ximo 120 meses (10 a帽os)'), - - // Informaci贸n legal - buildingLicense: z.string().max(50).optional().or(z.literal('')), - licenseIssueDate: z.string().optional().or(z.literal('')), - licenseExpiryDate: z.string().optional().or(z.literal('')), - environmentalImpact: z.string().max(50).optional().or(z.literal('')), - landUseApproval: z.string().max(50).optional().or(z.literal('')), - approvedPlanNumber: z.string().max(50).optional().or(z.literal('')), - infonavitNumber: z.string().max(50).optional().or(z.literal('')), - fovisssteNumber: z.string().max(50).optional().or(z.literal('')), -}).refine( - (data) => data.buildableArea <= data.totalArea, - { - message: '脕rea construible no puede ser mayor al 谩rea total', - path: ['buildableArea'], - } -); - -export type ProjectFormData = z.infer; -``` - ---- - -## Components - -### ProjectList Component - -```tsx -// components/projects/ProjectList.tsx -import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - flexRender, - getCoreRowModel, - useReactTable, - ColumnDef, -} from '@tanstack/react-table'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { Skeleton } from '@/components/ui/skeleton'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Progress } from '@/components/ui/progress'; -import { MapPin, Eye, Edit } from 'lucide-react'; -import { useProjectsStore } from '../../stores/projects.store'; -import { Project } from '../../types/project.types'; -import { ProjectStatusBadge } from './ProjectStatusBadge'; -import { ProjectTypeBadge } from './ProjectTypeBadge'; -import { formatCurrency, formatDate } from '@core/lib/format'; - -export function ProjectList() { - const navigate = useNavigate(); - const { - projects, - total, - page, - limit, - isLoading, - filters, - fetchProjects, - setFilters, - } = useProjectsStore(); - - useEffect(() => { - fetchProjects(); - }, [filters]); - - const columns: ColumnDef[] = [ - { - accessorKey: 'projectCode', - header: 'C贸digo', - cell: ({ row }) => ( -
{row.original.projectCode}
- ), - }, - { - accessorKey: 'name', - header: 'Proyecto', - cell: ({ row }) => ( -
-
{row.original.name}
-
- - {row.original.municipality}, {row.original.state} -
-
- ), - }, - { - accessorKey: 'projectType', - header: 'Tipo', - cell: ({ row }) => , - }, - { - accessorKey: 'status', - header: 'Estado', - cell: ({ row }) => , - }, - { - accessorKey: 'clientName', - header: 'Cliente', - cell: ({ row }) => ( -
-
{row.original.clientName}
-
{row.original.clientRFC}
-
- ), - }, - { - accessorKey: 'contractAmount', - header: 'Monto', - cell: ({ row }) => ( -
- {formatCurrency(row.original.contractAmount)} -
- ), - }, - { - accessorKey: 'physicalProgress', - header: 'Avance F铆sico', - cell: ({ row }) => { - const progress = row.original.physicalProgress ?? 0; - return ( -
- -
- {progress.toFixed(1)}% -
-
- ); - }, - }, - { - accessorKey: 'contractStartDate', - header: 'Inicio', - cell: ({ row }) => ( -
{formatDate(row.original.contractStartDate)}
- ), - }, - { - id: 'actions', - header: 'Acciones', - cell: ({ row }) => ( -
- - -
- ), - }, - ]; - - const table = useReactTable({ - data: projects, - columns, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - pageCount: Math.ceil(total / limit), - }); - - if (isLoading) { - return ( -
- {[...Array(5)].map((_, i) => ( - - ))} -
- ); - } - - return ( -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - navigate(`/projects/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No se encontraron proyectos. - - - )} - -
-
- -
-
- Mostrando {projects.length} de {total} proyectos -
-
- - -
-
-
- ); -} -``` - -### ProjectForm Component - -```tsx -// components/projects/ProjectForm.tsx -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { Button } from '@/components/ui/button'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { - Project, - ProjectType, - ClientType, - ContractType, - ProjectTypeLabels, - ClientTypeLabels, - ContractTypeLabels, - CreateProjectDto -} from '../../types/project.types'; -import { projectFormSchema, ProjectFormData } from '../../types/project.validation'; - -interface ProjectFormProps { - project?: Project; - onSubmit: (data: CreateProjectDto) => Promise; - onCancel: () => void; - isLoading?: boolean; -} - -export function ProjectForm({ project, onSubmit, onCancel, isLoading }: ProjectFormProps) { - const form = useForm({ - resolver: zodResolver(projectFormSchema), - defaultValues: { - name: project?.name ?? '', - description: project?.description ?? '', - projectType: project?.projectType ?? ProjectType.FRACCIONAMIENTO_HORIZONTAL, - clientType: project?.clientType ?? ClientType.PUBLICO, - clientName: project?.clientName ?? '', - clientRFC: project?.clientRFC ?? '', - clientContactName: project?.clientContactName ?? '', - clientContactEmail: project?.clientContactEmail ?? '', - clientContactPhone: project?.clientContactPhone ?? '', - contractType: project?.contractType ?? ContractType.LLAVE_EN_MANO, - contractAmount: project?.contractAmount ?? 0, - address: project?.address ?? '', - state: project?.state ?? '', - municipality: project?.municipality ?? '', - postalCode: project?.postalCode ?? '', - latitude: project?.latitude, - longitude: project?.longitude, - totalArea: project?.totalArea ?? 0, - buildableArea: project?.buildableArea ?? 0, - biddingDate: project?.biddingDate ? new Date(project.biddingDate).toISOString().split('T')[0] : '', - awardDate: project?.awardDate ? new Date(project.awardDate).toISOString().split('T')[0] : '', - contractStartDate: project?.contractStartDate - ? new Date(project.contractStartDate).toISOString().split('T')[0] - : '', - actualStartDate: project?.actualStartDate - ? new Date(project.actualStartDate).toISOString().split('T')[0] - : '', - contractualDeadlineMonths: project?.contractualDeadlineMonths ?? 24, - buildingLicense: project?.buildingLicense ?? '', - licenseIssueDate: project?.licenseIssueDate - ? new Date(project.licenseIssueDate).toISOString().split('T')[0] - : '', - licenseExpiryDate: project?.licenseExpiryDate - ? new Date(project.licenseExpiryDate).toISOString().split('T')[0] - : '', - environmentalImpact: project?.environmentalImpact ?? '', - landUseApproval: project?.landUseApproval ?? '', - approvedPlanNumber: project?.approvedPlanNumber ?? '', - infonavitNumber: project?.infonavitNumber ?? '', - fovisssteNumber: project?.fovisssteNumber ?? '', - }, - }); - - const handleSubmit = async (data: ProjectFormData) => { - const dto: CreateProjectDto = { - ...data, - // Convertir strings vac铆os a undefined - description: data.description || undefined, - clientContactName: data.clientContactName || undefined, - clientContactEmail: data.clientContactEmail || undefined, - clientContactPhone: data.clientContactPhone || undefined, - biddingDate: data.biddingDate || undefined, - awardDate: data.awardDate || undefined, - actualStartDate: data.actualStartDate || undefined, - buildingLicense: data.buildingLicense || undefined, - licenseIssueDate: data.licenseIssueDate || undefined, - licenseExpiryDate: data.licenseExpiryDate || undefined, - environmentalImpact: data.environmentalImpact || undefined, - landUseApproval: data.landUseApproval || undefined, - approvedPlanNumber: data.approvedPlanNumber || undefined, - infonavitNumber: data.infonavitNumber || undefined, - fovisssteNumber: data.fovisssteNumber || undefined, - }; - await onSubmit(dto); - }; - - return ( -
- - - - General - Cliente - Ubicaci贸n - Legal - - - {/* Tab: General */} - - - - Informaci贸n General - - -
- ( - - Nombre del Proyecto * - - - - - - )} - /> - - ( - - Tipo de Proyecto * - - - - )} - /> -
- - ( - - Descripci贸n - -